X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=libavformat%2Fhlsenc.c;h=418f153c6f99f58679085bc6defdbf1b1077d284;hb=f7b7d51a37ddbd15be5611b86799fb6d3e443fff;hp=7ed121a5ddfecd96832209921a26ce76ebf36478;hpb=93bf0480c2dba12990f92cdf1927da2ff64db0cd;p=ffmpeg diff --git a/libavformat/hlsenc.c b/libavformat/hlsenc.c index 7ed121a5ddf..418f153c6f9 100644 --- a/libavformat/hlsenc.c +++ b/libavformat/hlsenc.c @@ -85,8 +85,14 @@ typedef enum HLSFlags { HLS_SECOND_LEVEL_SEGMENT_DURATION = (1 << 9), // include segment duration (microsec) in segment filenames when use_localtime e.g.: %%09t HLS_SECOND_LEVEL_SEGMENT_SIZE = (1 << 10), // include segment size (bytes) in segment filenames when use_localtime e.g.: %%014s HLS_TEMP_FILE = (1 << 11), + HLS_PERIODIC_REKEY = (1 << 12), } HLSFlags; +typedef enum { + SEGMENT_TYPE_MPEGTS, + SEGMENT_TYPE_FMP4, +} SegmentType; + typedef enum { PLAYLIST_TYPE_NONE, PLAYLIST_TYPE_EVENT, @@ -115,6 +121,9 @@ typedef struct HLSContext { uint32_t flags; // enum HLSFlags uint32_t pl_type; // enum PlaylistType char *segment_filename; + char *fmp4_init_filename; + int segment_type; + int fmp4_init_mode; int use_localtime; ///< flag to expand filename with localtime int use_localtime_mkdir;///< flag to mkdir dirname in timebased filename @@ -139,6 +148,7 @@ typedef struct HLSContext { HLSSegment *old_segments; char *basename; + char *base_output_dirname; char *vtt_basename; char *vtt_m3u8_name; char *baseurl; @@ -164,6 +174,7 @@ typedef struct HLSContext { double initial_prog_date_time; char current_segment_final_filename_fmt[1024]; // when renaming segments + char *user_agent; } HLSContext; static int get_int_from_double(double val) @@ -204,6 +215,22 @@ static int mkdir_p(const char *path) { return ret; } +static void set_http_options(AVFormatContext *s, AVDictionary **options, HLSContext *c) +{ + const char *proto = avio_find_protocol_name(s->filename); + int http_base_proto = proto ? (!av_strcasecmp(proto, "http") || !av_strcasecmp(proto, "https")) : 0; + + if (c->method) { + av_dict_set(options, "method", c->method, 0); + } else if (http_base_proto) { + av_log(c, AV_LOG_WARNING, "No HTTP method set, hls muxer defaulting to method PUT.\n"); + av_dict_set(options, "method", "PUT", 0); + } + if (c->user_agent) + av_dict_set(options, "user_agent", c->user_agent, 0); + +} + static int replace_int_data_in_filename(char *buf, int buf_size, const char *filename, char placeholder, int64_t number) { const char *p; @@ -256,6 +283,16 @@ fail: return -1; } +static void write_styp(AVIOContext *pb) +{ + avio_wb32(pb, 24); + ffio_wfourcc(pb, "styp"); + ffio_wfourcc(pb, "msdh"); + avio_wb32(pb, 0); /* minor */ + ffio_wfourcc(pb, "msdh"); + ffio_wfourcc(pb, "msix"); +} + static int hls_delete_old_segments(AVFormatContext *s, HLSContext *hls) { HLSSegment *segment, *previous_segment = NULL; @@ -508,9 +545,11 @@ static int read_chomp_line(AVIOContext *s, char *buf, int maxlen) static int hls_mux_init(AVFormatContext *s) { + AVDictionary *options = NULL; HLSContext *hls = s->priv_data; AVFormatContext *oc; AVFormatContext *vtt_oc = NULL; + int byterange_mode = (hls->flags & HLS_SINGLE_FILE) || (hls->max_seg_size > 0); int i, ret; ret = avformat_alloc_output_context2(&hls->avf, hls->oformat, NULL, NULL); @@ -547,13 +586,56 @@ static int hls_mux_init(AVFormatContext *s) if (!(st = avformat_new_stream(loc, NULL))) return AVERROR(ENOMEM); avcodec_parameters_copy(st->codecpar, s->streams[i]->codecpar); + if (!oc->oformat->codec_tag || + av_codec_get_id (oc->oformat->codec_tag, s->streams[i]->codecpar->codec_tag) == st->codecpar->codec_id || + av_codec_get_tag(oc->oformat->codec_tag, s->streams[i]->codecpar->codec_id) <= 0) { + st->codecpar->codec_tag = s->streams[i]->codecpar->codec_tag; + } else { + st->codecpar->codec_tag = 0; + } + st->sample_aspect_ratio = s->streams[i]->sample_aspect_ratio; st->time_base = s->streams[i]->time_base; av_dict_copy(&st->metadata, s->streams[i]->metadata, 0); } hls->start_pos = 0; hls->new_start = 1; + hls->fmp4_init_mode = 0; + + if (hls->segment_type == SEGMENT_TYPE_FMP4) { + if (hls->max_seg_size > 0) { + av_log(s, AV_LOG_WARNING, "Multi-file byterange mode is currently unsupported in the HLS muxer.\n"); + return AVERROR_PATCHWELCOME; + } + hls->fmp4_init_mode = !byterange_mode; + set_http_options(s, &options, hls); + if ((ret = s->io_open(s, &oc->pb, hls->base_output_dirname, AVIO_FLAG_WRITE, &options)) < 0) { + av_log(s, AV_LOG_ERROR, "Failed to open segment '%s'\n", hls->fmp4_init_filename); + return ret; + } + if (hls->format_options_str) { + ret = av_dict_parse_string(&hls->format_options, hls->format_options_str, "=", ":", 0); + if (ret < 0) { + av_log(s, AV_LOG_ERROR, "Could not parse format options list '%s'\n", + hls->format_options_str); + return ret; + } + } + + av_dict_copy(&options, hls->format_options, 0); + av_dict_set(&options, "fflags", "-autobsf", 0); + av_dict_set(&options, "movflags", "frag_custom+dash+delay_moov", 0); + ret = avformat_init_output(oc, &options); + if (ret < 0) + return ret; + if (av_dict_count(options)) { + av_log(s, AV_LOG_ERROR, "Some of the provided format options in '%s' are not recognized\n", hls->format_options_str); + av_dict_free(&options); + return AVERROR(EINVAL); + } + av_dict_free(&options); + } return 0; } @@ -722,6 +804,7 @@ static int hls_append_segment(struct AVFormatContext *s, HLSContext *hls, double { HLSSegment *en = av_malloc(sizeof(*en)); const char *filename; + int byterange_mode = (hls->flags & HLS_SINGLE_FILE) || (hls->max_seg_size > 0); int ret; if (!en) @@ -737,8 +820,8 @@ static int hls_append_segment(struct AVFormatContext *s, HLSContext *hls, double if (hls->use_localtime_mkdir) { filename = hls->avf->filename; } - if (find_segment_by_filename(hls->segments, filename) - || find_segment_by_filename(hls->old_segments, filename)) { + if ((find_segment_by_filename(hls->segments, filename) || find_segment_by_filename(hls->old_segments, filename)) + && !byterange_mode) { av_log(hls, AV_LOG_WARNING, "Duplicated segment filename detected: %s\n", filename); } av_strlcpy(en->filename, filename, sizeof(en->filename)); @@ -898,19 +981,6 @@ static void hls_free_segments(HLSSegment *p) } } -static void set_http_options(AVFormatContext *s, AVDictionary **options, HLSContext *c) -{ - const char *proto = avio_find_protocol_name(s->filename); - int http_base_proto = proto ? (!av_strcasecmp(proto, "http") || !av_strcasecmp(proto, "https")) : 0; - - if (c->method) { - av_dict_set(options, "method", c->method, 0); - } else if (http_base_proto) { - av_log(c, AV_LOG_WARNING, "No HTTP method set, hls muxer defaulting to method PUT.\n"); - av_dict_set(options, "method", "PUT", 0); - } -} - static void write_m3u8_head_block(HLSContext *hls, AVIOContext *out, int version, int target_duration, int64_t sequence) { @@ -960,6 +1030,10 @@ static int hls_window(AVFormatContext *s, int last) sequence = 0; } + if (hls->segment_type == SEGMENT_TYPE_FMP4) { + version = 7; + } + if (!use_rename && !warned_non_file++) av_log(s, AV_LOG_ERROR, "Cannot use rename on non file protocol, this may lead to races and temporary partial files\n"); @@ -1000,13 +1074,21 @@ static int hls_window(AVFormatContext *s, int last) avio_printf(out, "#EXT-X-DISCONTINUITY\n"); } - if (hls->flags & HLS_ROUND_DURATIONS) - avio_printf(out, "#EXTINF:%ld,\n", lrint(en->duration)); - else - avio_printf(out, "#EXTINF:%f,\n", en->duration); - if (byterange_mode) - avio_printf(out, "#EXT-X-BYTERANGE:%"PRIi64"@%"PRIi64"\n", - en->size, en->pos); + if ((hls->segment_type == SEGMENT_TYPE_FMP4) && (en == hls->segments)) { + avio_printf(out, "#EXT-X-MAP:URI=\"%s\"", hls->fmp4_init_filename); + if (hls->flags & HLS_SINGLE_FILE) { + avio_printf(out, ",BYTERANGE=\"%"PRId64"@%"PRId64"\"", en->size, en->pos); + } + avio_printf(out, "\n"); + } else { + if (hls->flags & HLS_ROUND_DURATIONS) + avio_printf(out, "#EXTINF:%ld,\n", lrint(en->duration)); + else + avio_printf(out, "#EXTINF:%f,\n", en->duration); + if (byterange_mode) + avio_printf(out, "#EXT-X-BYTERANGE:%"PRId64"@%"PRId64"\n", + en->size, en->pos); + } if (hls->flags & HLS_PROGRAM_DATE_TIME) { time_t tt, wrongsecs; int milli; @@ -1031,9 +1113,11 @@ static int hls_window(AVFormatContext *s, int last) avio_printf(out, "#EXT-X-PROGRAM-DATE-TIME:%s.%03d%s\n", buf0, milli, buf1); prog_date_time += en->duration; } - if (hls->baseurl) - avio_printf(out, "%s", hls->baseurl); - avio_printf(out, "%s\n", en->filename); + if (!((hls->segment_type == SEGMENT_TYPE_FMP4) && (en == hls->segments))) { + if (hls->baseurl) + avio_printf(out, "%s", hls->baseurl); + avio_printf(out, "%s\n", en->filename); + } } if (last && (hls->flags & HLS_OMIT_ENDLIST)==0) @@ -1157,12 +1241,15 @@ static int hls_start(AVFormatContext *s) av_log(s, AV_LOG_WARNING, "Cannot use both -hls_key_info_file and -hls_enc," " will use -hls_key_info_file priority\n"); } - if (c->key_info_file) { - if ((err = hls_encryption_start(s)) < 0) - goto fail; - } else { - if ((err = do_encrypt(s)) < 0) - goto fail; + + if (c->number <= 1 || (c->flags & HLS_PERIODIC_REKEY)) { + if (c->key_info_file) { + if ((err = hls_encryption_start(s)) < 0) + goto fail; + } else { + if ((err = do_encrypt(s)) < 0) + goto fail; + } } if ((err = av_dict_set(&options, "encryption_key", c->key_string, 0)) < 0) @@ -1193,15 +1280,19 @@ static int hls_start(AVFormatContext *s) } av_dict_free(&options); - /* We only require one PAT/PMT per segment. */ - if (oc->oformat->priv_class && oc->priv_data) { - char period[21]; + if (c->segment_type == SEGMENT_TYPE_FMP4 && !(c->flags & HLS_SINGLE_FILE)) { + write_styp(oc->pb); + } else { + /* We only require one PAT/PMT per segment. */ + if (oc->oformat->priv_class && oc->priv_data) { + char period[21]; - snprintf(period, sizeof(period), "%d", (INT_MAX / 2) - 1); + snprintf(period, sizeof(period), "%d", (INT_MAX / 2) - 1); - av_opt_set(oc->priv_data, "mpegts_flags", "resend_headers", 0); - av_opt_set(oc->priv_data, "sdt_period", period, 0); - av_opt_set(oc->priv_data, "pat_period", period, 0); + av_opt_set(oc->priv_data, "mpegts_flags", "resend_headers", 0); + av_opt_set(oc->priv_data, "sdt_period", period, 0); + av_opt_set(oc->priv_data, "pat_period", period, 0); + } } if (c->vtt_basename) { @@ -1217,14 +1308,19 @@ fail: return err; } -static const char * get_default_pattern_localtime_fmt(void) +static const char * get_default_pattern_localtime_fmt(AVFormatContext *s) { char b[21]; time_t t = time(NULL); struct tm *p, tmbuf; + HLSContext *hls = s->priv_data; + p = localtime_r(&t, &tmbuf); // no %s support when strftime returned error or left format string unchanged // also no %s support on MSVC, which invokes the invalid parameter handler on unsupported format strings, instead of returning an error + if (hls->segment_type == SEGMENT_TYPE_FMP4) { + return (HAVE_LIBC_MSVCRT || !strftime(b, sizeof(b), "%s", p) || !strcmp(b, "%s")) ? "-%Y%m%d%H%M%S.m4s" : "-%s.m4s"; + } return (HAVE_LIBC_MSVCRT || !strftime(b, sizeof(b), "%s", p) || !strcmp(b, "%s")) ? "-%Y%m%d%H%M%S.ts" : "-%s.ts"; } @@ -1232,14 +1328,17 @@ static int hls_write_header(AVFormatContext *s) { HLSContext *hls = s->priv_data; int ret, i; - char *p; + char *p = NULL; const char *pattern = "%d.ts"; - const char *pattern_localtime_fmt = get_default_pattern_localtime_fmt(); + const char *pattern_localtime_fmt = get_default_pattern_localtime_fmt(s); const char *vtt_pattern = "%d.vtt"; AVDictionary *options = NULL; - int basename_size; - int vtt_basename_size; + int basename_size = 0; + int vtt_basename_size = 0; + if (hls->segment_type == SEGMENT_TYPE_FMP4) { + pattern = "%d.m4s"; + } if ((hls->start_sequence_source_type == HLS_START_SEQUENCE_AS_SECONDS_SINCE_EPOCH) || (hls->start_sequence_source_type == HLS_START_SEQUENCE_AS_FORMATTED_DATETIME)) { time_t t = time(NULL); // we will need it in either case @@ -1288,7 +1387,11 @@ static int hls_write_header(AVFormatContext *s) "More than a single video stream present, " "expect issues decoding it.\n"); - hls->oformat = av_guess_format("mpegts", NULL, NULL); + if (hls->segment_type == SEGMENT_TYPE_FMP4) { + hls->oformat = av_guess_format("mp4", NULL, NULL); + } else { + hls->oformat = av_guess_format("mpegts", NULL, NULL); + } if (!hls->oformat) { ret = AVERROR_MUXER_NOT_FOUND; @@ -1310,8 +1413,13 @@ static int hls_write_header(AVFormatContext *s) goto fail; } } else { - if (hls->flags & HLS_SINGLE_FILE) - pattern = ".ts"; + if (hls->flags & HLS_SINGLE_FILE) { + if (hls->segment_type == SEGMENT_TYPE_FMP4) { + pattern = ".m4s"; + } else { + pattern = ".ts"; + } + } if (hls->use_localtime) { basename_size = strlen(s->filename) + strlen(pattern_localtime_fmt) + 1; @@ -1335,6 +1443,32 @@ static int hls_write_header(AVFormatContext *s) av_strlcat(hls->basename, pattern, basename_size); } } + + if (av_strcasecmp(hls->fmp4_init_filename, "init.mp4")) { + int fmp4_init_filename_len = strlen(hls->fmp4_init_filename) + 1; + hls->base_output_dirname = av_malloc(fmp4_init_filename_len); + if (!hls->base_output_dirname) { + ret = AVERROR(ENOMEM); + goto fail; + } + av_strlcpy(hls->base_output_dirname, hls->fmp4_init_filename, fmp4_init_filename_len); + } else { + hls->base_output_dirname = av_malloc(basename_size); + if (!hls->base_output_dirname) { + ret = AVERROR(ENOMEM); + goto fail; + } + + av_strlcpy(hls->base_output_dirname, s->filename, basename_size); + p = strrchr(hls->base_output_dirname, '/'); + if (p) { + *(p + 1) = '\0'; + av_strlcat(hls->base_output_dirname, hls->fmp4_init_filename, basename_size); + } else { + av_strlcpy(hls->base_output_dirname, hls->fmp4_init_filename, basename_size); + } + } + if (!hls->use_localtime) { ret = sls_flag_check_duration_size_index(hls); if (ret < 0) { @@ -1375,6 +1509,14 @@ static int hls_write_header(AVFormatContext *s) av_strlcat(hls->vtt_basename, vtt_pattern, vtt_basename_size); } + if ((hls->flags & HLS_SINGLE_FILE) && (hls->segment_type == SEGMENT_TYPE_FMP4)) { + hls->fmp4_init_filename = av_strdup(hls->basename); + if (!hls->fmp4_init_filename) { + ret = AVERROR(ENOMEM); + goto fail; + } + } + if ((ret = hls_mux_init(s)) < 0) goto fail; @@ -1389,8 +1531,10 @@ static int hls_write_header(AVFormatContext *s) } } - if ((ret = hls_start(s)) < 0) - goto fail; + if (hls->segment_type != SEGMENT_TYPE_FMP4 || hls->flags & HLS_SINGLE_FILE) { + if ((ret = hls_start(s)) < 0) + goto fail; + } av_dict_copy(&options, hls->format_options, 0); ret = avformat_write_header(hls->avf, &options); @@ -1428,6 +1572,7 @@ fail: av_dict_free(&options); if (ret < 0) { + av_freep(&hls->fmp4_init_filename); av_freep(&hls->basename); av_freep(&hls->vtt_basename); av_freep(&hls->key_basename); @@ -1447,7 +1592,7 @@ static int hls_write_packet(AVFormatContext *s, AVPacket *pkt) AVStream *st = s->streams[pkt->stream_index]; int64_t end_pts = hls->recording_time * hls->number; int is_ref_pkt = 1; - int ret, can_split = 1; + int ret = 0, can_split = 1; int stream_index = 0; if (hls->sequence - hls->nb_entries > hls->start_sequence && hls->init_time > 0) { @@ -1494,7 +1639,7 @@ static int hls_write_packet(AVFormatContext *s, AVPacket *pkt) } } - if (can_split && av_compare_ts(pkt->pts - hls->start_pts, st->time_base, + if (hls->fmp4_init_mode || can_split && av_compare_ts(pkt->pts - hls->start_pts, st->time_base, end_pts, AV_TIME_BASE_Q) >= 0) { int64_t new_start_pos; char *old_filename = av_strdup(hls->avf->filename); @@ -1504,7 +1649,7 @@ static int hls_write_packet(AVFormatContext *s, AVPacket *pkt) return AVERROR(ENOMEM); } - av_write_frame(oc, NULL); /* Flush any buffered data */ + av_write_frame(hls->avf, NULL); /* Flush any buffered data */ new_start_pos = avio_tell(hls->avf->pb); hls->size = new_start_pos - hls->start_pos; @@ -1517,12 +1662,18 @@ static int hls_write_packet(AVFormatContext *s, AVPacket *pkt) } if ((hls->flags & HLS_TEMP_FILE) && oc->filename[0]) { if (!(hls->flags & HLS_SINGLE_FILE) || (hls->max_seg_size <= 0)) - if (hls->avf->oformat->priv_class && hls->avf->priv_data) + if ((hls->avf->oformat->priv_class && hls->avf->priv_data) && hls->segment_type != SEGMENT_TYPE_FMP4) av_opt_set(hls->avf->priv_data, "mpegts_flags", "resend_headers", 0); hls_rename_temp_file(s, oc); } - ret = hls_append_segment(s, hls, hls->duration, hls->start_pos, hls->size); + if (hls->fmp4_init_mode) { + hls->number--; + } + + if (!hls->fmp4_init_mode || byterange_mode) + ret = hls_append_segment(s, hls, hls->duration, hls->start_pos, hls->size); + hls->start_pos = new_start_pos; if (ret < 0) { av_free(old_filename); @@ -1532,6 +1683,7 @@ static int hls_write_packet(AVFormatContext *s, AVPacket *pkt) hls->end_pts = pkt->pts; hls->duration = 0; + hls->fmp4_init_mode = 0; if (hls->flags & HLS_SINGLE_FILE) { hls->number++; } else if (hls->max_seg_size > 0) { @@ -1555,9 +1707,10 @@ static int hls_write_packet(AVFormatContext *s, AVPacket *pkt) return ret; } - if ((ret = hls_window(s, 0)) < 0) { - return ret; - } + if (!hls->fmp4_init_mode || byterange_mode) + if ((ret = hls_window(s, 0)) < 0) { + return ret; + } } ret = ff_write_chained(oc, stream_index, pkt, s, 0); @@ -1599,12 +1752,14 @@ static int hls_write_trailer(struct AVFormatContext *s) ff_format_io_close(s, &vtt_oc->pb); } av_freep(&hls->basename); + av_freep(&hls->base_output_dirname); av_freep(&hls->key_basename); avformat_free_context(oc); hls->avf = NULL; hls_window(s, 1); + av_freep(&hls->fmp4_init_filename); if (vtt_oc) { av_freep(&hls->vtt_basename); av_freep(&hls->vtt_m3u8_name); @@ -1639,6 +1794,10 @@ static const AVOption options[] = { {"hls_enc_key_url", "url to access the key to decrypt the segments", OFFSET(key_url), AV_OPT_TYPE_STRING, {.str = NULL}, 0, 0, E}, {"hls_enc_iv", "hex-coded 16 byte initialization vector", OFFSET(iv), AV_OPT_TYPE_STRING, .flags = E}, {"hls_subtitle_path", "set path of hls subtitles", OFFSET(subtitle_filename), AV_OPT_TYPE_STRING, {.str = NULL}, 0, 0, E}, + {"hls_segment_type", "set hls segment files type", OFFSET(segment_type), AV_OPT_TYPE_INT, {.i64 = SEGMENT_TYPE_MPEGTS }, 0, SEGMENT_TYPE_FMP4, E, "segment_type"}, + {"mpegts", "make segment file to mpegts files in m3u8", 0, AV_OPT_TYPE_CONST, {.i64 = SEGMENT_TYPE_MPEGTS }, 0, UINT_MAX, E, "segment_type"}, + {"fmp4", "make segment file to fragment mp4 files in m3u8", 0, AV_OPT_TYPE_CONST, {.i64 = SEGMENT_TYPE_FMP4 }, 0, UINT_MAX, E, "segment_type"}, + {"hls_fmp4_init_filename", "set fragment mp4 file init filename", OFFSET(fmp4_init_filename), AV_OPT_TYPE_STRING, {.str = "init.mp4"}, 0, 0, E}, {"hls_flags", "set flags affecting HLS playlist and media file generation", OFFSET(flags), AV_OPT_TYPE_FLAGS, {.i64 = 0 }, 0, UINT_MAX, E, "flags"}, {"single_file", "generate a single media file indexed with byte ranges", 0, AV_OPT_TYPE_CONST, {.i64 = HLS_SINGLE_FILE }, 0, UINT_MAX, E, "flags"}, {"temp_file", "write segment to temporary file and rename when complete", 0, AV_OPT_TYPE_CONST, {.i64 = HLS_TEMP_FILE }, 0, UINT_MAX, E, "flags"}, @@ -1652,6 +1811,7 @@ static const AVOption options[] = { {"second_level_segment_index", "include segment index in segment filenames when use_localtime", 0, AV_OPT_TYPE_CONST, {.i64 = HLS_SECOND_LEVEL_SEGMENT_INDEX }, 0, UINT_MAX, E, "flags"}, {"second_level_segment_duration", "include segment duration in segment filenames when use_localtime", 0, AV_OPT_TYPE_CONST, {.i64 = HLS_SECOND_LEVEL_SEGMENT_DURATION }, 0, UINT_MAX, E, "flags"}, {"second_level_segment_size", "include segment size in segment filenames when use_localtime", 0, AV_OPT_TYPE_CONST, {.i64 = HLS_SECOND_LEVEL_SEGMENT_SIZE }, 0, UINT_MAX, E, "flags"}, + {"periodic_rekey", "reload keyinfo file periodically for re-keying", 0, AV_OPT_TYPE_CONST, {.i64 = HLS_PERIODIC_REKEY }, 0, UINT_MAX, E, "flags"}, {"use_localtime", "set filename expansion with strftime at segment creation", OFFSET(use_localtime), AV_OPT_TYPE_BOOL, {.i64 = 0 }, 0, 1, E }, {"use_localtime_mkdir", "create last directory component in strftime-generated filename", OFFSET(use_localtime_mkdir), AV_OPT_TYPE_BOOL, {.i64 = 0 }, 0, 1, E }, {"hls_playlist_type", "set the HLS playlist type", OFFSET(pl_type), AV_OPT_TYPE_INT, {.i64 = PLAYLIST_TYPE_NONE }, 0, PLAYLIST_TYPE_NB-1, E, "pl_type" }, @@ -1662,6 +1822,7 @@ static const AVOption options[] = { {"generic", "start_number value (default)", 0, AV_OPT_TYPE_CONST, {.i64 = HLS_START_SEQUENCE_AS_START_NUMBER }, INT_MIN, INT_MAX, E, "start_sequence_source_type" }, {"epoch", "seconds since epoch", 0, AV_OPT_TYPE_CONST, {.i64 = HLS_START_SEQUENCE_AS_SECONDS_SINCE_EPOCH }, INT_MIN, INT_MAX, E, "start_sequence_source_type" }, {"datetime", "current datetime as YYYYMMDDhhmmss", 0, AV_OPT_TYPE_CONST, {.i64 = HLS_START_SEQUENCE_AS_FORMATTED_DATETIME }, INT_MIN, INT_MAX, E, "start_sequence_source_type" }, + {"http_user_agent", "override User-Agent field in HTTP header", OFFSET(user_agent), AV_OPT_TYPE_STRING, {.str = NULL}, 0, 0, E}, { NULL }, }; @@ -1681,7 +1842,7 @@ AVOutputFormat ff_hls_muxer = { .audio_codec = AV_CODEC_ID_AAC, .video_codec = AV_CODEC_ID_H264, .subtitle_codec = AV_CODEC_ID_WEBVTT, - .flags = AVFMT_NOFILE | AVFMT_ALLOW_FLUSH, + .flags = AVFMT_NOFILE | AVFMT_GLOBALHEADER | AVFMT_ALLOW_FLUSH, .write_header = hls_write_header, .write_packet = hls_write_packet, .write_trailer = hls_write_trailer,