/* GStreamer
 *
 * SPDX-License-Identifier: LGPL-2.1
 *
 * Copyright (C) 2013 Google Inc. All rights reserved.
 * Copyright (C) 2013 Orange
 * Copyright (C) 2013-2017 Apple Inc. All rights reserved.
 * Copyright (C) 2014 Sebastian Dröge <sebastian@centricular.com>
 * Copyright (C) 2015, 2016 Igalia, S.L
 * Copyright (C) 2015, 2016 Metrological Group B.V.
 * Copyright (C) 2022, 2023 Collabora Ltd.
 *
 * 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 along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

/**
 * SECTION:gstmediasource
 * @title: GstMediaSource
 * @short_description: Media Source
 * @symbols:
 * - GstMediaSource
 *
 * #GstMediaSource is the entry point into the W3C Media Source API. It offers
 * functionality similar to #GstAppSrc for client-side web or JavaScript
 * applications decoupling the source of media from its processing and playback.
 *
 * To interact with a Media Source, connect it to a #GstMseSrc that is in some
 * #GstPipeline using gst_media_source_attach(). Then create at least one
 * #GstSourceBuffer using gst_media_source_add_source_buffer(). Finally, feed
 * some media data to the Source Buffer(s) using
 * gst_source_buffer_append_buffer() and play the pipeline.
 *
 * Since: 1.24
 */

/**
 * GstMediaSource:
 * Since: 1.24
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <gst/mse/mse-enumtypes.h>
#include "gstmediasource.h"
#include "gstmediasource-private.h"

#include "gstmselogging-private.h"
#include "gstmsemediatype-private.h"
#include "gstsourcebuffer-private.h"
#include "gstsourcebufferlist-private.h"

#include "gstmsesrc.h"
#include "gstmsesrc-private.h"

G_DEFINE_TYPE (GstMediaSource, gst_media_source, GST_TYPE_OBJECT);
G_DEFINE_QUARK (gst_media_source_error_quark, gst_media_source_error);

enum
{
  PROP_0,

  PROP_SOURCE_BUFFERS,
  PROP_ACTIVE_SOURCE_BUFFERS,
  PROP_READY_STATE,
  PROP_POSITION,
  PROP_DURATION,

  N_PROPS,
};

typedef enum
{
  ON_SOURCE_OPEN,
  ON_SOURCE_ENDED,
  ON_SOURCE_CLOSE,

  N_SIGNALS,
} MediaSourceEvent;

typedef struct
{
  GstDataQueueItem item;
  MediaSourceEvent event;
} MediaSourceEventItem;

static GParamSpec *properties[N_PROPS];
static guint signals[N_SIGNALS];

#define DEFAULT_READY_STATE GST_MEDIA_SOURCE_READY_STATE_CLOSED
#define DEFAULT_POSITION    GST_CLOCK_TIME_NONE
#define DEFAULT_DURATION    GST_CLOCK_TIME_NONE

static void rebuild_active_source_buffers_unlocked (GstMediaSource * self);
static void detach_unlocked (GstMediaSource * self);
static void set_duration_unlocked (GstMediaSource * self,
    GstClockTime duration);

/**
 * gst_media_source_is_type_supported:
 * @type: (transfer none): A MIME type value
 *
 * Determines whether the current Media Source configuration can process media
 * of the supplied @type.
 *
 * Returns: `TRUE` when supported, `FALSE` otherwise
 *
 * Since: 1.24
 */
gboolean
gst_media_source_is_type_supported (const gchar * type)
{
  gst_mse_init_logging ();
  g_return_val_if_fail (type != NULL, FALSE);

  if (g_strcmp0 (type, "") == 0) {
    return FALSE;
  }

  GstMediaSourceMediaType media_type = GST_MEDIA_SOURCE_MEDIA_TYPE_INIT;
  if (!gst_media_source_media_type_parse (&media_type, type)) {
    return FALSE;
  }

  gboolean supported = gst_media_source_media_type_is_supported (&media_type);

  gst_media_source_media_type_reset (&media_type);

  return supported;
}

/**
 * gst_media_source_new:
 *
 * Creates a new #GstMediaSource instance. The instance is in the
 * %GST_MEDIA_SOURCE_READY_STATE_CLOSED state and is not associated with any
 * media player.
 *
 * [Specification](https://www.w3.org/TR/media-source-2/#dom-mediasource-constructor)
 *
 * Returns: (transfer full): a new #GstMediaSource instance
 * Since: 1.24
 */
GstMediaSource *
gst_media_source_new (void)
{
  gst_mse_init_logging ();
  return g_object_ref_sink (g_object_new (GST_TYPE_MEDIA_SOURCE, NULL));
}

static inline void
empty_buffers (GstMediaSource * self)
{
  for (guint i = 0;; i++) {
    GstSourceBuffer *buf = gst_source_buffer_list_index (self->buffers, i);
    if (buf == NULL) {
      break;
    }
    gst_object_unparent (GST_OBJECT_CAST (buf));
    gst_object_unref (buf);
  }
  gst_source_buffer_list_remove_all (self->buffers);
}

static void
gst_media_source_dispose (GObject * object)
{
  GstMediaSource *self = (GstMediaSource *) object;

  detach_unlocked (self);

  g_clear_object (&self->active_buffers);

  if (self->buffers) {
    empty_buffers (self);
  }
  gst_clear_object (&self->buffers);

  gst_clear_object (&self->event_queue);

  G_OBJECT_CLASS (gst_media_source_parent_class)->dispose (object);
}

static void
gst_media_source_get_property (GObject * object, guint prop_id, GValue * value,
    GParamSpec * pspec)
{
  GstMediaSource *self = GST_MEDIA_SOURCE (object);

  switch (prop_id) {
    case PROP_SOURCE_BUFFERS:
      g_value_take_object (value, gst_media_source_get_source_buffers (self));
      break;
    case PROP_ACTIVE_SOURCE_BUFFERS:
      g_value_take_object (value,
          gst_media_source_get_active_source_buffers (self));
      break;
    case PROP_READY_STATE:
      g_value_set_enum (value, gst_media_source_get_ready_state (self));
      break;
    case PROP_POSITION:
      g_value_set_uint64 (value, gst_media_source_get_position (self));
      break;
    case PROP_DURATION:
      g_value_set_uint64 (value, gst_media_source_get_duration (self));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
  }
}

static void
gst_media_source_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstMediaSource *self = GST_MEDIA_SOURCE (object);

  switch (prop_id) {
    case PROP_DURATION:{
      GstClockTime duration = (GstClockTime) g_value_get_uint64 (value);
      gst_media_source_set_duration (self, duration, NULL);
      break;
    }
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
  }
}

static void
gst_media_source_class_init (GstMediaSourceClass * klass)
{
  GObjectClass *oclass = G_OBJECT_CLASS (klass);

  oclass->dispose = GST_DEBUG_FUNCPTR (gst_media_source_dispose);
  oclass->get_property = GST_DEBUG_FUNCPTR (gst_media_source_get_property);
  oclass->set_property = GST_DEBUG_FUNCPTR (gst_media_source_set_property);

  /**
   * GstMediaSource:source-buffers:
   *
   * A #GstSourceBufferList of every #GstSourceBuffer in this Media Source
   *
   * [Specification](https://www.w3.org/TR/media-source-2/#dom-mediasource-sourcebuffers)
   *
   * Since: 1.24
   */
  properties[PROP_SOURCE_BUFFERS] = g_param_spec_object ("source-buffers",
      "Source Buffers",
      "A SourceBufferList of all SourceBuffers in this Media Source",
      GST_TYPE_SOURCE_BUFFER_LIST, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  /**
   * GstMediaSource:active-source-buffers:
   *
   * A #GstSourceBufferList of every #GstSourceBuffer in this Media Source that
   * is considered active
   *
   * [Specification](https://www.w3.org/TR/media-source-2/#dom-mediasource-activesourcebuffers)
   *
   * Since: 1.24
   */
  properties[PROP_ACTIVE_SOURCE_BUFFERS] =
      g_param_spec_object ("active-source-buffers", "Active Source Buffers",
      "A SourceBufferList of all SourceBuffers that are active in this Media Source",
      GST_TYPE_SOURCE_BUFFER_LIST, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  /**
   * GstMediaSource:ready-state:
   *
   * The Ready State of the Media Source
   *
   * [Specification](https://www.w3.org/TR/media-source-2/#dom-mediasource-readystate)
   *
   * Since: 1.24
   */
  properties[PROP_READY_STATE] = g_param_spec_enum ("ready-state",
      "Ready State",
      "The Ready State of the Media Source",
      GST_TYPE_MEDIA_SOURCE_READY_STATE, DEFAULT_READY_STATE, G_PARAM_READABLE |
      G_PARAM_STATIC_STRINGS);

  /**
   * GstMediaSource:position:
   *
   * The position of the player consuming from the Media Source
   *
   * Since: 1.24
   */
  properties[PROP_POSITION] = g_param_spec_uint64 ("position",
      "Position",
      "The Position of the Media Source as a GstClockTime",
      GST_CLOCK_TIME_NONE, G_MAXUINT64, DEFAULT_DURATION, G_PARAM_READWRITE |
      G_PARAM_STATIC_STRINGS);

  /**
   * GstMediaSource:duration:
   *
   * The Duration of the Media Source as a #GstClockTime
   *
   * [Specification](https://www.w3.org/TR/media-source-2/#dom-mediasource-duration)
   *
   * Since: 1.24
   */
  properties[PROP_DURATION] = g_param_spec_uint64 ("duration",
      "Duration",
      "The Duration of the Media Source as a GstClockTime",
      GST_CLOCK_TIME_NONE, G_MAXUINT64, DEFAULT_DURATION, G_PARAM_READWRITE |
      G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (oclass, N_PROPS, properties);

  /**
   * GstMediaSource::on-source-open:
   * @self: The #GstMediaSource that has just opened
   *
   * Emitted when @self has been opened.
   *
   * [Specification](https://www.w3.org/TR/media-source-2/#dom-mediasource-onsourceopen)
   *
   * Since: 1.24
   */
  signals[ON_SOURCE_OPEN] = g_signal_new ("on-source-open",
      G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0);

  /**
   * GstMediaSource::on-source-ended:
   * @self: The #GstMediaSource that has just ended
   *
   * Emitted when @self has ended, normally through
   * gst_media_source_end_of_stream().
   *
   * [Specification](https://www.w3.org/TR/media-source-2/#dom-mediasource-onsourceended)
   *
   * Since: 1.24
   */
  signals[ON_SOURCE_ENDED] = g_signal_new ("on-source-ended",
      G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0);

  /**
   * GstMediaSource::on-source-closed:
   * @self: The #GstMediaSource that has just closed
   *
   * Emitted when @self has closed, normally when detached from a #GstMseSrc.
   *
   * [Specification](https://www.w3.org/TR/media-source-2/#dom-mediasource-onsourceclose)
   *
   * Since: 1.24
   */
  signals[ON_SOURCE_CLOSE] = g_signal_new ("on-source-close",
      G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0);

}

static inline void
reset_live_seekable_range (GstMediaSource * self)
{
  self->live_seekable_range.start = 0;
  self->live_seekable_range.end = 0;
}

static inline gboolean
is_updating (GstMediaSource * self)
{
  for (guint i = 0;; i++) {
    GstSourceBuffer *buf = gst_source_buffer_list_index (self->buffers, i);
    if (buf == NULL)
      break;
    gboolean updating = gst_source_buffer_get_updating (buf);
    gst_object_unref (buf);
    if (updating) {
      return TRUE;
    }
  }
  return FALSE;
}

static inline gboolean
is_attached (GstMediaSource * self)
{
  return GST_IS_MSE_SRC (self->element);
}

static inline void
network_error (GstMediaSource * self)
{
  if (is_attached (self)) {
    gst_mse_src_network_error (self->element);
  }
}

static inline void
decode_error (GstMediaSource * self)
{
  if (is_attached (self)) {
    gst_mse_src_decode_error (self->element);
  }
}

static inline void
update_duration (GstMediaSource * self)
{
  if (is_attached (self)) {
    gst_mse_src_set_duration (self->element, self->duration);
  }
}

static void
schedule_event (GstMediaSource * self, MediaSourceEvent event)
{
  MediaSourceEventItem item = {
    .item = {.destroy = g_free,.visible = TRUE,.size = 1,.object = NULL},
    .event = event,
  };

  gst_mse_event_queue_push (self->event_queue, g_memdup2 (&item,
          sizeof (MediaSourceEventItem)));
}

static void
dispatch_event (MediaSourceEventItem * item, GstMediaSource * self)
{
  g_signal_emit (self, signals[item->event], 0);
}

static void
gst_media_source_init (GstMediaSource * self)
{
  self->buffers = gst_source_buffer_list_new ();
  self->active_buffers = gst_source_buffer_list_new ();
  self->ready_state = DEFAULT_READY_STATE;
  self->duration = DEFAULT_DURATION;
  reset_live_seekable_range (self);
  self->element = NULL;
  self->event_queue =
      gst_mse_event_queue_new ((GstMseEventQueueCallback) dispatch_event, self);
}

/**
 * gst_media_source_attach:
 * @self: #GstMediaSource instance
 * @element: (transfer none): #GstMseSrc source Element
 *
 * Associates @self with @element.
 * Normally, the Element will be part of a #GstPipeline that plays back the data
 * submitted to the Media Source's Source Buffers.
 *
 * #GstMseSrc is a special source element that is designed to consume media from
 * a #GstMediaSource.
 *
 * [Specification](https://www.w3.org/TR/media-source-2/#dfn-attaching-to-a-media-element)
 *
 * Since: 1.24
 */
void
gst_media_source_attach (GstMediaSource * self, GstMseSrc * element)
{
  g_return_if_fail (GST_IS_MEDIA_SOURCE (self));
  g_return_if_fail (GST_IS_MSE_SRC (element));

  GST_OBJECT_LOCK (self);
  if (is_attached (self)) {
    detach_unlocked (self);
  }

  self->element = gst_object_ref (element);
  gst_mse_src_attach (element, self);

  self->ready_state = GST_MEDIA_SOURCE_READY_STATE_OPEN;
  GST_OBJECT_UNLOCK (self);

  schedule_event (self, ON_SOURCE_OPEN);
}

static void
detach_unlocked (GstMediaSource * self)
{
  self->ready_state = GST_MEDIA_SOURCE_READY_STATE_CLOSED;
  set_duration_unlocked (self, GST_CLOCK_TIME_NONE);

  gst_source_buffer_list_remove_all (self->active_buffers);
  empty_buffers (self);

  if (is_attached (self)) {
    gst_mse_src_detach (self->element);
    gst_clear_object (&self->element);
  }

  schedule_event (self, ON_SOURCE_CLOSE);
}

/**
 * gst_media_source_detach:
 * @self: #GstMediaSource instance
 *
 * Detaches @self from any #GstMseSrc element that it may be associated with.
 *
 * Since: 1.24
 */
void
gst_media_source_detach (GstMediaSource * self)
{
  g_return_if_fail (GST_IS_MEDIA_SOURCE (self));

  GST_OBJECT_LOCK (self);
  detach_unlocked (self);
  GST_OBJECT_UNLOCK (self);
}

/**
 * gst_media_source_get_source_element:
 * @self: #GstMediaSource instance
 *
 * Gets the #GstMseSrc currently attached to @self or `NULL` if there is none.
 *
 * Returns: (transfer full) (nullable): a #GstMseSrc instance or `NULL`
 */
GstMseSrc *
gst_media_source_get_source_element (GstMediaSource * self)
{
  g_return_val_if_fail (GST_IS_MEDIA_SOURCE (self), NULL);
  GST_OBJECT_LOCK (self);
  GstMseSrc *element = self->element == NULL ? NULL
      : gst_object_ref (self->element);
  GST_OBJECT_UNLOCK (self);
  return element;
}

void
gst_media_source_open (GstMediaSource * self)
{
  g_return_if_fail (GST_IS_MEDIA_SOURCE (self));
  if (self->ready_state != GST_MEDIA_SOURCE_READY_STATE_OPEN) {
    self->ready_state = GST_MEDIA_SOURCE_READY_STATE_OPEN;
    schedule_event (self, ON_SOURCE_OPEN);
  }
}

/**
 * gst_media_source_get_source_buffers:
 * @self: #GstMediaSource instance
 *
 * Gets a #GstSourceBufferList containing all the Source Buffers currently
 * associated with this Media Source. This object will reflect any future
 * changes to the parent Media Source as well.
 *
 * [Specification](https://www.w3.org/TR/media-source-2/#dom-mediasource-sourcebuffers)
 *
 * Returns: (transfer full): a #GstSourceBufferList instance
 * Since: 1.24
 */
GstSourceBufferList *
gst_media_source_get_source_buffers (GstMediaSource * self)
{
  g_return_val_if_fail (GST_IS_MEDIA_SOURCE (self), NULL);
  return g_object_ref (self->buffers);
}

/**
 * gst_media_source_get_active_source_buffers:
 * @self: #GstMediaSource instance
 *
 * Gets a #GstSourceBufferList containing all the Source Buffers currently
 * associated with this Media Source that are considered "active."
 * For a Source Buffer to be considered active, either its video track is
 * selected, its audio track is enabled, or its text track is visible or hidden.
 * This object will reflect any future changes to the parent Media Source as
 * well.
 *
 * [Specification](https://www.w3.org/TR/media-source-2/#dom-mediasource-activesourcebuffers)
 *
 * Returns: (transfer full): a new #GstSourceBufferList instance
 * Since: 1.24
 */
GstSourceBufferList *
gst_media_source_get_active_source_buffers (GstMediaSource * self)
{
  g_return_val_if_fail (GST_IS_MEDIA_SOURCE (self), NULL);
  return g_object_ref (self->active_buffers);
}

/**
 * gst_media_source_get_ready_state:
 * @self: #GstMediaSource instance
 *
 * Gets the current Ready State of the Media Source.
 *
 * [Specification](https://www.w3.org/TR/media-source-2/#dom-mediasource-readystate)
 *
 * Returns: the current #GstMediaSourceReadyState value
 * Since: 1.24
 */
GstMediaSourceReadyState
gst_media_source_get_ready_state (GstMediaSource * self)
{
  g_return_val_if_fail (GST_IS_MEDIA_SOURCE (self), DEFAULT_READY_STATE);

  GST_OBJECT_LOCK (self);
  GstMediaSourceReadyState ready_state = self->ready_state;
  GST_OBJECT_UNLOCK (self);

  return ready_state;
}

/**
 * gst_media_source_get_position:
 * @self: #GstMediaSource instance
 *
 * Gets the current playback position of the Media Source.
 *
 * Returns: the current playback position as a #GstClockTime
 * Since: 1.24
 */
GstClockTime
gst_media_source_get_position (GstMediaSource * self)
{
  g_return_val_if_fail (GST_IS_MEDIA_SOURCE (self), DEFAULT_POSITION);

  GST_OBJECT_LOCK (self);
  GstClockTime position =
      is_attached (self) ? gst_mse_src_get_position (self->element) :
      DEFAULT_POSITION;
  GST_OBJECT_UNLOCK (self);

  return position;
}

/**
 * gst_media_source_get_duration:
 * @self: #GstMediaSource instance
 *
 * Gets the current duration of @self.
 *
 * [Specification](https://www.w3.org/TR/media-source-2/#dom-mediasource-duration)
 *
 * Returns: the current duration as a #GstClockTime
 * Since: 1.24
 */
GstClockTime
gst_media_source_get_duration (GstMediaSource * self)
{
  g_return_val_if_fail (GST_IS_MEDIA_SOURCE (self), DEFAULT_DURATION);

  GST_OBJECT_LOCK (self);
  GstClockTime duration =
      self->ready_state ==
      GST_MEDIA_SOURCE_READY_STATE_CLOSED ? GST_CLOCK_TIME_NONE :
      self->duration;
  GST_OBJECT_UNLOCK (self);

  return duration;
}

static void
set_duration_unlocked (GstMediaSource * self, GstClockTime duration)
{
  self->duration = duration;
  update_duration (self);
}

/**
 * gst_media_source_set_duration:
 * @self: #GstMediaSource instance
 * @duration: The new duration to apply to @self.
 * @error: (out) (optional) (nullable) (transfer full): the resulting error or `NULL`
 *
 * Sets the duration of @self.
 *
 * [Specification](https://www.w3.org/TR/media-source-2/#dom-mediasource-duration)
 *
 * Returns: `TRUE` on success, `FALSE` otherwise
 * Since: 1.24
 */
gboolean
gst_media_source_set_duration (GstMediaSource * self, GstClockTime duration,
    GError ** error)
{
  g_return_val_if_fail (GST_IS_MEDIA_SOURCE (self), FALSE);

  GST_OBJECT_LOCK (self);
  self->duration = duration;
  update_duration (self);
  GST_OBJECT_UNLOCK (self);

  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_DURATION]);
  return TRUE;
}

static void
on_received_init_segment (G_GNUC_UNUSED GstSourceBuffer * source_buffer,
    gpointer user_data)
{
  GstMediaSource *self = GST_MEDIA_SOURCE (user_data);

  GST_OBJECT_LOCK (self);

  if (!is_attached (self)) {
    GST_DEBUG_OBJECT (self, "received init segment while detached, ignoring");
    GST_OBJECT_UNLOCK (self);
    return;
  }

  GPtrArray *all_tracks = g_ptr_array_new ();

  for (guint i = 0;; i++) {
    GstSourceBuffer *buf = gst_source_buffer_list_index (self->buffers, i);
    if (buf == NULL) {
      break;
    }
    GPtrArray *tracks = gst_source_buffer_get_all_tracks (buf);
    g_ptr_array_extend (all_tracks, tracks, NULL, NULL);
    g_ptr_array_unref (tracks);
    gst_object_unref (buf);
  }

  GST_OBJECT_UNLOCK (self);

  gst_mse_src_emit_streams (self->element,
      (GstMediaSourceTrack **) all_tracks->pdata, all_tracks->len);

  g_ptr_array_unref (all_tracks);
}

static void
on_duration_changed (G_GNUC_UNUSED GstSourceBuffer * source_buffer,
    gpointer user_data)
{
  GstMediaSource *self = GST_MEDIA_SOURCE (user_data);
  GstClockTime current = self->duration;
  GstClockTime max = 0;
  for (guint i = 0;; i++) {
    GstSourceBuffer *buf = gst_source_buffer_list_index (self->buffers, i);
    if (buf == NULL) {
      break;
    }
    GstClockTime duration = gst_source_buffer_get_duration (buf);
    if (GST_CLOCK_TIME_IS_VALID (duration)) {
      max = MAX (max, duration);
    }
    gst_object_unref (buf);
  }
  if (current == max) {
    return;
  }
  GST_DEBUG_OBJECT (self, "updating %" GST_TIMEP_FORMAT "=>%" GST_TIMEP_FORMAT,
      &current, &max);
  gst_media_source_set_duration (self, max, NULL);
}

static GHashTable *
source_buffer_list_as_set (GstSourceBufferList * list)
{
  GHashTable *buffers = g_hash_table_new_full (g_direct_hash, g_direct_equal,
      gst_object_unref, NULL);
  for (guint i = 0;; i++) {
    GstSourceBuffer *buf = gst_source_buffer_list_index (list, i);
    if (buf == NULL) {
      break;
    }
    g_hash_table_add (buffers, buf);
  }
  return buffers;
}

static void
rebuild_active_source_buffers_unlocked (GstMediaSource * self)
{
  GST_DEBUG_OBJECT (self, "rebuilding active source buffers");
  GHashTable *previously_active =
      source_buffer_list_as_set (self->active_buffers);

  gst_source_buffer_list_notify_freeze (self->active_buffers);
  gst_source_buffer_list_remove_all (self->active_buffers);

  gboolean added = FALSE;
  gboolean removed = FALSE;

  for (guint i = 0;; i++) {
    GstSourceBuffer *buf = gst_source_buffer_list_index (self->buffers, i);
    if (buf == NULL) {
      break;
    }
    if (gst_source_buffer_get_active (buf)) {
      gst_source_buffer_list_append (self->active_buffers, buf);
      added |= !g_hash_table_contains (previously_active, buf);
    } else {
      gst_source_buffer_list_append (self->active_buffers, buf);
      removed |= g_hash_table_contains (previously_active, buf);
    }
    gst_object_unref (buf);
  }
  g_hash_table_unref (previously_active);

  gst_source_buffer_list_notify_cancel (self->active_buffers);
  gst_source_buffer_list_notify_thaw (self->active_buffers);

  if (added) {
    GST_DEBUG_OBJECT (self, "notifying active source buffer added");
    gst_source_buffer_list_notify_added (self->active_buffers);
  }
  if (removed) {
    GST_DEBUG_OBJECT (self, "notifying active source buffer removed");
    gst_source_buffer_list_notify_removed (self->active_buffers);
  }
}

static void
on_active_state_changed (GstSourceBuffer * source_buffer, gpointer user_data)
{
  GstMediaSource *self = GST_MEDIA_SOURCE (user_data);

  GST_OBJECT_LOCK (self);
  rebuild_active_source_buffers_unlocked (self);
  GST_OBJECT_UNLOCK (self);
}

/**
 * gst_media_source_add_source_buffer:
 * @self: #GstMediaSource instance
 * @type: (transfer none): A MIME type describing the format of the incoming media
 * @error: (out) (optional) (nullable) (transfer full): the resulting error or `NULL`
 *
 * Add a #GstSourceBuffer to this #GstMediaSource of the specified media type.
 * The Media Source must be in the #GstMediaSourceReadyState %GST_MEDIA_SOURCE_READY_STATE_OPEN.
 *
 * [Specification](https://www.w3.org/TR/media-source-2/#dom-mediasource-addsourcebuffer)
 *
 * Returns: (transfer full): a new #GstSourceBuffer instance on success, otherwise `NULL`
 * Since: 1.24
 */
GstSourceBuffer *
gst_media_source_add_source_buffer (GstMediaSource * self, const gchar * type,
    GError ** error)
{
  g_return_val_if_fail (GST_IS_MEDIA_SOURCE (self), NULL);
  g_return_val_if_fail (type != NULL, NULL);

  if (g_strcmp0 (type, "") == 0) {
    g_set_error (error,
        GST_MEDIA_SOURCE_ERROR, GST_MEDIA_SOURCE_ERROR_TYPE,
        "supplied content type is empty");
    return NULL;
  }

  if (!gst_media_source_is_type_supported (type)) {
    g_set_error (error,
        GST_MEDIA_SOURCE_ERROR, GST_MEDIA_SOURCE_ERROR_NOT_SUPPORTED,
        "unsupported content type");
    return NULL;
  }

  GST_OBJECT_LOCK (self);

  if (self->ready_state != GST_MEDIA_SOURCE_READY_STATE_OPEN) {
    g_set_error (error,
        GST_MEDIA_SOURCE_ERROR, GST_MEDIA_SOURCE_ERROR_INVALID_STATE,
        "media source is not open");
    goto error;
  }

  GstSourceBufferCallbacks callbacks = {
    .duration_changed = on_duration_changed,
    .received_init_segment = on_received_init_segment,
    .active_state_changed = on_active_state_changed,
  };

  GError *source_buffer_error = NULL;
  GstSourceBuffer *buf = gst_source_buffer_new_with_callbacks (type,
      GST_OBJECT (self), &callbacks, self, &source_buffer_error);
  if (source_buffer_error) {
    g_propagate_prefixed_error (error, source_buffer_error,
        "failed to create source buffer");
    gst_clear_object (&buf);
    goto error;
  }

  gst_source_buffer_list_append (self->buffers, buf);

  GST_OBJECT_UNLOCK (self);

  return buf;

error:
  GST_OBJECT_UNLOCK (self);
  return NULL;
}

/**
 * gst_media_source_remove_source_buffer:
 * @self: #GstMediaSource instance
 * @buffer: (transfer none): #GstSourceBuffer instance
 * @error: (out) (optional) (nullable) (transfer full): the resulting error or `NULL`
 *
 * Remove @buffer from @self.
 *
 * @buffer must have been created as a child of @self and @self must be in the
 * #GstMediaSourceReadyState %GST_MEDIA_SOURCE_READY_STATE_OPEN.
 *
 * [Specification](https://www.w3.org/TR/media-source-2/#dom-mediasource-removesourcebuffer)
 *
 * Returns: `TRUE` on success, `FALSE` otherwise
 * Since: 1.24
 */
gboolean
gst_media_source_remove_source_buffer (GstMediaSource * self,
    GstSourceBuffer * buffer, GError ** error)
{
  g_return_val_if_fail (GST_IS_MEDIA_SOURCE (self), FALSE);
  g_return_val_if_fail (GST_IS_SOURCE_BUFFER (buffer), FALSE);

  GST_OBJECT_LOCK (self);

  if (!gst_source_buffer_list_contains (self->buffers, buffer)) {
    g_set_error (error,
        GST_MEDIA_SOURCE_ERROR, GST_MEDIA_SOURCE_ERROR_NOT_FOUND,
        "the supplied source buffer was not found in this media source");
    goto error;
  }

  if (gst_source_buffer_get_updating (buffer))
    gst_source_buffer_teardown (buffer);

  gst_source_buffer_list_remove (self->active_buffers, buffer);

  gst_object_unparent (GST_OBJECT (buffer));
  gst_source_buffer_list_remove (self->buffers, buffer);

  GST_OBJECT_UNLOCK (self);

  return TRUE;

error:
  GST_OBJECT_UNLOCK (self);
  return FALSE;
}

static void
abort_all_source_buffers (GstMediaSource * self)
{
  for (guint i = 0;; i++) {
    GstSourceBuffer *buf = gst_source_buffer_list_index (self->buffers, i);
    if (buf == NULL) {
      return;
    }
    GST_LOG_OBJECT (self, "shutting down %" GST_PTR_FORMAT, buf);
    gst_source_buffer_abort (buf, NULL);
    gst_object_unref (buf);
  }
}

/**
 * gst_media_source_end_of_stream:
 * @self: #GstMediaSource instance
 * @eos_error: The error type, if any
 * @error: (out) (optional) (nullable) (transfer full): the resulting error or `NULL`
 *
 * Mark @self as reaching the end of stream, disallowing new data inputs.
 *
 * [Specification](https://www.w3.org/TR/media-source-2/#dom-mediasource-endofstream)
 *
 * Returns: `TRUE` on success, `FALSE` otherwise
 * Since: 1.24
 */
gboolean
gst_media_source_end_of_stream (GstMediaSource * self,
    GstMediaSourceEOSError eos_error, GError ** error)
{
  g_return_val_if_fail (GST_IS_MEDIA_SOURCE (self), FALSE);

  GST_OBJECT_LOCK (self);

  if (self->ready_state != GST_MEDIA_SOURCE_READY_STATE_OPEN) {
    g_set_error (error,
        GST_MEDIA_SOURCE_ERROR, GST_MEDIA_SOURCE_ERROR_INVALID_STATE,
        "media source is not open");
    goto error;
  }

  if (is_updating (self)) {
    g_set_error (error,
        GST_MEDIA_SOURCE_ERROR, GST_MEDIA_SOURCE_ERROR_INVALID_STATE,
        "some buffers are still updating");
    goto error;
  }

  self->ready_state = GST_MEDIA_SOURCE_READY_STATE_ENDED;
  schedule_event (self, ON_SOURCE_ENDED);

  GST_OBJECT_UNLOCK (self);

  switch (eos_error) {
    case GST_MEDIA_SOURCE_EOS_ERROR_NETWORK:
      network_error (self);
      break;
    case GST_MEDIA_SOURCE_EOS_ERROR_DECODE:
      decode_error (self);
      break;
    default:
      update_duration (self);
      abort_all_source_buffers (self);
      break;
  }

  return TRUE;

error:
  GST_OBJECT_UNLOCK (self);
  return FALSE;
}

/**
 * gst_media_source_set_live_seekable_range:
 * @self: #GstMediaSource instance
 * @start: The earliest point in the stream considered seekable
 * @end: The latest point in the stream considered seekable
 * @error: (out) (optional) (nullable) (transfer full): the resulting error or `NULL`
 *
 * Set the live seekable range for @self. This range informs the component
 * playing this Media Source what it can allow the user to seek through.
 *
 * If the ready state is not %GST_MEDIA_SOURCE_READY_STATE_OPEN, or the supplied
 * @start time is later than @end it will fail and set an error.
 *
 * [Specification](https://www.w3.org/TR/media-source-2/#dom-mediasource-setliveseekablerange)
 *
 * Returns: `TRUE` on success, `FALSE` otherwise
 * Since: 1.24
 */
gboolean
gst_media_source_set_live_seekable_range (GstMediaSource * self,
    GstClockTime start, GstClockTime end, GError ** error)
{
  g_return_val_if_fail (GST_IS_MEDIA_SOURCE (self), FALSE);

  GST_OBJECT_LOCK (self);

  if (self->ready_state != GST_MEDIA_SOURCE_READY_STATE_OPEN) {
    g_set_error (error,
        GST_MEDIA_SOURCE_ERROR, GST_MEDIA_SOURCE_ERROR_INVALID_STATE,
        "media source is not open");
    goto error;
  }

  if (start > end) {
    g_set_error (error,
        GST_MEDIA_SOURCE_ERROR, GST_MEDIA_SOURCE_ERROR_TYPE,
        "bad time range: start must be earlier than end");
    goto error;
  }

  self->live_seekable_range.start = start;
  self->live_seekable_range.end = end;

  GST_OBJECT_UNLOCK (self);

  return TRUE;

error:
  GST_OBJECT_UNLOCK (self);
  return FALSE;
}

/**
 * gst_media_source_clear_live_seekable_range:
 * @self: #GstMediaSource instance
 * @error: (out) (optional) (nullable) (transfer full): the resulting error or `NULL`
 *
 * Clear the live seekable range for @self. This will inform the component
 * playing this Media Source that there is no seekable time range.
 *
 * If the ready state is not %GST_MEDIA_SOURCE_READY_STATE_OPEN, it will fail
 * and set an error.
 *
 * [Specification](https://www.w3.org/TR/media-source-2/#dom-mediasource-clearliveseekablerange)
 *
 * Returns: `TRUE` on success, `FALSE` otherwise
 * Since: 1.24
 */
gboolean
gst_media_source_clear_live_seekable_range (GstMediaSource * self,
    GError ** error)
{
  g_return_val_if_fail (GST_IS_MEDIA_SOURCE (self), FALSE);

  GST_OBJECT_LOCK (self);

  if (self->ready_state != GST_MEDIA_SOURCE_READY_STATE_OPEN) {
    g_set_error (error,
        GST_MEDIA_SOURCE_ERROR, GST_MEDIA_SOURCE_ERROR_INVALID_STATE,
        "media source is not open");
    goto error;
  }

  reset_live_seekable_range (self);

  GST_OBJECT_UNLOCK (self);

  return TRUE;

error:
  GST_OBJECT_UNLOCK (self);
  return FALSE;
}

/**
 * gst_media_source_get_live_seekable_range:
 * @self: #GstMediaSource instance
 * @range: (out caller-allocates) (transfer none): time range
 *
 * Get the live seekable range of @self. Will fill in the supplied @range with
 * the current live seekable range.
 *
 * Since: 1.24
 */
void
gst_media_source_get_live_seekable_range (GstMediaSource * self,
    GstMediaSourceRange * range)
{
  g_return_if_fail (GST_IS_MEDIA_SOURCE (self));
  g_return_if_fail (range != NULL);

  GST_OBJECT_LOCK (self);
  range->start = self->live_seekable_range.start;
  range->end = self->live_seekable_range.end;
  GST_OBJECT_UNLOCK (self);
}

void
gst_media_source_seek (GstMediaSource * self, GstClockTime time)
{
  g_return_if_fail (GST_IS_MEDIA_SOURCE (self));

  GST_OBJECT_LOCK (self);
  for (guint i = 0;; i++) {
    GstSourceBuffer *buf = gst_source_buffer_list_index (self->buffers, i);
    if (buf == NULL) {
      goto done;
    }
    gst_source_buffer_seek (buf, time);
    gst_object_unref (buf);
  }
done:
  GST_OBJECT_UNLOCK (self);
}
