Skip to content

Commit

Permalink
Merge pull request #2 from SRGSSR/fix/closed-caption-for-apple
Browse files Browse the repository at this point in the history
set CLOSED-CAPTIONS attribute to NONE
  • Loading branch information
shugli authored Apr 5, 2022
2 parents 6615847 + 20e36a6 commit 200ea0b
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 1 deletion.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
97 changes: 97 additions & 0 deletions vod/hls/m3u8_builder.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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)
{
Expand Down
10 changes: 10 additions & 0 deletions vod/media_set.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
132 changes: 131 additions & 1 deletion vod/media_set_parser.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
};

Expand All @@ -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;
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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 }
};

Expand Down Expand Up @@ -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;

Expand All @@ -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 }
};

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
&params[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
Expand Down

0 comments on commit 200ea0b

Please sign in to comment.