mirror of
https://github.com/FFmpeg/FFmpeg.git
synced 2024-11-26 19:01:44 +02:00
avformat/hlsenc: closed caption tags in the master playlist
This commit is contained in:
parent
8a4cc0a256
commit
1948b76a1b
@ -901,6 +901,43 @@ and they are mapped to the two video only variant streams with audio group names
|
||||
|
||||
By default, a single hls variant containing all the encoded streams is created.
|
||||
|
||||
@item cc_stream_map
|
||||
Map string which specifies different closed captions groups and their
|
||||
attributes. The closed captions stream groups are separated by space.
|
||||
Expected string format is like this
|
||||
"ccgroup:<group name>,instreamid:<INSTREAM-ID>,language:<language code> ....".
|
||||
'ccgroup' and 'instreamid' are mandatory attributes. 'language' is an optional
|
||||
attribute.
|
||||
The closed captions groups configured using this option are mapped to different
|
||||
variant streams by providing the same 'ccgroup' name in the
|
||||
@code{var_stream_map} string. If @code{var_stream_map} is not set, then the
|
||||
first available ccgroup in @code{cc_stream_map} is mapped to the output variant
|
||||
stream. The examples for these two use cases are given below.
|
||||
|
||||
@example
|
||||
ffmpeg -re -i in.ts -b:v 1000k -b:a 64k -a53cc 1 -f hls \
|
||||
-cc_stream_map "ccgroup:cc,instreamid:CC1,language:en" \
|
||||
-master_pl_name master.m3u8 \
|
||||
http://example.com/live/out.m3u8
|
||||
@end example
|
||||
This example adds @code{#EXT-X-MEDIA} tag with @code{TYPE=CLOSED-CAPTIONS} in
|
||||
the master playlist with group name 'cc', langauge 'en' (english) and
|
||||
INSTREAM-ID 'CC1'. Also, it adds @code{CLOSED-CAPTIONS} attribute with group
|
||||
name 'cc' for the output variant stream.
|
||||
@example
|
||||
ffmpeg -re -i in.ts -b:v:0 1000k -b:v:1 256k -b:a:0 64k -b:a:1 32k \
|
||||
-a53cc:0 1 -a53cc:1 1\
|
||||
-map 0:v -map 0:a -map 0:v -map 0:a -f hls \
|
||||
-cc_stream_map "ccgroup:cc,instreamid:CC1,language:en ccgroup:cc,instreamid:CC2,language:sp" \
|
||||
-var_stream_map "v:0,a:0,ccgroup:cc v:1,a:1,ccgroup:cc" \
|
||||
-master_pl_name master.m3u8 \
|
||||
http://example.com/live/out_%v.m3u8
|
||||
@end example
|
||||
This example adds two @code{#EXT-X-MEDIA} tags with @code{TYPE=CLOSED-CAPTIONS} in
|
||||
the master playlist for the INSTREAM-IDs 'CC1' and 'CC2'. Also, it adds
|
||||
@code{CLOSED-CAPTIONS} attribute with group name 'cc' for the two output variant
|
||||
streams.
|
||||
|
||||
@item master_pl_name
|
||||
Create HLS master playlist with the given name.
|
||||
|
||||
|
@ -820,7 +820,7 @@ static int write_manifest(AVFormatContext *s, int final)
|
||||
stream_bitrate += max_audio_bitrate;
|
||||
}
|
||||
get_hls_playlist_name(playlist_file, sizeof(playlist_file), NULL, i);
|
||||
ff_hls_write_stream_info(st, out, stream_bitrate, playlist_file, agroup, NULL);
|
||||
ff_hls_write_stream_info(st, out, stream_bitrate, playlist_file, agroup, NULL, NULL);
|
||||
}
|
||||
avio_close(out);
|
||||
if (use_rename)
|
||||
|
@ -152,9 +152,16 @@ typedef struct VariantStream {
|
||||
unsigned int nb_streams;
|
||||
int m3u8_created; /* status of media play-list creation */
|
||||
char *agroup; /* audio group name */
|
||||
char *ccgroup; /* closed caption group name */
|
||||
char *baseurl;
|
||||
} VariantStream;
|
||||
|
||||
typedef struct ClosedCaptionsStream {
|
||||
char *ccgroup; /* closed caption group name */
|
||||
char *instreamid; /* closed captions INSTREAM-ID */
|
||||
char *language; /* closed captions langauge */
|
||||
} ClosedCaptionsStream;
|
||||
|
||||
typedef struct HLSContext {
|
||||
const AVClass *class; // Class for private options.
|
||||
int64_t start_sequence;
|
||||
@ -203,11 +210,14 @@ typedef struct HLSContext {
|
||||
|
||||
VariantStream *var_streams;
|
||||
unsigned int nb_varstreams;
|
||||
ClosedCaptionsStream *cc_streams;
|
||||
unsigned int nb_ccstreams;
|
||||
|
||||
int master_m3u8_created; /* status of master play-list creation */
|
||||
char *master_m3u8_url; /* URL of the master m3u8 file */
|
||||
int version; /* HLS version */
|
||||
char *var_stream_map; /* user specified variant stream map string */
|
||||
char *cc_stream_map; /* user specified closed caption streams map string */
|
||||
char *master_pl_name;
|
||||
unsigned int master_publish_rate;
|
||||
int http_persistent;
|
||||
@ -1167,7 +1177,8 @@ static int create_master_playlist(AVFormatContext *s,
|
||||
AVDictionary *options = NULL;
|
||||
unsigned int i, j;
|
||||
int m3u8_name_size, ret, bandwidth;
|
||||
char *m3u8_rel_name;
|
||||
char *m3u8_rel_name, *ccgroup;
|
||||
ClosedCaptionsStream *ccs;
|
||||
|
||||
input_vs->m3u8_created = 1;
|
||||
if (!hls->master_m3u8_created) {
|
||||
@ -1194,6 +1205,16 @@ static int create_master_playlist(AVFormatContext *s,
|
||||
|
||||
ff_hls_write_playlist_version(hls->m3u8_out, hls->version);
|
||||
|
||||
for (i = 0; i < hls->nb_ccstreams; i++) {
|
||||
ccs = &(hls->cc_streams[i]);
|
||||
avio_printf(hls->m3u8_out, "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS");
|
||||
avio_printf(hls->m3u8_out, ",GROUP-ID=\"%s\"", ccs->ccgroup);
|
||||
avio_printf(hls->m3u8_out, ",NAME=\"%s\"", ccs->instreamid);
|
||||
if (ccs->language)
|
||||
avio_printf(hls->m3u8_out, ",LANGUAGE=\"%s\"", ccs->language);
|
||||
avio_printf(hls->m3u8_out, ",INSTREAM-ID=\"%s\"\n", ccs->instreamid);
|
||||
}
|
||||
|
||||
/* For audio only variant streams add #EXT-X-MEDIA tag with attributes*/
|
||||
for (i = 0; i < hls->nb_varstreams; i++) {
|
||||
vs = &(hls->var_streams[i]);
|
||||
@ -1278,8 +1299,23 @@ static int create_master_playlist(AVFormatContext *s,
|
||||
bandwidth += aud_st->codecpar->bit_rate;
|
||||
bandwidth += bandwidth / 10;
|
||||
|
||||
ccgroup = NULL;
|
||||
if (vid_st && vs->ccgroup) {
|
||||
/* check if this group name is available in the cc map string */
|
||||
for (j = 0; j < hls->nb_ccstreams; j++) {
|
||||
ccs = &(hls->cc_streams[j]);
|
||||
if (!av_strcasecmp(ccs->ccgroup, vs->ccgroup)) {
|
||||
ccgroup = vs->ccgroup;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (j == hls->nb_ccstreams)
|
||||
av_log(NULL, AV_LOG_WARNING, "mapping ccgroup %s not found\n",
|
||||
vs->ccgroup);
|
||||
}
|
||||
|
||||
ff_hls_write_stream_info(vid_st, hls->m3u8_out, bandwidth, m3u8_rel_name,
|
||||
aud_st ? vs->agroup : NULL, vs->codec_attr);
|
||||
aud_st ? vs->agroup : NULL, vs->codec_attr, ccgroup);
|
||||
|
||||
av_freep(&m3u8_rel_name);
|
||||
}
|
||||
@ -1766,6 +1802,11 @@ static int parse_variant_stream_mapstring(AVFormatContext *s)
|
||||
if (!vs->agroup)
|
||||
return AVERROR(ENOMEM);
|
||||
continue;
|
||||
} else if (av_strstart(keyval, "ccgroup:", &val)) {
|
||||
vs->ccgroup = av_strdup(val);
|
||||
if (!vs->ccgroup)
|
||||
return AVERROR(ENOMEM);
|
||||
continue;
|
||||
} else if (av_strstart(keyval, "v:", &val)) {
|
||||
codec_type = AVMEDIA_TYPE_VIDEO;
|
||||
} else if (av_strstart(keyval, "a:", &val)) {
|
||||
@ -1796,9 +1837,94 @@ static int parse_variant_stream_mapstring(AVFormatContext *s)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_cc_stream_mapstring(AVFormatContext *s)
|
||||
{
|
||||
HLSContext *hls = s->priv_data;
|
||||
int nb_ccstreams;
|
||||
char *p, *q, *saveptr1, *saveptr2, *ccstr, *keyval;
|
||||
const char *val;
|
||||
ClosedCaptionsStream *ccs;
|
||||
|
||||
p = av_strdup(hls->cc_stream_map);
|
||||
q = p;
|
||||
while(av_strtok(q, " \t", &saveptr1)) {
|
||||
q = NULL;
|
||||
hls->nb_ccstreams++;
|
||||
}
|
||||
av_freep(&p);
|
||||
|
||||
hls->cc_streams = av_mallocz(sizeof(*hls->cc_streams) * hls->nb_ccstreams);
|
||||
if (!hls->cc_streams)
|
||||
return AVERROR(ENOMEM);
|
||||
|
||||
p = hls->cc_stream_map;
|
||||
nb_ccstreams = 0;
|
||||
while (ccstr = av_strtok(p, " \t", &saveptr1)) {
|
||||
p = NULL;
|
||||
|
||||
if (nb_ccstreams < hls->nb_ccstreams)
|
||||
ccs = &(hls->cc_streams[nb_ccstreams++]);
|
||||
else
|
||||
return AVERROR(EINVAL);
|
||||
|
||||
while (keyval = av_strtok(ccstr, ",", &saveptr2)) {
|
||||
ccstr = NULL;
|
||||
|
||||
if (av_strstart(keyval, "ccgroup:", &val)) {
|
||||
ccs->ccgroup = av_strdup(val);
|
||||
if (!ccs->ccgroup)
|
||||
return AVERROR(ENOMEM);
|
||||
} else if (av_strstart(keyval, "instreamid:", &val)) {
|
||||
ccs->instreamid = av_strdup(val);
|
||||
if (!ccs->instreamid)
|
||||
return AVERROR(ENOMEM);
|
||||
} else if (av_strstart(keyval, "language:", &val)) {
|
||||
ccs->language = av_strdup(val);
|
||||
if (!ccs->language)
|
||||
return AVERROR(ENOMEM);
|
||||
} else {
|
||||
av_log(s, AV_LOG_ERROR, "Invalid keyval %s\n", keyval);
|
||||
return AVERROR(EINVAL);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ccs->ccgroup || !ccs->instreamid) {
|
||||
av_log(s, AV_LOG_ERROR, "Insufficient parameters in cc stream map string\n");
|
||||
return AVERROR(EINVAL);
|
||||
}
|
||||
|
||||
if (av_strstart(ccs->instreamid, "CC", &val)) {
|
||||
if(atoi(val) < 1 || atoi(val) > 4) {
|
||||
av_log(s, AV_LOG_ERROR, "Invalid instream ID CC index %d in %s, range 1-4\n",
|
||||
atoi(val), ccs->instreamid);
|
||||
return AVERROR(EINVAL);
|
||||
}
|
||||
} else if (av_strstart(ccs->instreamid, "SERVICE", &val)) {
|
||||
if(atoi(val) < 1 || atoi(val) > 63) {
|
||||
av_log(s, AV_LOG_ERROR, "Invalid instream ID SERVICE index %d in %s, range 1-63 \n",
|
||||
atoi(val), ccs->instreamid);
|
||||
return AVERROR(EINVAL);
|
||||
}
|
||||
} else {
|
||||
av_log(s, AV_LOG_ERROR, "Invalid instream ID %s, supported are CCn or SERIVICEn\n",
|
||||
ccs->instreamid);
|
||||
return AVERROR(EINVAL);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int update_variant_stream_info(AVFormatContext *s) {
|
||||
HLSContext *hls = s->priv_data;
|
||||
unsigned int i;
|
||||
int ret = 0;
|
||||
|
||||
if (hls->cc_stream_map) {
|
||||
ret = parse_cc_stream_mapstring(s);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (hls->var_stream_map) {
|
||||
return parse_variant_stream_mapstring(s);
|
||||
@ -1816,6 +1942,13 @@ static int update_variant_stream_info(AVFormatContext *s) {
|
||||
if (!hls->var_streams[0].streams)
|
||||
return AVERROR(ENOMEM);
|
||||
|
||||
//by default, the first available ccgroup is mapped to the variant stream
|
||||
if (hls->nb_ccstreams) {
|
||||
hls->var_streams[0].ccgroup = av_strdup(hls->cc_streams[0].ccgroup);
|
||||
if (!hls->var_streams[0].ccgroup)
|
||||
return AVERROR(ENOMEM);
|
||||
}
|
||||
|
||||
for (i = 0; i < s->nb_streams; i++)
|
||||
hls->var_streams[0].streams[i] = s->streams[i];
|
||||
}
|
||||
@ -2192,13 +2325,22 @@ failed:
|
||||
av_freep(&vs->m3u8_name);
|
||||
av_freep(&vs->streams);
|
||||
av_freep(&vs->agroup);
|
||||
av_freep(&vs->ccgroup);
|
||||
av_freep(&vs->baseurl);
|
||||
}
|
||||
|
||||
for (i = 0; i < hls->nb_ccstreams; i++) {
|
||||
ClosedCaptionsStream *ccs = &hls->cc_streams[i];
|
||||
av_freep(&ccs->ccgroup);
|
||||
av_freep(&ccs->instreamid);
|
||||
av_freep(&ccs->language);
|
||||
}
|
||||
|
||||
ff_format_io_close(s, &hls->m3u8_out);
|
||||
ff_format_io_close(s, &hls->sub_m3u8_out);
|
||||
av_freep(&hls->key_basename);
|
||||
av_freep(&hls->var_streams);
|
||||
av_freep(&hls->cc_streams);
|
||||
av_freep(&hls->master_m3u8_url);
|
||||
return 0;
|
||||
}
|
||||
@ -2535,13 +2677,21 @@ fail:
|
||||
av_freep(&vs->vtt_m3u8_name);
|
||||
av_freep(&vs->streams);
|
||||
av_freep(&vs->agroup);
|
||||
av_freep(&vs->ccgroup);
|
||||
av_freep(&vs->baseurl);
|
||||
if (vs->avf)
|
||||
avformat_free_context(vs->avf);
|
||||
if (vs->vtt_avf)
|
||||
avformat_free_context(vs->vtt_avf);
|
||||
}
|
||||
for (i = 0; i < hls->nb_ccstreams; i++) {
|
||||
ClosedCaptionsStream *ccs = &hls->cc_streams[i];
|
||||
av_freep(&ccs->ccgroup);
|
||||
av_freep(&ccs->instreamid);
|
||||
av_freep(&ccs->language);
|
||||
}
|
||||
av_freep(&hls->var_streams);
|
||||
av_freep(&hls->cc_streams);
|
||||
av_freep(&hls->master_m3u8_url);
|
||||
}
|
||||
|
||||
@ -2601,6 +2751,7 @@ static const AVOption options[] = {
|
||||
{"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},
|
||||
{"var_stream_map", "Variant stream map string", OFFSET(var_stream_map), AV_OPT_TYPE_STRING, {.str = NULL}, 0, 0, E},
|
||||
{"cc_stream_map", "Closed captions stream map string", OFFSET(cc_stream_map), AV_OPT_TYPE_STRING, {.str = NULL}, 0, 0, E},
|
||||
{"master_pl_name", "Create HLS master playlist with this name", OFFSET(master_pl_name), AV_OPT_TYPE_STRING, {.str = NULL}, 0, 0, E},
|
||||
{"master_pl_publish_rate", "Publish master play list every after this many segment intervals", OFFSET(master_publish_rate), AV_OPT_TYPE_INT, {.i64 = 0}, 0, UINT_MAX, E},
|
||||
{"http_persistent", "Use persistent HTTP connections", OFFSET(http_persistent), AV_OPT_TYPE_BOOL, {.i64 = 0 }, 0, 1, E },
|
||||
|
@ -47,7 +47,8 @@ void ff_hls_write_audio_rendition(AVIOContext *out, char *agroup,
|
||||
|
||||
void ff_hls_write_stream_info(AVStream *st, AVIOContext *out,
|
||||
int bandwidth, char *filename, char *agroup,
|
||||
char *codecs) {
|
||||
char *codecs, char *ccgroup) {
|
||||
|
||||
if (!out || !filename)
|
||||
return;
|
||||
|
||||
@ -65,6 +66,8 @@ void ff_hls_write_stream_info(AVStream *st, AVIOContext *out,
|
||||
avio_printf(out, ",CODECS=\"%s\"", codecs);
|
||||
if (agroup && strlen(agroup) > 0)
|
||||
avio_printf(out, ",AUDIO=\"group_%s\"", agroup);
|
||||
if (ccgroup && strlen(ccgroup) > 0)
|
||||
avio_printf(out, ",CLOSED-CAPTIONS=\"%s\"", ccgroup);
|
||||
avio_printf(out, "\n%s\n\n", filename);
|
||||
}
|
||||
|
||||
|
@ -41,7 +41,7 @@ void ff_hls_write_audio_rendition(AVIOContext *out, char *agroup,
|
||||
char *filename, int name_id, int is_default);
|
||||
void ff_hls_write_stream_info(AVStream *st, AVIOContext *out,
|
||||
int bandwidth, char *filename, char *agroup,
|
||||
char *codecs);
|
||||
char *codecs, char *ccgroup);
|
||||
void ff_hls_write_playlist_header(AVIOContext *out, int version, int allowcache,
|
||||
int target_duration, int64_t sequence,
|
||||
uint32_t playlist_type);
|
||||
|
Loading…
Reference in New Issue
Block a user