hlsdemux2: Implement LL-HLS flag and part-hold-back/hold-back in live.

Add a flag to hlsdemux to enable or disable LL-HLS handling.

When LL-HLS is enabled and an LL-HLS playlist is loaded, use the part-hold-back
threshold to choose a starting segment.

For live streams that aren't LL-HLS, use the provided hold-back attribute, or
fall back to landing 3 segments from the end.

Make the gst_hls_media_playlist_seek() method able to choose a partial segment
within 2 target durations of the end of the playlist when requested.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/3883>
This commit is contained in:
Jan Schmidt 2022-08-13 05:44:24 +10:00 committed by GStreamer Marge Bot
parent 9848c1a1a1
commit 43e042c4b7
5 changed files with 293 additions and 65 deletions

View file

@ -67,9 +67,11 @@ enum
PROP_0,
PROP_START_BITRATE,
PROP_LLHLS_ENABLED,
};
#define DEFAULT_START_BITRATE 0
#define DEFAULT_LLHLS_ENABLED TRUE
/* Maximum values for mpeg-ts DTS values */
#define MPEG_TS_MAX_PTS (((((guint64)1) << 33) * (guint64)100000) / 9)
@ -233,6 +235,9 @@ gst_hls_demux_set_property (GObject * object, guint prop_id,
case PROP_START_BITRATE:
demux->start_bitrate = g_value_get_uint (value);
break;
case PROP_LLHLS_ENABLED:
demux->llhls_enabled = g_value_get_boolean (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@ -249,6 +254,9 @@ gst_hls_demux_get_property (GObject * object, guint prop_id,
case PROP_START_BITRATE:
g_value_set_uint (value, demux->start_bitrate);
break;
case PROP_LLHLS_ENABLED:
g_value_set_boolean (value, demux->llhls_enabled);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@ -277,6 +285,11 @@ gst_hls_demux2_class_init (GstHLSDemux2Class * klass)
0, G_MAXUINT, DEFAULT_START_BITRATE,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (gobject_class, PROP_LLHLS_ENABLED,
g_param_spec_boolean ("llhls-enabled", "Enable LL-HLS support",
"Enable support for LL-HLS (Low Latency HLS) downloads",
DEFAULT_LLHLS_ENABLED, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
element_class->change_state = GST_DEBUG_FUNCPTR (gst_hls_demux_change_state);
gst_element_class_add_static_pad_template (element_class, &sinktemplate);
@ -302,6 +315,7 @@ gst_hls_demux2_class_init (GstHLSDemux2Class * klass)
static void
gst_hls_demux2_init (GstHLSDemux * demux)
{
demux->llhls_enabled = DEFAULT_LLHLS_ENABLED;
demux->keys = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
g_mutex_init (&demux->keys_lock);
}
@ -497,7 +511,6 @@ gst_hls_demux_stream_seek (GstAdaptiveDemux2Stream * stream, gboolean forward,
GstFlowReturn ret = GST_FLOW_OK;
GstHLSDemuxStream *hls_stream = GST_HLS_DEMUX_STREAM_CAST (stream);
GstHLSDemux *hlsdemux = (GstHLSDemux *) stream->demux;
GstM3U8MediaSegment *new_position;
GST_DEBUG_OBJECT (stream,
"is_variant:%d media:%p current_variant:%p forward:%d ts:%"
@ -514,17 +527,22 @@ gst_hls_demux_stream_seek (GstAdaptiveDemux2Stream * stream, gboolean forward,
}
}
/* FIXME: Allow jumping to partial segments in LL-HLS? */
new_position =
gst_hls_media_playlist_seek (hls_stream->playlist, forward, flags, ts);
if (new_position) {
/* Allow jumping to partial segments in the last 2 segments in LL-HLS */
if (hls_stream->llhls_enabled)
flags |= GST_HLS_M3U8_SEEK_FLAG_ALLOW_PARTIAL;
GstM3U8SeekResult seek_result;
if (gst_hls_media_playlist_seek (hls_stream->playlist, forward, flags, ts,
&seek_result)) {
if (hls_stream->current_segment)
gst_m3u8_media_segment_unref (hls_stream->current_segment);
hls_stream->current_segment = new_position;
hls_stream->in_partial_segments = FALSE;
hls_stream->current_segment = seek_result.segment;
hls_stream->in_partial_segments = seek_result.found_partial_segment;
hls_stream->part_idx = seek_result.part_idx;
hls_stream->reset_pts = TRUE;
if (final_ts)
*final_ts = new_position->stream_time;
*final_ts = seek_result.stream_time;
} else {
GST_WARNING_OBJECT (stream, "Seeking failed");
ret = GST_FLOW_ERROR;
@ -547,6 +565,8 @@ create_common_hls_stream (GstHLSDemux * demux, const gchar * name)
GstAdaptiveDemux2Stream *stream;
stream = g_object_new (GST_TYPE_HLS_DEMUX_STREAM, "name", name, NULL);
GST_HLS_DEMUX_STREAM (stream)->llhls_enabled = demux->llhls_enabled;
gst_adaptive_demux2_add_stream ((GstAdaptiveDemux *) demux, stream);
return stream;
@ -943,9 +963,21 @@ gst_hls_demux_process_manifest (GstAdaptiveDemux * demux, GstBuffer * buf)
ret = gst_hls_demux_setup_streams (demux);
if (simple_media_playlist) {
GstM3U8SeekResult seek_result;
hlsdemux->main_stream->playlist = simple_media_playlist;
hlsdemux->main_stream->current_segment =
gst_hls_media_playlist_get_starting_segment (simple_media_playlist);
if (!gst_hls_media_playlist_get_starting_segment (simple_media_playlist,
hlsdemux->main_stream->llhls_enabled, &seek_result)) {
GST_DEBUG_OBJECT (hlsdemux->main_stream,
"Failed to find a segment to start at");
return FALSE;
}
hlsdemux->main_stream->current_segment = seek_result.segment;
hlsdemux->main_stream->in_partial_segments =
seek_result.found_partial_segment;
hlsdemux->main_stream->part_idx = seek_result.part_idx;
setup_initial_playlist (hlsdemux, simple_media_playlist);
gst_hls_update_time_mappings (hlsdemux, simple_media_playlist);
gst_hls_media_playlist_dump (simple_media_playlist);
@ -1687,6 +1719,7 @@ gst_hls_demux_stream_finish_fragment (GstAdaptiveDemux2Stream * stream)
if (hls_stream->current_segment == NULL) {
/* We can't advance, we just return OK for now and let the base class
* trigger a new download (or fail and resync itself) */
GST_DEBUG_OBJECT (stream, "Can't advance - current_segment is NULL");
return GST_FLOW_OK;
}
@ -1905,7 +1938,7 @@ gst_hls_demux_stream_advance_fragment (GstAdaptiveDemux2Stream * stream)
new_segment =
gst_hls_media_playlist_advance_fragment (hlsdemux_stream->playlist,
hlsdemux_stream->current_segment, stream->demux->segment.rate > 0,
TRUE /* FIXME: Only in LL-HLS mode */ );
hlsdemux_stream->llhls_enabled);
if (new_segment) {
hlsdemux_stream->reset_pts = FALSE;
@ -2225,23 +2258,32 @@ gst_hls_demux_reset_for_lost_sync (GstHLSDemux * hlsdemux)
if (hls_stream->is_variant) {
GstHLSTimeMap *map;
GstM3U8SeekResult seek_result;
/* Resynchronize the variant stream */
g_assert (stream->current_position != GST_CLOCK_STIME_NONE);
hls_stream->current_segment =
gst_hls_media_playlist_get_starting_segment (hls_stream->playlist);
hls_stream->current_segment->stream_time = stream->current_position;
gst_hls_media_playlist_recalculate_stream_time (hls_stream->playlist,
hls_stream->current_segment);
GST_DEBUG_OBJECT (stream,
"Resynced variant playlist to %" GST_STIME_FORMAT,
GST_STIME_ARGS (stream->current_position));
map =
gst_hls_find_time_map (hlsdemux,
hls_stream->current_segment->discont_sequence);
if (map)
map->internal_time = GST_CLOCK_TIME_NONE;
gst_hls_update_time_mappings (hlsdemux, hls_stream->playlist);
gst_hls_media_playlist_dump (hls_stream->playlist);
if (gst_hls_media_playlist_get_starting_segment (hls_stream->playlist,
hls_stream->llhls_enabled, &seek_result)) {
hls_stream->current_segment = seek_result.segment;
hls_stream->in_partial_segments = seek_result.found_partial_segment;
hls_stream->part_idx = seek_result.part_idx;
hls_stream->current_segment->stream_time = stream->current_position;
gst_hls_media_playlist_recalculate_stream_time (hls_stream->playlist,
hls_stream->current_segment);
GST_DEBUG_OBJECT (stream,
"Resynced variant playlist to %" GST_STIME_FORMAT,
GST_STIME_ARGS (stream->current_position));
map =
gst_hls_find_time_map (hlsdemux,
hls_stream->current_segment->discont_sequence);
if (map)
map->internal_time = GST_CLOCK_TIME_NONE;
gst_hls_update_time_mappings (hlsdemux, hls_stream->playlist);
gst_hls_media_playlist_dump (hls_stream->playlist);
} else {
GST_ERROR_OBJECT (stream, "Failed to locate a segment to restart at!");
}
} else {
/* Force playlist update for the rendition streams, it will resync to the
* variant stream on the next round */
@ -2314,7 +2356,7 @@ gst_hls_demux_stream_update_media_playlist (GstHLSDemux * demux,
GST_STIME_ARGS (stream->current_segment->stream_time),
GST_STR_NULL (stream->current_segment->uri));
/* Use best-effort techniques to find the correponding current media segment
/* Use best-effort techniques to find the corresponding current media segment
* in the new playlist. This might be off in some cases, but it doesn't matter
* since we will be checking the embedded timestamp later */
new_segment =
@ -2496,18 +2538,17 @@ gst_hls_demux_stream_update_fragment_info (GstAdaptiveDemux2Stream * stream)
if (hlsdemux_stream->current_segment == NULL) {
GST_LOG_OBJECT (stream, "No current segment");
if (stream->current_position == GST_CLOCK_TIME_NONE) {
GST_DEBUG_OBJECT (stream, "Setting up initial segment");
hlsdemux_stream->current_segment =
gst_hls_media_playlist_get_starting_segment
(hlsdemux_stream->playlist);
GstM3U8SeekResult seek_result;
if (hlsdemux_stream->current_segment->partial_only) {
/* FIXME: We might find an independent partial segment
* that's still old enough (beyond the part_hold_back threshold)
* but closer to the live edge than the start of the segment. This
* check should be done inside get_starting_segment() */
hlsdemux_stream->in_partial_segments = TRUE;
hlsdemux_stream->part_idx = 0;
GST_DEBUG_OBJECT (stream, "Setting up initial segment");
if (gst_hls_media_playlist_get_starting_segment
(hlsdemux_stream->playlist, hlsdemux_stream->llhls_enabled,
&seek_result)) {
hlsdemux_stream->current_segment = seek_result.segment;
hlsdemux_stream->in_partial_segments =
seek_result.found_partial_segment;
hlsdemux_stream->part_idx = seek_result.part_idx;
}
} else {
if (gst_hls_media_playlist_has_lost_sync (hlsdemux_stream->playlist,

View file

@ -95,6 +95,11 @@ struct _GstHLSDemuxStream
/* A stream either variants or renditions */
gboolean is_variant;
/* A copy of the demuxer flag, stored when the
* stream is created, so it can't change after
* the stream starts downloading things */
gboolean llhls_enabled;
/* Rendition-specific fields */
GstStreamType rendition_type; /* FIXME: Also used by variant streams */
gchar *lang;
@ -192,6 +197,9 @@ struct _GstHLSDemux2
/* Initial bitrate to use before any bandwidth measurement */
guint start_bitrate;
/* Whether LL-HLS (Low Latency HLS) features are enabled */
gboolean llhls_enabled;
/* Decryption key cache: url => GstHLSKey */
GHashTable *keys;
GMutex keys_lock;

View file

@ -1215,28 +1215,116 @@ gst_hls_media_playlist_has_same_data (GstHLSMediaPlaylist * self,
* segment group that disappears before we're done with it.
* We want a segment or partial that contains a keyframe if possible
*/
GstM3U8MediaSegment *
gboolean
gst_hls_media_playlist_seek (GstHLSMediaPlaylist * playlist, gboolean forward,
GstSeekFlags flags, GstClockTimeDiff ts)
GstSeekFlags flags, GstClockTimeDiff ts, GstM3U8SeekResult * seek_result)
{
gboolean snap_nearest =
(flags & GST_SEEK_FLAG_SNAP_NEAREST) == GST_SEEK_FLAG_SNAP_NEAREST;
gboolean snap_after =
(flags & GST_SEEK_FLAG_SNAP_AFTER) == GST_SEEK_FLAG_SNAP_AFTER;
gboolean want_keyunit = (flags & GST_SEEK_FLAG_KEY_UNIT);
guint idx;
GstM3U8MediaSegment *res = NULL;
guint res_part_idx = 0;
GstClockTime partial_window_start = GST_CLOCK_TIME_NONE;
GST_DEBUG ("ts:%" GST_STIME_FORMAT " forward:%d playlist uri: %s",
GST_DEBUG ("target ts:%" GST_STIME_FORMAT " forward:%d playlist uri: %s",
GST_STIME_ARGS (ts), forward, playlist->uri);
/* Can't seek if there's no segments */
if (playlist->segments->len < 1)
return FALSE;
/* Calculate the threshold at which we might start inspecting partial segments */
if (flags & GST_HLS_M3U8_SEEK_FLAG_ALLOW_PARTIAL) {
GstM3U8MediaSegment *last_seg =
g_ptr_array_index (playlist->segments, playlist->segments->len - 1);
GstClockTime playlist_end = last_seg->stream_time + last_seg->duration;
if (playlist_end >= 2 * playlist->targetduration)
partial_window_start = playlist_end - 2 * playlist->targetduration;
else
partial_window_start = last_seg->stream_time;
GST_DEBUG ("Partial segment threshold %" GST_TIME_FORMAT,
GST_TIME_ARGS (partial_window_start));
}
for (idx = 0; idx < playlist->segments->len; idx++) {
GstM3U8MediaSegment *cand = g_ptr_array_index (playlist->segments, idx);
/* If only full segments was request, skip any segment that
* only has EXT-X-PARTs attached */
if (cand->partial_only && !(flags & GST_HLS_M3U8_SEEK_FLAG_ALLOW_PARTIAL))
continue;
if (flags & GST_HLS_M3U8_SEEK_FLAG_ALLOW_PARTIAL &&
GST_CLOCK_TIME_IS_VALID (partial_window_start) &&
cand->stream_time + cand->duration > partial_window_start) {
/* Permitted to land at a partial segment, but only do so if
* they are in the last 2 target durations of the playlist, so we can
* be fairly sure we'll have to time download them all before
* they get removed.
*
* 6.2.2: EXT-X-PART tags SHOULD be removed from the Playlist after they are
* greater than three Target Durations from the end of the Playlist.
* Clients MUST be able to download the Partial Segment for at least
* three Target Durations after the EXT-X-PART tag is removed from the
* Playlist.
*/
if (cand->partial_segments != NULL) {
guint part_idx;
guint last_independent_idx = 0;
for (part_idx = 0; part_idx < cand->partial_segments->len; part_idx++) {
GstM3U8PartialSegment *part =
g_ptr_array_index (cand->partial_segments, part_idx);
GST_LOG ("Inspecting partial segment sn:%" G_GINT64_FORMAT
" idx %u stream_time:%" GST_STIME_FORMAT " duration:%"
GST_TIME_FORMAT, cand->sequence, part_idx,
GST_STIME_ARGS (part->stream_time),
GST_TIME_ARGS (part->duration));
if ((forward & snap_after) || snap_nearest) {
if (!want_keyunit || part->independent) {
if (part->stream_time >= ts ||
(snap_nearest
&& (ts - part->stream_time < part->duration / 2))) {
res = cand;
res_part_idx = part_idx;
goto partial_seg_out;
}
}
} else if (!forward && snap_after) {
GstClockTime next_pos = cand->stream_time + cand->duration;
if (!want_keyunit || part->independent) {
if (next_pos <= ts && ts < next_pos + cand->duration) {
res = cand;
res_part_idx = part_idx;
goto partial_seg_out;
}
}
} else if (part->stream_time <= ts
&& ts < part->stream_time + part->duration) {
res = cand;
if (!want_keyunit || part->independent)
res_part_idx = part_idx;
else
res_part_idx = last_independent_idx;
goto partial_seg_out;
}
if (part->independent)
last_independent_idx = part_idx;
}
}
} else if (cand->partial_only) {
/* If only full segments were requested or we're still outside the partial segment
* window, skip the last segment if it only has EXT-X-PARTs attached */
continue;
}
/* For full segment alignment, we ignore the KEY_UNIT flag and assume
* all segments have a keyframe, since HLS doesn't give us reliable info
* about that */
if ((forward & snap_after) || snap_nearest) {
if (cand->stream_time >= ts ||
(snap_nearest && (ts - cand->stream_time < cand->duration / 2))) {
@ -1262,12 +1350,37 @@ out:
GST_DEBUG ("Returning segment sn:%" G_GINT64_FORMAT " stream_time:%"
GST_STIME_FORMAT " duration:%" GST_TIME_FORMAT, res->sequence,
GST_STIME_ARGS (res->stream_time), GST_TIME_ARGS (res->duration));
gst_m3u8_media_segment_ref (res);
} else {
GST_DEBUG ("Couldn't find a match");
seek_result->stream_time = res->stream_time;
seek_result->segment = gst_m3u8_media_segment_ref (res);
seek_result->found_partial_segment = res->partial_only;
seek_result->part_idx = 0;
return TRUE;
}
return res;
GST_DEBUG ("Couldn't find a match");
return FALSE;
partial_seg_out:
if (res && res->partial_segments != NULL
&& res_part_idx < res->partial_segments->len) {
GstM3U8PartialSegment *part =
g_ptr_array_index (res->partial_segments, res_part_idx);
GST_DEBUG ("Returning partial segment sn:%" G_GINT64_FORMAT
" part_idx %u stream_time:%" GST_STIME_FORMAT " duration:%"
GST_TIME_FORMAT, res->sequence, res_part_idx,
GST_STIME_ARGS (part->stream_time), GST_TIME_ARGS (part->duration));
seek_result->stream_time = part->stream_time;
seek_result->segment = gst_m3u8_media_segment_ref (res);
seek_result->found_partial_segment = TRUE;
seek_result->part_idx = res_part_idx;
return TRUE;
}
GST_DEBUG ("Couldn't find a match");
return FALSE;
}
static gboolean
@ -1780,10 +1893,11 @@ gst_hls_media_playlist_sync_to_segment (GstHLSMediaPlaylist * playlist,
return res;
}
GstM3U8MediaSegment *
gst_hls_media_playlist_get_starting_segment (GstHLSMediaPlaylist * self)
gboolean
gst_hls_media_playlist_get_starting_segment (GstHLSMediaPlaylist * self,
gboolean low_latency, GstM3U8SeekResult * seek_result)
{
GstM3U8MediaSegment *res;
GstM3U8MediaSegment *res = NULL;
GST_DEBUG ("playlist %s", self->uri);
@ -1791,20 +1905,79 @@ gst_hls_media_playlist_get_starting_segment (GstHLSMediaPlaylist * self)
/* For non-live, we just grab the first one */
res = g_ptr_array_index (self->segments, 0);
} else {
/* Live playlist */
res =
g_ptr_array_index (self->segments,
MAX ((gint) self->segments->len - GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE -
1, 0));
GstClockTime hold_back = GST_CLOCK_TIME_NONE;
/* Live playlist. If low-latency, use the PART-HOLD-BACK specified distance
* from the end, otherwise HOLD-BACK distance or if that's not provided,
* then 3 target durations */
if (low_latency) {
if (GST_CLOCK_TIME_IS_VALID (self->part_hold_back))
hold_back = self->part_hold_back;
else if (GST_CLOCK_TIME_IS_VALID (self->partial_targetduration))
hold_back = 3 * self->partial_targetduration;
} else {
if (GST_CLOCK_TIME_IS_VALID (self->hold_back))
hold_back = self->hold_back;
else if (GST_CLOCK_TIME_IS_VALID (self->targetduration))
hold_back = 3 * self->targetduration;
}
if (GST_CLOCK_TIME_IS_VALID (hold_back)) {
GstSeekFlags flags = GST_SEEK_FLAG_SNAP_BEFORE | GST_SEEK_FLAG_KEY_UNIT;
GstM3U8MediaSegment *last_seg =
g_ptr_array_index (self->segments, self->segments->len - 1);
GstClockTime playlist_duration =
last_seg->stream_time + last_seg->duration;
GstClockTime target_ts;
/* Clamp the hold back so we don't go below zero */
if (hold_back > playlist_duration)
hold_back = playlist_duration;
target_ts = playlist_duration - hold_back;
GST_DEBUG ("Hold back is %" GST_TIME_FORMAT
" Looking for a segment before %" GST_TIME_FORMAT,
GST_TIME_ARGS (hold_back), GST_TIME_ARGS (target_ts));
if (low_latency)
flags |= GST_HLS_M3U8_SEEK_FLAG_ALLOW_PARTIAL;
if (gst_hls_media_playlist_seek (self, TRUE, flags, target_ts,
seek_result)) {
#ifndef GST_DISABLE_GST_DEBUG
GstClockTime distance_from_edge =
playlist_duration - seek_result->stream_time;
GST_DEBUG ("Found starting position %" GST_TIME_FORMAT " which is %"
GST_TIME_FORMAT " from the live edge",
GST_TIME_ARGS (seek_result->stream_time),
GST_TIME_ARGS (distance_from_edge));
#endif
return TRUE;
}
}
/* Worst case fallback, start 3 fragments from the end */
if (res == NULL) {
res =
g_ptr_array_index (self->segments,
MAX ((gint) self->segments->len -
GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE - 1, 0));
}
}
if (res) {
GST_DEBUG ("Using segment sn:%" G_GINT64_FORMAT " dsn:%" G_GINT64_FORMAT,
res->sequence, res->discont_sequence);
gst_m3u8_media_segment_ref (res);
seek_result->stream_time = res->stream_time;
seek_result->segment = gst_m3u8_media_segment_ref (res);
seek_result->found_partial_segment = FALSE;
seek_result->part_idx = 0;
return TRUE;
}
return res;
return FALSE;
}
/* Calls this to carry over stream time, DSN, ... from one playlist to another.

View file

@ -290,8 +290,9 @@ gst_hls_media_playlist_advance_fragment (GstHLSMediaPlaylist * m3u8,
gboolean forward,
gboolean allow_partial_only_segment);
GstM3U8MediaSegment *
gst_hls_media_playlist_get_starting_segment (GstHLSMediaPlaylist *self);
gboolean
gst_hls_media_playlist_get_starting_segment (GstHLSMediaPlaylist *self, gboolean low_latency,
GstM3U8SeekResult *seek_result);
GstClockTime
gst_hls_media_playlist_get_duration (GstHLSMediaPlaylist * m3u8);
@ -311,11 +312,12 @@ gboolean
gst_hls_media_playlist_has_lost_sync (GstHLSMediaPlaylist * m3u8,
GstClockTime position);
GstM3U8MediaSegment *
gst_hls_media_playlist_seek (GstHLSMediaPlaylist *playlist,
gboolean
gst_hls_media_playlist_seek (GstHLSMediaPlaylist *playlist,
gboolean forward,
GstSeekFlags flags,
GstClockTimeDiff ts);
GstClockTimeDiff ts,
GstM3U8SeekResult *seek_result);
gboolean
gst_hls_media_playlist_find_position (GstHLSMediaPlaylist *playlist,

View file

@ -675,11 +675,15 @@ GST_START_TEST (test_advance_fragment)
{
GstHLSMediaPlaylist *pl;
GstM3U8MediaSegment *mf;
GstM3U8SeekResult seek_result;
pl = load_m3u8 (BYTE_RANGES_PLAYLIST);
/* Check the next fragment */
mf = gst_hls_media_playlist_get_starting_segment (pl);
fail_unless (gst_hls_media_playlist_get_starting_segment (pl, FALSE,
&seek_result) == TRUE);
mf = seek_result.segment;
fail_unless (mf != NULL);
assert_equals_int (mf->discont, FALSE);
assert_equals_string (mf->uri, "http://media.example.com/all.ts");