]> git.sesse.net Git - ffmpeg/blob - libavformat/applehttpproto.c
471f94bd2aeddccb085d3904948c1b4ff1f258fa
[ffmpeg] / libavformat / applehttpproto.c
1 /*
2  * Apple HTTP Live Streaming Protocol Handler
3  * Copyright (c) 2010 Martin Storsjo
4  *
5  * This file is part of FFmpeg.
6  *
7  * FFmpeg is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU Lesser General Public
9  * License as published by the Free Software Foundation; either
10  * version 2.1 of the License, or (at your option) any later version.
11  *
12  * FFmpeg is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Lesser General Public License for more details.
16  *
17  * You should have received a copy of the GNU Lesser General Public
18  * License along with FFmpeg; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
20  */
21
22 /**
23  * @file
24  * Apple HTTP Live Streaming Protocol Handler
25  * http://tools.ietf.org/html/draft-pantos-http-live-streaming
26  */
27
28 #define _XOPEN_SOURCE 600
29 #include "libavutil/avstring.h"
30 #include "avformat.h"
31 #include "internal.h"
32 #include <unistd.h>
33
34 /*
35  * An apple http stream consists of a playlist with media segment files,
36  * played sequentially. There may be several playlists with the same
37  * video content, in different bandwidth variants, that are played in
38  * parallel (preferrably only one bandwidth variant at a time). In this case,
39  * the user supplied the url to a main playlist that only lists the variant
40  * playlists.
41  *
42  * If the main playlist doesn't point at any variants, we still create
43  * one anonymous toplevel variant for this, to maintain the structure.
44  */
45
46 struct segment {
47     int duration;
48     char url[MAX_URL_SIZE];
49 };
50
51 struct variant {
52     int bandwidth;
53     char url[MAX_URL_SIZE];
54 };
55
56 typedef struct AppleHTTPContext {
57     char playlisturl[MAX_URL_SIZE];
58     int target_duration;
59     int start_seq_no;
60     int finished;
61     int n_segments;
62     struct segment **segments;
63     int n_variants;
64     struct variant **variants;
65     int cur_seq_no;
66     URLContext *seg_hd;
67     int64_t last_load_time;
68 } AppleHTTPContext;
69
70 static int read_chomp_line(AVIOContext *s, char *buf, int maxlen)
71 {
72     int len = ff_get_line(s, buf, maxlen);
73     while (len > 0 && isspace(buf[len - 1]))
74         buf[--len] = '\0';
75     return len;
76 }
77
78 static void make_absolute_url(char *buf, int size, const char *base,
79                               const char *rel)
80 {
81     char *sep;
82     /* Absolute path, relative to the current server */
83     if (base && strstr(base, "://") && rel[0] == '/') {
84         if (base != buf)
85             av_strlcpy(buf, base, size);
86         sep = strstr(buf, "://");
87         if (sep) {
88             sep += 3;
89             sep = strchr(sep, '/');
90             if (sep)
91                 *sep = '\0';
92         }
93         av_strlcat(buf, rel, size);
94         return;
95     }
96     /* If rel actually is an absolute url, just copy it */
97     if (!base || strstr(rel, "://") || rel[0] == '/') {
98         av_strlcpy(buf, rel, size);
99         return;
100     }
101     if (base != buf)
102         av_strlcpy(buf, base, size);
103     /* Remove the file name from the base url */
104     sep = strrchr(buf, '/');
105     if (sep)
106         sep[1] = '\0';
107     else
108         buf[0] = '\0';
109     while (av_strstart(rel, "../", NULL) && sep) {
110         /* Remove the path delimiter at the end */
111         sep[0] = '\0';
112         sep = strrchr(buf, '/');
113         /* If the next directory name to pop off is "..", break here */
114         if (!strcmp(sep ? &sep[1] : buf, "..")) {
115             /* Readd the slash we just removed */
116             av_strlcat(buf, "/", size);
117             break;
118         }
119         /* Cut off the directory name */
120         if (sep)
121             sep[1] = '\0';
122         else
123             buf[0] = '\0';
124         rel += 3;
125     }
126     av_strlcat(buf, rel, size);
127 }
128
129 static void free_segment_list(AppleHTTPContext *s)
130 {
131     int i;
132     for (i = 0; i < s->n_segments; i++)
133         av_free(s->segments[i]);
134     av_freep(&s->segments);
135     s->n_segments = 0;
136 }
137
138 static void free_variant_list(AppleHTTPContext *s)
139 {
140     int i;
141     for (i = 0; i < s->n_variants; i++)
142         av_free(s->variants[i]);
143     av_freep(&s->variants);
144     s->n_variants = 0;
145 }
146
147 struct variant_info {
148     char bandwidth[20];
149 };
150
151 static void handle_variant_args(struct variant_info *info, const char *key,
152                                 int key_len, char **dest, int *dest_len)
153 {
154     if (!strncmp(key, "BANDWIDTH=", key_len)) {
155         *dest     =        info->bandwidth;
156         *dest_len = sizeof(info->bandwidth);
157     }
158 }
159
160 static int parse_playlist(URLContext *h, const char *url)
161 {
162     AppleHTTPContext *s = h->priv_data;
163     AVIOContext *in;
164     int ret = 0, duration = 0, is_segment = 0, is_variant = 0, bandwidth = 0;
165     char line[1024];
166     const char *ptr;
167
168     if ((ret = avio_open(&in, url, URL_RDONLY)) < 0)
169         return ret;
170
171     read_chomp_line(in, line, sizeof(line));
172     if (strcmp(line, "#EXTM3U"))
173         return AVERROR_INVALIDDATA;
174
175     free_segment_list(s);
176     s->finished = 0;
177     while (!url_feof(in)) {
178         read_chomp_line(in, line, sizeof(line));
179         if (av_strstart(line, "#EXT-X-STREAM-INF:", &ptr)) {
180             struct variant_info info = {{0}};
181             is_variant = 1;
182             ff_parse_key_value(ptr, (ff_parse_key_val_cb) handle_variant_args,
183                                &info);
184             bandwidth = atoi(info.bandwidth);
185         } else if (av_strstart(line, "#EXT-X-TARGETDURATION:", &ptr)) {
186             s->target_duration = atoi(ptr);
187         } else if (av_strstart(line, "#EXT-X-MEDIA-SEQUENCE:", &ptr)) {
188             s->start_seq_no = atoi(ptr);
189         } else if (av_strstart(line, "#EXT-X-ENDLIST", &ptr)) {
190             s->finished = 1;
191         } else if (av_strstart(line, "#EXTINF:", &ptr)) {
192             is_segment = 1;
193             duration = atoi(ptr);
194         } else if (av_strstart(line, "#", NULL)) {
195             continue;
196         } else if (line[0]) {
197             if (is_segment) {
198                 struct segment *seg = av_malloc(sizeof(struct segment));
199                 if (!seg) {
200                     ret = AVERROR(ENOMEM);
201                     goto fail;
202                 }
203                 seg->duration = duration;
204                 make_absolute_url(seg->url, sizeof(seg->url), url, line);
205                 dynarray_add(&s->segments, &s->n_segments, seg);
206                 is_segment = 0;
207             } else if (is_variant) {
208                 struct variant *var = av_malloc(sizeof(struct variant));
209                 if (!var) {
210                     ret = AVERROR(ENOMEM);
211                     goto fail;
212                 }
213                 var->bandwidth = bandwidth;
214                 make_absolute_url(var->url, sizeof(var->url), url, line);
215                 dynarray_add(&s->variants, &s->n_variants, var);
216                 is_variant = 0;
217             }
218         }
219     }
220     s->last_load_time = av_gettime();
221
222 fail:
223     avio_close(in);
224     return ret;
225 }
226
227 static int applehttp_open(URLContext *h, const char *uri, int flags)
228 {
229     AppleHTTPContext *s;
230     int ret, i;
231     const char *nested_url;
232
233     if (flags & (URL_WRONLY | URL_RDWR))
234         return AVERROR_NOTSUPP;
235
236     s = av_mallocz(sizeof(AppleHTTPContext));
237     if (!s)
238         return AVERROR(ENOMEM);
239     h->priv_data = s;
240     h->is_streamed = 1;
241
242     if (av_strstart(uri, "applehttp+", &nested_url)) {
243         av_strlcpy(s->playlisturl, nested_url, sizeof(s->playlisturl));
244     } else if (av_strstart(uri, "applehttp://", &nested_url)) {
245         av_strlcpy(s->playlisturl, "http://", sizeof(s->playlisturl));
246         av_strlcat(s->playlisturl, nested_url, sizeof(s->playlisturl));
247     } else {
248         av_log(NULL, AV_LOG_ERROR, "Unsupported url %s\n", uri);
249         ret = AVERROR(EINVAL);
250         goto fail;
251     }
252
253     if ((ret = parse_playlist(h, s->playlisturl)) < 0)
254         goto fail;
255
256     if (s->n_segments == 0 && s->n_variants > 0) {
257         int max_bandwidth = 0, maxvar = -1;
258         for (i = 0; i < s->n_variants; i++) {
259             if (s->variants[i]->bandwidth > max_bandwidth || i == 0) {
260                 max_bandwidth = s->variants[i]->bandwidth;
261                 maxvar = i;
262             }
263         }
264         av_strlcpy(s->playlisturl, s->variants[maxvar]->url,
265                    sizeof(s->playlisturl));
266         if ((ret = parse_playlist(h, s->playlisturl)) < 0)
267             goto fail;
268     }
269
270     if (s->n_segments == 0) {
271         av_log(NULL, AV_LOG_WARNING, "Empty playlist\n");
272         ret = AVERROR(EIO);
273         goto fail;
274     }
275     s->cur_seq_no = s->start_seq_no;
276     if (!s->finished && s->n_segments >= 3)
277         s->cur_seq_no = s->start_seq_no + s->n_segments - 3;
278
279     return 0;
280
281 fail:
282     av_free(s);
283     return ret;
284 }
285
286 static int applehttp_read(URLContext *h, uint8_t *buf, int size)
287 {
288     AppleHTTPContext *s = h->priv_data;
289     const char *url;
290     int ret;
291
292 start:
293     if (s->seg_hd) {
294         ret = url_read(s->seg_hd, buf, size);
295         if (ret > 0)
296             return ret;
297     }
298     if (s->seg_hd) {
299         url_close(s->seg_hd);
300         s->seg_hd = NULL;
301         s->cur_seq_no++;
302     }
303 retry:
304     if (!s->finished) {
305         int64_t now = av_gettime();
306         if (now - s->last_load_time >= s->target_duration*1000000)
307             if ((ret = parse_playlist(h, s->playlisturl)) < 0)
308                 return ret;
309     }
310     if (s->cur_seq_no < s->start_seq_no) {
311         av_log(NULL, AV_LOG_WARNING,
312                "skipping %d segments ahead, expired from playlist\n",
313                s->start_seq_no - s->cur_seq_no);
314         s->cur_seq_no = s->start_seq_no;
315     }
316     if (s->cur_seq_no - s->start_seq_no >= s->n_segments) {
317         if (s->finished)
318             return AVERROR_EOF;
319         while (av_gettime() - s->last_load_time < s->target_duration*1000000) {
320             if (url_interrupt_cb())
321                 return AVERROR(EINTR);
322             usleep(100*1000);
323         }
324         goto retry;
325     }
326     url = s->segments[s->cur_seq_no - s->start_seq_no]->url,
327     av_log(NULL, AV_LOG_DEBUG, "opening %s\n", url);
328     ret = url_open(&s->seg_hd, url, URL_RDONLY);
329     if (ret < 0) {
330         if (url_interrupt_cb())
331             return AVERROR(EINTR);
332         av_log(NULL, AV_LOG_WARNING, "Unable to open %s\n", url);
333         s->cur_seq_no++;
334         goto retry;
335     }
336     goto start;
337 }
338
339 static int applehttp_close(URLContext *h)
340 {
341     AppleHTTPContext *s = h->priv_data;
342
343     free_segment_list(s);
344     free_variant_list(s);
345     url_close(s->seg_hd);
346     av_free(s);
347     return 0;
348 }
349
350 URLProtocol ff_applehttp_protocol = {
351     "applehttp",
352     applehttp_open,
353     applehttp_read,
354     NULL, /* write */
355     NULL, /* seek */
356     applehttp_close,
357     .flags = URL_PROTOCOL_FLAG_NESTED_SCHEME,
358 };