Commit | Line | Data |
---|---|---|
f6fa7814 DM |
1 | /* |
2 | * Copyright (c) 2014 Martin Storsjo | |
3 | * | |
4 | * This file is part of FFmpeg. | |
5 | * | |
6 | * FFmpeg is free software; you can redistribute it and/or | |
7 | * modify it under the terms of the GNU Lesser General Public | |
8 | * License as published by the Free Software Foundation; either | |
9 | * version 2.1 of the License, or (at your option) any later version. | |
10 | * | |
11 | * FFmpeg is distributed in the hope that it will be useful, | |
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
14 | * Lesser General Public License for more details. | |
15 | * | |
16 | * You should have received a copy of the GNU Lesser General Public | |
17 | * License along with FFmpeg; if not, write to the Free Software | |
18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | |
19 | */ | |
20 | ||
21 | #include <stdio.h> | |
22 | #include <string.h> | |
23 | ||
24 | #include "libavformat/avformat.h" | |
25 | #include "libavutil/avstring.h" | |
26 | #include "libavutil/intreadwrite.h" | |
27 | #include "libavutil/mathematics.h" | |
28 | ||
29 | static int usage(const char *argv0, int ret) | |
30 | { | |
31 | fprintf(stderr, "%s -out foo.mpd file1\n", argv0); | |
32 | return ret; | |
33 | } | |
34 | ||
35 | struct Track { | |
36 | const char *name; | |
37 | int64_t duration; | |
38 | int bitrate; | |
39 | int track_id; | |
40 | int is_audio, is_video; | |
41 | int width, height; | |
42 | int sample_rate, channels; | |
43 | int timescale; | |
44 | char codec_str[30]; | |
45 | int64_t sidx_start, sidx_length; | |
46 | int64_t earliest_presentation; | |
47 | uint32_t earliest_presentation_timescale; | |
48 | }; | |
49 | ||
50 | struct Tracks { | |
51 | int nb_tracks; | |
52 | int64_t duration; | |
53 | struct Track **tracks; | |
54 | int multiple_tracks_per_file; | |
55 | }; | |
56 | ||
57 | static void set_codec_str(AVCodecContext *codec, char *str, int size) | |
58 | { | |
59 | switch (codec->codec_id) { | |
60 | case AV_CODEC_ID_H264: | |
61 | snprintf(str, size, "avc1"); | |
62 | if (codec->extradata_size >= 4 && codec->extradata[0] == 1) { | |
63 | av_strlcatf(str, size, ".%02x%02x%02x", | |
64 | codec->extradata[1], codec->extradata[2], codec->extradata[3]); | |
65 | } | |
66 | break; | |
67 | case AV_CODEC_ID_AAC: | |
68 | snprintf(str, size, "mp4a.40"); // 0x40 is the mp4 object type for AAC | |
69 | if (codec->extradata_size >= 2) { | |
70 | int aot = codec->extradata[0] >> 3; | |
71 | if (aot == 31) | |
72 | aot = ((AV_RB16(codec->extradata) >> 5) & 0x3f) + 32; | |
73 | av_strlcatf(str, size, ".%d", aot); | |
74 | } | |
75 | break; | |
76 | } | |
77 | } | |
78 | ||
79 | static int find_sidx(struct Tracks *tracks, int start_index, | |
80 | const char *file) | |
81 | { | |
82 | int err = 0; | |
83 | AVIOContext *f = NULL; | |
84 | int i; | |
85 | ||
86 | if ((err = avio_open2(&f, file, AVIO_FLAG_READ, NULL, NULL)) < 0) | |
87 | goto fail; | |
88 | ||
89 | while (!f->eof_reached) { | |
90 | int64_t pos = avio_tell(f); | |
91 | int32_t size, tag; | |
92 | ||
93 | size = avio_rb32(f); | |
94 | tag = avio_rb32(f); | |
95 | if (size < 8) | |
96 | break; | |
97 | if (tag == MKBETAG('s', 'i', 'd', 'x')) { | |
98 | int version, track_id; | |
99 | uint32_t timescale; | |
100 | int64_t earliest_presentation; | |
101 | version = avio_r8(f); | |
102 | avio_rb24(f); /* flags */ | |
103 | track_id = avio_rb32(f); | |
104 | timescale = avio_rb32(f); | |
105 | earliest_presentation = version ? avio_rb64(f) : avio_rb32(f); | |
106 | for (i = start_index; i < tracks->nb_tracks; i++) { | |
107 | struct Track *track = tracks->tracks[i]; | |
108 | if (!track->sidx_start) { | |
109 | track->sidx_start = pos; | |
110 | track->sidx_length = size; | |
111 | } else if (pos == track->sidx_start + track->sidx_length) { | |
112 | track->sidx_length = pos + size - track->sidx_start; | |
113 | } | |
114 | if (track->track_id == track_id) { | |
115 | track->earliest_presentation = earliest_presentation; | |
116 | track->earliest_presentation_timescale = timescale; | |
117 | } | |
118 | } | |
119 | } | |
120 | if (avio_seek(f, pos + size, SEEK_SET) != pos + size) | |
121 | break; | |
122 | } | |
123 | ||
124 | fail: | |
125 | if (f) | |
126 | avio_close(f); | |
127 | return err; | |
128 | } | |
129 | ||
130 | static int handle_file(struct Tracks *tracks, const char *file) | |
131 | { | |
132 | AVFormatContext *ctx = NULL; | |
133 | int err = 0, i, orig_tracks = tracks->nb_tracks; | |
134 | char errbuf[50], *ptr; | |
135 | struct Track *track; | |
136 | ||
137 | err = avformat_open_input(&ctx, file, NULL, NULL); | |
138 | if (err < 0) { | |
139 | av_strerror(err, errbuf, sizeof(errbuf)); | |
140 | fprintf(stderr, "Unable to open %s: %s\n", file, errbuf); | |
141 | return 1; | |
142 | } | |
143 | ||
144 | err = avformat_find_stream_info(ctx, NULL); | |
145 | if (err < 0) { | |
146 | av_strerror(err, errbuf, sizeof(errbuf)); | |
147 | fprintf(stderr, "Unable to identify %s: %s\n", file, errbuf); | |
148 | goto fail; | |
149 | } | |
150 | ||
151 | if (ctx->nb_streams < 1) { | |
152 | fprintf(stderr, "No streams found in %s\n", file); | |
153 | goto fail; | |
154 | } | |
155 | if (ctx->nb_streams > 1) | |
156 | tracks->multiple_tracks_per_file = 1; | |
157 | ||
158 | for (i = 0; i < ctx->nb_streams; i++) { | |
159 | struct Track **temp; | |
160 | AVStream *st = ctx->streams[i]; | |
161 | ||
162 | if (st->codec->bit_rate == 0) { | |
163 | fprintf(stderr, "Skipping track %d in %s as it has zero bitrate\n", | |
164 | st->id, file); | |
165 | continue; | |
166 | } | |
167 | ||
168 | track = av_mallocz(sizeof(*track)); | |
169 | if (!track) { | |
170 | err = AVERROR(ENOMEM); | |
171 | goto fail; | |
172 | } | |
173 | temp = av_realloc(tracks->tracks, | |
174 | sizeof(*tracks->tracks) * (tracks->nb_tracks + 1)); | |
175 | if (!temp) { | |
176 | av_free(track); | |
177 | err = AVERROR(ENOMEM); | |
178 | goto fail; | |
179 | } | |
180 | tracks->tracks = temp; | |
181 | tracks->tracks[tracks->nb_tracks] = track; | |
182 | ||
183 | track->name = file; | |
184 | if ((ptr = strrchr(file, '/'))) | |
185 | track->name = ptr + 1; | |
186 | ||
187 | track->bitrate = st->codec->bit_rate; | |
188 | track->track_id = st->id; | |
189 | track->timescale = st->time_base.den; | |
190 | track->duration = st->duration; | |
191 | track->is_audio = st->codec->codec_type == AVMEDIA_TYPE_AUDIO; | |
192 | track->is_video = st->codec->codec_type == AVMEDIA_TYPE_VIDEO; | |
193 | ||
194 | if (!track->is_audio && !track->is_video) { | |
195 | fprintf(stderr, | |
196 | "Track %d in %s is neither video nor audio, skipping\n", | |
197 | track->track_id, file); | |
198 | av_freep(&tracks->tracks[tracks->nb_tracks]); | |
199 | continue; | |
200 | } | |
201 | ||
202 | tracks->duration = FFMAX(tracks->duration, | |
203 | av_rescale_rnd(track->duration, AV_TIME_BASE, | |
204 | track->timescale, AV_ROUND_UP)); | |
205 | ||
206 | if (track->is_audio) { | |
207 | track->channels = st->codec->channels; | |
208 | track->sample_rate = st->codec->sample_rate; | |
209 | } | |
210 | if (track->is_video) { | |
211 | track->width = st->codec->width; | |
212 | track->height = st->codec->height; | |
213 | } | |
214 | set_codec_str(st->codec, track->codec_str, sizeof(track->codec_str)); | |
215 | ||
216 | tracks->nb_tracks++; | |
217 | } | |
218 | ||
219 | avformat_close_input(&ctx); | |
220 | ||
221 | err = find_sidx(tracks, orig_tracks, file); | |
222 | ||
223 | fail: | |
224 | if (ctx) | |
225 | avformat_close_input(&ctx); | |
226 | return err; | |
227 | } | |
228 | ||
229 | static void write_time(FILE *out, int64_t time, int decimals, enum AVRounding round) | |
230 | { | |
231 | int seconds = time / AV_TIME_BASE; | |
232 | int fractions = time % AV_TIME_BASE; | |
233 | int minutes = seconds / 60; | |
234 | int hours = minutes / 60; | |
235 | fractions = av_rescale_rnd(fractions, pow(10, decimals), AV_TIME_BASE, round); | |
236 | seconds %= 60; | |
237 | minutes %= 60; | |
238 | fprintf(out, "PT"); | |
239 | if (hours) | |
240 | fprintf(out, "%dH", hours); | |
241 | if (hours || minutes) | |
242 | fprintf(out, "%dM", minutes); | |
243 | fprintf(out, "%d.%0*dS", seconds, decimals, fractions); | |
244 | } | |
245 | ||
246 | static int output_mpd(struct Tracks *tracks, const char *filename) | |
247 | { | |
248 | FILE *out; | |
249 | int i, j, ret = 0; | |
250 | struct Track **adaptation_sets_buf[2] = { NULL }; | |
251 | struct Track ***adaptation_sets; | |
252 | int nb_tracks_buf[2] = { 0 }; | |
253 | int *nb_tracks; | |
254 | int set, nb_sets; | |
255 | int64_t latest_start = 0; | |
256 | ||
257 | if (!tracks->multiple_tracks_per_file) { | |
258 | adaptation_sets = adaptation_sets_buf; | |
259 | nb_tracks = nb_tracks_buf; | |
260 | nb_sets = 2; | |
261 | for (i = 0; i < 2; i++) { | |
262 | adaptation_sets[i] = av_malloc(sizeof(*adaptation_sets[i]) * tracks->nb_tracks); | |
263 | if (!adaptation_sets[i]) { | |
264 | ret = AVERROR(ENOMEM); | |
265 | goto err; | |
266 | } | |
267 | } | |
268 | for (i = 0; i < tracks->nb_tracks; i++) { | |
269 | int set_index = -1; | |
270 | if (tracks->tracks[i]->is_video) | |
271 | set_index = 0; | |
272 | else if (tracks->tracks[i]->is_audio) | |
273 | set_index = 1; | |
274 | else | |
275 | continue; | |
276 | adaptation_sets[set_index][nb_tracks[set_index]++] = tracks->tracks[i]; | |
277 | } | |
278 | } else { | |
279 | adaptation_sets = &tracks->tracks; | |
280 | nb_tracks = &tracks->nb_tracks; | |
281 | nb_sets = 1; | |
282 | } | |
283 | ||
284 | out = fopen(filename, "w"); | |
285 | if (!out) { | |
286 | ret = AVERROR(errno); | |
287 | perror(filename); | |
288 | return ret; | |
289 | } | |
290 | fprintf(out, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); | |
291 | fprintf(out, "<MPD xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" | |
292 | "\txmlns=\"urn:mpeg:dash:schema:mpd:2011\"\n" | |
293 | "\txmlns:xlink=\"http://www.w3.org/1999/xlink\"\n" | |
294 | "\txsi:schemaLocation=\"urn:mpeg:DASH:schema:MPD:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd\"\n" | |
295 | "\tprofiles=\"urn:mpeg:dash:profile:isoff-on-demand:2011\"\n" | |
296 | "\ttype=\"static\"\n"); | |
297 | fprintf(out, "\tmediaPresentationDuration=\""); | |
298 | write_time(out, tracks->duration, 1, AV_ROUND_DOWN); | |
299 | fprintf(out, "\"\n"); | |
300 | fprintf(out, "\tminBufferTime=\"PT5S\">\n"); | |
301 | ||
302 | for (i = 0; i < tracks->nb_tracks; i++) { | |
303 | int64_t start = av_rescale_rnd(tracks->tracks[i]->earliest_presentation, | |
304 | AV_TIME_BASE, | |
305 | tracks->tracks[i]->earliest_presentation_timescale, | |
306 | AV_ROUND_UP); | |
307 | latest_start = FFMAX(start, latest_start); | |
308 | } | |
309 | fprintf(out, "\t<Period start=\""); | |
310 | write_time(out, latest_start, 3, AV_ROUND_UP); | |
311 | fprintf(out, "\">\n"); | |
312 | ||
313 | ||
314 | for (set = 0; set < nb_sets; set++) { | |
315 | if (nb_tracks[set] == 0) | |
316 | continue; | |
317 | fprintf(out, "\t\t<AdaptationSet segmentAlignment=\"true\">\n"); | |
318 | if (nb_sets == 1) { | |
319 | for (i = 0; i < nb_tracks[set]; i++) { | |
320 | struct Track *track = adaptation_sets[set][i]; | |
321 | if (strcmp(track->name, adaptation_sets[set][0]->name)) | |
322 | break; | |
323 | fprintf(out, "\t\t\t<ContentComponent id=\"%d\" contentType=\"%s\" />\n", track->track_id, track->is_audio ? "audio" : "video"); | |
324 | } | |
325 | } | |
326 | ||
327 | for (i = 0; i < nb_tracks[set]; ) { | |
328 | struct Track *first_track = adaptation_sets[set][i]; | |
329 | int width = 0, height = 0, sample_rate = 0, channels = 0, bitrate = 0; | |
330 | fprintf(out, "\t\t\t<Representation id=\"%d\" codecs=\"", i); | |
331 | for (j = i; j < nb_tracks[set]; j++) { | |
332 | struct Track *track = adaptation_sets[set][j]; | |
333 | if (strcmp(track->name, first_track->name)) | |
334 | break; | |
335 | if (track->is_audio) { | |
336 | sample_rate = track->sample_rate; | |
337 | channels = track->channels; | |
338 | } | |
339 | if (track->is_video) { | |
340 | width = track->width; | |
341 | height = track->height; | |
342 | } | |
343 | bitrate += track->bitrate; | |
344 | if (j > i) | |
345 | fprintf(out, ","); | |
346 | fprintf(out, "%s", track->codec_str); | |
347 | } | |
348 | fprintf(out, "\" mimeType=\"%s/mp4\" bandwidth=\"%d\"", | |
349 | width ? "video" : "audio", bitrate); | |
350 | if (width > 0 && height > 0) | |
351 | fprintf(out, " width=\"%d\" height=\"%d\"", width, height); | |
352 | if (sample_rate > 0) | |
353 | fprintf(out, " audioSamplingRate=\"%d\"", sample_rate); | |
354 | fprintf(out, ">\n"); | |
355 | if (channels > 0) | |
356 | fprintf(out, "\t\t\t\t<AudioChannelConfiguration schemeIdUri=\"urn:mpeg:dash:23003:3:audio_channel_configuration:2011\" value=\"%d\" />\n", channels); | |
357 | fprintf(out, "\t\t\t\t<BaseURL>%s</BaseURL>\n", first_track->name); | |
358 | fprintf(out, "\t\t\t\t<SegmentBase indexRange=\"%"PRId64"-%"PRId64"\" />\n", first_track->sidx_start, first_track->sidx_start + first_track->sidx_length - 1); | |
359 | fprintf(out, "\t\t\t</Representation>\n"); | |
360 | i = j; | |
361 | } | |
362 | fprintf(out, "\t\t</AdaptationSet>\n"); | |
363 | } | |
364 | fprintf(out, "\t</Period>\n"); | |
365 | fprintf(out, "</MPD>\n"); | |
366 | ||
367 | fclose(out); | |
368 | err: | |
369 | for (i = 0; i < 2; i++) | |
370 | av_free(adaptation_sets_buf[i]); | |
371 | return ret; | |
372 | } | |
373 | ||
374 | static void clean_tracks(struct Tracks *tracks) | |
375 | { | |
376 | int i; | |
377 | for (i = 0; i < tracks->nb_tracks; i++) { | |
378 | av_freep(&tracks->tracks[i]); | |
379 | } | |
380 | av_freep(&tracks->tracks); | |
381 | tracks->nb_tracks = 0; | |
382 | } | |
383 | ||
384 | int main(int argc, char **argv) | |
385 | { | |
386 | const char *out = NULL; | |
387 | struct Tracks tracks = { 0 }; | |
388 | int i; | |
389 | ||
390 | av_register_all(); | |
391 | ||
392 | for (i = 1; i < argc; i++) { | |
393 | if (!strcmp(argv[i], "-out")) { | |
394 | out = argv[i + 1]; | |
395 | i++; | |
396 | } else if (argv[i][0] == '-') { | |
397 | return usage(argv[0], 1); | |
398 | } else { | |
399 | if (handle_file(&tracks, argv[i])) | |
400 | return 1; | |
401 | } | |
402 | } | |
403 | if (!tracks.nb_tracks || !out) | |
404 | return usage(argv[0], 1); | |
405 | ||
406 | output_mpd(&tracks, out); | |
407 | ||
408 | clean_tracks(&tracks); | |
409 | ||
410 | return 0; | |
411 | } |