2 * WebM DASH Manifest XML muxer
3 * Copyright (c) 2014 Vignesh Venkatasubramanian
5 * This file is part of FFmpeg.
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.
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.
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
23 * WebM DASH Specification:
24 * https://sites.google.com/a/webmproject.org/wiki/adaptive-streaming/webm-dash-specification
25 * ISO DASH Specification:
26 * http://standards.iso.org/ittf/PubliclyAvailableStandards/c065274_ISO_IEC_23009-1_2014.zip
34 #include "avio_internal.h"
37 #include "libavutil/avstring.h"
38 #include "libavutil/dict.h"
39 #include "libavutil/opt.h"
40 #include "libavutil/time_internal.h"
42 typedef struct AdaptationSet
{
48 typedef struct WebMDashMuxContext
{
50 char *adaptation_sets
;
53 int representation_id
;
55 int chunk_start_index
;
58 double time_shift_buffer_depth
;
59 int minimum_update_period
;
63 static const char *get_codec_name(int codec_id
)
70 case AV_CODEC_ID_VORBIS
:
72 case AV_CODEC_ID_OPUS
:
78 static double get_duration(AVFormatContext
*s
)
82 for (i
= 0; i
< s
->nb_streams
; i
++) {
83 AVDictionaryEntry
*duration
= av_dict_get(s
->streams
[i
]->metadata
,
85 if (!duration
|| atof(duration
->value
) < 0) continue;
86 if (atof(duration
->value
) > max
) max
= atof(duration
->value
);
91 static int write_header(AVFormatContext
*s
)
93 WebMDashMuxContext
*w
= s
->priv_data
;
94 double min_buffer_time
= 1.0;
95 avio_printf(s
->pb
, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
96 avio_printf(s
->pb
, "<MPD\n");
97 avio_printf(s
->pb
, " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n");
98 avio_printf(s
->pb
, " xmlns=\"urn:mpeg:DASH:schema:MPD:2011\"\n");
99 avio_printf(s
->pb
, " xsi:schemaLocation=\"urn:mpeg:DASH:schema:MPD:2011\"\n");
100 avio_printf(s
->pb
, " type=\"%s\"\n", w
->is_live
? "dynamic" : "static");
102 avio_printf(s
->pb
, " mediaPresentationDuration=\"PT%gS\"\n",
105 avio_printf(s
->pb
, " minBufferTime=\"PT%gS\"\n", min_buffer_time
);
106 avio_printf(s
->pb
, " profiles=\"%s\"%s",
107 w
->is_live
? "urn:mpeg:dash:profile:isoff-live:2011" : "urn:webm:dash:profile:webm-on-demand:2012",
108 w
->is_live
? "\n" : ">\n");
110 time_t local_time
= time(NULL
);
111 struct tm gmt_buffer
;
112 struct tm
*gmt
= gmtime_r(&local_time
, &gmt_buffer
);
114 if (!strftime(gmt_iso
, 21, "%Y-%m-%dT%H:%M:%SZ", gmt
)) {
115 return AVERROR_UNKNOWN
;
118 av_strlcpy(gmt_iso
, "", 1);
120 avio_printf(s
->pb
, " availabilityStartTime=\"%s\"\n", gmt_iso
);
121 avio_printf(s
->pb
, " timeShiftBufferDepth=\"PT%gS\"\n", w
->time_shift_buffer_depth
);
122 avio_printf(s
->pb
, " minimumUpdatePeriod=\"PT%dS\"", w
->minimum_update_period
);
123 avio_printf(s
->pb
, ">\n");
124 if (w
->utc_timing_url
) {
125 avio_printf(s
->pb
, "<UTCTiming\n");
126 avio_printf(s
->pb
, " schemeIdUri=\"urn:mpeg:dash:utc:http-iso:2014\"\n");
127 avio_printf(s
->pb
, " value=\"%s\"/>\n", w
->utc_timing_url
);
133 static void write_footer(AVFormatContext
*s
)
135 avio_printf(s
->pb
, "</MPD>\n");
138 static int subsegment_alignment(AVFormatContext
*s
, AdaptationSet
*as
) {
140 AVDictionaryEntry
*gold
= av_dict_get(s
->streams
[as
->streams
[0]]->metadata
,
141 CUE_TIMESTAMPS
, NULL
, 0);
143 for (i
= 1; i
< as
->nb_streams
; i
++) {
144 AVDictionaryEntry
*ts
= av_dict_get(s
->streams
[as
->streams
[i
]]->metadata
,
145 CUE_TIMESTAMPS
, NULL
, 0);
146 if (!ts
|| strncmp(gold
->value
, ts
->value
, strlen(gold
->value
))) return 0;
151 static int bitstream_switching(AVFormatContext
*s
, AdaptationSet
*as
) {
153 AVDictionaryEntry
*gold_track_num
= av_dict_get(s
->streams
[as
->streams
[0]]->metadata
,
154 TRACK_NUMBER
, NULL
, 0);
155 AVCodecParameters
*gold_par
= s
->streams
[as
->streams
[0]]->codecpar
;
156 if (!gold_track_num
) return 0;
157 for (i
= 1; i
< as
->nb_streams
; i
++) {
158 AVDictionaryEntry
*track_num
= av_dict_get(s
->streams
[as
->streams
[i
]]->metadata
,
159 TRACK_NUMBER
, NULL
, 0);
160 AVCodecParameters
*par
= s
->streams
[as
->streams
[i
]]->codecpar
;
162 strncmp(gold_track_num
->value
, track_num
->value
, strlen(gold_track_num
->value
)) ||
163 gold_par
->codec_id
!= par
->codec_id
||
164 gold_par
->extradata_size
!= par
->extradata_size
||
165 memcmp(gold_par
->extradata
, par
->extradata
, par
->extradata_size
)) {
173 * Writes a Representation within an Adaptation Set. Returns 0 on success and
176 static int write_representation(AVFormatContext
*s
, AVStream
*stream
, char *id
,
177 int output_width
, int output_height
,
178 int output_sample_rate
) {
179 WebMDashMuxContext
*w
= s
->priv_data
;
180 AVDictionaryEntry
*irange
= av_dict_get(stream
->metadata
, INITIALIZATION_RANGE
, NULL
, 0);
181 AVDictionaryEntry
*cues_start
= av_dict_get(stream
->metadata
, CUES_START
, NULL
, 0);
182 AVDictionaryEntry
*cues_end
= av_dict_get(stream
->metadata
, CUES_END
, NULL
, 0);
183 AVDictionaryEntry
*filename
= av_dict_get(stream
->metadata
, FILENAME
, NULL
, 0);
184 AVDictionaryEntry
*bandwidth
= av_dict_get(stream
->metadata
, BANDWIDTH
, NULL
, 0);
185 if ((w
->is_live
&& (!filename
)) ||
186 (!w
->is_live
&& (!irange
|| !cues_start
|| !cues_end
|| !filename
|| !bandwidth
))) {
187 return AVERROR_INVALIDDATA
;
189 avio_printf(s
->pb
, "<Representation id=\"%s\"", id
);
190 // FIXME: For live, This should be obtained from the input file or as an AVOption.
191 avio_printf(s
->pb
, " bandwidth=\"%s\"",
192 w
->is_live
? (stream
->codecpar
->codec_type
== AVMEDIA_TYPE_AUDIO
? "128000" : "1000000") : bandwidth
->value
);
193 if (stream
->codecpar
->codec_type
== AVMEDIA_TYPE_VIDEO
&& output_width
)
194 avio_printf(s
->pb
, " width=\"%d\"", stream
->codecpar
->width
);
195 if (stream
->codecpar
->codec_type
== AVMEDIA_TYPE_VIDEO
&& output_height
)
196 avio_printf(s
->pb
, " height=\"%d\"", stream
->codecpar
->height
);
197 if (stream
->codecpar
->codec_type
== AVMEDIA_TYPE_AUDIO
&& output_sample_rate
)
198 avio_printf(s
->pb
, " audioSamplingRate=\"%d\"", stream
->codecpar
->sample_rate
);
200 // For live streams, Codec and Mime Type always go in the Representation tag.
201 avio_printf(s
->pb
, " codecs=\"%s\"", get_codec_name(stream
->codecpar
->codec_id
));
202 avio_printf(s
->pb
, " mimeType=\"%s/webm\"",
203 stream
->codecpar
->codec_type
== AVMEDIA_TYPE_VIDEO
? "video" : "audio");
204 // For live streams, subsegments always start with key frames. So this
206 avio_printf(s
->pb
, " startsWithSAP=\"1\"");
207 avio_printf(s
->pb
, ">");
209 avio_printf(s
->pb
, ">\n");
210 avio_printf(s
->pb
, "<BaseURL>%s</BaseURL>\n", filename
->value
);
211 avio_printf(s
->pb
, "<SegmentBase\n");
212 avio_printf(s
->pb
, " indexRange=\"%s-%s\">\n", cues_start
->value
, cues_end
->value
);
213 avio_printf(s
->pb
, "<Initialization\n");
214 avio_printf(s
->pb
, " range=\"0-%s\" />\n", irange
->value
);
215 avio_printf(s
->pb
, "</SegmentBase>\n");
217 avio_printf(s
->pb
, "</Representation>\n");
222 * Checks if width of all streams are the same. Returns 1 if true, 0 otherwise.
224 static int check_matching_width(AVFormatContext
*s
, AdaptationSet
*as
) {
226 if (as
->nb_streams
< 2) return 1;
227 first_width
= s
->streams
[as
->streams
[0]]->codecpar
->width
;
228 for (i
= 1; i
< as
->nb_streams
; i
++)
229 if (first_width
!= s
->streams
[as
->streams
[i
]]->codecpar
->width
)
235 * Checks if height of all streams are the same. Returns 1 if true, 0 otherwise.
237 static int check_matching_height(AVFormatContext
*s
, AdaptationSet
*as
) {
239 if (as
->nb_streams
< 2) return 1;
240 first_height
= s
->streams
[as
->streams
[0]]->codecpar
->height
;
241 for (i
= 1; i
< as
->nb_streams
; i
++)
242 if (first_height
!= s
->streams
[as
->streams
[i
]]->codecpar
->height
)
248 * Checks if sample rate of all streams are the same. Returns 1 if true, 0 otherwise.
250 static int check_matching_sample_rate(AVFormatContext
*s
, AdaptationSet
*as
) {
251 int first_sample_rate
, i
;
252 if (as
->nb_streams
< 2) return 1;
253 first_sample_rate
= s
->streams
[as
->streams
[0]]->codecpar
->sample_rate
;
254 for (i
= 1; i
< as
->nb_streams
; i
++)
255 if (first_sample_rate
!= s
->streams
[as
->streams
[i
]]->codecpar
->sample_rate
)
260 static void free_adaptation_sets(AVFormatContext
*s
) {
261 WebMDashMuxContext
*w
= s
->priv_data
;
263 for (i
= 0; i
< w
->nb_as
; i
++) {
264 av_freep(&w
->as
[i
].streams
);
271 * Parses a live header filename and computes the representation id,
272 * initialization pattern and the media pattern. Pass NULL if you don't want to
273 * compute any of those 3. Returns 0 on success and non-zero on failure.
275 * Name of the header file should conform to the following pattern:
276 * <file_description>_<representation_id>.hdr where <file_description> can be
277 * anything. The chunks should be named according to the following pattern:
278 * <file_description>_<representation_id>_<chunk_number>.chk
280 static int parse_filename(char *filename
, char **representation_id
,
281 char **initialization_pattern
, char **media_pattern
) {
282 char *underscore_pos
= NULL
;
283 char *period_pos
= NULL
;
284 char *temp_pos
= NULL
;
285 char *filename_str
= av_strdup(filename
);
286 if (!filename_str
) return AVERROR(ENOMEM
);
287 temp_pos
= av_stristr(filename_str
, "_");
289 underscore_pos
= temp_pos
+ 1;
290 temp_pos
= av_stristr(temp_pos
+ 1, "_");
292 if (!underscore_pos
) return AVERROR_INVALIDDATA
;
293 period_pos
= av_stristr(underscore_pos
, ".");
294 if (!period_pos
) return AVERROR_INVALIDDATA
;
295 *(underscore_pos
- 1) = 0;
296 if (representation_id
) {
297 *representation_id
= av_malloc(period_pos
- underscore_pos
+ 1);
298 if (!(*representation_id
)) return AVERROR(ENOMEM
);
299 av_strlcpy(*representation_id
, underscore_pos
, period_pos
- underscore_pos
+ 1);
301 if (initialization_pattern
) {
302 *initialization_pattern
= av_asprintf("%s_$RepresentationID$.hdr",
304 if (!(*initialization_pattern
)) return AVERROR(ENOMEM
);
307 *media_pattern
= av_asprintf("%s_$RepresentationID$_$Number$.chk",
309 if (!(*media_pattern
)) return AVERROR(ENOMEM
);
311 av_free(filename_str
);
316 * Writes an Adaptation Set. Returns 0 on success and < 0 on failure.
318 static int write_adaptation_set(AVFormatContext
*s
, int as_index
)
320 WebMDashMuxContext
*w
= s
->priv_data
;
321 AdaptationSet
*as
= &w
->as
[as_index
];
322 AVCodecParameters
*par
= s
->streams
[as
->streams
[0]]->codecpar
;
323 AVDictionaryEntry
*lang
;
325 static const char boolean
[2][6] = { "false", "true" };
326 int subsegmentStartsWithSAP
= 1;
328 // Width, Height and Sample Rate will go in the AdaptationSet tag if they
329 // are the same for all contained Representations. otherwise, they will go
330 // on their respective Representation tag. For live streams, they always go
331 // in the Representation tag.
332 int width_in_as
= 1, height_in_as
= 1, sample_rate_in_as
= 1;
333 if (par
->codec_type
== AVMEDIA_TYPE_VIDEO
) {
334 width_in_as
= !w
->is_live
&& check_matching_width(s
, as
);
335 height_in_as
= !w
->is_live
&& check_matching_height(s
, as
);
337 sample_rate_in_as
= !w
->is_live
&& check_matching_sample_rate(s
, as
);
340 avio_printf(s
->pb
, "<AdaptationSet id=\"%s\"", as
->id
);
341 avio_printf(s
->pb
, " mimeType=\"%s/webm\"",
342 par
->codec_type
== AVMEDIA_TYPE_VIDEO
? "video" : "audio");
343 avio_printf(s
->pb
, " codecs=\"%s\"", get_codec_name(par
->codec_id
));
345 lang
= av_dict_get(s
->streams
[as
->streams
[0]]->metadata
, "language", NULL
, 0);
346 if (lang
) avio_printf(s
->pb
, " lang=\"%s\"", lang
->value
);
348 if (par
->codec_type
== AVMEDIA_TYPE_VIDEO
&& width_in_as
)
349 avio_printf(s
->pb
, " width=\"%d\"", par
->width
);
350 if (par
->codec_type
== AVMEDIA_TYPE_VIDEO
&& height_in_as
)
351 avio_printf(s
->pb
, " height=\"%d\"", par
->height
);
352 if (par
->codec_type
== AVMEDIA_TYPE_AUDIO
&& sample_rate_in_as
)
353 avio_printf(s
->pb
, " audioSamplingRate=\"%d\"", par
->sample_rate
);
355 avio_printf(s
->pb
, " bitstreamSwitching=\"%s\"",
356 boolean
[bitstream_switching(s
, as
)]);
357 avio_printf(s
->pb
, " subsegmentAlignment=\"%s\"",
358 boolean
[w
->is_live
|| subsegment_alignment(s
, as
)]);
360 for (i
= 0; i
< as
->nb_streams
; i
++) {
361 AVDictionaryEntry
*kf
= av_dict_get(s
->streams
[as
->streams
[i
]]->metadata
,
362 CLUSTER_KEYFRAME
, NULL
, 0);
363 if (!w
->is_live
&& (!kf
|| !strncmp(kf
->value
, "0", 1))) subsegmentStartsWithSAP
= 0;
365 avio_printf(s
->pb
, " subsegmentStartsWithSAP=\"%d\"", subsegmentStartsWithSAP
);
366 avio_printf(s
->pb
, ">\n");
369 AVDictionaryEntry
*filename
=
370 av_dict_get(s
->streams
[as
->streams
[0]]->metadata
, FILENAME
, NULL
, 0);
371 char *initialization_pattern
= NULL
;
372 char *media_pattern
= NULL
;
373 int ret
= parse_filename(filename
->value
, NULL
, &initialization_pattern
,
376 avio_printf(s
->pb
, "<ContentComponent id=\"1\" type=\"%s\"/>\n",
377 par
->codec_type
== AVMEDIA_TYPE_VIDEO
? "video" : "audio");
378 avio_printf(s
->pb
, "<SegmentTemplate");
379 avio_printf(s
->pb
, " timescale=\"1000\"");
380 avio_printf(s
->pb
, " duration=\"%d\"", w
->chunk_duration
);
381 avio_printf(s
->pb
, " media=\"%s\"", media_pattern
);
382 avio_printf(s
->pb
, " startNumber=\"%d\"", w
->chunk_start_index
);
383 avio_printf(s
->pb
, " initialization=\"%s\"", initialization_pattern
);
384 avio_printf(s
->pb
, "/>\n");
385 av_free(initialization_pattern
);
386 av_free(media_pattern
);
389 for (i
= 0; i
< as
->nb_streams
; i
++) {
390 char *representation_id
= NULL
;
393 AVDictionaryEntry
*filename
=
394 av_dict_get(s
->streams
[as
->streams
[i
]]->metadata
, FILENAME
, NULL
, 0);
396 return AVERROR(EINVAL
);
397 if (ret
= parse_filename(filename
->value
, &representation_id
, NULL
, NULL
))
400 representation_id
= av_asprintf("%d", w
->representation_id
++);
401 if (!representation_id
) return AVERROR(ENOMEM
);
403 ret
= write_representation(s
, s
->streams
[as
->streams
[i
]],
404 representation_id
, !width_in_as
,
405 !height_in_as
, !sample_rate_in_as
);
406 av_free(representation_id
);
409 avio_printf(s
->pb
, "</AdaptationSet>\n");
413 static int to_integer(char *p
, int len
)
416 char *q
= av_malloc(sizeof(char) * len
);
418 return AVERROR(ENOMEM
);
419 av_strlcpy(q
, p
, len
);
425 static int parse_adaptation_sets(AVFormatContext
*s
)
427 WebMDashMuxContext
*w
= s
->priv_data
;
428 char *p
= w
->adaptation_sets
;
430 enum { new_set
, parsed_id
, parsing_streams
} state
;
431 if (!w
->adaptation_sets
) {
432 av_log(s
, AV_LOG_ERROR
, "The 'adaptation_sets' option must be set.\n");
433 return AVERROR(EINVAL
);
435 // syntax id=0,streams=0,1,2 id=1,streams=3,4 and so on
437 while (p
< w
->adaptation_sets
+ strlen(w
->adaptation_sets
)) {
440 else if (state
== new_set
&& !strncmp(p
, "id=", 3)) {
441 void *mem
= av_realloc(w
->as
, sizeof(*w
->as
) * (w
->nb_as
+ 1));
444 return AVERROR(ENOMEM
);
447 w
->as
[w
->nb_as
- 1].nb_streams
= 0;
448 w
->as
[w
->nb_as
- 1].streams
= NULL
;
449 p
+= 3; // consume "id="
450 q
= w
->as
[w
->nb_as
- 1].id
;
451 comma
= strchr(p
, ',');
452 if (!comma
|| comma
- p
>= sizeof(w
->as
[w
->nb_as
- 1].id
)) {
453 av_log(s
, AV_LOG_ERROR
, "'id' in 'adaptation_sets' is malformed.\n");
454 return AVERROR(EINVAL
);
456 while (*p
!= ',') *q
++ = *p
++;
460 } else if (state
== parsed_id
&& !strncmp(p
, "streams=", 8)) {
461 p
+= 8; // consume "streams="
462 state
= parsing_streams
;
463 } else if (state
== parsing_streams
) {
464 struct AdaptationSet
*as
= &w
->as
[w
->nb_as
- 1];
465 int ret
= av_reallocp_array(&as
->streams
, ++as
->nb_streams
,
466 sizeof(*as
->streams
));
470 while (*q
!= '\0' && *q
!= ',' && *q
!= ' ') q
++;
471 as
->streams
[as
->nb_streams
- 1] = to_integer(p
, q
- p
+ 1);
472 if (as
->streams
[as
->nb_streams
- 1] < 0 ||
473 as
->streams
[as
->nb_streams
- 1] >= s
->nb_streams
) {
474 av_log(s
, AV_LOG_ERROR
, "Invalid value for 'streams' in adapation_sets.\n");
475 return AVERROR(EINVAL
);
477 if (*q
== '\0') break;
478 if (*q
== ' ') state
= new_set
;
487 static int webm_dash_manifest_write_header(AVFormatContext
*s
)
492 WebMDashMuxContext
*w
= s
->priv_data
;
494 for (unsigned i
= 0; i
< s
->nb_streams
; i
++) {
495 enum AVCodecID codec_id
= s
->streams
[i
]->codecpar
->codec_id
;
496 if (codec_id
!= AV_CODEC_ID_VP8
&& codec_id
!= AV_CODEC_ID_VP9
&&
497 codec_id
!= AV_CODEC_ID_VORBIS
&& codec_id
!= AV_CODEC_ID_OPUS
)
498 return AVERROR(EINVAL
);
501 ret
= parse_adaptation_sets(s
);
505 ret
= write_header(s
);
509 avio_printf(s
->pb
, "<Period id=\"0\"");
510 avio_printf(s
->pb
, " start=\"PT%gS\"", start
);
512 avio_printf(s
->pb
, " duration=\"PT%gS\"", get_duration(s
));
514 avio_printf(s
->pb
, " >\n");
516 for (i
= 0; i
< w
->nb_as
; i
++) {
517 ret
= write_adaptation_set(s
, i
);
523 avio_printf(s
->pb
, "</Period>\n");
526 free_adaptation_sets(s
);
527 return ret
< 0 ? ret
: 0;
530 static int webm_dash_manifest_write_packet(AVFormatContext
*s
, AVPacket
*pkt
)
535 static int webm_dash_manifest_write_trailer(AVFormatContext
*s
)
537 free_adaptation_sets(s
);
541 #define OFFSET(x) offsetof(WebMDashMuxContext, x)
542 static const AVOption options
[] = {
543 { "adaptation_sets", "Adaptation sets. Syntax: id=0,streams=0,1,2 id=1,streams=3,4 and so on", OFFSET(adaptation_sets
), AV_OPT_TYPE_STRING
, { 0 }, 0, 0, AV_OPT_FLAG_ENCODING_PARAM
},
544 { "debug_mode", "[private option - users should never set this]. Create deterministic output", OFFSET(debug_mode
), AV_OPT_TYPE_BOOL
, {.i64
= 0}, 0, 1, AV_OPT_FLAG_ENCODING_PARAM
},
545 { "live", "create a live stream manifest", OFFSET(is_live
), AV_OPT_TYPE_BOOL
, {.i64
= 0}, 0, 1, AV_OPT_FLAG_ENCODING_PARAM
},
546 { "chunk_start_index", "start index of the chunk", OFFSET(chunk_start_index
), AV_OPT_TYPE_INT
, {.i64
= 0}, 0, INT_MAX
, AV_OPT_FLAG_ENCODING_PARAM
},
547 { "chunk_duration_ms", "duration of each chunk (in milliseconds)", OFFSET(chunk_duration
), AV_OPT_TYPE_INT
, {.i64
= 1000}, 0, INT_MAX
, AV_OPT_FLAG_ENCODING_PARAM
},
548 { "utc_timing_url", "URL of the page that will return the UTC timestamp in ISO format", OFFSET(utc_timing_url
), AV_OPT_TYPE_STRING
, { 0 }, 0, 0, AV_OPT_FLAG_ENCODING_PARAM
},
549 { "time_shift_buffer_depth", "Smallest time (in seconds) shifting buffer for which any Representation is guaranteed to be available.", OFFSET(time_shift_buffer_depth
), AV_OPT_TYPE_DOUBLE
, { .dbl
= 60.0 }, 1.0, DBL_MAX
, AV_OPT_FLAG_ENCODING_PARAM
},
550 { "minimum_update_period", "Minimum Update Period (in seconds) of the manifest.", OFFSET(minimum_update_period
), AV_OPT_TYPE_INT
, { .i64
= 0 }, 0, INT_MAX
, AV_OPT_FLAG_ENCODING_PARAM
},
554 #if CONFIG_WEBM_DASH_MANIFEST_MUXER
555 static const AVClass webm_dash_class
= {
556 .class_name
= "WebM DASH Manifest muxer",
557 .item_name
= av_default_item_name
,
559 .version
= LIBAVUTIL_VERSION_INT
,
562 AVOutputFormat ff_webm_dash_manifest_muxer
= {
563 .name
= "webm_dash_manifest",
564 .long_name
= NULL_IF_CONFIG_SMALL("WebM DASH Manifest"),
565 .mime_type
= "application/xml",
567 .priv_data_size
= sizeof(WebMDashMuxContext
),
568 .write_header
= webm_dash_manifest_write_header
,
569 .write_packet
= webm_dash_manifest_write_packet
,
570 .write_trailer
= webm_dash_manifest_write_trailer
,
571 .priv_class
= &webm_dash_class
,