From 2c822735ba0fe92d317f9dbc5a4eb886d10274b2 Mon Sep 17 00:00:00 2001 From: Edward Hervey Date: Tue, 24 Jan 2023 17:03:21 +0100 Subject: [PATCH] hlsdemux2: Add HLS playlist loader Add a helper that asynchronously loads and refreshes the playlist for HLS streams. Part-of: --- .../hls/gsthlsdemux-playlist-loader.c | 678 ++++++++++++++++++ .../hls/gsthlsdemux-playlist-loader.h | 80 +++ .../adaptivedemux2/hls/gsthlsdemux-stream.c | 30 +- .../adaptivedemux2/hls/gsthlsdemux-stream.h | 4 + .../ext/adaptivedemux2/hls/meson.build | 1 + 5 files changed, 792 insertions(+), 1 deletion(-) create mode 100644 subprojects/gst-plugins-good/ext/adaptivedemux2/hls/gsthlsdemux-playlist-loader.c create mode 100644 subprojects/gst-plugins-good/ext/adaptivedemux2/hls/gsthlsdemux-playlist-loader.h diff --git a/subprojects/gst-plugins-good/ext/adaptivedemux2/hls/gsthlsdemux-playlist-loader.c b/subprojects/gst-plugins-good/ext/adaptivedemux2/hls/gsthlsdemux-playlist-loader.c new file mode 100644 index 0000000000..c9605a300d --- /dev/null +++ b/subprojects/gst-plugins-good/ext/adaptivedemux2/hls/gsthlsdemux-playlist-loader.c @@ -0,0 +1,678 @@ +/* GStreamer + * + * Copyright (C) 2014 Samsung Electronics. All rights reserved. + * Author: Thiago Santos + * + * Copyright (C) 2021-2022 Centricular Ltd + * Author: Edward Hervey + * Author: Jan Schmidt + * + * 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "gsthlsdemux.h" +#include "gsthlsdemux-playlist-loader.h" +#include "m3u8.h" + +GST_DEBUG_CATEGORY_EXTERN (gst_hls_demux2_debug); +#define GST_CAT_DEFAULT gst_hls_demux2_debug + +typedef enum _PlaylistLoaderState PlaylistLoaderState; + +enum _PlaylistLoaderState +{ + PLAYLIST_LOADER_STATE_STOPPED = 0, + PLAYLIST_LOADER_STATE_STARTING, + PLAYLIST_LOADER_STATE_LOADING, + PLAYLIST_LOADER_STATE_WAITING, +}; + +struct _GstHLSDemuxPlaylistLoaderPrivate +{ + GstAdaptiveDemux *demux; + gboolean llhls_enabled; + + GstHLSDemuxPlaylistLoaderSuccessCallback success_cb; + GstHLSDemuxPlaylistLoaderErrorCallback error_cb; + gpointer userdata; + + GstAdaptiveDemuxLoop *scheduler_task; + DownloadHelper *download_helper; + DownloadRequest *download_request; + + PlaylistLoaderState state; + guint pending_cb_id; + + gchar *base_uri; + gchar *target_playlist_uri; + + gchar *loading_playlist_uri; + + gboolean delta_merge_failed; + gchar *current_playlist_uri; + GstHLSMediaPlaylist *current_playlist; +}; + +#define gst_hls_demux_playlist_loader_parent_class parent_class +G_DEFINE_TYPE_WITH_PRIVATE (GstHLSDemuxPlaylistLoader, + gst_hls_demux_playlist_loader, GST_TYPE_OBJECT); + +static void gst_hls_demux_playlist_loader_finalize (GObject * object); +static gboolean gst_hls_demux_playlist_loader_update (GstHLSDemuxPlaylistLoader + * pl); +static void start_playlist_download (GstHLSDemuxPlaylistLoader * pl, + GstHLSDemuxPlaylistLoaderPrivate * priv); + +/* Takes ownership of the loop ref */ +GstHLSDemuxPlaylistLoader * +gst_hls_demux_playlist_loader_new (GstAdaptiveDemux * demux, + DownloadHelper * download_helper, gboolean llhls_enabled) +{ + GstHLSDemuxPlaylistLoader *pl = + g_object_new (GST_TYPE_HLS_DEMUX_PLAYLIST_LOADER, NULL); + GstHLSDemuxPlaylistLoaderPrivate *priv = pl->priv; + + priv->demux = demux; + priv->llhls_enabled = llhls_enabled; + priv->scheduler_task = gst_adaptive_demux_get_loop (demux); + priv->download_helper = download_helper; + + return pl; +} + +void +gst_hls_demux_playlist_loader_set_callbacks (GstHLSDemuxPlaylistLoader * pl, + GstHLSDemuxPlaylistLoaderSuccessCallback success_cb, + GstHLSDemuxPlaylistLoaderErrorCallback error_cb, gpointer userdata) +{ + GstHLSDemuxPlaylistLoaderPrivate *priv = pl->priv; + + priv->success_cb = success_cb; + priv->error_cb = error_cb; + priv->userdata = userdata; +} + +static void +gst_hls_demux_playlist_loader_class_init (GstHLSDemuxPlaylistLoaderClass * + klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + + gobject_class->finalize = gst_hls_demux_playlist_loader_finalize; +} + +static void +gst_hls_demux_playlist_loader_init (GstHLSDemuxPlaylistLoader * pl) +{ + pl->priv = gst_hls_demux_playlist_loader_get_instance_private (pl); +} + +static void +gst_hls_demux_playlist_loader_finalize (GObject * object) +{ + GstHLSDemuxPlaylistLoader *pl = GST_HLS_DEMUX_PLAYLIST_LOADER (object); + GstHLSDemuxPlaylistLoaderPrivate *priv = pl->priv; + + if (priv->pending_cb_id != 0) { + gst_adaptive_demux_loop_cancel_call (priv->scheduler_task, + priv->pending_cb_id); + priv->pending_cb_id = 0; + } + + if (priv->download_request) { + downloadhelper_cancel_request (priv->download_helper, + priv->download_request); + download_request_unref (priv->download_request); + priv->download_request = NULL; + } + + if (priv->scheduler_task) + gst_adaptive_demux_loop_unref (priv->scheduler_task); + + g_free (priv->base_uri); + g_free (priv->target_playlist_uri); + g_free (priv->loading_playlist_uri); + + if (priv->current_playlist) + gst_hls_media_playlist_unref (priv->current_playlist); + g_free (priv->current_playlist_uri); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +schedule_state_update (GstHLSDemuxPlaylistLoader * pl, + GstHLSDemuxPlaylistLoaderPrivate * priv) +{ + g_assert (priv->pending_cb_id == 0); + priv->pending_cb_id = + gst_adaptive_demux_loop_call (priv->scheduler_task, + (GSourceFunc) gst_hls_demux_playlist_loader_update, + gst_object_ref (pl), (GDestroyNotify) gst_object_unref); +} + +static void +schedule_next_playlist_load (GstHLSDemuxPlaylistLoader * pl, + GstHLSDemuxPlaylistLoaderPrivate * priv, GstClockTime next_load_interval) +{ + GST_LOG_OBJECT (pl, "Scheduling next playlist reload"); + g_assert (priv->pending_cb_id == 0); + priv->state = PLAYLIST_LOADER_STATE_WAITING; + priv->pending_cb_id = + gst_adaptive_demux_loop_call_delayed (priv->scheduler_task, + next_load_interval, + (GSourceFunc) gst_hls_demux_playlist_loader_update, + gst_object_ref (pl), (GDestroyNotify) gst_object_unref); +} + +void +gst_hls_demux_playlist_loader_start (GstHLSDemuxPlaylistLoader * pl) +{ + GstHLSDemuxPlaylistLoaderPrivate *priv = pl->priv; + + if (priv->state != PLAYLIST_LOADER_STATE_STOPPED) + return; /* Already active */ + + GST_DEBUG_OBJECT (pl, "Starting playlist loading"); + priv->state = PLAYLIST_LOADER_STATE_STARTING; + schedule_state_update (pl, priv); +} + +void +gst_hls_demux_playlist_loader_stop (GstHLSDemuxPlaylistLoader * pl) +{ + GstHLSDemuxPlaylistLoaderPrivate *priv = pl->priv; + + if (priv->state == PLAYLIST_LOADER_STATE_STOPPED) + return; /* Not runnning */ + + GST_DEBUG_OBJECT (pl, "Stopping playlist loading"); + + if (priv->pending_cb_id != 0) { + gst_adaptive_demux_loop_cancel_call (priv->scheduler_task, + priv->pending_cb_id); + priv->pending_cb_id = 0; + } + + if (priv->download_request) { + downloadhelper_cancel_request (priv->download_helper, + priv->download_request); + download_request_unref (priv->download_request); + priv->download_request = NULL; + } +} + +void +gst_hls_demux_playlist_loader_set_playlist_uri (GstHLSDemuxPlaylistLoader * pl, + const gchar * base_uri, const gchar * new_playlist_uri) +{ + GstHLSDemuxPlaylistLoaderPrivate *priv = pl->priv; + + gboolean playlist_uri_change = (priv->target_playlist_uri == NULL + || g_strcmp0 (new_playlist_uri, priv->target_playlist_uri) != 0); + + if (!playlist_uri_change) + return; + + GST_DEBUG_OBJECT (pl, "Setting target playlist URI to %s", new_playlist_uri); + + g_free (priv->base_uri); + g_free (priv->target_playlist_uri); + + priv->base_uri = g_strdup (base_uri); + priv->target_playlist_uri = g_strdup (new_playlist_uri); + priv->delta_merge_failed = FALSE; + + switch (priv->state) { + case PLAYLIST_LOADER_STATE_STOPPED: + return; /* Not runnning */ + case PLAYLIST_LOADER_STATE_STARTING: + case PLAYLIST_LOADER_STATE_LOADING: + /* If there's no pending state check, trigger one */ + if (priv->pending_cb_id == 0) { + GST_LOG_OBJECT (pl, "Scheduling state update from state %d", + priv->state); + schedule_state_update (pl, priv); + } + break; + case PLAYLIST_LOADER_STATE_WAITING: + return; /* Already waiting for the next time to load a live playlist */ + } +} + +/* Check that the current playlist matches the target URI, and return + * a ref to it if so */ +GstHLSMediaPlaylist * +gst_hls_demux_stream_get_playlist_for_uri (GstHLSDemuxPlaylistLoader * pl, + const gchar * target_playlist_uri) +{ + GstHLSDemuxPlaylistLoaderPrivate *priv = pl->priv; + + if (priv->current_playlist == NULL + || !g_str_equal (target_playlist_uri, priv->current_playlist_uri)) + return NULL; + + return gst_hls_media_playlist_ref (priv->current_playlist); +} + +enum PlaylistDownloadParamFlags +{ + PLAYLIST_DOWNLOAD_FLAG_SKIP_V1 = (1 << 0), + PLAYLIST_DOWNLOAD_FLAG_SKIP_V2 = (1 << 1), /* V2 also skips date-ranges */ + PLAYLIST_DOWNLOAD_FLAG_BLOCKING_REQUEST = (1 << 2), +}; + +struct PlaylistDownloadParams +{ + enum PlaylistDownloadParamFlags flags; + gint64 next_msn, next_part; +}; + +#define HLS_SKIP_QUERY_KEY "_HLS_skip" +#define HLS_MSN_QUERY_KEY "_HLS_msn" +#define HLS_PART_QUERY_KEY "_HLS_part" + +static gchar * +apply_directives_to_uri (GstHLSDemuxPlaylistLoader * pl, + const gchar * playlist_uri, struct PlaylistDownloadParams *dl_params) +{ + GstUri *uri = gst_uri_from_string (playlist_uri); + + if (dl_params->flags & PLAYLIST_DOWNLOAD_FLAG_SKIP_V1) { + GST_LOG_OBJECT (pl, "Doing HLS skip (v1) request"); + gst_uri_set_query_value (uri, HLS_SKIP_QUERY_KEY, "YES"); + } else if (dl_params->flags & PLAYLIST_DOWNLOAD_FLAG_SKIP_V2) { + GST_LOG_OBJECT (pl, "Doing HLS skip (v2) request"); + gst_uri_set_query_value (uri, HLS_SKIP_QUERY_KEY, "v2"); + } else { + gst_uri_remove_query_key (uri, HLS_SKIP_QUERY_KEY); + } + + if (dl_params->flags & PLAYLIST_DOWNLOAD_FLAG_BLOCKING_REQUEST + && dl_params->next_msn != -1) { + GST_LOG_OBJECT (pl, + "Doing HLS blocking request for URI %s with MSN %" G_GINT64_FORMAT + " part %" G_GINT64_FORMAT, playlist_uri, dl_params->next_msn, + dl_params->next_part); + + gchar *next_msn_str = + g_strdup_printf ("%" G_GINT64_FORMAT, dl_params->next_msn); + gst_uri_set_query_value (uri, HLS_MSN_QUERY_KEY, next_msn_str); + g_free (next_msn_str); + + if (dl_params->next_part != -1) { + gchar *next_part_str = + g_strdup_printf ("%" G_GINT64_FORMAT, dl_params->next_part); + gst_uri_set_query_value (uri, HLS_PART_QUERY_KEY, next_part_str); + g_free (next_part_str); + } else { + gst_uri_remove_query_key (uri, HLS_PART_QUERY_KEY); + } + } + + /* Produce the resulting URI with query arguments in UTF-8 order + * as required by the HLS spec: + * `Clients using Delivery Directives (Section 6.2.5) MUST ensure that + * all query parameters appear in UTF-8 order within the URI.` + */ + GList *keys = gst_uri_get_query_keys (uri); + if (keys) + keys = g_list_sort (keys, (GCompareFunc) g_strcmp0); + gchar *out_uri = gst_uri_to_string_with_keys (uri, keys); + gst_uri_unref (uri); + + return out_uri; +} + +static GstClockTime +get_playlist_reload_interval (GstHLSDemuxPlaylistLoader * pl, + GstHLSDemuxPlaylistLoaderPrivate * priv, GstHLSMediaPlaylist * playlist) +{ + if (playlist == NULL) + return GST_CLOCK_TIME_NONE; /* No playlist yet */ + + /* Use the most recent segment (or part segment) duration, as per + * https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-6.3.4 + */ + GstClockTime target_duration = GST_CLOCK_TIME_NONE; + GstClockTime min_reload_interval = playlist->targetduration / 2; + + if (playlist->segments->len) { + GstM3U8MediaSegment *last_seg = + g_ptr_array_index (playlist->segments, playlist->segments->len - 1); + + target_duration = last_seg->duration; + + if (priv->llhls_enabled && last_seg->partial_segments) { + GstM3U8PartialSegment *last_part = + g_ptr_array_index (last_seg->partial_segments, + last_seg->partial_segments->len - 1); + + target_duration = last_part->duration; + if (GST_CLOCK_TIME_IS_VALID (playlist->partial_targetduration)) { + min_reload_interval = playlist->partial_targetduration / 2; + } else { + min_reload_interval = target_duration / 2; + } + } + } else if (priv->llhls_enabled + && GST_CLOCK_TIME_IS_VALID (playlist->partial_targetduration)) { + target_duration = playlist->partial_targetduration; + min_reload_interval = target_duration / 2; + } else if (playlist->version > 5) { + target_duration = playlist->targetduration; + } + + if (playlist->reloaded && target_duration > min_reload_interval) { + GST_DEBUG_OBJECT (pl, + "Playlist didn't change previously, returning lower update interval"); + target_duration = min_reload_interval; + } + + return target_duration; +} + +static void +on_download_complete (DownloadRequest * download, DownloadRequestState state, + GstHLSDemuxPlaylistLoader * pl) +{ + GstHLSDemuxPlaylistLoaderPrivate *priv = pl->priv; + + if (priv->state != PLAYLIST_LOADER_STATE_LOADING) { + GST_DEBUG_OBJECT (pl, "Loader state changed to %d. Aborting", priv->state); + return; + } + + if (!g_str_equal (priv->target_playlist_uri, priv->loading_playlist_uri)) { + /* This callback happened just as the playlist URI was updated. There should be + * a pending state update scheduled, but we can just kick off the new download + * immediately */ + GST_DEBUG_OBJECT (pl, + "Target playlist URI changed from %s to %s. Discarding download", + priv->loading_playlist_uri, priv->target_playlist_uri); + start_playlist_download (pl, priv); + return; + } + + GST_DEBUG_OBJECT (pl, "Handling completed playlist download for URI %s", + download->uri); + + /* If we got a permanent redirect, use that as the new + * playlist URI, otherwise set the base URI of the playlist + * to the redirect target if any (NULL if there was no redirect) */ + GstHLSMediaPlaylist *playlist = NULL; + gchar *base_uri, *uri; + + if (download->redirect_permanent && download->redirect_uri) { + uri = g_strdup (download->redirect_uri); + base_uri = NULL; + } else { + uri = g_strdup (download->uri); + base_uri = g_strdup (download->redirect_uri); + } + /* FIXME: Strip HLS request params from the playlist and redirect URI */ + + /* Calculate the newest time we know this playlist was valid to store on the HLS Media Playlist */ + GstClockTime playlist_ts = + MAX (0, GST_CLOCK_DIFF (download_request_get_age (download), + download->download_start_time)); + + GstBuffer *buf = download_request_take_buffer (download); + + /* there should be a buf if there wasn't an error (handled above) */ + g_assert (buf); + + gchar *playlist_data = gst_hls_buf_to_utf8_text (buf); + gst_buffer_unref (buf); + + if (playlist_data == NULL) { + GST_WARNING_OBJECT (pl, "Couldn't validate playlist encoding"); + goto delay_retry_out; + } + + GstHLSMediaPlaylist *current_playlist = priv->current_playlist; + gboolean playlist_uri_change = (current_playlist == NULL + || g_strcmp0 (priv->loading_playlist_uri, + priv->current_playlist_uri) != 0); + + if (!playlist_uri_change && current_playlist + && gst_hls_media_playlist_has_same_data (current_playlist, + playlist_data)) { + GST_DEBUG_OBJECT (pl, "playlist data was unchanged"); + playlist = gst_hls_media_playlist_ref (current_playlist); + playlist->reloaded = TRUE; + g_free (playlist_data); + } else { + playlist = + gst_hls_media_playlist_parse (playlist_data, playlist_ts, uri, + base_uri); + if (!playlist) { + GST_WARNING_OBJECT (pl, "Couldn't parse playlist"); + } + } + + /* Transfer over any skipped segments from the current playlist if + * we did a delta playlist update */ + if (!playlist_uri_change && current_playlist && playlist + && playlist->skipped_segments > 0) { + if (!gst_hls_media_playlist_sync_skipped_segments (playlist, + current_playlist)) { + GST_DEBUG_OBJECT (pl, + "Could not merge delta update to playlist. Retrying with full request"); + + gst_hls_media_playlist_unref (playlist); + + /* Delta playlist update failed. Load a full playlist */ + priv->delta_merge_failed = TRUE; + start_playlist_download (pl, priv); + goto out; + } + } + + g_free (priv->current_playlist_uri); + if (priv->current_playlist) + gst_hls_media_playlist_unref (priv->current_playlist); + + /* FIXME: If there was a redirect, use that for the next update */ + priv->current_playlist_uri = g_strdup (priv->loading_playlist_uri); + priv->current_playlist = playlist; + + if (priv->success_cb) + priv->success_cb (pl, priv->current_playlist_uri, priv->current_playlist, + priv->userdata); + + g_free (priv->loading_playlist_uri); + priv->loading_playlist_uri = NULL; + + if (gst_hls_media_playlist_is_live (playlist)) { + /* Schedule the next playlist load. If we can do a blocking load, + * do it immediately, otherwise delayed */ + if (playlist->can_block_reload) { + start_playlist_download (pl, priv); + } else { + GstClockTime delay = get_playlist_reload_interval (pl, priv, playlist); + schedule_next_playlist_load (pl, priv, delay); + } + } + +out: + g_free (uri); + g_free (base_uri); + return; + +delay_retry_out: + /* Got invalid playlist data, retry soon */ + schedule_next_playlist_load (pl, priv, 100 * GST_MSECOND); + goto out; +} + +static void +on_download_error (DownloadRequest * download, DownloadRequestState state, + GstHLSDemuxPlaylistLoader * pl) +{ + GstHLSDemuxPlaylistLoaderPrivate *priv = pl->priv; + + if (priv->state != PLAYLIST_LOADER_STATE_LOADING) { + GST_DEBUG_OBJECT (pl, "Loader state changed to %d. Aborting", priv->state); + return; + } + + GST_WARNING_OBJECT (pl, + "Couldn't retrieve playlist, got HTTP status code %d", + download->status_code); + + if (priv->error_cb) + priv->error_cb (pl, priv->loading_playlist_uri, priv->userdata); + + start_playlist_download (pl, priv); +} + +static void +start_playlist_download (GstHLSDemuxPlaylistLoader * pl, + GstHLSDemuxPlaylistLoaderPrivate * priv) +{ + gboolean allow_skip = !priv->delta_merge_failed; + + const gchar *base_uri = priv->base_uri; + const gchar *orig_uri = priv->target_playlist_uri; + + /* Can't download yet */ + if (orig_uri == NULL) + return; + + struct PlaylistDownloadParams dl_params = { 0, }; + + GstHLSMediaPlaylist *current_playlist = priv->current_playlist; + + /* If there's no previous playlist, or the URI changed this + * is not a refresh/update but a switch to a new playlist */ + gboolean playlist_uri_change = (current_playlist == NULL + || g_strcmp0 (orig_uri, priv->current_playlist_uri) != 0); + + if (!playlist_uri_change) { + GST_LOG_OBJECT (pl, "Updating the playlist"); + + /* See if we can do a delta playlist update (if the playlist age is less than + * one half of the Skip Boundary */ + if (GST_CLOCK_TIME_IS_VALID (current_playlist->skip_boundary) && allow_skip) { + GstClockTime now = gst_adaptive_demux2_get_monotonic_time (priv->demux); + GstClockTimeDiff playlist_age = + GST_CLOCK_DIFF (current_playlist->playlist_ts, now); + + if (GST_CLOCK_TIME_IS_VALID (current_playlist->playlist_ts) && + playlist_age <= current_playlist->skip_boundary / 2) { + if (current_playlist->can_skip_dateranges) { + dl_params.flags |= PLAYLIST_DOWNLOAD_FLAG_SKIP_V2; + } else { + dl_params.flags |= PLAYLIST_DOWNLOAD_FLAG_SKIP_V1; + } + } + } else if (GST_CLOCK_TIME_IS_VALID (current_playlist->skip_boundary)) { + GST_DEBUG_OBJECT (pl, + "Doing full playlist update after failed delta request"); + } + } + + /* Blocking playlist reload check */ + if (current_playlist != NULL && current_playlist->can_block_reload) { + if (playlist_uri_change) { + /* FIXME: We're changing playlist, but if there's a EXT-X-RENDITION-REPORT + * for the new playlist we might be able to use it to do a blocking request */ + } else { + /* Get the next MSN (and/or possibly part number) for the request params */ + gst_hls_media_playlist_get_next_msn_and_part (current_playlist, + priv->llhls_enabled, &dl_params.next_msn, &dl_params.next_part); + dl_params.flags |= PLAYLIST_DOWNLOAD_FLAG_BLOCKING_REQUEST; + } + } + + gchar *target_uri = apply_directives_to_uri (pl, orig_uri, &dl_params); + + if (priv->download_request == NULL) { + priv->download_request = download_request_new_uri (target_uri); + + download_request_set_callbacks (priv->download_request, + (DownloadRequestEventCallback) on_download_complete, + (DownloadRequestEventCallback) on_download_error, + (DownloadRequestEventCallback) NULL, + (DownloadRequestEventCallback) NULL, pl); + } else { + download_request_set_uri (priv->download_request, target_uri, 0, -1); + } + + GST_DEBUG_OBJECT (pl, "Submitting playlist download request for URI %s", + target_uri); + g_free (target_uri); + + g_free (priv->loading_playlist_uri); + priv->loading_playlist_uri = g_strdup (orig_uri); + priv->state = PLAYLIST_LOADER_STATE_LOADING; + + if (!downloadhelper_submit_request (priv->download_helper, + base_uri, DOWNLOAD_FLAG_COMPRESS | DOWNLOAD_FLAG_FORCE_REFRESH, + priv->download_request, NULL)) { + /* Failed to submit the download - could be invalid URI, but + * could just mean the download helper was stopped */ + priv->state = PLAYLIST_LOADER_STATE_STOPPED; + } +} + +static gboolean +gst_hls_demux_playlist_loader_update (GstHLSDemuxPlaylistLoader * pl) +{ + GstHLSDemuxPlaylistLoaderPrivate *priv = pl->priv; + + GST_LOG_OBJECT (pl, "Updating at state %d", priv->state); + priv->pending_cb_id = 0; + + switch (priv->state) { + case PLAYLIST_LOADER_STATE_STOPPED: + break; + case PLAYLIST_LOADER_STATE_STARTING: + if (priv->target_playlist_uri) + start_playlist_download (pl, priv); + break; + case PLAYLIST_LOADER_STATE_LOADING: + /* A download is in progress, but if we reach here it's + * because the target playlist URI got updated, so check + * for cancelling the current download. */ + if (g_str_equal (priv->target_playlist_uri, priv->current_playlist_uri)) + break; + + /* A download is in progress. Cancel it and trigger a new one */ + if (priv->download_request) { + GST_DEBUG_OBJECT (pl, + "Playlist URI changed from %s to %s. Cancelling current download", + priv->target_playlist_uri, priv->current_playlist_uri); + downloadhelper_cancel_request (priv->download_helper, + priv->download_request); + download_request_unref (priv->download_request); + priv->download_request = NULL; + } + start_playlist_download (pl, priv); + break; + case PLAYLIST_LOADER_STATE_WAITING: + /* We were waiting until time to load a playlist. Load it now */ + start_playlist_download (pl, priv); + break; + } + + return G_SOURCE_REMOVE; +} diff --git a/subprojects/gst-plugins-good/ext/adaptivedemux2/hls/gsthlsdemux-playlist-loader.h b/subprojects/gst-plugins-good/ext/adaptivedemux2/hls/gsthlsdemux-playlist-loader.h new file mode 100644 index 0000000000..3180eae8bb --- /dev/null +++ b/subprojects/gst-plugins-good/ext/adaptivedemux2/hls/gsthlsdemux-playlist-loader.h @@ -0,0 +1,80 @@ +/* GStreamer + * + * Copyright (C) 2014 Samsung Electronics. All rights reserved. + * Author: Thiago Santos + * + * Copyright (C) 2021-2022 Centricular Ltd + * Author: Edward Hervey + * Author: Jan Schmidt + * + * 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. + */ +#ifndef _GST_HLS_DEMUX_PLAYLIST_LOADER_H_ +#define _GST_HLS_DEMUX_PLAYLIST_LOADER_H_ + +#include +#include "gstadaptivedemux.h" +#include "gstadaptivedemuxutils.h" +#include "downloadhelper.h" +#include "downloadrequest.h" + +G_BEGIN_DECLS + +#define GST_TYPE_HLS_DEMUX_PLAYLIST_LOADER (gst_hls_demux_playlist_loader_get_type()) +#define GST_HLS_DEMUX_PLAYLIST_LOADER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_HLS_DEMUX_PLAYLIST_LOADER,GstHLSDemuxPlaylistLoader)) +#define GST_HLS_DEMUX_PLAYLIST_LOADER_CAST(obj) ((GstHLSDemuxPlaylistLoader *)obj) + +typedef struct _GstHLSDemuxPlaylistLoader GstHLSDemuxPlaylistLoader; +typedef struct _GstHLSDemuxPlaylistLoaderClass GstHLSDemuxPlaylistLoaderClass; +typedef struct _GstHLSDemuxPlaylistLoaderPrivate GstHLSDemuxPlaylistLoaderPrivate; + +typedef void (*GstHLSDemuxPlaylistLoaderSuccessCallback) (GstHLSDemuxPlaylistLoader *pl, + const gchar *playlist_uri, GstHLSMediaPlaylist *new_playlist, gpointer userdata); +typedef void (*GstHLSDemuxPlaylistLoaderErrorCallback) (GstHLSDemuxPlaylistLoader *pl, + const gchar *playlist_uri, gpointer userdata); + +struct _GstHLSDemuxPlaylistLoaderClass +{ + GstObjectClass parent_class; +}; + +struct _GstHLSDemuxPlaylistLoader +{ + GstObject object; + GstHLSDemuxPlaylistLoaderPrivate *priv; +}; + +GType gst_hls_demux_playlist_loader_get_type(void); + +GstHLSDemuxPlaylistLoader *gst_hls_demux_playlist_loader_new(GstAdaptiveDemux *demux, + DownloadHelper *download_helper, gboolean llhls_enabled); + +void gst_hls_demux_playlist_loader_set_callbacks (GstHLSDemuxPlaylistLoader *pl, + GstHLSDemuxPlaylistLoaderSuccessCallback success_cb, + GstHLSDemuxPlaylistLoaderErrorCallback error_cb, + gpointer userdata); + +void gst_hls_demux_playlist_loader_start (GstHLSDemuxPlaylistLoader *pl); +void gst_hls_demux_playlist_loader_stop (GstHLSDemuxPlaylistLoader *pl); + +void gst_hls_demux_playlist_loader_set_playlist_uri (GstHLSDemuxPlaylistLoader *pl, + const gchar *base_uri, const gchar *current_playlist_uri); +GstHLSMediaPlaylist *gst_hls_demux_stream_get_playlist_for_uri (GstHLSDemuxPlaylistLoader *pl, + const gchar *target_playlist_uri); + +G_END_DECLS +#endif diff --git a/subprojects/gst-plugins-good/ext/adaptivedemux2/hls/gsthlsdemux-stream.c b/subprojects/gst-plugins-good/ext/adaptivedemux2/hls/gsthlsdemux-stream.c index 11b512bca9..8977da1091 100644 --- a/subprojects/gst-plugins-good/ext/adaptivedemux2/hls/gsthlsdemux-stream.c +++ b/subprojects/gst-plugins-good/ext/adaptivedemux2/hls/gsthlsdemux-stream.c @@ -1056,6 +1056,12 @@ gst_hls_demux_stream_finalize (GObject * object) gst_buffer_replace (&hls_stream->pending_typefind_buffer, NULL); gst_buffer_replace (&hls_stream->pending_segment_data, NULL); + if (hls_stream->playlistloader) { + gst_hls_demux_playlist_loader_stop (hls_stream->playlistloader); + gst_object_unparent (GST_OBJECT (hls_stream->playlistloader)); + gst_object_unref (hls_stream->playlistloader); + } + if (hls_stream->preloader) { gst_hls_demux_preloader_free (hls_stream->preloader); hls_stream->preloader = NULL; @@ -1225,7 +1231,7 @@ struct PlaylistDownloadParams #define HLS_MSN_QUERY_KEY "_HLS_msn" #define HLS_PART_QUERY_KEY "_HLS_part" -gchar * +static gchar * apply_directives_to_uri (GstHLSDemuxStream * stream, const gchar * playlist_uri, struct PlaylistDownloadParams *dl_params) { @@ -1288,6 +1294,11 @@ download_media_playlist (GstHLSDemuxStream * stream, gchar * orig_uri, const gchar *main_uri = gst_adaptive_demux_get_manifest_ref_uri (demux); struct PlaylistDownloadParams dl_params; + /* FIXME: Set this URI when the variant is changed */ + if (stream->playlistloader) + gst_hls_demux_playlist_loader_set_playlist_uri (stream->playlistloader, + main_uri, orig_uri); + retry: memset (&dl_params, 0, sizeof (struct PlaylistDownloadParams)); @@ -2014,6 +2025,18 @@ gst_hls_demux_stream_start (GstAdaptiveDemux2Stream * stream) if (!gst_hls_demux_stream_can_start (stream)) return; + /* Start the playlist loader */ + GstHLSDemuxStream *hls_stream = GST_HLS_DEMUX_STREAM_CAST (stream); + + if (hls_stream->playlistloader == NULL) { + GstAdaptiveDemux *demux = stream->demux; + + hls_stream->playlistloader = + gst_hls_demux_playlist_loader_new (demux, demux->download_helper, + hls_stream->llhls_enabled); + } + gst_hls_demux_playlist_loader_start (hls_stream->playlistloader); + /* Chain up, to start the downloading */ GST_ADAPTIVE_DEMUX2_STREAM_CLASS (stream_parent_class)->start (stream); } @@ -2021,6 +2044,11 @@ gst_hls_demux_stream_start (GstAdaptiveDemux2Stream * stream) static void gst_hls_demux_stream_stop (GstAdaptiveDemux2Stream * stream) { + GstHLSDemuxStream *hls_stream = GST_HLS_DEMUX_STREAM_CAST (stream); + + if (hls_stream->playlistloader) + gst_hls_demux_playlist_loader_stop (hls_stream->playlistloader); + /* Chain up, to stop the downloading */ GST_ADAPTIVE_DEMUX2_STREAM_CLASS (stream_parent_class)->stop (stream); } diff --git a/subprojects/gst-plugins-good/ext/adaptivedemux2/hls/gsthlsdemux-stream.h b/subprojects/gst-plugins-good/ext/adaptivedemux2/hls/gsthlsdemux-stream.h index 3ca4a44860..da1ade1ccf 100644 --- a/subprojects/gst-plugins-good/ext/adaptivedemux2/hls/gsthlsdemux-stream.h +++ b/subprojects/gst-plugins-good/ext/adaptivedemux2/hls/gsthlsdemux-stream.h @@ -30,6 +30,7 @@ #include #include "gsthlsdemux.h" #include "gsthlsdemux-preloader.h" +#include "gsthlsdemux-playlist-loader.h" #if defined(HAVE_OPENSSL) #include @@ -86,6 +87,9 @@ struct _GstHLSDemuxStream gboolean playlist_fetched; GstClockTime playlist_last_update_time; + /* Playlist loading helper */ + GstHLSDemuxPlaylistLoader *playlistloader; + /* The media playlist currently used */ GstHLSMediaPlaylist *playlist; diff --git a/subprojects/gst-plugins-good/ext/adaptivedemux2/hls/meson.build b/subprojects/gst-plugins-good/ext/adaptivedemux2/hls/meson.build index 40948a8a00..a9ce7ae6ae 100644 --- a/subprojects/gst-plugins-good/ext/adaptivedemux2/hls/meson.build +++ b/subprojects/gst-plugins-good/ext/adaptivedemux2/hls/meson.build @@ -1,6 +1,7 @@ hls_sources = [ 'hls/gsthlsdemux.c', 'hls/gsthlsdemux-stream.c', + 'hls/gsthlsdemux-playlist-loader.c', 'hls/gsthlsdemux-preloader.c', 'hls/gsthlsdemux-util.c', 'hls/gsthlselement.c',