hlsdemux2: Add parsing of partial segments

Add partial segments to each media segment, and potentially create a trailing
dummy segment if there are partial segments at the end of the playlist

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/3883>
This commit is contained in:
Jan Schmidt 2022-06-08 01:35:44 +10:00 committed by GStreamer Marge Bot
parent fac7177354
commit 07f51396af
2 changed files with 233 additions and 6 deletions

View file

@ -112,10 +112,32 @@ gst_m3u8_media_segment_unref (GstM3U8MediaSegment * self)
g_free (self->key); g_free (self->key);
if (self->datetime) if (self->datetime)
g_date_time_unref (self->datetime); g_date_time_unref (self->datetime);
if (self->partial_segments)
g_ptr_array_free (self->partial_segments, TRUE);
g_free (self); g_free (self);
} }
} }
GstM3U8PartialSegment *
gst_m3u8_partial_segment_ref (GstM3U8PartialSegment * part)
{
g_assert (part != NULL && part->ref_count > 0);
g_atomic_int_add (&part->ref_count, 1);
return part;
}
void
gst_m3u8_partial_segment_unref (GstM3U8PartialSegment * part)
{
g_return_if_fail (part != NULL && part->ref_count > 0);
if (g_atomic_int_dec_and_test (&part->ref_count)) {
g_free (part->uri);
g_free (part);
}
}
static GstM3U8InitFile * static GstM3U8InitFile *
gst_m3u8_init_file_new (gchar * uri, gint64 size, gint64 offset) gst_m3u8_init_file_new (gchar * uri, gint64 size, gint64 offset)
{ {
@ -343,6 +365,7 @@ gst_hls_media_playlist_new (const gchar * uri, const gchar * base_uri)
m3u8->version = 1; m3u8->version = 1;
m3u8->type = GST_HLS_PLAYLIST_TYPE_UNDEFINED; m3u8->type = GST_HLS_PLAYLIST_TYPE_UNDEFINED;
m3u8->targetduration = GST_CLOCK_TIME_NONE; m3u8->targetduration = GST_CLOCK_TIME_NONE;
m3u8->partial_targetduration = GST_CLOCK_TIME_NONE;
m3u8->media_sequence = 0; m3u8->media_sequence = 0;
m3u8->discont_sequence = 0; m3u8->discont_sequence = 0;
m3u8->endlist = FALSE; m3u8->endlist = FALSE;
@ -377,6 +400,8 @@ gst_hls_media_playlist_dump (GstHLSMediaPlaylist * self)
GST_DEBUG ("targetduration : %" GST_TIME_FORMAT, GST_DEBUG ("targetduration : %" GST_TIME_FORMAT,
GST_TIME_ARGS (self->targetduration)); GST_TIME_ARGS (self->targetduration));
GST_DEBUG ("partial segment targetduration : %" GST_TIME_FORMAT,
GST_TIME_ARGS (self->partial_targetduration));
GST_DEBUG ("media_sequence : %" G_GINT64_FORMAT, self->media_sequence); GST_DEBUG ("media_sequence : %" G_GINT64_FORMAT, self->media_sequence);
GST_DEBUG ("discont_sequence : %" G_GINT64_FORMAT, self->discont_sequence); GST_DEBUG ("discont_sequence : %" G_GINT64_FORMAT, self->discont_sequence);
@ -398,6 +423,7 @@ gst_hls_media_playlist_dump (GstHLSMediaPlaylist * self)
GST_DEBUG (" sequence:%" G_GINT64_FORMAT " discont_sequence:%" GST_DEBUG (" sequence:%" G_GINT64_FORMAT " discont_sequence:%"
G_GINT64_FORMAT, segment->sequence, segment->discont_sequence); G_GINT64_FORMAT, segment->sequence, segment->discont_sequence);
GST_DEBUG (" partial only: %s", segment->partial_only ? "YES" : "NO");
GST_DEBUG (" stream_time : %" GST_STIME_FORMAT, GST_DEBUG (" stream_time : %" GST_STIME_FORMAT,
GST_STIME_ARGS (segment->stream_time)); GST_STIME_ARGS (segment->stream_time));
GST_DEBUG (" duration : %" GST_TIME_FORMAT, GST_DEBUG (" duration : %" GST_TIME_FORMAT,
@ -412,6 +438,25 @@ gst_hls_media_playlist_dump (GstHLSMediaPlaylist * self)
} }
GST_DEBUG (" uri : %s %" G_GUINT64_FORMAT " %" G_GINT64_FORMAT, GST_DEBUG (" uri : %s %" G_GUINT64_FORMAT " %" G_GINT64_FORMAT,
segment->uri, segment->offset, segment->size); segment->uri, segment->offset, segment->size);
GST_DEBUG (" is gap : %s", segment->is_gap ? "YES" : "NO");
if (segment->partial_segments != NULL) {
guint part_idx;
for (part_idx = 0; part_idx < segment->partial_segments->len; part_idx++) {
GstM3U8PartialSegment *part =
g_ptr_array_index (segment->partial_segments, part_idx);
GST_DEBUG (" partial segment %u:", part_idx);
GST_DEBUG (" uri : %s %" G_GUINT64_FORMAT " %"
G_GINT64_FORMAT, part->uri, part->offset, part->size);
GST_DEBUG (" stream_time : %" GST_STIME_FORMAT,
GST_STIME_ARGS (part->stream_time));
GST_DEBUG (" duration : %" GST_TIME_FORMAT,
GST_TIME_ARGS (part->duration));
GST_DEBUG (" is gap : %s", part->is_gap ? "YES" : "NO");
GST_DEBUG (" independent : %s", part->independent ? "YES" : "NO");
}
}
} }
#endif #endif
} }
@ -479,6 +524,62 @@ gst_hls_media_playlist_postprocess_pdt (GstHLSMediaPlaylist * self)
} }
} }
static GstM3U8PartialSegment *
gst_m3u8_parse_partial_segment (gchar * data, const gchar * base_uri)
{
gchar *v, *a;
gboolean have_duration = FALSE;
GstM3U8PartialSegment *part = g_new0 (GstM3U8PartialSegment, 1);
part->ref_count = 1;
part->stream_time = GST_CLOCK_STIME_NONE;
part->size = -1;
while (data != NULL && parse_attributes (&data, &a, &v)) {
if (strcmp (a, "URI") == 0) {
g_free (part->uri);
part->uri = uri_join (base_uri, v);
} else if (strcmp (a, "DURATION") == 0) {
if (!time_from_double_in_string (v, NULL, &part->duration)) {
GST_WARNING ("Can't read EXT-X-PART duration");
goto malformed_line;
}
have_duration = TRUE;
} else if (strcmp (a, "INDEPENDENT") == 0) {
part->independent = g_ascii_strcasecmp (v, "yes") == 0;
} else if (strcmp (a, "GAP") == 0) {
part->is_gap = g_ascii_strcasecmp (v, "yes") == 0;
} else if (strcmp (a, "BYTERANGE") == 0) {
if (int64_from_string (v, &v, &part->size)) {
goto malformed_line;
}
if (*v == '@' && !int64_from_string (v + 1, &v, &part->offset)) {
goto malformed_line;
}
}
}
if (part->uri == NULL || !have_duration) {
goto required_attributes_missing;
}
return part;
required_attributes_missing:
{
GST_WARNING
("EXT-X-PART description is missing required URI or DURATION attributes");
gst_m3u8_partial_segment_unref (part);
return NULL;
}
malformed_line:
{
GST_WARNING ("Invalid EXT-X-PART entry in playlist");
gst_m3u8_partial_segment_unref (part);
return NULL;
}
}
/* Parse and create a new GstHLSMediaPlaylist */ /* Parse and create a new GstHLSMediaPlaylist */
GstHLSMediaPlaylist * GstHLSMediaPlaylist *
gst_hls_media_playlist_parse (gchar * data, const gchar * uri, gst_hls_media_playlist_parse (gchar * data, const gchar * uri,
@ -487,7 +588,7 @@ gst_hls_media_playlist_parse (gchar * data, const gchar * uri,
gchar *input_data = data; gchar *input_data = data;
GstHLSMediaPlaylist *self; GstHLSMediaPlaylist *self;
gint val; gint val;
GstClockTime duration; GstClockTime duration, partial_duration;
gchar *title, *end; gchar *title, *end;
gboolean discontinuity = FALSE; gboolean discontinuity = FALSE;
gchar *current_key = NULL; gchar *current_key = NULL;
@ -499,6 +600,7 @@ gst_hls_media_playlist_parse (gchar * data, const gchar * uri,
GDateTime *date_time = NULL; GDateTime *date_time = NULL;
GstM3U8InitFile *last_init_file = NULL; GstM3U8InitFile *last_init_file = NULL;
GstM3U8MediaSegment *previous = NULL; GstM3U8MediaSegment *previous = NULL;
GPtrArray *partial_segments = NULL;
gboolean is_gap = FALSE; gboolean is_gap = FALSE;
GST_LOG ("uri: %s", uri); GST_LOG ("uri: %s", uri);
@ -523,6 +625,7 @@ gst_hls_media_playlist_parse (gchar * data, const gchar * uri,
self->last_data = g_strdup (data); self->last_data = g_strdup (data);
duration = 0; duration = 0;
partial_duration = 0;
title = NULL; title = NULL;
data += 7; data += 7;
while (TRUE) { while (TRUE) {
@ -552,17 +655,22 @@ gst_hls_media_playlist_parse (gchar * data, const gchar * uri,
data = NULL; data = NULL;
date_time = NULL; date_time = NULL;
duration = 0; duration = 0;
partial_duration = 0;
g_free (title); g_free (title);
title = NULL; title = NULL;
discontinuity = FALSE; discontinuity = FALSE;
size = offset = -1; size = offset = -1;
is_gap = FALSE; is_gap = FALSE;
if (partial_segments != NULL) {
g_ptr_array_free (partial_segments, TRUE);
partial_segments = NULL;
}
goto next_line; goto next_line;
} }
if (data != NULL) { if (data != NULL) {
GstM3U8MediaSegment *file; GstM3U8MediaSegment *file;
/* We can finally create the segment */ /* We can finally create the segment */
/* The disconinuity sequence number is only stored if the header has /* The discontinuity sequence number is only stored if the header has
* EXT-X-DISCONTINUITY-SEQUENCE present. */ * EXT-X-DISCONTINUITY-SEQUENCE present. */
file = file =
gst_m3u8_media_segment_new (data, title, duration, mediasequence++, gst_m3u8_media_segment_new (data, title, duration, mediasequence++,
@ -593,8 +701,12 @@ gst_hls_media_playlist_parse (gchar * data, const gchar * uri,
if (last_init_file) if (last_init_file)
file->init_file = gst_m3u8_init_file_ref (last_init_file); file->init_file = gst_m3u8_init_file_ref (last_init_file);
file->partial_segments = partial_segments;
partial_segments = NULL;
date_time = NULL; /* Ownership was passed to the segment */ date_time = NULL; /* Ownership was passed to the segment */
duration = 0; duration = 0;
partial_duration = 0;
title = NULL; /* Ownership was passed to the segment */ title = NULL; /* Ownership was passed to the segment */
discontinuity = FALSE; discontinuity = FALSE;
size = offset = -1; size = offset = -1;
@ -763,6 +875,36 @@ gst_hls_media_playlist_parse (gchar * data, const gchar * uri,
} }
} else if (g_str_has_prefix (data_ext_x, "GAP:")) { } else if (g_str_has_prefix (data_ext_x, "GAP:")) {
is_gap = TRUE; is_gap = TRUE;
} else if (g_str_has_prefix (data_ext_x, "PART:")) {
GstM3U8PartialSegment *part = NULL;
part =
gst_m3u8_parse_partial_segment (data + strlen ("#EXT-X-PART:"),
self->base_uri ? self->base_uri : self->uri);
if (part == NULL)
goto next_line;
if (partial_segments == NULL) {
partial_segments = g_ptr_array_new_full (2,
(GDestroyNotify) gst_m3u8_partial_segment_unref);
}
g_ptr_array_add (partial_segments, part);
partial_duration += part->duration;
} else if (g_str_has_prefix (data_ext_x, "PART-INF:")) {
gchar *v, *a;
data += strlen ("#EXT-X-PART-INF:");
while (data != NULL && parse_attributes (&data, &a, &v)) {
if (strcmp (a, "PART-TARGET") == 0) {
if (!time_from_double_in_string (v, NULL,
&self->partial_targetduration)) {
GST_WARNING ("Invalid PART-TARGET");
goto next_line;
}
}
}
} else { } else {
GST_LOG ("Ignored line: %s", data); GST_LOG ("Ignored line: %s", data);
} }
@ -777,6 +919,59 @@ gst_hls_media_playlist_parse (gchar * data, const gchar * uri,
data = g_utf8_next_char (end); /* skip \n */ data = g_utf8_next_char (end); /* skip \n */
} }
/* If there are trailing partial segments at the end,
* create a dummy segment to hold them */
if (partial_segments != NULL) {
GstM3U8MediaSegment *file;
GST_DEBUG ("Creating dummy segment for trailing partial segments");
/* The discontinuity sequence number is only stored if the header has
* EXT-X-DISCONTINUITY-SEQUENCE present. */
file =
gst_m3u8_media_segment_new (NULL, title, partial_duration,
mediasequence++, dsn, size, offset);
file->partial_only = TRUE;
self->duration += partial_duration;
file->is_gap = is_gap;
/* set encryption params */
if (current_key != NULL) {
file->key = g_strdup (current_key);
if (have_iv) {
memcpy (file->iv, iv, sizeof (iv));
} else {
/* An EXT-X-KEY tag with a KEYFORMAT of "identity" that does
* not have an IV attribute indicates that the Media Sequence
* Number is to be used as the IV when decrypting a Media
* Segment, by putting its big-endian binary representation
* into a 16-octet (128-bit) buffer and padding (on the left)
* with zeros. */
guint8 *iv = file->iv + 12;
GST_WRITE_UINT32_BE (iv, file->sequence);
}
}
file->datetime = date_time;
file->discont = discontinuity;
if (last_init_file)
file->init_file = gst_m3u8_init_file_ref (last_init_file);
file->partial_segments = partial_segments;
partial_segments = NULL;
date_time = NULL; /* Ownership was passed to the partial segment */
duration = 0;
partial_duration = 0;
title = NULL; /* Ownership was passed to the partial segment */
discontinuity = FALSE;
size = offset = -1;
g_ptr_array_add (self->segments, file);
previous = file;
}
/* Clean up date that wasn't freed / handed to a segment */ /* Clean up date that wasn't freed / handed to a segment */
g_free (current_key); g_free (current_key);
current_key = NULL; current_key = NULL;
@ -1445,7 +1640,7 @@ gst_hls_media_playlist_is_live (GstHLSMediaPlaylist * m3u8)
return is_live; return is_live;
} }
gchar * static gchar *
uri_join (const gchar * uri1, const gchar * uri2) uri_join (const gchar * uri1, const gchar * uri2)
{ {
gchar *uri_copy, *tmp, *ret = NULL; gchar *uri_copy, *tmp, *ret = NULL;
@ -1617,7 +1812,7 @@ gst_hls_rendition_stream_type_get_name (GstHLSRenditionStreamType mtype)
return nicks[mtype]; return nicks[mtype];
} }
/* returns unquoted copy of string */ /* returns copy of string with surrounding quotation marks removed */
static gchar * static gchar *
gst_m3u8_unquote (const gchar * str) gst_m3u8_unquote (const gchar * str)
{ {

View file

@ -35,6 +35,7 @@ G_BEGIN_DECLS
typedef struct _GstHLSMediaPlaylist GstHLSMediaPlaylist; typedef struct _GstHLSMediaPlaylist GstHLSMediaPlaylist;
typedef struct _GstHLSTimeMap GstHLSTimeMap; typedef struct _GstHLSTimeMap GstHLSTimeMap;
typedef struct _GstM3U8MediaSegment GstM3U8MediaSegment; typedef struct _GstM3U8MediaSegment GstM3U8MediaSegment;
typedef struct _GstM3U8PartialSegment GstM3U8PartialSegment;
typedef struct _GstM3U8InitFile GstM3U8InitFile; typedef struct _GstM3U8InitFile GstM3U8InitFile;
typedef struct _GstHLSRenditionStream GstHLSRenditionStream; typedef struct _GstHLSRenditionStream GstHLSRenditionStream;
typedef struct _GstM3U8Client GstM3U8Client; typedef struct _GstM3U8Client GstM3U8Client;
@ -43,6 +44,7 @@ typedef struct _GstHLSMasterPlaylist GstHLSMasterPlaylist;
#define GST_HLS_MEDIA_PLAYLIST(m) ((GstHLSMediaPlaylist*)m) #define GST_HLS_MEDIA_PLAYLIST(m) ((GstHLSMediaPlaylist*)m)
#define GST_M3U8_MEDIA_SEGMENT(f) ((GstM3U8MediaSegment*)f) #define GST_M3U8_MEDIA_SEGMENT(f) ((GstM3U8MediaSegment*)f)
#define GST_M3U8_PARTIAL_SEGMENT(p) ((GstM3U8PartialSegment*)p)
#define GST_HLS_MEDIA_PLAYLIST_LOCK(m) g_mutex_lock (&m->lock); #define GST_HLS_MEDIA_PLAYLIST_LOCK(m) g_mutex_lock (&m->lock);
#define GST_HLS_MEDIA_PLAYLIST_UNLOCK(m) g_mutex_unlock (&m->lock); #define GST_HLS_MEDIA_PLAYLIST_UNLOCK(m) g_mutex_unlock (&m->lock);
@ -82,6 +84,8 @@ struct _GstHLSMediaPlaylist
/* Media Playlist Tags */ /* Media Playlist Tags */
GstClockTime targetduration; /* EXT-X-TARGETDURATION, default GST_CLOCK_TIME_NONE */ GstClockTime targetduration; /* EXT-X-TARGETDURATION, default GST_CLOCK_TIME_NONE */
GstClockTime partial_targetduration; /* EXT-X-PART-INF, default GST_CLOCK_TIME_NONE */
gint64 media_sequence; /* EXT-X-MEDIA-SEQUENCE, MSN of the first Media gint64 media_sequence; /* EXT-X-MEDIA-SEQUENCE, MSN of the first Media
Segment in the playlist. */ Segment in the playlist. */
gint64 discont_sequence; /* EXT-X-DISCONTINUITY-SEQUENCE. Default : 0 */ gint64 discont_sequence; /* EXT-X-DISCONTINUITY-SEQUENCE. Default : 0 */
@ -127,6 +131,31 @@ GstHLSMediaPlaylist * gst_hls_media_playlist_ref (GstHLSMediaPlaylist * m3u8);
void gst_hls_media_playlist_unref (GstHLSMediaPlaylist * m3u8); void gst_hls_media_playlist_unref (GstHLSMediaPlaylist * m3u8);
/**
* GstM3U8PartialSegment:
*
* Official term in RFC : "Partial Segment"
*
*/
struct _GstM3U8PartialSegment
{
gboolean is_gap; /* TRUE if this part is a gap */
gboolean independent; /* TRUE if there is an I-frame in the partial segment */
gchar *uri;
gint64 offset, size;
GstClockTimeDiff stream_time; /* Computed stream time */
GstClockTime duration;
gint ref_count; /* ATOMIC */
};
GstM3U8PartialSegment *
gst_m3u8_partial_segment_ref (GstM3U8PartialSegment *part);
void
gst_m3u8_partial_segment_unref (GstM3U8PartialSegment *part);
/** /**
* GstM3U8MediaSegment: * GstM3U8MediaSegment:
* *
@ -137,6 +166,7 @@ void gst_hls_media_playlist_unref (GstHLSMediaPlaylist * m3u8);
struct _GstM3U8MediaSegment struct _GstM3U8MediaSegment
{ {
gboolean is_gap; /* TRUE if EXT-X-GAP was present for this segment */ gboolean is_gap; /* TRUE if EXT-X-GAP was present for this segment */
gboolean partial_only; /* TRUE if this is the last segment in a playlist consisting of only EXT-X-PART and no full URL */
gchar *title; gchar *title;
GstClockTimeDiff stream_time; /* Computed stream time */ GstClockTimeDiff stream_time; /* Computed stream time */
@ -148,9 +178,12 @@ struct _GstM3U8MediaSegment
gchar *key; gchar *key;
guint8 iv[16]; guint8 iv[16];
gint64 offset, size; gint64 offset, size;
gint ref_count; /* ATOMIC */
GstM3U8InitFile *init_file; /* Media Initialization (hold ref) */ GstM3U8InitFile *init_file; /* Media Initialization (hold ref) */
GDateTime *datetime; /* EXT-X-PROGRAM-DATE-TIME */ GDateTime *datetime; /* EXT-X-PROGRAM-DATE-TIME */
GPtrArray *partial_segments; /* If there are Partial Segments for this Media Segment */
gint ref_count; /* ATOMIC */
}; };
struct _GstM3U8InitFile struct _GstM3U8InitFile
@ -170,7 +203,6 @@ gst_m3u8_media_segment_ref (GstM3U8MediaSegment * mfile);
void void
gst_m3u8_media_segment_unref (GstM3U8MediaSegment * mfile); gst_m3u8_media_segment_unref (GstM3U8MediaSegment * mfile);
gboolean gboolean
gst_hls_media_playlist_has_same_data (GstHLSMediaPlaylist * m3u8, gst_hls_media_playlist_has_same_data (GstHLSMediaPlaylist * m3u8,
gchar * playlist_data); gchar * playlist_data);