diff --git a/README.md b/README.md index fe4f8279..144ee956 100644 --- a/README.md +++ b/README.md @@ -457,6 +457,10 @@ Optional fields: Setting this parameter is equivalent to passing /clipFrom/ on the URL. * `clipTo` - integer, contains a timestamp indicating where the returned stream should end. Setting this parameter is equivalent to passing /clipTo/ on the URL. +* `closedCaptions` - array of closed captions objects (see below), containing languages and ids + of any embedded CEA-608 / CEA-708 captions. If an empty array is provided, the module will output + `CLOSED-CAPTIONS=NONE` on each `EXT-X-STREAM-INF` tag. If the list does not appear in the JSON, the + module will not output any `CLOSED-CAPTIONS` fields in the playlist. Live fields: * `firstClipTime` - integer, mandatory for all live playlists unless `clipTimes` is specified. @@ -622,6 +626,17 @@ Mandatory fields: * `id` - a string that identifies the notification, this id can be referenced by `vod_notification_uri` using the variable `$vod_notification_id` +#### Closed Captions + +Mandatory fields: +* `id` - a string that identifies the embedded captions. This will become the `INSTREAM-ID` field and must +have one of the following values: `CC1`, `CC3`, `CC3`, `CC4`, or `SERVICEn`, where `n` is between 1 and 63. +* `label` - a friendly string that indicates the language of the closed caption track. + +Optional fields: +* `language` - a 3-letter (ISO-639-2) language code that indicates the language of the closed caption track. + + ### Security #### Authorization diff --git a/vod/hls/m3u8_builder.c b/vod/hls/m3u8_builder.c index f484fa12..6a1328ce 100644 --- a/vod/hls/m3u8_builder.c +++ b/vod/hls/m3u8_builder.c @@ -19,17 +19,22 @@ #define M3U8_EXT_MEDIA_AD "CHARACTERISTICS=\"public.accessibility.describes-video\"," #define M3U8_EXT_MEDIA_SDH "CHARACTERISTICS=\"public.accessibility.describes-music-and-sound\"," #define M3U8_EXT_MEDIA_URI "URI=\"" +#define M3U8_EXT_MEDIA_INSTREAM_ID "INSTREAM-ID=\"%V\"" #define M3U8_EXT_MEDIA_CHANNELS "CHANNELS=\"%uD\"," #define M3U8_EXT_MEDIA_TYPE_AUDIO "AUDIO" #define M3U8_EXT_MEDIA_TYPE_SUBTITLES "SUBTITLES" +#define M3U8_EXT_MEDIA_TYPE_CLOSED_CAPTIONS "CLOSED-CAPTIONS" #define M3U8_EXT_MEDIA_GROUP_ID_AUDIO "audio" #define M3U8_EXT_MEDIA_GROUP_ID_SUBTITLES "subs" +#define M3U8_EXT_MEDIA_GROUP_ID_CLOSED_CAPTIONS "cc" #define M3U8_STREAM_TAG_AUDIO ",AUDIO=\"" M3U8_EXT_MEDIA_GROUP_ID_AUDIO "%uD\"" #define M3U8_STREAM_TAG_SUBTITLES ",SUBTITLES=\"" M3U8_EXT_MEDIA_GROUP_ID_SUBTITLES "%uD\"" +#define M3U8_STREAM_TAG_CLOSED_CAPTIONS ",CLOSED-CAPTIONS=\"" M3U8_EXT_MEDIA_GROUP_ID_CLOSED_CAPTIONS "%uD\"" +#define M3U8_STREAM_TAG_NO_CLOSED_CAPTIONS ",CLOSED-CAPTIONS=NONE" #define M3U8_VIDEO_RANGE_SDR ",VIDEO-RANGE=SDR" #define M3U8_VIDEO_RANGE_PQ ",VIDEO-RANGE=PQ" @@ -812,6 +817,73 @@ m3u8_builder_append_index_url( return p; } +static size_t +m3u8_builder_closed_captions_get_size( + media_set_t* media_set, + request_context_t* request_context) +{ + media_closed_captions_t* closed_captions; + size_t result = 0; + size_t base; + + base = + sizeof(M3U8_EXT_MEDIA_BASE) - 1 + + sizeof(M3U8_EXT_MEDIA_TYPE_CLOSED_CAPTIONS) - 1 + + sizeof(M3U8_EXT_MEDIA_GROUP_ID_CLOSED_CAPTIONS) - 1 + VOD_INT32_LEN + + sizeof(M3U8_EXT_MEDIA_LANG) - 1 + + LANG_ISO639_3_LEN + + sizeof(M3U8_EXT_MEDIA_INSTREAM_ID) - 1 + + sizeof(M3U8_EXT_MEDIA_DEFAULT) - 1; + + for (closed_captions = media_set->closed_captions; closed_captions < media_set->closed_captions_end; closed_captions++) + { + result += base + closed_captions->id.len + closed_captions->label.len + sizeof("\n") - 1; + } + + return result + sizeof("\n") - 1; +} + +static u_char* +m3u8_builder_closed_captions_write( + u_char* p, + media_set_t* media_set) +{ + media_closed_captions_t* closed_captions; + uint32_t index = 0; + + for (closed_captions = media_set->closed_captions; closed_captions < media_set->closed_captions_end; closed_captions++) + { + p = vod_sprintf(p, M3U8_EXT_MEDIA_BASE, + M3U8_EXT_MEDIA_TYPE_CLOSED_CAPTIONS, + M3U8_EXT_MEDIA_GROUP_ID_CLOSED_CAPTIONS, + index, + (vod_str_t*) &closed_captions->label); + + if (closed_captions->language != 0) + { + p = vod_sprintf(p, M3U8_EXT_MEDIA_LANG, + lang_get_rfc_5646_name(closed_captions->language)); + } + + if (closed_captions == media_set->closed_captions) + { + p = vod_copy(p, M3U8_EXT_MEDIA_DEFAULT, sizeof(M3U8_EXT_MEDIA_DEFAULT) - 1); + } + else + { + p = vod_copy(p, M3U8_EXT_MEDIA_NON_DEFAULT, sizeof(M3U8_EXT_MEDIA_NON_DEFAULT) - 1); + } + + p = vod_sprintf(p, M3U8_EXT_MEDIA_INSTREAM_ID, (vod_str_t*) &closed_captions->id); + + *p++ = '\n'; + } + + *p++ = '\n'; + + return p; +} + static size_t m3u8_builder_ext_x_media_tags_get_size( adaptation_sets_t* adaptation_sets, @@ -1126,6 +1198,14 @@ m3u8_builder_write_variants( { p = vod_sprintf(p, M3U8_STREAM_TAG_SUBTITLES, 0); } + if (media_set->closed_captions < media_set->closed_captions_end) + { + p = vod_sprintf(p, M3U8_STREAM_TAG_CLOSED_CAPTIONS, 0); + } + else if (media_set->closed_captions != NULL) + { + p = vod_copy(p, M3U8_STREAM_TAG_NO_CLOSED_CAPTIONS, sizeof(M3U8_STREAM_TAG_NO_CLOSED_CAPTIONS) - 1); + } *p++ = '\n'; // output the url @@ -1320,6 +1400,18 @@ m3u8_builder_build_master_playlist( max_video_stream_inf += sizeof(M3U8_STREAM_TAG_SUBTITLES) - 1 + VOD_INT32_LEN; } + if (media_set->closed_captions < media_set->closed_captions_end) + { + result_size += m3u8_builder_closed_captions_get_size(media_set, request_context); + + max_video_stream_inf += sizeof(M3U8_STREAM_TAG_CLOSED_CAPTIONS) - 1; + } + else if (media_set->closed_captions != NULL) + { + max_video_stream_inf += sizeof(M3U8_STREAM_TAG_NO_CLOSED_CAPTIONS) - 1; + } + + // variants muxed_tracks = adaptation_sets.first->type == ADAPTATION_TYPE_MUXED ? MEDIA_TYPE_COUNT : 1; @@ -1389,6 +1481,11 @@ m3u8_builder_build_master_playlist( MEDIA_TYPE_SUBTITLE); } + if (media_set->closed_captions < media_set->closed_captions_end) + { + p = m3u8_builder_closed_captions_write(p, media_set); + } + // output variants if (variant_set_count > 1) { diff --git a/vod/media_set.h b/vod/media_set.h index f554d07d..8acaf1db 100644 --- a/vod/media_set.h +++ b/vod/media_set.h @@ -15,6 +15,7 @@ #define MAX_LOOK_AHEAD_SEGMENTS (2) #define MAX_NOTIFICATIONS (1024) +#define MAX_CLOSED_CAPTIONS (67) #define MAX_CLIPS (128) #define MAX_CLIPS_PER_REQUEST (16) #define MAX_SEQUENCES (32) @@ -104,6 +105,12 @@ typedef struct media_notification_s { vod_str_t id; } media_notification_t; +typedef struct { + vod_str_t id; + language_id_t language; + vod_str_t label; +} media_closed_captions_t; + typedef struct { uint64_t start_time; uint32_t duration; @@ -146,6 +153,9 @@ typedef struct { media_notification_t* notifications_head; + media_closed_captions_t* closed_captions; + media_closed_captions_t* closed_captions_end; + // initialized while applying filters uint32_t track_count[MEDIA_TYPE_COUNT]; // sum of track count in all sequences per clip uint32_t total_track_count; diff --git a/vod/media_set_parser.c b/vod/media_set_parser.c index 30375363..8f9d4b29 100644 --- a/vod/media_set_parser.c +++ b/vod/media_set_parser.c @@ -39,7 +39,7 @@ enum { MEDIA_SET_PARAM_NOTIFICATIONS, MEDIA_SET_PARAM_CLIP_FROM, MEDIA_SET_PARAM_CLIP_TO, - + MEDIA_SET_PARAM_CLOSED_CAPTIONS, MEDIA_SET_PARAM_COUNT }; @@ -57,6 +57,14 @@ enum { MEDIA_NOTIFICATION_PARAM_COUNT }; +enum { + MEDIA_CLOSED_CAPTIONS_PARAM_ID, + MEDIA_CLOSED_CAPTIONS_PARAM_LANGUAGE, + MEDIA_CLOSED_CAPTIONS_PARAM_LABEL, + + MEDIA_CLOSED_CAPTIONS_PARAM_COUNT +}; + typedef struct { media_filter_parse_context_t base; get_clip_ranges_result_t clip_ranges; @@ -135,6 +143,13 @@ static json_object_key_def_t media_notification_params[] = { { vod_null_string, 0, 0 } }; +static json_object_key_def_t media_closed_captions_params[] = { + { vod_string("id"), VOD_JSON_STRING, MEDIA_CLOSED_CAPTIONS_PARAM_ID }, + { vod_string("language"), VOD_JSON_STRING, MEDIA_CLOSED_CAPTIONS_PARAM_LANGUAGE }, + { vod_string("label"), VOD_JSON_STRING, MEDIA_CLOSED_CAPTIONS_PARAM_LABEL }, + { vod_null_string, 0, 0 } +}; + static json_object_key_def_t media_clip_params[] = { { vod_string("firstKeyFrameOffset"), VOD_JSON_INT, MEDIA_CLIP_PARAM_FIRST_KEY_FRAME_OFFSET }, { vod_string("keyFrameDurations"), VOD_JSON_ARRAY, MEDIA_CLIP_PARAM_KEY_FRAME_DURATIONS }, @@ -162,6 +177,7 @@ static json_object_key_def_t media_set_params[] = { { vod_string("notifications"), VOD_JSON_ARRAY, MEDIA_SET_PARAM_NOTIFICATIONS }, { vod_string("clipFrom"), VOD_JSON_INT, MEDIA_SET_PARAM_CLIP_FROM }, { vod_string("clipTo"), VOD_JSON_INT, MEDIA_SET_PARAM_CLIP_TO }, + { vod_string("closedCaptions"), VOD_JSON_ARRAY, MEDIA_SET_PARAM_CLOSED_CAPTIONS}, { vod_null_string, 0, 0 } }; @@ -192,6 +208,7 @@ static vod_hash_t media_clip_source_hash; static vod_hash_t media_clip_union_hash; static vod_hash_t media_sequence_hash; static vod_hash_t media_notification_hash; +static vod_hash_t media_closed_captions_hash; static vod_hash_t media_set_hash; static vod_hash_t media_clip_hash; @@ -202,6 +219,7 @@ static hash_definition_t hash_definitions[] = { HASH_TABLE(media_clip_union), HASH_TABLE(media_notification), HASH_TABLE(media_clip), + HASH_TABLE(media_closed_captions), { NULL, NULL, 0, NULL } }; @@ -761,6 +779,106 @@ media_set_sequence_id_exists(request_params_t* request_params, vod_str_t* id) return FALSE; } +static vod_status_t +media_set_parse_closed_captions( + request_context_t* request_context, + media_set_t* media_set, + vod_json_array_t* array) +{ + media_closed_captions_t* cur_output; + vod_json_value_t* params[MEDIA_CLOSED_CAPTIONS_PARAM_COUNT]; + vod_array_part_t* part; + vod_json_object_t* cur_pos; + vod_status_t rc; + + if (array->type != VOD_JSON_OBJECT && array->count > 0) + { + vod_log_error(VOD_LOG_ERR, request_context->log, 0, + "media_set_parse_closed_captions: invalid closed caption type %d expected object", array->type); + return VOD_BAD_MAPPING; + } + + if (array->count > MAX_CLOSED_CAPTIONS) + { + vod_log_error(VOD_LOG_ERR, request_context->log, 0, + "media_set_parse_closed_captions: invalid number of elements in the closed captions array %uz", array->count); + return VOD_BAD_MAPPING; + } + + cur_output = vod_alloc(request_context->pool, sizeof(cur_output[0]) * array->count); + if (cur_output == NULL) + { + vod_log_debug0(VOD_LOG_DEBUG_LEVEL, request_context->log, 0, + "media_set_parse_closed_captions: vod_alloc failed"); + return VOD_ALLOC_FAILED; + } + + media_set->closed_captions = cur_output; + + part = &array->part; + for (cur_pos = part->first; ; cur_pos++) + { + if ((void*)cur_pos >= part->last) + { + if (part->next == NULL) + { + break; + } + + part = part->next; + cur_pos = part->first; + } + + vod_memzero(params, sizeof(params)); + + vod_json_get_object_values(cur_pos, &media_closed_captions_hash, params); + + if (params[MEDIA_CLOSED_CAPTIONS_PARAM_ID] == NULL) + { + vod_log_error(VOD_LOG_ERR, request_context->log, 0, + "media_set_parse_closed_captions: missing id in closed captions object"); + return VOD_BAD_MAPPING; + } + + if (params[MEDIA_CLOSED_CAPTIONS_PARAM_LABEL] == NULL) + { + vod_log_error(VOD_LOG_ERR, request_context->log, 0, + "media_set_parse_closed_captions: missing label in closed captions object"); + return VOD_BAD_MAPPING; + } + + if (params[MEDIA_CLOSED_CAPTIONS_PARAM_LANGUAGE] != NULL) + { + rc = media_set_parse_language(request_context, params[MEDIA_CLOSED_CAPTIONS_PARAM_LANGUAGE], &cur_output->language); + if (rc != VOD_OK) + { + return rc; + } + } else + { + cur_output->language = 0; + } + + rc = media_set_parse_null_term_string(&request_context, params[MEDIA_CLOSED_CAPTIONS_PARAM_ID], &cur_output->id); + if (rc != VOD_OK) + { + return rc; + } + + rc = media_set_parse_null_term_string(&request_context, params[MEDIA_CLOSED_CAPTIONS_PARAM_LABEL], &cur_output->label); + if (rc != VOD_OK) + { + return rc; + } + + cur_output++; + } + + media_set->closed_captions_end = cur_output; + + return VOD_OK; +} + static vod_status_t media_set_parse_sequences( request_context_t* request_context, @@ -2272,6 +2390,18 @@ media_set_parse_json( return VOD_BAD_REQUEST; } + if (params[MEDIA_SET_PARAM_CLOSED_CAPTIONS] != NULL) + { + rc = media_set_parse_closed_captions( + request_context, + result, + ¶ms[MEDIA_SET_PARAM_CLOSED_CAPTIONS]->v.arr); + if (rc != VOD_OK) + { + return rc; + } + } + if (params[MEDIA_SET_PARAM_DURATIONS] == NULL) { // no durations in the json -> simple vod stream