avformat/hlsenc: closed caption tags in the master playlist

This commit is contained in:
Vishwanath Dixit 2018-01-24 11:42:57 +08:00 committed by Steven Liu
parent 8a4cc0a256
commit 1948b76a1b
5 changed files with 196 additions and 5 deletions

View File

@ -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.

View File

@ -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)

View File

@ -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 },

View File

@ -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);
}

View File

@ -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);