/*
 *  Copyright (C) 2009, 2010 Sebastian Dröge <sebastian.droege@collabora.co.uk>
 *  Copyright (C) 2013 Collabora Ltd.
 *  Copyright (C) 2013 Orange
 *  Copyright (C) 2014, 2015 Sebastian Dröge <sebastian@centricular.com>
 *  Copyright (C) 2015, 2016, 2018, 2019, 2020, 2021 Metrological Group B.V.
 *  Copyright (C) 2015, 2016, 2018, 2019, 2020, 2021 Igalia, S.L
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser 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
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "config.h"
#include "WebKitMediaSourceGStreamer.h"

#if ENABLE(VIDEO) && ENABLE(MEDIA_SOURCE) && USE(GSTREAMER)

#include "GStreamerCommon.h"
#include "MediaSourceTrackGStreamer.h"
#include "VideoTrackPrivateGStreamer.h"
#include <cassert>
#include <gst/gst.h>
#include <wtf/Condition.h>
#include <wtf/DataMutex.h>
#include <wtf/HashMap.h>
#include <wtf/MainThread.h>
#include <wtf/MainThreadData.h>
#include <wtf/RefPtr.h>
#include <wtf/glib/WTFGType.h>
#include <wtf/text/AtomString.h>
#include <wtf/text/AtomStringHash.h>
#include <wtf/text/CString.h>

using namespace WTF;
using namespace WebCore;

GST_DEBUG_CATEGORY_STATIC(webkit_media_src_debug);
#define GST_CAT_DEFAULT webkit_media_src_debug

#define webkit_media_src_parent_class parent_class
#define WEBKIT_MEDIA_SRC_CATEGORY_INIT GST_DEBUG_CATEGORY_INIT(webkit_media_src_debug, "webkitmediasrc", 0, "websrc element");

static GstStaticPadTemplate srcTemplate = GST_STATIC_PAD_TEMPLATE("src_%s", GST_PAD_SRC,
    GST_PAD_SOMETIMES, GST_STATIC_CAPS_ANY);

enum {
    PROP_0,
    PROP_N_AUDIO,
    PROP_N_VIDEO,
    PROP_N_TEXT,
    PROP_LAST
};

struct Stream;

struct WebKitMediaSrcPrivate {
    HashMap<AtomString, RefPtr<Stream>> streams;
    Stream* streamByName(const AtomString& name)
    {
        ASSERT(isMainThread());
        Stream* stream = streams.get(name);
        ASSERT(stream);
        return stream;
    }

    // Used for stream-start events, shared by all streams.
    const unsigned groupId { gst_util_group_id_next() };

    // Set once when the source is started. Not changed after.
    GRefPtr<GstStreamCollection> collection;
    bool isStarted() { return collection; }

    // Changed on seeks.
    GstClockTime startTime { 0 };
    double rate { 1.0 };

    // Only used by URI Handler API implementation.
    GUniquePtr<char> uri;
};

static void webKitMediaSrcUriHandlerInit(gpointer, gpointer);
static void webKitMediaSrcConstructed(GObject*);
static GstStateChangeReturn webKitMediaSrcChangeState(GstElement*, GstStateChange);
static gboolean webKitMediaSrcActivateMode(GstPad*, GstObject*, GstPadMode, gboolean activate);
static void webKitMediaSrcLoop(void*);
static void webKitMediaSrcTearDownStream(WebKitMediaSrc* source, const AtomString& name);
static void webKitMediaSrcGetProperty(GObject*, unsigned propId, GValue*, GParamSpec*);
static void webKitMediaSrcStreamFlush(Stream*, bool isSeekingFlush);
static gboolean webKitMediaSrcSendEvent(GstElement*, GstEvent*);

#define webkit_media_src_parent_class parent_class

struct WebKitMediaSrcPadPrivate {
    RefPtr<Stream> stream;
};

struct WebKitMediaSrcPad {
    GstPad parent;
    WebKitMediaSrcPadPrivate* priv;
};

struct WebKitMediaSrcPadClass {
    GstPadClass parent;
};

namespace WTF {

template<> GRefPtr<WebKitMediaSrcPad> adoptGRef(WebKitMediaSrcPad* ptr)
{
    ASSERT(!ptr || !g_object_is_floating(ptr));
    return GRefPtr<WebKitMediaSrcPad>(ptr, GRefPtrAdopt);
}

template<> WebKitMediaSrcPad* refGPtr<WebKitMediaSrcPad>(WebKitMediaSrcPad* ptr)
{
    if (ptr)
        gst_object_ref_sink(GST_OBJECT(ptr));

    return ptr;
}

template<> void derefGPtr<WebKitMediaSrcPad>(WebKitMediaSrcPad* ptr)
{
    if (ptr)
        gst_object_unref(ptr);
}

} // namespace WTF

static GType webkit_media_src_pad_get_type();
WEBKIT_DEFINE_TYPE(WebKitMediaSrcPad, webkit_media_src_pad, GST_TYPE_PAD);
#define WEBKIT_TYPE_MEDIA_SRC_PAD (webkit_media_src_pad_get_type())
#define WEBKIT_MEDIA_SRC_PAD(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), WEBKIT_TYPE_MEDIA_SRC_PAD, WebKitMediaSrcPad))

static void webkit_media_src_pad_class_init(WebKitMediaSrcPadClass*)
{
}

WEBKIT_DEFINE_TYPE_WITH_CODE(WebKitMediaSrc, webkit_media_src, GST_TYPE_ELEMENT,
    G_IMPLEMENT_INTERFACE(GST_TYPE_URI_HANDLER, webKitMediaSrcUriHandlerInit);
    GST_DEBUG_CATEGORY_INIT(webkit_media_src_debug, "webkitmediasrc", 0, "WebKit MSE source element"));

struct Stream : public ThreadSafeRefCounted<Stream> {
    Stream(WebKitMediaSrc* source, GRefPtr<GstPad>&& pad, Ref<MediaSourceTrackGStreamer>&& track, GRefPtr<GstStream>&& streamInfo)
        : source(source)
        , pad(WTFMove(pad))
        , track(WTFMove(track))
        , streamInfo(WTFMove(streamInfo))
        , streamingMembersDataMutex(GRefPtr(this->track->initialCaps()), source->priv->startTime, source->priv->rate)
    {
        ASSERT(this->track->initialCaps());
    }

    WebKitMediaSrc* const source;
    GRefPtr<GstPad> const pad;
    Ref<MediaSourceTrackGStreamer> track;
    GRefPtr<GstStream> streamInfo;

    struct StreamingMembers {
        StreamingMembers(GRefPtr<GstCaps>&& initialCaps, GstClockTime startTime, double rate)
            : pendingInitialCaps(WTFMove(initialCaps))
        {
            gst_segment_init(&segment, GST_FORMAT_TIME);
            segment.start = segment.time = startTime;
            segment.rate = rate;
            ASSERT(pendingInitialCaps);
        }

        bool hasPushedStreamCollectionEvent { false };
        bool wasStreamStartSent { false };
        bool doesNeedSegmentEvent { true };
        bool hasPushedFirstBuffer { false }; // Used to get a pipeline dump of the pipeline before buffers are flowing.
        GstSegment segment;
        GRefPtr<GstCaps> pendingInitialCaps;
        GRefPtr<GstCaps> previousCaps; // Caps from enqueued samples are compared to these to push CAPS events as needed.
        Condition padLinkedOrFlushedCondition;
        Condition queueChangedOrFlushedCondition;
        bool isFlushing { false };

        // Flushes before any buffer has been popped from the queue and sent downstream can be avoided just
        // by clearing the queue.
        bool hasPoppedFirstObject { false };
    };
    DataMutex<StreamingMembers> streamingMembersDataMutex;
};

static GRefPtr<GstElement> findPipeline(GRefPtr<GstElement> element)
{
    while (true) {
        GRefPtr<GstElement> parentElement = adoptGRef(GST_ELEMENT(gst_element_get_parent(element.get())));
        if (!parentElement)
            return element;
        element = parentElement;
    }
}

static GstStreamType gstStreamType(TrackPrivateBaseGStreamer::TrackType type)
{
    switch (type) {
    case TrackPrivateBaseGStreamer::TrackType::Video:
        return GST_STREAM_TYPE_VIDEO;
    case TrackPrivateBaseGStreamer::TrackType::Audio:
        return GST_STREAM_TYPE_AUDIO;
    case TrackPrivateBaseGStreamer::TrackType::Text:
        return GST_STREAM_TYPE_TEXT;
    default:
        GST_ERROR("Received unexpected stream type");
        return GST_STREAM_TYPE_UNKNOWN;
    }
}

#ifndef GST_DISABLE_GST_DEBUG
static const char* streamTypeToString(TrackPrivateBaseGStreamer::TrackType type)
{
    switch (type) {
    case TrackPrivateBaseGStreamer::TrackType::Audio:
        return "Audio";
    case TrackPrivateBaseGStreamer::TrackType::Video:
        return "Video";
    case TrackPrivateBaseGStreamer::TrackType::Text:
        return "Text";
    default:
    case TrackPrivateBaseGStreamer::TrackType::Unknown:
        return "Unknown";
    }
}
#endif // GST_DISABLE_GST_DEBUG

static void webkit_media_src_class_init(WebKitMediaSrcClass* klass)
{
    GObjectClass* oklass = G_OBJECT_CLASS(klass);
    GstElementClass* eklass = GST_ELEMENT_CLASS(klass);

    oklass->constructed = webKitMediaSrcConstructed;
    oklass->get_property = webKitMediaSrcGetProperty;

    gst_element_class_add_static_pad_template_with_gtype(eklass, &srcTemplate, webkit_media_src_pad_get_type());

    gst_element_class_set_static_metadata(eklass, "WebKit MediaSource source element", "Source/Network", "Feeds samples coming from WebKit MediaSource object", "Igalia <aboya@igalia.com>");

    eklass->change_state = webKitMediaSrcChangeState;
    eklass->send_event = webKitMediaSrcSendEvent;
    g_object_class_install_property(oklass,
        PROP_N_AUDIO,
        g_param_spec_int("n-audio", "Number Audio", "Total number of audio streams",
        0, G_MAXINT, 0, GParamFlags(G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)));
    g_object_class_install_property(oklass,
        PROP_N_VIDEO,
        g_param_spec_int("n-video", "Number Video", "Total number of video streams",
        0, G_MAXINT, 0, GParamFlags(G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)));
    g_object_class_install_property(oklass,
        PROP_N_TEXT,
        g_param_spec_int("n-text", "Number Text", "Total number of text streams",
        0, G_MAXINT, 0, GParamFlags(G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)));
}

static void webKitMediaSrcConstructed(GObject* object)
{
    GST_CALL_PARENT(G_OBJECT_CLASS, constructed, (object));

    ASSERT(isMainThread());
    GST_OBJECT_FLAG_SET(object, GST_ELEMENT_FLAG_SOURCE);
}

void webKitMediaSrcEmitStreams(WebKitMediaSrc* source, const Vector<RefPtr<MediaSourceTrackGStreamer>>& tracks)
{
    ASSERT(isMainThread());
    ASSERT(!source->priv->isStarted());
    GST_DEBUG_OBJECT(source, "Emitting STREAM_COLLECTION");

    source->priv->collection = adoptGRef(gst_stream_collection_new("WebKitMediaSrc"));
    for (const auto& track : tracks) {
        GST_DEBUG_OBJECT(source, "Adding stream with trackId '%s' of type %s with caps %" GST_PTR_FORMAT, track->trackId().string().utf8().data(), streamTypeToString(track->type()), track->initialCaps().get());

        GRefPtr<WebKitMediaSrcPad> pad = WEBKIT_MEDIA_SRC_PAD(g_object_new(webkit_media_src_pad_get_type(), "name", makeString("src_", track->trackId()).utf8().data(), "direction", GST_PAD_SRC, NULL));
        gst_pad_set_activatemode_function(GST_PAD(pad.get()), webKitMediaSrcActivateMode);

        ASSERT(track->initialCaps());
        auto stream = adoptRef(new Stream(source, GRefPtr<GstPad>(GST_PAD(pad.get())), *track,
            adoptGRef(gst_stream_new(track->trackId().string().utf8().data(), track->initialCaps().get(), gstStreamType(track->type()), GST_STREAM_FLAG_SELECT))));
        pad->priv->stream = stream;

        gst_stream_collection_add_stream(source->priv->collection.get(), GRefPtr<GstStream>(stream->streamInfo.get()).leakRef());
        source->priv->streams.set(track->trackId(), WTFMove(stream));
    }

    gst_element_post_message(GST_ELEMENT(source), gst_message_new_stream_collection(GST_OBJECT(source), source->priv->collection.get()));

    for (const RefPtr<Stream>& stream: source->priv->streams.values()) {
        // Workaround: gst_element_add_pad() should already call gst_pad_set_active() if the element is PAUSED or
        // PLAYING. Unfortunately, as of GStreamer 1.18.2 it does so with the element lock taken, causing a deadlock
        // in gst_pad_start_task(), who tries to post a `stream-status` message in the element, which also requires
        // the element lock. Activating the pad beforehand avoids that codepath.
        // https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/210
        // FIXME: Remove this workaround when the bug gets fixed and versions without the bug are no longer in use.
        GstState state;
        gst_element_get_state(GST_ELEMENT(source), &state, nullptr, 0);
        if (state > GST_STATE_READY)
            gst_pad_set_active(GST_PAD(stream->pad.get()), true);

        GST_DEBUG_OBJECT(source, "Adding pad '%s' for stream with name '%s'", GST_OBJECT_NAME(stream->pad.get()), stream->track->trackId().string().utf8().data());
        gst_element_add_pad(GST_ELEMENT(source), GST_PAD(stream->pad.get()));
    }
    GST_DEBUG_OBJECT(source, "All pads added");
}

static void webKitMediaSrcTearDownStream(WebKitMediaSrc* source, const AtomString& name)
{
    ASSERT(isMainThread());
    Stream* stream = source->priv->streamByName(name);
    GST_DEBUG_OBJECT(source, "Tearing down stream '%s'", name.string().utf8().data());

    // Flush the source element **and** downstream. We want to stop the streaming thread and for that we need all elements downstream to be idle.
    webKitMediaSrcStreamFlush(stream, false);
    // Stop the thread now.
    gst_pad_set_active(stream->pad.get(), false);

    if (source->priv->isStarted())
        gst_element_remove_pad(GST_ELEMENT(source), stream->pad.get());
    source->priv->streams.remove(name);
}

static gboolean webKitMediaSrcActivateMode(GstPad* pad, GstObject* source, GstPadMode mode, gboolean active)
{
    if (mode != GST_PAD_MODE_PUSH) {
        GST_ERROR_OBJECT(source, "Unexpected pad mode in WebKitMediaSrc");
        return false;
    }

    if (active)
        gst_pad_start_task(pad, webKitMediaSrcLoop, pad, nullptr);
    else {
        // Unblock the streaming thread.
        RefPtr<Stream>& stream = WEBKIT_MEDIA_SRC_PAD(pad)->priv->stream;
        {
            DataMutexLocker streamingMembers { stream->streamingMembersDataMutex };
            streamingMembers->isFlushing = true;
            streamingMembers->padLinkedOrFlushedCondition.notifyOne();
            streamingMembers->queueChangedOrFlushedCondition.notifyOne();
        }
        // Following gstbasesrc implementation, this code is not flushing downstream.
        // If there is any possibility of the streaming thread being blocked downstream the caller MUST flush before.
        // Otherwise a deadlock would occur as the next function tries to join the thread.
        gst_pad_stop_task(pad);
        {
            DataMutexLocker streamingMembers { stream->streamingMembersDataMutex };
            streamingMembers->isFlushing = false;
        }
    }
    return true;
}

static void webKitMediaSrcPadLinked(GstPad* pad, GstPad*, void*)
{
    RefPtr<Stream>& stream = WEBKIT_MEDIA_SRC_PAD(pad)->priv->stream;
    DataMutexLocker streamingMembers { stream->streamingMembersDataMutex };
    streamingMembers->padLinkedOrFlushedCondition.notifyOne();
}

static void webKitMediaSrcWaitForPadLinkedOrFlush(GstPad* pad, DataMutexLocker<Stream::StreamingMembers>& streamingMembers)
{
    {
        auto locker = GstObjectLocker(pad);
        if (LIKELY(GST_PAD_IS_LINKED(pad)))
            return;

        GST_DEBUG_OBJECT(pad, "Waiting for the pad to be linked...");
        g_signal_connect(pad, "linked", G_CALLBACK(webKitMediaSrcPadLinked), nullptr);
    }

    assertIsHeld(streamingMembers.mutex());
    streamingMembers->padLinkedOrFlushedCondition.wait(streamingMembers.mutex());

    g_signal_handlers_disconnect_by_func(pad, reinterpret_cast<void*>(webKitMediaSrcPadLinked), nullptr);
    GST_DEBUG_OBJECT(pad, "Finished waiting for the pad to be linked.");
}

// Called with STREAM_LOCK.
static void webKitMediaSrcLoop(void* userData)
{
    GstPad* pad = GST_PAD(userData);
    RefPtr<Stream>& stream = WEBKIT_MEDIA_SRC_PAD(pad)->priv->stream;

    DataMutexLocker streamingMembers { stream->streamingMembersDataMutex };
    if (streamingMembers->isFlushing) {
        gst_pad_pause_task(pad);
        return;
    }

    // Since the pad can and will be added when the element is in PLAYING state, this task can start running
    // before the pad is linked. Wait for the pad to be linked to avoid buffers being lost to not-linked errors.
    webKitMediaSrcWaitForPadLinkedOrFlush(pad, streamingMembers);
    if (streamingMembers->isFlushing) {
        gst_pad_pause_task(pad);
        return;
    }
    ASSERT(gst_pad_is_linked(pad));

    // By keeping the lock we are guaranteed that a flush will not happen while we send essential events.
    // These events should never block downstream, so the lock should be released in little time in every
    // case.

    if (!streamingMembers->hasPushedStreamCollectionEvent) {
        GST_DEBUG_OBJECT(pad, "Pushing STREAM_COLLECTION event.");
        bool wasStreamCollectionSent = gst_pad_push_event(stream->pad.get(), gst_event_new_stream_collection(stream->source->priv->collection.get()));
        streamingMembers->hasPushedStreamCollectionEvent = true;
        GST_DEBUG_OBJECT(pad, "STREAM_COLLECTION event has been pushed, %s was returned.", boolForPrinting(wasStreamCollectionSent));
        // Initial events like this must go through, flushes (including tearing down the element) is not allowed until
        // `hasPushedFirstBuffer` has been set to true.
        ASSERT(wasStreamCollectionSent);
    }

    if (!streamingMembers->wasStreamStartSent) {
        GUniquePtr<char> streamId { g_strdup_printf("mse/%s", stream->track->trackId().string().utf8().data()) };
        GRefPtr<GstEvent> event = adoptGRef(gst_event_new_stream_start(streamId.get()));
        gst_event_set_group_id(event.get(), stream->source->priv->groupId);
        gst_event_set_stream(event.get(), stream->streamInfo.get());

        GST_DEBUG_OBJECT(pad, "Pushing STREAM_START event.");
        bool wasStreamStartSent = gst_pad_push_event(pad, event.leakRef());
        streamingMembers->wasStreamStartSent = wasStreamStartSent;
        GST_DEBUG_OBJECT(pad, "STREAM_START event pushed, %s was returned.", boolForPrinting(wasStreamStartSent));
        ASSERT(wasStreamStartSent);
    }

    if (streamingMembers->pendingInitialCaps) {
        GRefPtr<GstEvent> event = adoptGRef(gst_event_new_caps(streamingMembers->pendingInitialCaps.get()));

        GST_DEBUG_OBJECT(pad, "Pushing initial CAPS event: %" GST_PTR_FORMAT, streamingMembers->pendingInitialCaps.get());
        bool wasCapsEventSent = gst_pad_push_event(pad, event.leakRef());
        GST_DEBUG_OBJECT(pad, "Pushed initial CAPS event, %s was returned.", boolForPrinting(wasCapsEventSent));

        streamingMembers->previousCaps = WTFMove(streamingMembers->pendingInitialCaps);
        ASSERT(!streamingMembers->pendingInitialCaps);
    }

    GRefPtr<GstMiniObject> object;
    {
        DataMutexLocker queue { stream->track->queueDataMutex() };
        if (!queue->isEmpty()) {
            object = queue->pop();
            streamingMembers->hasPoppedFirstObject = true;
            GST_TRACE_OBJECT(pad, "Queue not empty, popped %" GST_PTR_FORMAT, object.get());
        } else {
            queue->notifyWhenNotEmpty([&object, stream](GRefPtr<GstMiniObject>&& receivedObject) {
                ASSERT(isMainThread());
                DataMutexLocker streamingMembers { stream->streamingMembersDataMutex };
                ASSERT(!streamingMembers->isFlushing);

                object = WTFMove(receivedObject);
                streamingMembers->hasPoppedFirstObject = true;
                streamingMembers->queueChangedOrFlushedCondition.notifyAll();
            });
            GST_TRACE_OBJECT(pad, "Waiting for objects to be pushed to the track queue.");
        }
    }

    // Wait to receive an object from the queue (if we didn't get one already) or flush.
    streamingMembers->queueChangedOrFlushedCondition.wait(streamingMembers.mutex(), [&]() {
        return streamingMembers->isFlushing || object;
    });
    {
        // Ensure that notifyWhenNotEmpty()'s callback (if any) is cleared after this point.
        DataMutexLocker queue { stream->track->queueDataMutex() };
        queue->resetNotEmptyHandler();
    }
    if (streamingMembers->isFlushing) {
        gst_pad_pause_task(pad);
        return;
    }

    // We wait to get a sample before emitting the first segment. This way, if we get a seek before any
    // enqueue, we're sending only one segment. This also ensures that when such a seek is made, where we also
    // omit the flush (see webKitMediaSrcFlush) we actually emit the updated, correct segment.
    if (streamingMembers->doesNeedSegmentEvent) {
        GST_DEBUG_OBJECT(pad, "Need new SEGMENT event, pushing it: %" GST_SEGMENT_FORMAT, &streamingMembers->segment);
        bool result = gst_pad_push_event(pad, gst_event_new_segment(&streamingMembers->segment));
        GST_DEBUG_OBJECT(pad, "SEGMENT event pushed, result = %s.", boolForPrinting(result));
        ASSERT(result);
        streamingMembers->doesNeedSegmentEvent = false;
    }

    if (GST_IS_SAMPLE(object.get())) {
        GRefPtr<GstSample> sample = adoptGRef(GST_SAMPLE(object.leakRef()));

        if (!gst_caps_is_equal(gst_sample_get_caps(sample.get()), streamingMembers->previousCaps.get())) {
            // This sample needs new caps (typically because of a quality change).
            GST_DEBUG_OBJECT(pad, "Pushing new CAPS event: %" GST_PTR_FORMAT, gst_sample_get_caps(sample.get()));
            bool result = gst_pad_push_event(stream->pad.get(), gst_event_new_caps(gst_sample_get_caps(sample.get())));
            GST_DEBUG_OBJECT(pad, "CAPS event pushed, result = %s.", boolForPrinting(result));
            ASSERT(result);
            streamingMembers->previousCaps = gst_sample_get_caps(sample.get());
        }

        GRefPtr<GstBuffer> buffer = gst_sample_get_buffer(sample.get());
        sample.clear();

        bool pushingFirstBuffer = !streamingMembers->hasPushedFirstBuffer;
        if (pushingFirstBuffer) {
            GST_DEBUG_OBJECT(pad, "Sending first buffer on this pad.");
            GUniquePtr<char> fileName { g_strdup_printf("playback-pipeline-before-playback-%s", stream->track->trackId().string().utf8().data()) };
            GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS(GST_BIN(findPipeline(GRefPtr<GstElement>(GST_ELEMENT(stream->source))).get()),
                GST_DEBUG_GRAPH_SHOW_ALL, fileName.get());
            streamingMembers->hasPushedFirstBuffer = true;
        }

        // Push the buffer without the streamingMembers lock so that flushes can happen while it travels downstream.
        streamingMembers.unlockEarly();

        ASSERT(GST_BUFFER_PTS_IS_VALID(buffer.get()));
        GST_TRACE_OBJECT(pad, "Pushing buffer downstream: %" GST_PTR_FORMAT, buffer.get());
        GstFlowReturn result = gst_pad_push(pad, buffer.leakRef());
        if (result != GST_FLOW_OK && result != GST_FLOW_FLUSHING) {
            GST_ERROR_OBJECT(pad, "Pushing buffer returned %s", gst_flow_get_name(result));
            GUniquePtr<char> fileName { g_strdup_printf("playback-pipeline-pushing-buffer-failed-%s", stream->track->trackId().string().utf8().data()) };
            GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS(GST_BIN(findPipeline(GRefPtr<GstElement>(GST_ELEMENT(stream->source))).get()),
                GST_DEBUG_GRAPH_SHOW_ALL, fileName.get());
            gst_pad_pause_task(pad);
        }
    } else if (GST_IS_EVENT(object.get())) {
        // EOS events and other enqueued events are also sent unlocked so they can react to flushes if necessary.
        GRefPtr<GstEvent> event = GRefPtr<GstEvent>(GST_EVENT(object.leakRef()));

        streamingMembers.unlockEarly();
        GST_DEBUG_OBJECT(pad, "Pushing event downstream: %" GST_PTR_FORMAT, event.get());
        bool eventHandled = gst_pad_push_event(pad, GRefPtr<GstEvent>(event).leakRef());
        if (!eventHandled)
            GST_DEBUG_OBJECT(pad, "Pushed event was not handled: %" GST_PTR_FORMAT, event.get());
    } else
        ASSERT_NOT_REACHED();
}

static void webKitMediaSrcStreamFlush(Stream* stream, bool isSeekingFlush)
{
    ASSERT(isMainThread());
    bool skipFlush = false;
    GST_DEBUG_OBJECT(stream->source, "Flush requested for stream '%s'. isSeekingFlush = %s",
        stream->track->trackId().string().utf8().data(), boolForPrinting(isSeekingFlush));

    {
        DataMutexLocker streamingMembers { stream->streamingMembersDataMutex };

        if (!streamingMembers->hasPoppedFirstObject) {
            GST_DEBUG_OBJECT(stream->source, "Flush request for stream '%s' occurred before hasPoppedFirstObject, just clearing the queue and readjusting the segment.", stream->track->trackId().string().utf8().data());
            DataMutexLocker queue { stream->track->queueDataMutex() };
            // We use clear() instead of flush() because the WebKitMediaSrc streaming thread could be waiting
            // for the queue. flush() would cancel the notEmptyCallback therefore leaving the streaming thread
            // stuck waiting forever.
            queue->clear();
            skipFlush = true;
        }
    }

    if (!skipFlush) {
        // Signal the loop() function to stop waiting for any condition variable, pause the task and return,
        // which will keeping the streaming thread idle.
        GST_DEBUG_OBJECT(stream->pad.get(), "Taking the StreamingMembers mutex and setting isFlushing = true.");
        {
            DataMutexLocker streamingMembers { stream->streamingMembersDataMutex };
            DataMutexLocker queue { stream->track->queueDataMutex() };

            streamingMembers->isFlushing = true;
            queue->flush(); // Clear the queue and cancel any waiting callback.

            streamingMembers->queueChangedOrFlushedCondition.notifyAll();
            streamingMembers->padLinkedOrFlushedCondition.notifyAll();
        }

        // Flush downstream. This will stop processing in downstream elements and if the streaming thread was in a
        // downstream chain() function, it will quickly return to the loop() function, which thanks to the
        // previous section will also quickly end.
        GST_DEBUG_OBJECT(stream->pad.get(), "Sending FLUSH_START downstream.");
        gst_pad_push_event(stream->pad.get(), gst_event_new_flush_start());
        GST_DEBUG_OBJECT(stream->pad.get(), "FLUSH_START sent.");
    }

    // Adjust segment. This is different for seeks and non-seeking flushes.
    if (isSeekingFlush) {
        // In the case of seeking flush we are resetting the timeline (see the flush stop later).
        // The resulting segment is brand new, but with a different start time.
        WebKitMediaSrcPrivate* priv = stream->source->priv;
        DataMutexLocker streamingMembers { stream->streamingMembersDataMutex };
        streamingMembers->segment.base = 0;
        streamingMembers->segment.rate = priv->rate;
        streamingMembers->segment.start = streamingMembers->segment.time = priv->startTime;
    } else {
        // In the case of non-seeking flushes we don't reset the timeline, so instead we need to increase the `base` field
        // by however running time we're starting after the flush.

        GstClockTime pipelineStreamTime;
        gst_element_query_position(findPipeline(GRefPtr<GstElement>(GST_ELEMENT(stream->source))).get(), GST_FORMAT_TIME,
            reinterpret_cast<gint64*>(&pipelineStreamTime));
        // GST_CLOCK_TIME_NONE is returned when the pipeline is not yet pre-rolled (e.g. just after a seek). In this case
        // we don't need to adjust the segment though, as running time has not advanced.
        if (GST_CLOCK_TIME_IS_VALID(pipelineStreamTime)) {
            DataMutexLocker streamingMembers { stream->streamingMembersDataMutex };
            // We need to increase the base by the running time accumulated during the previous segment.

            GstClockTime pipelineRunningTime = gst_segment_to_running_time(&streamingMembers->segment, GST_FORMAT_TIME, pipelineStreamTime);
            assert(GST_CLOCK_TIME_IS_VALID(pipelineRunningTime));
            GST_DEBUG_OBJECT(stream->source, "Resetting segment to current pipeline running time (%" GST_TIME_FORMAT") and stream time (%" GST_TIME_FORMAT ")",
                GST_TIME_ARGS(pipelineRunningTime), GST_TIME_ARGS(pipelineStreamTime));
            streamingMembers->segment.base = pipelineRunningTime;

            streamingMembers->segment.start = streamingMembers->segment.time = static_cast<GstClockTime>(pipelineStreamTime);
        }
    }

    if (!skipFlush) {
        // By taking the stream lock we are waiting for the streaming thread task to stop if it hadn't yet.
        GST_DEBUG_OBJECT(stream->pad.get(), "Taking the STREAM_LOCK.");
        auto streamLock = GstPadStreamLocker(stream->pad.get());
        {
            GST_DEBUG_OBJECT(stream->pad.get(), "Taking the StreamingMembers mutex again.");
            DataMutexLocker streamingMembers { stream->streamingMembersDataMutex };
            GST_DEBUG_OBJECT(stream->pad.get(), "StreamingMembers mutex taken, using it to set isFlushing = false.");
            streamingMembers->isFlushing = false;
            streamingMembers->doesNeedSegmentEvent = true;
        }

        GST_DEBUG_OBJECT(stream->pad.get(), "Sending FLUSH_STOP downstream (resetTime = %s).", boolForPrinting(isSeekingFlush));
        // Since FLUSH_STOP is a synchronized event, we send it while we still hold the stream lock of the pad.
        gst_pad_push_event(stream->pad.get(), gst_event_new_flush_stop(isSeekingFlush));
        GST_DEBUG_OBJECT(stream->pad.get(), "FLUSH_STOP sent.");

        GST_DEBUG_OBJECT(stream->pad.get(), "Starting webKitMediaSrcLoop task and releasing the STREAM_LOCK.");
        gst_pad_start_task(stream->pad.get(), webKitMediaSrcLoop, stream->pad.get(), nullptr);
    }

    GST_DEBUG_OBJECT(stream->source, "Flush request for stream '%s' (isSeekingFlush = %s) satisfied.",
        stream->track->trackId().string().utf8().data(), boolForPrinting(isSeekingFlush));
}

void webKitMediaSrcFlush(WebKitMediaSrc* source, const AtomString& streamName)
{
    ASSERT(isMainThread());
    GST_DEBUG_OBJECT(source, "Received non-seek flush request for stream '%s'.", streamName.string().utf8().data());
    Stream* stream = source->priv->streamByName(streamName);

    webKitMediaSrcStreamFlush(stream, false);
}

static void webKitMediaSrcSeek(WebKitMediaSrc* source, uint64_t startTime, double rate)
{
    ASSERT(isMainThread());
    source->priv->startTime = startTime;
    source->priv->rate = rate;
    GST_DEBUG_OBJECT(source, "Seek requested to time %" GST_TIME_FORMAT " with rate %f.", GST_TIME_ARGS(startTime), rate);

    for (const RefPtr<Stream>& stream : source->priv->streams.values())
        webKitMediaSrcStreamFlush(stream.get(), true);
}

static int countStreamsOfType(WebKitMediaSrc* source, WebCore::TrackPrivateBaseGStreamer::TrackType type)
{
    // Barring pipeline dumps someone may add during debugging, WebKit will only read these properties (n-video etc.) from the main thread.
    return std::count_if(source->priv->streams.begin(), source->priv->streams.end(), [type](auto item) {
        return item.value->track->type() == type;
    });
}

static void webKitMediaSrcGetProperty(GObject* object, unsigned propId, GValue* value, GParamSpec* pspec)
{
    WebKitMediaSrc* source = WEBKIT_MEDIA_SRC(object);

    switch (propId) {
    case PROP_N_AUDIO:
        g_value_set_int(value, countStreamsOfType(source, WebCore::TrackPrivateBaseGStreamer::TrackType::Audio));
        break;
    case PROP_N_VIDEO:
        g_value_set_int(value, countStreamsOfType(source, WebCore::TrackPrivateBaseGStreamer::TrackType::Video));
        break;
    case PROP_N_TEXT:
        g_value_set_int(value, countStreamsOfType(source, WebCore::TrackPrivateBaseGStreamer::TrackType::Text));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propId, pspec);
    }
}

static GstStateChangeReturn webKitMediaSrcChangeState(GstElement* element, GstStateChange transition)
{
    WebKitMediaSrc* source = WEBKIT_MEDIA_SRC(element);
    switch (transition) {
    case GST_STATE_CHANGE_PAUSED_TO_READY:
        GST_DEBUG_OBJECT(source, "Downgrading to READY state, tearing down all streams...");
        while (!source->priv->streams.isEmpty())
            webKitMediaSrcTearDownStream(source, source->priv->streams.begin()->key);
        break;
    case GST_STATE_CHANGE_READY_TO_PAUSED:
        if (source->priv->isStarted()) {
            GST_FIXME_OBJECT(source, "Resuming state from READY -> PAUSED after a downgrade is not implemented. Expect failure.");
        }
        break;
    default:
        break;
    }
    return GST_ELEMENT_CLASS(webkit_media_src_parent_class)->change_state(element, transition);
}

static gboolean webKitMediaSrcSendEvent(GstElement* element, GstEvent* event)
{
    switch (GST_EVENT_TYPE(event)) {
    case GST_EVENT_SEEK: {
        double rate;
        GstFormat format;
        GstSeekType startType;
        int64_t start;
        gst_event_parse_seek(event, &rate, &format, nullptr, &startType, &start, nullptr, nullptr);
        if (format != GST_FORMAT_TIME || startType != GST_SEEK_TYPE_SET) {
            GST_ERROR_OBJECT(element, "Rejecting unsupported seek event: %" GST_PTR_FORMAT, event);
            return false;
        }
        GST_DEBUG_OBJECT(element, "Handling seek event: %" GST_PTR_FORMAT, event);
        webKitMediaSrcSeek(WEBKIT_MEDIA_SRC(element), start, rate);
        return true;
    }
    default:
        return GST_ELEMENT_CLASS(webkit_media_src_parent_class)->send_event(element, event);
    }
}

// URI handler interface. It's only purpose is for the element to be instantiated by playbin on "mediasourceblob:"
// URIs. The actual URI does not matter.
static GstURIType webKitMediaSrcUriGetType(GType)
{
    return GST_URI_SRC;
}

static const gchar* const* webKitMediaSrcGetProtocols(GType)
{
    static const char* protocols[] = {"mediasourceblob", nullptr };
    return protocols;
}

static gchar* webKitMediaSrcGetUri(GstURIHandler* handler)
{
    WebKitMediaSrc* source = WEBKIT_MEDIA_SRC(handler);

    auto locker = GstObjectLocker(source);
    return g_strdup(source->priv->uri.get());
}

static gboolean webKitMediaSrcSetUri(GstURIHandler* handler, const gchar* uri, GError**)
{
    WebKitMediaSrc* source = WEBKIT_MEDIA_SRC(handler);

    if (GST_STATE(source) >= GST_STATE_PAUSED) {
        GST_ERROR_OBJECT(source, "URI can only be set in states < PAUSED");
        return false;
    }

    auto locker = GstObjectLocker(source);
    source->priv->uri = GUniquePtr<char>(g_strdup(uri));
    return TRUE;
}

static void webKitMediaSrcUriHandlerInit(void* gIface, void*)
{
    GstURIHandlerInterface* iface = (GstURIHandlerInterface *) gIface;

    iface->get_type = webKitMediaSrcUriGetType;
    iface->get_protocols = webKitMediaSrcGetProtocols;
    iface->get_uri = webKitMediaSrcGetUri;
    iface->set_uri = webKitMediaSrcSetUri;
}

namespace WTF {
template <> GRefPtr<WebKitMediaSrc> adoptGRef(WebKitMediaSrc* ptr)
{
    ASSERT(!ptr || !g_object_is_floating(G_OBJECT(ptr)));
    return GRefPtr<WebKitMediaSrc>(ptr, GRefPtrAdopt);
}

template <> WebKitMediaSrc* refGPtr<WebKitMediaSrc>(WebKitMediaSrc* ptr)
{
    if (ptr)
        gst_object_ref_sink(GST_OBJECT(ptr));

    return ptr;
}

template <> void derefGPtr<WebKitMediaSrc>(WebKitMediaSrc* ptr)
{
    if (ptr)
        gst_object_unref(ptr);
}
} // namespace WTF

#endif // USE(GSTREAMER)
