diff --git a/README.md b/README.md index aa982906..93f6fcc3 100644 --- a/README.md +++ b/README.md @@ -1689,8 +1689,19 @@ padding is added as needed. * **default**: `off` * **context**: `http`, `server`, `location` -When enabled, an ID3 TEXT frame will be outputted in each TS segment, containing a JSON with the absolute segment timestamp. -The timestamp is measured in milliseconds since the epoch (unixtime x 1000), the JSON structure is: `{"timestamp":1459779115000}` +When enabled, an ID3 TEXT frame is outputted in each TS segment. +The content of the ID3 TEXT frame can be set using the directive `vod_hls_mpegts_id3_data`. + +#### vod_hls_mpegts_id3_data +* **syntax**: `vod_hls_mpegts_id3_data string` +* **default**: `{"timestamp":$vod_segment_time,"sequenceId":"$vod_sequence_id"}` +* **context**: `http`, `server`, `location` + +Sets the data of the ID3 TEXT frame outputted in each TS segment, when `vod_hls_mpegts_output_id3_timestamps` is set to `on`. +When the directive is not set, the ID3 frames contain by default a JSON object of the format `{"timestamp":1459779115000,"sequenceId":"{id}"}`: +- `timestamp` - an absolute time measured in milliseconds since the epoch (unixtime x 1000). +- `sequenceId` - the id field of the sequence object, as specified in the mapping JSON. The field is omitted when the sequence id is empty / not specified in the mapping JSON. +The parameter value can contain variables. #### vod_hls_mpegts_align_pts * **syntax**: `vod_hls_mpegts_align_pts on/off` @@ -1821,6 +1832,7 @@ The module adds the following nginx variables: `EXPIRED` - the current server time is larger than `expirationTime` `ALLOC_FAILED` - the module failed to allocate memory `UNEXPECTED` - a scenario that is not supposed to happen, most likely a bug in the module +* `$vod_segment_time` - for segment requests, contains the absolute timestamp of the first frame in the segment, measured in milliseconds since the epoch (unixtime x 1000). * `$vod_segment_duration` - for segment requests, contains the duration of the segment in milliseconds * `$vod_frames_bytes_read` - for segment requests, total number of bytes read while processing media frames diff --git a/config b/config index 931d1e38..7a627a95 100644 --- a/config +++ b/config @@ -399,6 +399,7 @@ VOD_SRCS="$VOD_SRCS \ $ngx_addon_dir/vod/language_code.c \ $ngx_addon_dir/vod/manifest_utils.c \ $ngx_addon_dir/vod/media_format.c \ + $ngx_addon_dir/vod/media_set.c \ $ngx_addon_dir/vod/media_set_parser.c \ $ngx_addon_dir/vod/mkv/ebml.c \ $ngx_addon_dir/vod/mkv/mkv_builder.c \ diff --git a/ngx_http_vod_hls.c b/ngx_http_vod_hls.c index 7371f521..4cbf9edd 100644 --- a/ngx_http_vod_hls.c +++ b/ngx_http_vod_hls.c @@ -38,6 +38,10 @@ #define SUPPORTED_CODECS (SUPPORTED_CODECS_MP4 | SUPPORTED_CODECS_TS) +#define ID3_TEXT_JSON_FORMAT "{\"timestamp\":%uL}%Z" +#define ID3_TEXT_JSON_SEQUENCE_ID_PREFIX_FORMAT "{\"timestamp\":%uL,\"sequenceId\":\"" +#define ID3_TEXT_JSON_SEQUENCE_ID_SUFFIX "\"}" + // content types static u_char m3u8_content_type[] = "application/vnd.apple.mpegurl"; @@ -266,6 +270,103 @@ ngx_http_vod_hls_init_segment_encryption( } #endif // NGX_HAVE_OPENSSL_EVP +static ngx_int_t +ngx_http_vod_hls_get_default_id3_data(ngx_http_vod_submodule_context_t* submodule_context, ngx_str_t* id3_data) +{ + media_set_t* media_set; + vod_str_t* sequence_id; + int64_t timestamp; + u_char* p; + size_t sequence_id_escape; + size_t data_size; + + media_set = &submodule_context->media_set; + sequence_id = &media_set->sequences[0].id; + if (sequence_id->len != 0) + { + sequence_id_escape = vod_escape_json(NULL, sequence_id->data, sequence_id->len); + data_size = sizeof(ID3_TEXT_JSON_SEQUENCE_ID_PREFIX_FORMAT) + VOD_INT64_LEN + + sequence_id->len + sequence_id_escape + + sizeof(ID3_TEXT_JSON_SEQUENCE_ID_SUFFIX); + } + else + { + sequence_id_escape = 0; + data_size = sizeof(ID3_TEXT_JSON_FORMAT) + VOD_INT64_LEN; + } + + timestamp = media_set_get_segment_time_millis(media_set); + + p = ngx_pnalloc(submodule_context->request_context.pool, data_size); + if (p == NULL) + { + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, submodule_context->request_context.log, 0, + "ngx_http_vod_hls_get_default_id3_data: ngx_pnalloc failed"); + return ngx_http_vod_status_to_ngx_error(submodule_context->r, VOD_ALLOC_FAILED); + } + + id3_data->data = p; + + if (sequence_id->len != 0) + { + p = vod_sprintf(p, ID3_TEXT_JSON_SEQUENCE_ID_PREFIX_FORMAT, timestamp); + if (sequence_id_escape) + { + p = (u_char*)vod_escape_json(p, sequence_id->data, sequence_id->len); + } + else + { + p = vod_copy(p, sequence_id->data, sequence_id->len); + } + p = vod_copy(p, ID3_TEXT_JSON_SEQUENCE_ID_SUFFIX, sizeof(ID3_TEXT_JSON_SEQUENCE_ID_SUFFIX)); + + } + else + { + p = vod_sprintf(p, ID3_TEXT_JSON_FORMAT, timestamp); + } + + id3_data->len = p - id3_data->data; + + return NGX_OK; +} + +static ngx_int_t +ngx_http_vod_hls_init_muxer_conf(ngx_http_vod_submodule_context_t* submodule_context, hls_mpegts_muxer_conf_t* conf) +{ + ngx_http_vod_hls_loc_conf_t* hls_conf; + + hls_conf = &submodule_context->conf->hls; + + conf->interleave_frames = hls_conf->interleave_frames; + conf->align_frames = hls_conf->align_frames; + conf->align_pts = hls_conf->align_pts; + + if (!hls_conf->output_id3_timestamps) + { + conf->id3_data.data = NULL; + conf->id3_data.len = 0; + return NGX_OK; + } + + if (hls_conf->id3_data != NULL) + { + if (ngx_http_complex_value( + submodule_context->r, + hls_conf->id3_data, + &conf->id3_data) != NGX_OK) + { + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, submodule_context->request_context.log, 0, + "ngx_http_vod_hls_init_muxer_conf: ngx_http_complex_value failed"); + return NGX_ERROR; + } + + return NGX_OK; + } + + return ngx_http_vod_hls_get_default_id3_data(submodule_context, &conf->id3_data); +} + static ngx_int_t ngx_http_vod_hls_handle_master_playlist( ngx_http_vod_submodule_context_t* submodule_context, @@ -407,6 +508,7 @@ ngx_http_vod_hls_handle_iframe_playlist( ngx_str_t* content_type) { ngx_http_vod_loc_conf_t* conf = submodule_context->conf; + hls_mpegts_muxer_conf_t muxer_conf; ngx_str_t base_url = ngx_null_string; vod_status_t rc; @@ -442,10 +544,16 @@ ngx_http_vod_hls_handle_iframe_playlist( return ngx_http_vod_status_to_ngx_error(submodule_context->r, VOD_BAD_REQUEST); } + rc = ngx_http_vod_hls_init_muxer_conf(submodule_context, &muxer_conf); + if (rc != NGX_OK) + { + return rc; + } + rc = m3u8_builder_build_iframe_playlist( &submodule_context->request_context, &conf->hls.m3u8_config, - &conf->hls.mpegts_muxer_config, + &muxer_conf, &base_url, &submodule_context->media_set, response); @@ -500,6 +608,7 @@ ngx_http_vod_hls_init_ts_frame_processor( ngx_str_t* content_type) { hls_encryption_params_t encryption_params; + hls_mpegts_muxer_conf_t muxer_conf; hls_muxer_state_t* state; vod_status_t rc; bool_t reuse_output_buffers; @@ -528,9 +637,15 @@ ngx_http_vod_hls_init_ts_frame_processor( reuse_output_buffers = FALSE; #endif // NGX_HAVE_OPENSSL_EVP + rc = ngx_http_vod_hls_init_muxer_conf(submodule_context, &muxer_conf); + if (rc != NGX_OK) + { + return rc; + } + rc = hls_muxer_init_segment( &submodule_context->request_context, - &submodule_context->conf->hls.mpegts_muxer_config, + &muxer_conf, &encryption_params, submodule_context->request_params.segment_index, &submodule_context->media_set, @@ -996,10 +1111,10 @@ ngx_http_vod_hls_create_loc_conf( conf->absolute_master_urls = NGX_CONF_UNSET; conf->absolute_index_urls = NGX_CONF_UNSET; conf->absolute_iframe_urls = NGX_CONF_UNSET; - conf->mpegts_muxer_config.interleave_frames = NGX_CONF_UNSET; - conf->mpegts_muxer_config.align_frames = NGX_CONF_UNSET; - conf->mpegts_muxer_config.output_id3_timestamps = NGX_CONF_UNSET; - conf->mpegts_muxer_config.align_pts = NGX_CONF_UNSET; + conf->interleave_frames = NGX_CONF_UNSET; + conf->align_frames = NGX_CONF_UNSET; + conf->align_pts = NGX_CONF_UNSET; + conf->output_id3_timestamps = NGX_CONF_UNSET; conf->encryption_method = NGX_CONF_UNSET_UINT; conf->m3u8_config.output_iframes_playlist = NGX_CONF_UNSET; conf->m3u8_config.force_unmuxed_segments = NGX_CONF_UNSET; @@ -1034,11 +1149,15 @@ ngx_http_vod_hls_merge_loc_conf( ngx_conf_merge_value(conf->m3u8_config.force_unmuxed_segments, prev->m3u8_config.force_unmuxed_segments, 0); ngx_conf_merge_uint_value(conf->m3u8_config.container_format, prev->m3u8_config.container_format, HLS_CONTAINER_AUTO); - ngx_conf_merge_value(conf->mpegts_muxer_config.interleave_frames, prev->mpegts_muxer_config.interleave_frames, 0); - ngx_conf_merge_value(conf->mpegts_muxer_config.align_frames, prev->mpegts_muxer_config.align_frames, 1); - ngx_conf_merge_value(conf->mpegts_muxer_config.output_id3_timestamps, prev->mpegts_muxer_config.output_id3_timestamps, 0); - ngx_conf_merge_value(conf->mpegts_muxer_config.align_pts, prev->mpegts_muxer_config.align_pts, 0); - + ngx_conf_merge_value(conf->interleave_frames, prev->interleave_frames, 0); + ngx_conf_merge_value(conf->align_frames, prev->align_frames, 1); + ngx_conf_merge_value(conf->align_pts, prev->align_pts, 0); + ngx_conf_merge_value(conf->output_id3_timestamps, prev->output_id3_timestamps, 0); + if (conf->id3_data == NULL) + { + conf->id3_data = prev->id3_data; + } + ngx_conf_merge_uint_value(conf->encryption_method, prev->encryption_method, HLS_ENC_NONE); m3u8_builder_init_config( diff --git a/ngx_http_vod_hls_commands.h b/ngx_http_vod_hls_commands.h index 92c90a38..5f68b33c 100644 --- a/ngx_http_vod_hls_commands.h +++ b/ngx_http_vod_hls_commands.h @@ -111,28 +111,35 @@ NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, ngx_conf_set_flag_slot, NGX_HTTP_LOC_CONF_OFFSET, - BASE_OFFSET + offsetof(ngx_http_vod_hls_loc_conf_t, mpegts_muxer_config.interleave_frames), + BASE_OFFSET + offsetof(ngx_http_vod_hls_loc_conf_t, interleave_frames), NULL }, { ngx_string("vod_hls_mpegts_align_frames"), NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, ngx_conf_set_flag_slot, NGX_HTTP_LOC_CONF_OFFSET, - BASE_OFFSET + offsetof(ngx_http_vod_hls_loc_conf_t, mpegts_muxer_config.align_frames), + BASE_OFFSET + offsetof(ngx_http_vod_hls_loc_conf_t, align_frames), NULL }, { ngx_string("vod_hls_mpegts_output_id3_timestamps"), NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, ngx_conf_set_flag_slot, NGX_HTTP_LOC_CONF_OFFSET, - BASE_OFFSET + offsetof(ngx_http_vod_hls_loc_conf_t, mpegts_muxer_config.output_id3_timestamps), + BASE_OFFSET + offsetof(ngx_http_vod_hls_loc_conf_t, output_id3_timestamps), + NULL }, + + { ngx_string("vod_hls_mpegts_id3_data"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, + ngx_http_set_complex_value_slot, + NGX_HTTP_LOC_CONF_OFFSET, + BASE_OFFSET + offsetof(ngx_http_vod_hls_loc_conf_t, id3_data), NULL }, { ngx_string("vod_hls_mpegts_align_pts"), NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, ngx_conf_set_flag_slot, NGX_HTTP_LOC_CONF_OFFSET, - BASE_OFFSET + offsetof(ngx_http_vod_hls_loc_conf_t, mpegts_muxer_config.align_pts), + BASE_OFFSET + offsetof(ngx_http_vod_hls_loc_conf_t, align_pts), NULL }, { ngx_string("vod_hls_force_unmuxed_segments"), diff --git a/ngx_http_vod_hls_conf.h b/ngx_http_vod_hls_conf.h index 4ba97d92..8a3098c5 100644 --- a/ngx_http_vod_hls_conf.h +++ b/ngx_http_vod_hls_conf.h @@ -12,7 +12,11 @@ typedef struct ngx_flag_t absolute_index_urls; ngx_flag_t absolute_iframe_urls; ngx_str_t master_file_name_prefix; - hls_mpegts_muxer_conf_t mpegts_muxer_config; + bool_t interleave_frames; + bool_t align_frames; + bool_t align_pts; + bool_t output_id3_timestamps; + ngx_http_complex_value_t* id3_data; vod_uint_t encryption_method; ngx_http_complex_value_t* encryption_key_uri; diff --git a/ngx_http_vod_module.c b/ngx_http_vod_module.c index 44ab3db3..e6370d51 100644 --- a/ngx_http_vod_module.c +++ b/ngx_http_vod_module.c @@ -593,6 +593,47 @@ ngx_http_vod_set_notification_id_var(ngx_http_request_t *r, ngx_http_variable_va return NGX_OK; } +static ngx_int_t +ngx_http_vod_set_segment_time_var(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data) +{ + ngx_http_vod_ctx_t* ctx; + media_set_t* media_set; + int64_t value; + u_char* p; + + ctx = ngx_http_get_module_ctx(r, ngx_http_vod_module); + if (ctx == NULL) + { + v->not_found = 1; + return NGX_OK; + } + + media_set = &ctx->submodule_context.media_set; + if (media_set->filtered_tracks >= media_set->filtered_tracks_end) + { + v->not_found = 1; + return NGX_OK; + } + + p = ngx_pnalloc(r->pool, NGX_INT64_LEN); + if (p == NULL) + { + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "ngx_http_vod_set_segment_time_var: ngx_pnalloc failed"); + return NGX_ERROR; + } + + value = media_set_get_segment_time_millis(media_set); + + v->data = p; + v->len = ngx_sprintf(p, "%L", value) - p; + v->valid = 1; + v->no_cacheable = 1; + v->not_found = 0; + + return NGX_OK; +} + static ngx_int_t ngx_http_vod_set_segment_duration_var(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data) { @@ -688,6 +729,7 @@ static ngx_http_vod_variable_t ngx_http_vod_variables[] = { DEFINE_VAR(dynamic_mapping), DEFINE_VAR(request_params), DEFINE_VAR(notification_id), + DEFINE_VAR(segment_time), DEFINE_VAR(segment_duration), { ngx_string("vod_frames_bytes_read"), ngx_http_vod_set_uint32_var, offsetof(ngx_http_vod_ctx_t, frames_bytes_read) }, }; diff --git a/vod/hls/hls_muxer.c b/vod/hls/hls_muxer.c index 26630ddc..1588cf71 100644 --- a/vod/hls/hls_muxer.c +++ b/vod/hls/hls_muxer.c @@ -9,16 +9,11 @@ #include "eac3_encrypt_filter.h" #endif // VOD_HAVE_OPENSSL_EVP -#define ID3_TEXT_JSON_FORMAT "{\"timestamp\":%uL}%Z" -#define ID3_TEXT_JSON_SEQUENCE_ID_PREFIX_FORMAT "{\"timestamp\":%uL,\"sequenceId\":\"" -#define ID3_TEXT_JSON_SEQUENCE_ID_SUFFIX "\"}" - // from ffmpeg mpegtsenc #define DEFAULT_PES_HEADER_FREQ 16 #define DEFAULT_PES_PAYLOAD_SIZE ((DEFAULT_PES_HEADER_FREQ - 1) * 184 + 170) #define hls_rescale_millis(millis) ((millis) * (HLS_TIMESCALE / 1000)) -#define hls_rescale_to_millis(ts) ((ts) / (HLS_TIMESCALE / 1000)) // typedefs typedef struct { @@ -165,11 +160,7 @@ hls_muxer_init_id3_stream( id3_track_t* last_track; id3_track_t* cur_track; vod_status_t rc; - vod_str_t* sequence_id; - u_char* p; - size_t sequence_id_escape; - size_t data_size; - int64_t timestamp; + bool_t frame_added; void* frames_source_context; cur_stream = state->last_stream; @@ -185,31 +176,16 @@ hls_muxer_init_id3_stream( return rc; } - if (!conf->output_id3_timestamps) + if (conf->id3_data.len <= 0) { state->id3_context = NULL; return VOD_OK; } - // get the data size - sequence_id = &media_set->sequences[0].id; - if (sequence_id->len != 0) - { - sequence_id_escape = vod_escape_json(NULL, sequence_id->data, sequence_id->len); - data_size = sizeof(ID3_TEXT_JSON_SEQUENCE_ID_PREFIX_FORMAT) + VOD_INT64_LEN + - sequence_id->len + sequence_id_escape + - sizeof(ID3_TEXT_JSON_SEQUENCE_ID_SUFFIX); - } - else - { - sequence_id_escape = 0; - data_size = sizeof(ID3_TEXT_JSON_FORMAT) + VOD_INT64_LEN; - } - // allocate the context context = vod_alloc(state->request_context->pool, sizeof(*context) + - (sizeof(context->first_track[0]) + data_size) * media_set->clip_count); + (sizeof(context->first_track[0])) * media_set->clip_count); if (context == NULL) { vod_log_debug0(VOD_LOG_DEBUG_LEVEL, state->request_context->log, 0, @@ -220,8 +196,6 @@ hls_muxer_init_id3_stream( cur_track = (void*)(context + 1); last_track = cur_track + media_set->clip_count; - p = (void*)last_track; - // init the memory frames source rc = frames_source_memory_init(state->request_context, &frames_source_context); if (rc != VOD_OK) @@ -232,6 +206,7 @@ hls_muxer_init_id3_stream( // init the tracks context->first_track = cur_track; ref_track = media_set->filtered_tracks; + frame_added = FALSE; for (; cur_track < last_track; cur_track++, ref_track += media_set->total_track_count) { @@ -246,36 +221,17 @@ hls_muxer_init_id3_stream( dest_track->frames.next = NULL; dest_track->frames.first_frame = &cur_track->frame; dest_track->frames.last_frame = &cur_track->frame; - if (ref_track->frame_count > 0) + if (ref_track->frame_count > 0 && !frame_added) { + frame_added = TRUE; dest_track->frames.last_frame++; } dest_track->frames.frames_source = &frames_source_memory; dest_track->frames.frames_source_context = frames_source_context; // init the frame - cur_track->frame.offset = (uintptr_t)p; - timestamp = ref_track->original_clip_time + - hls_rescale_to_millis(ref_track->first_frame_time_offset); - if (sequence_id->len != 0) - { - p = vod_sprintf(p, ID3_TEXT_JSON_SEQUENCE_ID_PREFIX_FORMAT, timestamp); - if (sequence_id_escape) - { - p = (u_char*)vod_escape_json(p, sequence_id->data, sequence_id->len); - } - else - { - p = vod_copy(p, sequence_id->data, sequence_id->len); - } - p = vod_copy(p, ID3_TEXT_JSON_SEQUENCE_ID_SUFFIX, sizeof(ID3_TEXT_JSON_SEQUENCE_ID_SUFFIX)); - - } - else - { - p = vod_sprintf(p, ID3_TEXT_JSON_FORMAT, timestamp); - } - cur_track->frame.size = (uintptr_t)p - cur_track->frame.offset; + cur_track->frame.offset = (uintptr_t)conf->id3_data.data; + cur_track->frame.size = conf->id3_data.len; cur_track->frame.duration = 0; cur_track->frame.key_frame = 1; cur_track->frame.pts_delay = 0; diff --git a/vod/hls/hls_muxer.h b/vod/hls/hls_muxer.h index 6684d343..1b16d300 100644 --- a/vod/hls/hls_muxer.h +++ b/vod/hls/hls_muxer.h @@ -27,7 +27,7 @@ typedef struct { bool_t interleave_frames; bool_t align_frames; bool_t align_pts; - bool_t output_id3_timestamps; + vod_str_t id3_data; } hls_mpegts_muxer_conf_t; typedef struct { diff --git a/vod/media_set.c b/vod/media_set.c new file mode 100644 index 00000000..fee35c11 --- /dev/null +++ b/vod/media_set.c @@ -0,0 +1,25 @@ +#include "media_set.h" + +int64_t +media_set_get_segment_time_millis(media_set_t* media_set) +{ + media_track_t* cur_track; + + // try to find a track that has frames, if no track is found, fallback to the first track + for (cur_track = media_set->filtered_tracks; ; cur_track += media_set->total_track_count) + { + if (cur_track >= media_set->filtered_tracks_end) + { + cur_track = media_set->filtered_tracks; + break; + } + + if (cur_track->frame_count > 0) + { + break; + } + } + + return cur_track->original_clip_time + + rescale_time(cur_track->first_frame_time_offset, cur_track->media_info.timescale, 1000); +} diff --git a/vod/media_set.h b/vod/media_set.h index dff42ee7..76e310c9 100644 --- a/vod/media_set.h +++ b/vod/media_set.h @@ -188,4 +188,7 @@ typedef struct { uint32_t height; } request_params_t; + +int64_t media_set_get_segment_time_millis(media_set_t* media_set); + #endif //__MEDIA_SET_H__