X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=modules%2Fstream_filter%2Fhttplive.c;h=27d027a11528c796632fe2c09f824a69e338dda7;hb=9994020808c74044174d589418db790b41e00f0d;hp=d2b4ad95a3ec358c3962574459516055c98258ae;hpb=fa6e476e6697bf4b584330e53c6b94f5b50be721;p=vlc diff --git a/modules/stream_filter/httplive.c b/modules/stream_filter/httplive.c index d2b4ad95a3..27d027a115 100644 --- a/modules/stream_filter/httplive.c +++ b/modules/stream_filter/httplive.c @@ -61,7 +61,7 @@ vlc_module_end() typedef struct segment_s { int sequence; /* unique sequence number */ - int length; /* segment duration (seconds) */ + int duration; /* segment duration (seconds) */ uint64_t size; /* segment size in bytes */ uint64_t bandwidth; /* bandwidth usage of segments (bits per second)*/ @@ -77,6 +77,7 @@ typedef struct hls_stream_s int sequence; /* media sequence number */ int duration; /* maximum duration per segment (ms) */ uint64_t bandwidth; /* bandwidth usage of segments (bits per second)*/ + uint64_t size; /* stream length (segment->duration * hls->bandwidth/8) */ vlc_array_t *segments; /* list of segments */ vlc_url_t url; /* uri to m3u8 */ @@ -89,30 +90,44 @@ typedef struct VLC_COMMON_MEMBERS /* */ - int current; /* current hls_stream */ - int segment; /* current segment for downloading */ - vlc_array_t *hls_stream;/* bandwidth adaptation */ - stream_t *s; } hls_thread_t; struct stream_sys_t { access_t *p_access; /* HTTP access input */ + vlc_url_t m3u8; /* M3U8 url */ /* */ hls_thread_t *thread; - vlc_array_t *hls_stream;/* bandwidth adaptation */ + vlc_array_t *hls_stream;/* bandwidth adaptation */ + uint64_t bandwidth; /* measured bandwidth (bits per second) */ + + /* Download */ + struct hls_download_s + { + int current; /* current hls_stream */ + int segment; /* current segment for downloading */ + int seek; /* segment requested by seek (default -1) */ + vlc_mutex_t lock_wait; /* protect segment download counter */ + vlc_cond_t wait; /* some condition to wait on */ + } download; /* Playback */ - uint64_t offset; /* current offset in media */ - int current; /* current hls_stream */ - int segment; /* current segment for playback */ + struct hls_playback_s + { + uint64_t offset; /* current offset in media */ + int current; /* current hls_stream */ + int segment; /* current segment for playback */ + } playback; /* Playlist */ - mtime_t last; /* playlist last loaded */ - mtime_t wakeup; /* next reload time */ - int tries; /* times it was not changed */ + struct hls_playlist_s + { + mtime_t last; /* playlist last loaded */ + mtime_t wakeup; /* next reload time */ + int tries; /* times it was not changed */ + } playlist; /* state */ bool b_cache; /* can cache files */ @@ -136,6 +151,7 @@ static int AccessDownload(stream_t *s, segment_t *segment); static void* hls_Thread(vlc_object_t *); static int get_HTTPLivePlaylist(stream_t *s, hls_stream_t *hls); +static segment_t *segment_GetSegment(hls_stream_t *hls, int wanted); static void segment_Free(segment_t *segment); /**************************************************************************** @@ -180,6 +196,7 @@ static hls_stream_t *hls_New(vlc_array_t *hls_stream, int id, uint64_t bw, char hls->id = id; hls->bandwidth = bw; hls->duration = -1;/* unknown */ + hls->size = 0; hls->sequence = 0; /* default is 0 */ hls->version = 1; /* default protocol version */ hls->b_cache = true; @@ -233,27 +250,40 @@ static hls_stream_t *hls_GetLast(vlc_array_t *hls_stream) return (hls_stream_t *) hls_Get(hls_stream, count); } -static int hls_LowestBandwidthStream(vlc_array_t *hls_stream) +static hls_stream_t *hls_Find(vlc_array_t *hls_stream, hls_stream_t *hls_new) { int count = vlc_array_count(hls_stream); - int i_lowest = 0; - uint64_t lowest = 0; - - for (int i = 0; i < count; i++) + for (int n = 0; n < count; n++) { - hls_stream_t *hls = (hls_stream_t *) vlc_array_item_at_index(hls_stream, i); - if (lowest == 0) + hls_stream_t *hls = vlc_array_item_at_index(hls_stream, n); + if (hls) { - lowest = hls->bandwidth; - i_lowest = i; + /* compare */ + if ((hls->id == hls_new->id) && + (hls->bandwidth == hls_new->bandwidth)) + return hls; } - else if (hls->bandwidth < lowest) + } + return NULL; +} + +static uint64_t hls_GetStreamSize(hls_stream_t *hls) +{ + /* NOTE: Stream size is calculated based on segment duration and + * HLS stream bandwidth from the .m3u8 file. If these are not correct + * then the deviation from exact byte size will be big and the seek/ + * progressbar will not behave entirely as one expects. */ + uint64_t size = 0UL; + int count = vlc_array_count(hls->segments); + for (int n = 0; n < count; n++) + { + segment_t *segment = segment_GetSegment(hls, n); + if (segment) { - lowest = hls->bandwidth; - i_lowest = i; + size += (segment->duration * (hls->bandwidth / 8)); } } - return i_lowest; + return size; } /* Segment */ @@ -263,7 +293,7 @@ static segment_t *segment_New(hls_stream_t* hls, int duration, char *uri) if (segment == NULL) return NULL; - segment->length = duration; /* seconds */ + segment->duration = duration; /* seconds */ segment->size = 0; /* bytes */ segment->sequence = 0; segment->bandwidth = 0; @@ -297,6 +327,22 @@ static segment_t *segment_GetSegment(hls_stream_t *hls, int wanted) return (segment_t *) vlc_array_item_at_index(hls->segments, wanted); } +static segment_t *segment_Find(hls_stream_t *hls, int sequence) +{ + assert(hls); + + int count = vlc_array_count(hls->segments); + if (count <= 0) return NULL; + for (int n = 0; n < count; n++) + { + segment_t *segment = vlc_array_item_at_index(hls->segments, n); + if (segment == NULL) break; + if (segment->sequence == sequence) + return segment; + } + return NULL; +} + /* Parsing */ static char *parse_Attributes(const char *line, const char *attr) { @@ -329,35 +375,39 @@ static char *parse_Attributes(const char *line, const char *attr) return NULL; } -static char *relative_URI(stream_t *s, const char *uri, char *psz_uri) +static char *relative_URI(stream_t *s, const char *uri, const char *path) { + stream_sys_t *p_sys = s->p_sys; + char *p = strchr(uri, ':'); if (p != NULL) return NULL; - char *tmp; - if (asprintf(&tmp,"%s://%s", s->psz_access, s->psz_path) < 0) + char *psz_path = strdup(p_sys->m3u8.psz_path); + if (psz_path == NULL) return NULL; + p = strrchr(psz_path, '/'); + if (p) *p = '\0'; + + char *psz_uri = NULL; + if (p_sys->m3u8.psz_password || p_sys->m3u8.psz_username) { - s->p_sys->b_error = true; - return NULL; + if (asprintf(&psz_uri, "%s://%s:%s@%s%s/%s", p_sys->m3u8.psz_protocol, + p_sys->m3u8.psz_username, p_sys->m3u8.psz_password, + p_sys->m3u8.psz_host, path ? path : psz_path, uri) < 0) + goto fail; } - - char *psz_path = strrchr(tmp, '/'); - if (psz_path) *psz_path = '\0'; - - vlc_url_t url; - vlc_UrlParse(&url, tmp, 0); - if (asprintf(&psz_uri, "%s://%s%s/%s", - url.psz_protocol, url.psz_host, url.psz_path, uri) < 0) + else { - free(tmp); - vlc_UrlClean(&url); - s->p_sys->b_error = true; - return NULL; + if (asprintf(&psz_uri, "%s://%s%s/%s", p_sys->m3u8.psz_protocol, + p_sys->m3u8.psz_host, path ? path : psz_path, uri) < 0) + goto fail; } - vlc_UrlClean(&url); - free(tmp); + free(psz_path); return psz_uri; + +fail: + free(psz_path); + return NULL; } static void parse_SegmentInformation(stream_t *s, hls_stream_t *hls, char *p_read, char *uri) @@ -375,25 +425,33 @@ static void parse_SegmentInformation(stream_t *s, hls_stream_t *hls, char *p_rea return; } - char *psz_uri = NULL; - psz_uri = relative_URI(s, uri, psz_uri); + char *psz_path = strdup(hls->url.psz_path); + if (psz_path == NULL) + { + p_sys->b_error = true; + return; + } + char *p = strrchr(psz_path, '/'); + if (p) *p = '\0'; + char *psz_uri = relative_URI(s, uri, psz_path); + free(psz_path); vlc_mutex_lock(&hls->lock); segment_t *segment = segment_New(hls, duration, psz_uri ? psz_uri : uri); if (segment) - segment->sequence = hls->sequence + vlc_array_count(hls->segments); + segment->sequence = hls->sequence + vlc_array_count(hls->segments) - 1; if (duration > hls->duration) { msg_Err(s, "EXTINF:%d duration is larger then EXT-X-TARGETDURATION:%d", duration, hls->duration); } vlc_mutex_unlock(&hls->lock); + + free(psz_uri); } -static void parse_TargetDuration(stream_t *s, hls_stream_t *hls, char *p_read) +static int parse_TargetDuration(stream_t *s, hls_stream_t *hls, char *p_read) { - stream_sys_t *p_sys = s->p_sys; - assert(hls); int duration = -1; @@ -401,14 +459,15 @@ static void parse_TargetDuration(stream_t *s, hls_stream_t *hls, char *p_read) if (ret != 1) { msg_Err(s, "expected #EXT-X-TARGETDURATION:"); - p_sys->b_error = true; - return; + return VLC_EGENERIC; } hls->duration = duration; /* seconds */ + return VLC_SUCCESS; } -static void parse_StreamInformation(stream_t *s, char *p_read, char *uri) +static void parse_StreamInformation(stream_t *s, vlc_array_t **hls_stream, + char *p_read, char *uri) { stream_sys_t *p_sys = s->p_sys; @@ -445,18 +504,17 @@ static void parse_StreamInformation(stream_t *s, char *p_read, char *uri) msg_Info(s, "bandwidth adaption detected (program-id=%d, bandwidth=%"PRIu64").", id, bw); - char *psz_uri = NULL; - psz_uri = relative_URI(s, uri, psz_uri); + char *psz_uri = relative_URI(s, uri, NULL); - hls_stream_t *hls = hls_New(p_sys->hls_stream, id, bw, psz_uri ? psz_uri : uri); + hls_stream_t *hls = hls_New(*hls_stream, id, bw, psz_uri ? psz_uri : uri); if (hls == NULL) p_sys->b_error = true; + + free(psz_uri); } -static void parse_MediaSequence(stream_t *s, hls_stream_t *hls, char *p_read) +static int parse_MediaSequence(stream_t *s, hls_stream_t *hls, char *p_read) { - stream_sys_t *p_sys = s->p_sys; - assert(hls); int sequence; @@ -464,37 +522,37 @@ static void parse_MediaSequence(stream_t *s, hls_stream_t *hls, char *p_read) if (ret != 1) { msg_Err(s, "expected #EXT-X-MEDIA-SEQUENCE:"); - p_sys->b_error = true; - return; + return VLC_EGENERIC; } if (hls->sequence > 0) msg_Err(s, "EXT-X-MEDIA-SEQUENCE already present in playlist"); hls->sequence = sequence; + return VLC_SUCCESS; } -static void parse_Key(stream_t *s, hls_stream_t *hls, char *p_read) +static int parse_Key(stream_t *s, hls_stream_t *hls, char *p_read) { - stream_sys_t *p_sys = s->p_sys; - assert(hls); /* #EXT-X-KEY:METHOD=[,URI=""][,IV=] */ - char *attr; - attr = parse_Attributes(p_read, "METHOD"); + int err = VLC_SUCCESS; + char *attr = parse_Attributes(p_read, "METHOD"); if (attr == NULL) { msg_Err(s, "#EXT-X-KEY: expected METHOD="); - p_sys->b_error = true; + return err; } - else if (strncasecmp(attr, "NONE", 4) == 0) + + if (strncasecmp(attr, "NONE", 4) == 0) { + char *uri = parse_Attributes(p_read, "URI"); if (uri != NULL) { msg_Err(s, "#EXT-X-KEY: URI not expected"); - p_sys->b_error = true; + err = VLC_EGENERIC; } free(uri); /* IV is only supported in version 2 and above */ @@ -504,7 +562,7 @@ static void parse_Key(stream_t *s, hls_stream_t *hls, char *p_read) if (iv != NULL) { msg_Err(s, "#EXT-X-KEY: IV not expected"); - p_sys->b_error = true; + err = VLC_EGENERIC; } free(iv); } @@ -512,21 +570,21 @@ static void parse_Key(stream_t *s, hls_stream_t *hls, char *p_read) else { msg_Warn(s, "playback of encrypted HTTP Live media is not supported."); - p_sys->b_error = true; + err = VLC_EGENERIC; } free(attr); + return err; } -static void parse_ProgramDateTime(stream_t *s, hls_stream_t *hls, char *p_read) +static int parse_ProgramDateTime(stream_t *s, hls_stream_t *hls, char *p_read) { VLC_UNUSED(hls); msg_Dbg(s, "tag not supported: #EXT-X-PROGRAM-DATE-TIME %s", p_read); + return VLC_SUCCESS; } -static void parse_AllowCache(stream_t *s, hls_stream_t *hls, char *p_read) +static int parse_AllowCache(stream_t *s, hls_stream_t *hls, char *p_read) { - stream_sys_t *p_sys = s->p_sys; - assert(hls); char answer[4] = "\0"; @@ -534,17 +592,15 @@ static void parse_AllowCache(stream_t *s, hls_stream_t *hls, char *p_read) if (ret != 1) { msg_Err(s, "#EXT-X-ALLOW-CACHE, ignoring ..."); - p_sys->b_error = true; - return; + return VLC_EGENERIC; } hls->b_cache = (strncmp(answer, "NO", 2) != 0); + return VLC_SUCCESS; } -static void parse_Version(stream_t *s, hls_stream_t *hls, char *p_read) +static int parse_Version(stream_t *s, hls_stream_t *hls, char *p_read) { - stream_sys_t *p_sys = s->p_sys; - assert(hls); int version; @@ -552,8 +608,7 @@ static void parse_Version(stream_t *s, hls_stream_t *hls, char *p_read) if (ret != 1) { msg_Err(s, "#EXT-X-VERSION: no protocol version found, should be version 1."); - p_sys->b_error = true; - return; + return VLC_EGENERIC; } /* Check version */ @@ -561,48 +616,53 @@ static void parse_Version(stream_t *s, hls_stream_t *hls, char *p_read) if (hls->version != 1) { msg_Err(s, "#EXT-X-VERSION should be version 1 iso %d", version); - p_sys->b_error = true; + return VLC_EGENERIC; } + return VLC_SUCCESS; } -static void parse_EndList(stream_t *s, hls_stream_t *hls) +static int parse_EndList(stream_t *s, hls_stream_t *hls) { - stream_sys_t *p_sys = s->p_sys; - assert(hls); - p_sys->b_live = false; + s->p_sys->b_live = false; msg_Info(s, "video on demand (vod) mode"); + return VLC_SUCCESS; } -static void parse_Discontinuity(stream_t *s, hls_stream_t *hls, char *p_read) +static int parse_Discontinuity(stream_t *s, hls_stream_t *hls, char *p_read) { assert(hls); /* FIXME: Do we need to act on discontinuity ?? */ msg_Dbg(s, "#EXT-X-DISCONTINUITY %s", p_read); + return VLC_SUCCESS; } static void parse_M3U8ExtLine(stream_t *s, hls_stream_t *hls, char *line) { if (*line == '#') { + int err = VLC_SUCCESS; if (strncmp(line, "#EXT-X-TARGETDURATION", 21) == 0) - parse_TargetDuration(s, hls, line); - else if (strncmp(line, "#EXT-X-MEDIA-SEQUENCE", 22) == 0) - parse_MediaSequence(s, hls, line); - else if (strncmp(line, "#EXT-X-KEY", 11) == 0) - parse_Key(s, hls, line); - else if (strncmp(line, "#EXT-X-PROGRAM-DATE-TIME", 25) == 0) - parse_ProgramDateTime(s, hls, line); - else if (strncmp(line, "#EXT-X-ALLOW-CACHE", 17) == 0) - parse_AllowCache(s, hls, line); + err = parse_TargetDuration(s, hls, line); + else if (strncmp(line, "#EXT-X-MEDIA-SEQUENCE", 21) == 0) + err = parse_MediaSequence(s, hls, line); + else if (strncmp(line, "#EXT-X-KEY", 10) == 0) + err = parse_Key(s, hls, line); + else if (strncmp(line, "#EXT-X-PROGRAM-DATE-TIME", 24) == 0) + err = parse_ProgramDateTime(s, hls, line); + else if (strncmp(line, "#EXT-X-ALLOW-CACHE", 18) == 0) + err = parse_AllowCache(s, hls, line); else if (strncmp(line, "#EXT-X-DISCONTINUITY", 20) == 0) - parse_Discontinuity(s, hls, line); + err = parse_Discontinuity(s, hls, line); else if (strncmp(line, "#EXT-X-VERSION", 14) == 0) - parse_Version(s, hls, line); + err = parse_Version(s, hls, line); else if (strncmp(line, "#EXT-X-ENDLIST", 14) == 0) - parse_EndList(s, hls); + err = parse_EndList(s, hls); + + if (err != VLC_SUCCESS) + s->p_sys->b_error = true; } } @@ -673,6 +733,106 @@ static int get_HTTPLivePlaylist(stream_t *s, hls_stream_t *hls) AccessClose(s); return VLC_SUCCESS; +error: + free(line); + free(tmp); + AccessClose(s); + return VLC_EGENERIC; +} + +static int get_HTTPLiveMetaPlaylist(stream_t *s, vlc_array_t **streams) +{ + stream_sys_t *p_sys = s->p_sys; + assert(*streams); + + /* Download new playlist file from server */ + if (AccessOpen(s, &p_sys->m3u8) != VLC_SUCCESS) + return VLC_EGENERIC; + + /* Parse the rest of the reply */ + uint8_t *tmp = calloc(1, HTTPLIVE_MAX_LINE); + if (tmp == NULL) + { + AccessClose(s); + return VLC_ENOMEM; + } + + char *line = AccessReadLine(p_sys->p_access, tmp, HTTPLIVE_MAX_LINE); + if (strncmp(line, "#EXTM3U", 7) != 0) + { + msg_Err(s, "missing #EXTM3U tag"); + goto error; + } + free(line); + line = NULL; + + for( ; ; ) + { + line = AccessReadLine(p_sys->p_access, tmp, HTTPLIVE_MAX_LINE); + if (line == NULL) + { + msg_Dbg(s, "end of data"); + break; + } + + if (!vlc_object_alive(s)) + goto error; + + /* some more checks for actual data */ + if (strncmp(line, "#EXT-X-STREAM-INF", 17) == 0) + { + p_sys->b_meta = true; + char *uri = AccessReadLine(p_sys->p_access, tmp, HTTPLIVE_MAX_LINE); + if (uri == NULL) + p_sys->b_error = true; + else + { + parse_StreamInformation(s, streams, line, uri); + free(uri); + } + } + else if (strncmp(line, "#EXTINF", 7) == 0) + { + char *uri = AccessReadLine(p_sys->p_access, tmp, HTTPLIVE_MAX_LINE); + if (uri == NULL) + p_sys->b_error = true; + else + { + hls_stream_t *hls = hls_GetLast(*streams); + if (hls) + parse_SegmentInformation(s, hls, line, uri); + else + p_sys->b_error = true; + free(uri); + } + } + else + { + hls_stream_t *hls = hls_GetLast(*streams); + if ((hls == NULL) && (!p_sys->b_meta)) + { + hls = hls_New(*streams, -1, -1, NULL); + if (hls == NULL) + { + p_sys->b_error = true; + return VLC_ENOMEM; + } + } + parse_M3U8ExtLine(s, hls, line); + } + + /* Error during m3u8 parsing abort */ + if (p_sys->b_error) + goto error; + + free(line); + } + + free(line); + free(tmp); + AccessClose(s); + return VLC_SUCCESS; + error: free(line); free(tmp); @@ -732,7 +892,7 @@ static int parse_HTTPLiveStreaming(stream_t *s) p_sys->b_error = true; else { - parse_StreamInformation(s, p_read, uri); + parse_StreamInformation(s, &p_sys->hls_stream, p_read, uri); free(uri); } } @@ -791,32 +951,149 @@ static int parse_HTTPLiveStreaming(stream_t *s) } } + vlc_mutex_lock(&hls->lock); if (p_sys->b_live) { + /* There should at least be 3 segments of hls->duration */ int ok = 0; int num = vlc_array_count(hls->segments); for (int i = 0; i < num; i++) { segment_t *segment = segment_GetSegment(hls, i); - if (segment && segment->length >= hls->duration) + if (segment && segment->duration >= hls->duration) ok++; } if (ok < 3) { msg_Err(s, "cannot start live playback at this time, try again later."); + vlc_mutex_unlock(&hls->lock); return VLC_EGENERIC; } - /* Determine next time to reload playlist */ - p_sys->wakeup = p_sys->last + (hls->duration * 2 * (mtime_t)1000000); } - /* Can we cache files after playback */ - p_sys->b_cache = hls->b_cache; + else + { + /* Stream size (approximate) */ + hls->size = hls_GetStreamSize(hls); + } + vlc_mutex_unlock(&hls->lock); } return VLC_SUCCESS; } +/* Reload playlist */ +static int hls_UpdatePlaylist(stream_t *s, hls_stream_t *hls_new, hls_stream_t **hls) +{ + int count = vlc_array_count(hls_new->segments); + + msg_Info(s, "updating hls stream (program-id=%d, bandwidth=%"PRIu64") has %d segments", + hls_new->id, hls_new->bandwidth, count); + for (int n = 0; n < count; n++) + { + segment_t *p = segment_GetSegment(hls_new, n); + if (p == NULL) return VLC_EGENERIC; + + vlc_mutex_lock(&(*hls)->lock); + segment_t *segment = segment_Find(*hls, p->sequence); + if (segment) + { + /* they should be the same */ + if ((p->sequence != segment->sequence) || + (p->duration != segment->duration) || + (strcmp(p->url.psz_path, segment->url.psz_path) != 0)) + { + msg_Err(s, "existing segment %d found with different content", + p->sequence); + msg_Err(s, "- sequence: new=%d, old=%d", p->sequence, segment->sequence); + msg_Err(s, "- duration: new=%d, old=%d", p->duration, segment->duration); + msg_Err(s, "- file: new=%s, old=%s", p->url.psz_path, segment->url.psz_path); + } + } + else + { + int last = vlc_array_count((*hls)->segments) - 1; + segment_t *l = segment_GetSegment(*hls, last); + if (l == NULL) goto fail_and_unlock; + + if ((l->sequence + 1) == p->sequence) + { + vlc_array_append((*hls)->segments, p); + msg_Info(s, "- segment %d appended", p->sequence); + } + else /* there is a gap */ + { + msg_Err(s, "gap in sequence numbers found: new=%d expected old=%d", + p->sequence, l->sequence); + goto fail_and_unlock; + } + } + vlc_mutex_unlock(&(*hls)->lock); + } + return VLC_SUCCESS; + +fail_and_unlock: + vlc_mutex_unlock(&(*hls)->lock); + return VLC_EGENERIC; +} + +static int hls_ReloadPlaylist(stream_t *s) +{ + stream_sys_t *p_sys = s->p_sys; + + vlc_array_t *hls_streams = vlc_array_new(); + if (hls_streams == NULL) + return VLC_ENOMEM; + + msg_Info(s, "Reloading HLS live meta playlist"); + if (get_HTTPLiveMetaPlaylist(s, &hls_streams) != VLC_SUCCESS) + goto fail; + + int count = vlc_array_count(hls_streams); + + /* Is it a meta playlist? */ + if (p_sys->b_meta) + { + for (int n = 0; n < count; n++) + { + hls_stream_t *hls = hls_Get(hls_streams, n); + if (hls == NULL) goto fail; + + msg_Info(s, "parsing %s", hls->url.psz_path); + if (get_HTTPLivePlaylist(s, hls) != VLC_SUCCESS) + { + msg_Err(s, "could not parse playlist file from meta index." ); + goto fail; + } + } + } + + /* merge playlists */ + for (int n = 0; n < count; n++) + { + hls_stream_t *hls_new = hls_Get(hls_streams, n); + if (hls_new == NULL) goto fail; + + hls_stream_t *hls_old = hls_Find(p_sys->hls_stream, hls_new); + if (hls_old == NULL) + { /* new hls stream - append */ + vlc_array_append(p_sys->hls_stream, hls_new); + msg_Info(s, "new HLS stream appended (id=%d, bandwidth=%"PRIu64")", + hls_new->id, hls_new->bandwidth); + } + else if (hls_UpdatePlaylist(s, hls_new, &hls_old) != VLC_SUCCESS) + goto fail; + } + + vlc_array_destroy(hls_streams); + return VLC_SUCCESS; + +fail: + msg_Err(s, "reloading playlist failed"); + vlc_array_destroy(hls_streams); + return VLC_EGENERIC; +} + /**************************************************************************** * hls_Thread ****************************************************************************/ @@ -825,6 +1102,7 @@ static int BandwidthAdaptation(stream_t *s, int progid, uint64_t *bandwidth) stream_sys_t *p_sys = s->p_sys; int candidate = -1; uint64_t bw = *bandwidth; + uint64_t bw_candidate = 0; int count = vlc_array_count(p_sys->hls_stream); for (int n = 0; n < count; n++) @@ -836,22 +1114,21 @@ static int BandwidthAdaptation(stream_t *s, int progid, uint64_t *bandwidth) /* only consider streams with the same PROGRAM-ID */ if (hls->id == progid) { - if (bw >= hls->bandwidth) + if ((bw >= hls->bandwidth) && (bw_candidate < hls->bandwidth)) { msg_Dbg(s, "candidate %d bandwidth (bits/s) %"PRIu64" >= %"PRIu64, n, bw, hls->bandwidth); /* bits / s */ - *bandwidth = hls->bandwidth; + bw_candidate = hls->bandwidth; candidate = n; /* possible candidate */ } } } + *bandwidth = bw_candidate; return candidate; } static int Download(stream_t *s, hls_stream_t *hls, segment_t *segment, int *cur_stream) { - stream_sys_t *p_sys = s->p_sys; - assert(hls); assert(segment); @@ -863,6 +1140,21 @@ static int Download(stream_t *s, hls_stream_t *hls, segment_t *segment, int *cur return VLC_SUCCESS; } + /* sanity check - can we download this segment on time? */ + if (s->p_sys->bandwidth > 0) + { + uint64_t size = (segment->duration * hls->bandwidth); /* bits */ + int estimated = (int)(size / s->p_sys->bandwidth); + if (estimated > segment->duration) + { + msg_Err(s, "cannot quarantee smooth playback"); + msg_Warn(s,"downloading of segment %d takes %ds, which is longer then its playback (%ds)", + segment->sequence, estimated, segment->duration); + vlc_mutex_unlock(&segment->lock); + return VLC_EGENERIC; + } + } + mtime_t start = mdate(); if (AccessDownload(s, segment) != VLC_SUCCESS) { @@ -873,23 +1165,8 @@ static int Download(stream_t *s, hls_stream_t *hls, segment_t *segment, int *cur vlc_mutex_unlock(&segment->lock); - /* thread is not started yet */ - if (p_sys->thread == NULL) - { - msg_Info(s, "downloaded segment %d from stream %d", - p_sys->segment, p_sys->current); - return VLC_SUCCESS; - } - else - { - /* Do bandwidth calculations when there are at least 3 segments - downloaded */ - msg_Info(s, "downloaded segment %d from stream %d", - p_sys->thread->segment, p_sys->thread->current); - - if (p_sys->thread->segment - p_sys->segment < 3) - return VLC_SUCCESS; - } + msg_Info(s, "downloaded segment %d from stream %d", + segment->sequence, *cur_stream); /* check for division by zero */ double ms = (double)duration / 1000.0; /* ms */ @@ -897,7 +1174,7 @@ static int Download(stream_t *s, hls_stream_t *hls, segment_t *segment, int *cur return VLC_SUCCESS; uint64_t bw = ((double)(segment->size * 8) / ms) * 1000; /* bits / s */ - segment->bandwidth = bw; + s->p_sys->bandwidth = bw; if (hls->bandwidth != bw) { int newstream = BandwidthAdaptation(s, hls->id, &bw); @@ -923,50 +1200,93 @@ static void* hls_Thread(vlc_object_t *p_this) while (vlc_object_alive(p_this)) { - hls_stream_t *hls = hls_Get(client->hls_stream, client->current); + hls_stream_t *hls = hls_Get(p_sys->hls_stream, p_sys->download.current); assert(hls); + /* Sliding window (~60 seconds worth of movie) */ vlc_mutex_lock(&hls->lock); - segment_t *segment = segment_GetSegment(hls, client->segment); + int count = vlc_array_count(hls->segments); vlc_mutex_unlock(&hls->lock); /* Is there a new segment to process? */ - if (segment == NULL) + if ((!p_sys->b_live && (p_sys->playback.segment < (count - 6))) || + (p_sys->download.segment >= count)) { - if (!p_sys->b_live) break; - mwait(p_sys->wakeup); - } - else if (Download(client->s, hls, segment, &client->current) != VLC_SUCCESS) - { - if (!p_sys->b_live) break; + /* wait */ + vlc_mutex_lock(&p_sys->download.lock_wait); + while (((p_sys->download.segment - p_sys->playback.segment > 6) || + (p_sys->download.segment >= count)) && + (p_sys->download.seek == -1) && + (mdate() < p_sys->playlist.wakeup)) + { + vlc_cond_wait(&p_sys->download.wait, &p_sys->download.lock_wait); + if (!vlc_object_alive(p_this)) break; + } + /* */ + if (p_sys->download.seek >= 0) + { + p_sys->download.segment = p_sys->download.seek; + p_sys->download.seek = -1; + } + vlc_mutex_unlock(&p_sys->download.lock_wait); } - /* download succeeded */ - client->segment++; + if (!vlc_object_alive(p_this)) break; - /* FIXME: Reread the m3u8 index file */ + /* reload the m3u8 index file */ if (p_sys->b_live) { double wait = 1; mtime_t now = mdate(); - if (now >= p_sys->wakeup) + if (now >= p_sys->playlist.wakeup) { -#if 0 - /** FIXME: Implement m3u8 playlist reloading */ - if (!hls_ReloadPlaylist(client->s)) + if (hls_ReloadPlaylist(client->s) != VLC_SUCCESS) { /* No change in playlist, then backoff */ - p_sys->tries++; - if (p_sys->tries == 1) wait = 0.5; - else if (p_sys->tries == 2) wait = 1; - else if (p_sys->tries >= 3) wait = 3; + p_sys->playlist.tries++; + if (p_sys->playlist.tries == 1) wait = 0.5; + else if (p_sys->playlist.tries == 2) wait = 1; + else if (p_sys->playlist.tries >= 3) wait = 3; } -#endif + else p_sys->playlist.tries = 0; + /* determine next time to update playlist */ - p_sys->last = now; - p_sys->wakeup = now + ((mtime_t)(hls->duration * wait) * (mtime_t)1000000); + p_sys->playlist.last = now; + p_sys->playlist.wakeup = now + ((mtime_t)(hls->duration * wait) + * (mtime_t)1000000); + } + + if (!vlc_object_alive(p_this)) break; + } + + vlc_mutex_lock(&hls->lock); + segment_t *segment = segment_GetSegment(hls, p_sys->download.segment); + assert(segment); + vlc_mutex_unlock(&hls->lock); + + if (Download(client->s, hls, segment, &p_sys->download.current) != VLC_SUCCESS) + { + if (!vlc_object_alive(p_this)) break; + + if (!p_sys->b_live) + { + p_sys->b_error = true; + break; } } + + /* download succeeded */ + /* determine next segment to download */ + vlc_mutex_lock(&p_sys->download.lock_wait); + if (p_sys->download.seek >= 0) + { + p_sys->download.segment = p_sys->download.seek; + p_sys->download.seek = -1; + } + else if (p_sys->download.segment < count) + p_sys->download.segment++; + vlc_cond_signal(&p_sys->download.wait); + vlc_mutex_unlock(&p_sys->download.lock_wait); } vlc_restorecancel(canc); @@ -976,22 +1296,55 @@ static void* hls_Thread(vlc_object_t *p_this) static int Prefetch(stream_t *s, int *current) { stream_sys_t *p_sys = s->p_sys; + int stream; + + /* Try to pick best matching stream */ +again: + stream = *current; - hls_stream_t *hls = hls_Get(p_sys->hls_stream, p_sys->current); + hls_stream_t *hls = hls_Get(p_sys->hls_stream, *current); if (hls == NULL) return VLC_EGENERIC; - /* Download first 3 segments of this HLS stream */ - for (int i = 0; i < 3; i++) + segment_t *segment = segment_GetSegment(hls, p_sys->playback.segment); + if (segment == NULL ) + return VLC_EGENERIC; + + if (Download(s, hls, segment, current) != VLC_SUCCESS) + return VLC_EGENERIC; + + /* Found better bandwidth match, try again */ + if (*current != stream) + goto again; + + /* Download first 2 segments of this HLS stream */ + stream = *current; + for (int i = 0; i < 2; i++) { - segment_t *segment = segment_GetSegment(hls, p_sys->segment); + segment_t *segment = segment_GetSegment(hls, i); if (segment == NULL ) return VLC_EGENERIC; + if (segment->data) + { + p_sys->playback.segment++; + continue; + } + if (Download(s, hls, segment, current) != VLC_SUCCESS) return VLC_EGENERIC; - p_sys->segment++; + p_sys->playback.segment++; + + /* adapt bandwidth? */ + if (*current != stream) + { + hls_stream_t *hls = hls_Get(p_sys->hls_stream, *current); + if (hls == NULL) + return VLC_EGENERIC; + + stream = *current; + } } return VLC_SUCCESS; @@ -1171,8 +1524,19 @@ static int Open(vlc_object_t *p_this) if (p_sys == NULL) return VLC_ENOMEM; + char *psz_uri = NULL; + if (asprintf(&psz_uri,"%s://%s", s->psz_access, s->psz_path) < 0) + { + free(p_sys); + return VLC_ENOMEM; + } + vlc_UrlParse(&p_sys->m3u8, psz_uri, 0); + free(psz_uri); + + p_sys->bandwidth = -1; p_sys->b_live = true; p_sys->b_meta = false; + p_sys->b_error = false; p_sys->hls_stream = vlc_array_new(); if (p_sys->hls_stream == NULL) @@ -1187,15 +1551,14 @@ static int Open(vlc_object_t *p_this) s->pf_control = Control; /* Select first segment to play */ - p_sys->last = mdate(); if (parse_HTTPLiveStreaming(s) != VLC_SUCCESS) { goto fail; } - /* */ - int current = p_sys->current = hls_LowestBandwidthStream(p_sys->hls_stream); - p_sys->segment = 0; + /* Choose first HLS stream to start with */ + int current = p_sys->playback.current = 0; + p_sys->playback.segment = 0; if (Prefetch(s, ¤t) != VLC_SUCCESS) { @@ -1210,12 +1573,25 @@ static int Open(vlc_object_t *p_this) goto fail; } - p_sys->thread->hls_stream = p_sys->hls_stream; - p_sys->thread->current = current; - p_sys->thread->segment = p_sys->segment; - p_sys->segment = 0; /* reset to first segment */ + /* Initialize HLS live stream */ + if (p_sys->b_live) + { + hls_stream_t *hls = hls_Get(p_sys->hls_stream, current); + p_sys->playlist.last = mdate(); + p_sys->playlist.wakeup = p_sys->playlist.last + + ((mtime_t)hls->duration * UINT64_C(1000000)); + } + + p_sys->download.current = current; + p_sys->playback.current = current; + p_sys->download.segment = p_sys->playback.segment; + p_sys->download.seek = -1; + p_sys->playback.segment = 0; /* reset to first segment */ p_sys->thread->s = s; + vlc_mutex_init(&p_sys->download.lock_wait); + vlc_cond_init(&p_sys->download.wait); + if (vlc_thread_create(p_sys->thread, "HTTP Live Streaming client", hls_Thread, VLC_THREAD_PRIORITY_INPUT)) { @@ -1244,8 +1620,15 @@ static void Close(vlc_object_t *p_this) /* */ if (p_sys->thread) { + vlc_mutex_lock(&p_sys->download.lock_wait); vlc_object_kill(p_sys->thread); + vlc_cond_signal(&p_sys->download.wait); + vlc_mutex_unlock(&p_sys->download.lock_wait); + + /* */ vlc_thread_join(p_sys->thread); + vlc_mutex_destroy(&p_sys->download.lock_wait); + vlc_cond_destroy(&p_sys->download.wait); vlc_object_release(p_sys->thread); } @@ -1259,44 +1642,87 @@ static void Close(vlc_object_t *p_this) vlc_array_destroy(p_sys->hls_stream); /* */ + vlc_UrlClean(&p_sys->m3u8); free(p_sys); } /**************************************************************************** * Stream filters functions ****************************************************************************/ -static segment_t *NextSegment(stream_t *s) +static segment_t *GetSegment(stream_t *s) { stream_sys_t *p_sys = s->p_sys; segment_t *segment = NULL; - int i_stream = 0; + /* Is this segment of the current HLS stream ready? */ + hls_stream_t *hls = hls_Get(p_sys->hls_stream, p_sys->playback.current); + if (hls != NULL) + { + vlc_mutex_lock(&hls->lock); + segment = segment_GetSegment(hls, p_sys->playback.segment); + if (segment != NULL) + { + /* This segment is ready? */ + if (segment->data != NULL) + { + p_sys->b_cache = hls->b_cache; + vlc_mutex_unlock(&hls->lock); + goto check; + } + } + vlc_mutex_unlock(&hls->lock); + } + + /* Was the HLS stream changed to another bitrate? */ + int i_stream = 0; + segment = NULL; while(vlc_object_alive(s)) { /* Is the next segment ready */ hls_stream_t *hls = hls_Get(p_sys->hls_stream, i_stream); - if (hls == NULL) return NULL; + if (hls == NULL) + return NULL; - segment = segment_GetSegment(hls, p_sys->segment); - if (segment == NULL) return NULL; + vlc_mutex_lock(&hls->lock); + segment = segment_GetSegment(hls, p_sys->playback.segment); + if (segment == NULL) + { + vlc_mutex_unlock(&hls->lock); + break; + } + + vlc_mutex_lock(&p_sys->download.lock_wait); + int i_segment = p_sys->download.segment; + vlc_mutex_unlock(&p_sys->download.lock_wait); /* This segment is ready? */ - if (segment->data != NULL) - return segment; + if ((segment->data != NULL) && + (p_sys->playback.segment < i_segment)) + { + p_sys->playback.current = i_stream; + p_sys->b_cache = hls->b_cache; + vlc_mutex_unlock(&hls->lock); + goto check; + } + vlc_mutex_unlock(&hls->lock); - if (!p_sys->b_meta) return NULL; + if (!p_sys->b_meta) + break; /* Was the stream changed to another bitrate? */ i_stream++; if (i_stream >= vlc_array_count(p_sys->hls_stream)) - return NULL; -#if 0 - msg_Info(s, "playback is switching from stream %d to %d", - p_sys->current, i_stream); -#endif - p_sys->current = i_stream; + break; } + /* */ + return NULL; +check: + /* sanity check */ + if (p_sys->download.segment - p_sys->playback.segment == 0) + msg_Err(s, "playback will stall"); + else if (p_sys->download.segment - p_sys->playback.segment < 3) + msg_Warn(s, "playback in danger of stalling"); return segment; } @@ -1310,26 +1736,40 @@ static ssize_t hls_Read(stream_t *s, uint8_t *p_read, unsigned int i_read) /* Determine next segment to read. If this is a meta playlist and * bandwidth conditions changed, then the stream might have switched * to another bandwidth. */ - segment_t *segment = NextSegment(s); + segment_t *segment = GetSegment(s); if (segment == NULL) break; vlc_mutex_lock(&segment->lock); if (segment->data->i_buffer == 0) { - if (!p_sys->b_cache) + if (!p_sys->b_cache || p_sys->b_live) { block_Release(segment->data); segment->data = NULL; } - p_sys->segment++; + else + { /* reset playback pointer to start of buffer */ + uint64_t size = segment->size - segment->data->i_buffer; + if (size > 0) + { + segment->data->i_buffer += size; + segment->data->p_buffer -= size; + } + } + p_sys->playback.segment++; vlc_mutex_unlock(&segment->lock); + + /* signal download thread */ + vlc_mutex_lock(&p_sys->download.lock_wait); + vlc_cond_signal(&p_sys->download.wait); + vlc_mutex_unlock(&p_sys->download.lock_wait); continue; } if (segment->size == segment->data->i_buffer) msg_Info(s, "playing segment %d from stream %d", - p_sys->segment, p_sys->current); + segment->sequence, p_sys->playback.current); ssize_t len = -1; if (i_read <= segment->data->i_buffer) @@ -1359,6 +1799,9 @@ static int Read(stream_t *s, void *buffer, unsigned int i_read) assert(p_sys->hls_stream); + if (p_sys->b_error) + return 0; + if (buffer == NULL) { /* caller skips data, get big enough buffer */ @@ -1372,7 +1815,7 @@ static int Read(stream_t *s, void *buffer, unsigned int i_read) if (length < 0) return 0; - p_sys->offset += length; + p_sys->playback.offset += length; return length; } @@ -1383,17 +1826,18 @@ static int Peek(stream_t *s, const uint8_t **pp_peek, unsigned int i_peek) segment_t *segment; again: - segment = NextSegment(s); + segment = GetSegment(s); if (segment == NULL) { - msg_Err(s, "segment should have been available"); + msg_Err(s, "segment %d should have been available (stream %d)", + p_sys->playback.segment, p_sys->playback.current); return 0; /* eof? */ } vlc_mutex_lock(&segment->lock); /* remember segment to peek */ - int peek_segment = p_sys->segment; + int peek_segment = p_sys->playback.segment; do { if (i_peek < segment->data->i_buffer) @@ -1403,14 +1847,14 @@ again: } else { - p_sys->segment++; + p_sys->playback.segment++; vlc_mutex_unlock(&segment->lock); goto again; } } while ((curlen < i_peek) && vlc_object_alive(s)); /* restore segment to read */ - p_sys->segment = peek_segment; + p_sys->playback.segment = peek_segment; vlc_mutex_unlock(&segment->lock); @@ -1424,17 +1868,19 @@ static bool hls_MaySeek(stream_t *s) if (p_sys->hls_stream == NULL) return false; - hls_stream_t *hls = hls_Get(p_sys->hls_stream, p_sys->current); + hls_stream_t *hls = hls_Get(p_sys->hls_stream, p_sys->playback.current); if (hls == NULL) return false; if (p_sys->b_live) { - vlc_mutex_lock(&hls->lock); - int count = vlc_array_count(hls->segments); - bool may_seek = (p_sys->thread == NULL) ? false : - (p_sys->thread->segment < count - 2); - vlc_mutex_unlock(&hls->lock); - return may_seek; + vlc_mutex_lock(&hls->lock); + int count = vlc_array_count(hls->segments); + vlc_mutex_unlock(&hls->lock); + + vlc_mutex_lock(&p_sys->download.lock_wait); + bool may_seek = (p_sys->download.segment < (count - 2)); + vlc_mutex_unlock(&p_sys->download.lock_wait); + return may_seek; } return true; } @@ -1446,88 +1892,114 @@ static uint64_t GetStreamSize(stream_t *s) if (p_sys->b_live) return 0; - hls_stream_t *hls = hls_Get(p_sys->hls_stream, p_sys->current); + hls_stream_t *hls = hls_Get(p_sys->hls_stream, p_sys->playback.current); if (hls == NULL) return 0; - /* FIXME: Stream size is not entirely exacts at this point */ - uint64_t length = 0UL; - int count = vlc_array_count(hls->segments); - for (int n = 0; n < count; n++) - { - segment_t *segment = segment_GetSegment(hls, n); - if (segment) - { - length += (segment->size > 0) ? segment->size : - (segment->length * hls->bandwidth); - } - } - return length; + vlc_mutex_lock(&hls->lock); + uint64_t size = hls->size; + vlc_mutex_unlock(&hls->lock); + + return size; } static int segment_Seek(stream_t *s, uint64_t pos) { stream_sys_t *p_sys = s->p_sys; - /* Find the right offset */ - hls_stream_t *hls = hls_Get(p_sys->hls_stream, p_sys->current); + hls_stream_t *hls = hls_Get(p_sys->hls_stream, p_sys->playback.current); if (hls == NULL) return VLC_EGENERIC; - uint64_t length = 0; - bool b_found = false; + vlc_mutex_lock(&hls->lock); + bool b_found = false; + uint64_t length = 0; + uint64_t size = hls->size; int count = vlc_array_count(hls->segments); + for (int n = 0; n < count; n++) { - /* FIXME: Seeking in segments not dowloaded is not supported. */ - if (n >= p_sys->thread->segment) + segment_t *segment = vlc_array_item_at_index(hls->segments, n); + if (segment == NULL) { - msg_Err(s, "seeking in segment not downloaded yet."); + vlc_mutex_unlock(&hls->lock); return VLC_EGENERIC; } - segment_t *segment = vlc_array_item_at_index(hls->segments, n); + vlc_mutex_lock(&segment->lock); + length += segment->duration * (hls->bandwidth/8); + vlc_mutex_unlock(&segment->lock); + + if (!b_found && (pos <= length)) + { + if (count - n >= 3) + { + p_sys->playback.segment = n; + b_found = true; + break; + } + /* Do not search in last 3 segments */ + vlc_mutex_unlock(&hls->lock); + return VLC_EGENERIC; + } + } + + /* */ + if (!b_found && (pos >= size)) + { + p_sys->playback.segment = count - 1; + b_found = true; + } + + /* */ + if (b_found) + { + /* restore segment to start position */ + segment_t *segment = segment_GetSegment(hls, p_sys->playback.segment); if (segment == NULL) + { + vlc_mutex_unlock(&hls->lock); return VLC_EGENERIC; + } vlc_mutex_lock(&segment->lock); if (segment->data) { - length += segment->size; uint64_t size = segment->size -segment->data->i_buffer; if (size > 0) { segment->data->i_buffer += size; segment->data->p_buffer -= size; } + } + vlc_mutex_unlock(&segment->lock); - if (!b_found && (pos <= length)) - { - uint64_t used = length - pos; - segment->data->i_buffer -= used; - segment->data->p_buffer += used; + /* start download at current playback segment */ + vlc_mutex_unlock(&hls->lock); - count = p_sys->segment; - p_sys->segment = n; - b_found = true; - } - } - else + /* Wake up download thread */ + vlc_mutex_lock(&p_sys->download.lock_wait); + p_sys->download.seek = p_sys->playback.segment; + vlc_cond_signal(&p_sys->download.wait); + vlc_mutex_unlock(&p_sys->download.lock_wait); + + /* Wait for download to be finished */ + vlc_mutex_lock(&p_sys->download.lock_wait); + msg_Info(s, "seek to segment %d", p_sys->playback.segment); + while (((p_sys->download.seek != -1) || + (p_sys->download.segment - p_sys->playback.segment < 3)) && + (p_sys->download.segment < (count - 6))) { - /* FIXME: seeking is weird when seeking in segments - that have not been downloaded yet */ - length += segment->length * hls->bandwidth; - - if (!b_found && (pos <= length)) - { - count = p_sys->segment; - p_sys->segment = n; - b_found = true; - } + vlc_cond_wait(&p_sys->download.wait, &p_sys->download.lock_wait); + if (!vlc_object_alive(s) || s->b_error) break; } - vlc_mutex_unlock(&segment->lock); + vlc_mutex_unlock(&p_sys->download.lock_wait); + + return VLC_SUCCESS; } - return VLC_SUCCESS; + vlc_mutex_unlock(&hls->lock); + + return b_found ? VLC_SUCCESS : VLC_EGENERIC; } static int Control(stream_t *s, int i_query, va_list args) @@ -1541,7 +2013,7 @@ static int Control(stream_t *s, int i_query, va_list args) *(va_arg (args, bool *)) = hls_MaySeek(s); break; case STREAM_GET_POSITION: - *(va_arg (args, uint64_t *)) = p_sys->offset; + *(va_arg (args, uint64_t *)) = p_sys->playback.offset; break; case STREAM_SET_POSITION: if (hls_MaySeek(s)) @@ -1549,7 +2021,7 @@ static int Control(stream_t *s, int i_query, va_list args) uint64_t pos = (uint64_t)va_arg(args, uint64_t); if (segment_Seek(s, pos) == VLC_SUCCESS) { - p_sys->offset = pos; + p_sys->playback.offset = pos; break; } }