diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 752f9817..3d18ca58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: libssl-dev \ libpcre3-dev \ libxml2-dev \ - wget \ + curl \ unzip \ libopencore-amrwb0 \ libopencore-amrnb0 \ diff --git a/README.md b/README.md index 05e9d4cc..0c768e06 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ [Join the list of organizations using this video packager project](https://github.com/kaltura/nginx-vod-module/issues/730/). +For live video streaming, please use [Media-Framework](https://github.com/kaltura/media-framework/). + ### Features * On-the-fly repackaging of MP4 files to DASH, HDS, HLS, MSS @@ -20,7 +22,7 @@ * Fallback support for file not found in local/mapped modes (useful in multi-datacenter environments) -* Video codecs: H264, H265 (DASH/HLS), VP8 (DASH), VP9 (DASH), AV1 (DASH) +* Video codecs: H264, H265 (DASH/HLS), AV1 (DASH/HLS), VP8 (DASH), VP9 (DASH) * Audio codecs: AAC, MP3 (HLS/HDS/MSS), AC-3 (DASH/HLS), E-AC-3 (DASH/HLS), VORBIS (DASH), OPUS (DASH), FLAC (HLS), DTS (HLS) @@ -124,7 +126,7 @@ C Macro Configurations: ### Installation #### RHEL/CentOS 6/7 RPM -``` +```sh # rpm -ihv http://installrepo.kaltura.org/releases/kaltura-release.noarch.rpm # yum install kaltura-nginx ``` @@ -133,26 +135,26 @@ C Macro Configurations: *Ubuntu NOTE: before trying to install kaltura-nginx, you must also make sure the multiverse repo is enabled* For Debian Wheezy [7], Debian Jessie [8], Ubuntu 14.04 and 14.10, add this repo: -``` +```sh # wget -O - http://installrepo.kaltura.org/repo/apt/debian/kaltura-deb-curr.gpg.key|apt-key add - # echo "deb [arch=amd64] http://installrepo.kaltura.org/repo/apt/debian propus main" > /etc/apt/sources.list.d/kaltura.list ``` For Ubuntu 16.04, 16.10 add this repo: -``` +```sh # wget -O - http://installrepo.kaltura.org/repo/apt/xenial/kaltura-deb-curr-256.gpg.key|apt-key add - # echo "deb [arch=amd64] http://installrepo.kaltura.org/repo/apt/xenial propus main" > /etc/apt/sources.list.d/kaltura.list ``` For Ubuntu 20.04 add this repo: -``` +```sh # wget -O - http://installrepo.kaltura.org/repo/aptn/focal/kaltura-deb-256.gpg.key|apt-key add - # echo "deb [arch=amd64] http://installrepo.kaltura.org/repo/aptn/focal quasar main" > /etc/apt/sources.list.d/kaltura.list ``` Then install the kaltura-nginx package: -``` +```sh # apt-get update # apt-get install kaltura-nginx ``` @@ -266,7 +268,7 @@ But first, a couple of definitions: #### Simple mapping The JSON below maps the request URI to a single MP4 file: -``` +```json { "sequences": [ { @@ -287,7 +289,7 @@ possible to combine more complex JSONs using multi URL. #### Adaptive set As an alternative to using multi URL, an adaptive set can be defined via JSON: -``` +```json { "sequences": [ { @@ -313,7 +315,7 @@ As an alternative to using multi URL, an adaptive set can be defined via JSON: #### Playlist The JSON below will play 35 seconds of video1 followed by 22 seconds of video2: -``` +```json { "durations": [ 35000, 22000 ], "sequences": [ @@ -337,7 +339,7 @@ The JSON below will play 35 seconds of video1 followed by 22 seconds of video2: The JSON below takes video1, plays it at x1.5 and mixes the audio of the result with the audio of video2, after reducing it to 50% volume: -``` +```json { "sequences": [ { @@ -375,7 +377,7 @@ after reducing it to 50% volume: The JSON below is a sample of a continuous live stream (=a live stream in which all videos have exactly the same encoding parameters). In practice, this JSON will have to be generated by some script, since it is time dependent. (see test/playlist.php for a sample implementation) -``` +```json { "playlistType": "live", "discontinuity": false, @@ -404,7 +406,7 @@ In practice, this JSON will have to be generated by some script, since it is tim The JSON below is a sample of a non-continuous live stream (=a live stream in which the videos have different encoding parameters). In practice, this JSON will have to be generated by some script, since it is time dependent (see test/playlist.php for a sample implementation) -``` +```json { "playlistType": "live", "discontinuity": true, @@ -530,6 +532,8 @@ Optional fields: * `label` - a friendly string that identifies the sequence. If a language is specified, a default label will be automatically derived by it - e.g. if language is `ita`, by default `italiano` will be used as the label. +* `default` - a boolean that sets the value of the DEFAULT attribute of EXT-X-MEDIA tags using this sequence. + If not specified, the first EXT-X-MEDIA tag in each group returns DEFAULT=YES, while the others return DEFAULT=NO. * `bitrate` - an object that can be used to set the bitrate for the different media types, in bits per second. For example, `{"v": 900000, "a": 64000}`. If the bitrate is not supplied, nginx-vod-module will estimate it based on the last clip in the sequence. @@ -569,6 +573,7 @@ Mandatory fields: an empty captions file (useful in case only some videos in a playlist have captions) Optional fields: +* `id` - a string that identifies the source clip * `sourceType` - sets the interface that should be used to read the MP4 file, allowed values are: `file` and `http`. By default, the module uses `http` if `vod_remote_upstream_location` is set, and `file` otherwise. @@ -678,7 +683,7 @@ of the origin will be able to bypass the CDN token enforcement. If using Akamai, be accomplished using https://github.com/refractalize/nginx_mod_akamai_g2o. For other CDNs, it may be possible to configure the CDN to send a secret header to the origin and then simply enforce the header using an nginx if statement: -``` +```c if ($http_x_secret_origin_header != "secret value") { return 403; } @@ -740,7 +745,7 @@ The `vod_drm_upstream_location` parameter specifies an nginx location that is us and the request uri is configured using `vod_drm_request_uri` (this parameter can include nginx variables). The response of the DRM server is a JSON, with the following format: -``` +```json [{ "pssh": [{ "data": "CAESEGMyZjg2MTczN2NjNGYzODIaB2thbHR1cmEiCjBfbmptaWlwbXAqBVNEX0hE", @@ -761,7 +766,7 @@ The response of the DRM server is a JSON, with the following format: ##### Sample configurations Apple FairPlay HLS: -``` +```nginx location ~ ^/fpshls/p/\d+/(sp/\d+/)?serveFlavor/entryId/([^/]+)/(.*) { vod hls; vod_hls_encryption_method sample-aes; @@ -782,7 +787,7 @@ location ~ ^/fpshls/p/\d+/(sp/\d+/)?serveFlavor/entryId/([^/]+)/(.*) { ``` Common Encryption HLS: -``` +```nginx location ~ ^/cenchls/p/\d+/(sp/\d+/)?serveFlavor/entryId/([^/]+)/(.*) { vod hls; vod_hls_encryption_method sample-aes-cenc; @@ -1687,8 +1692,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` @@ -1698,6 +1714,13 @@ The timestamp is measured in milliseconds since the epoch (unixtime x 1000), the When enabled, the module will shift back the dts timestamps by the pts delay of the initial frame. This can help keep the pts timestamps aligned across multiple renditions. +#### vod_hls_encryption_output_iv +* **syntax**: `vod_hls_encryption_output_iv on/off` +* **default**: `off` +* **context**: `http`, `server`, `location` + +When enabled, the module outputs the `IV` attribute in returned `#EXT-X-KEY` tags. + ### Configuration directives - MSS #### vod_mss_manifest_file_name_prefix @@ -1819,6 +1842,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 @@ -1827,7 +1851,7 @@ Note: Configuration directives that can accept variables are explicitly marked a ### Sample configurations #### Local configuration - +```nginx http { upstream fallback { server fallback.kaltura.com:80; @@ -1873,9 +1897,10 @@ Note: Configuration directives that can accept variables are explicitly marked a } } } +``` #### Mapped configuration - +```nginx http { upstream kalapi { server www.kaltura.com:80; @@ -1936,9 +1961,10 @@ Note: Configuration directives that can accept variables are explicitly marked a } } } +``` #### Mapped + Remote configuration - +```nginx http { upstream jsonupstream { server jsonserver:80; @@ -1992,9 +2018,10 @@ Note: Configuration directives that can accept variables are explicitly marked a } } } +``` Set it up so that http://jsonserver:80/test.json returns the following JSON: - +```json { "sequences": [{ "clips": [{ @@ -2003,11 +2030,12 @@ Set it up so that http://jsonserver:80/test.json returns the following JSON: }] }] } +``` And use this stream URL - http://nginx-vod-server/hls/test.json/master.m3u8 #### Remote configuration - +```nginx http { upstream kalapi { server www.kaltura.com:80; @@ -2045,7 +2073,7 @@ And use this stream URL - http://nginx-vod-server/hls/test.json/master.m3u8 } } } - +``` ### Copyright & License All code in this project is released under the [AGPLv3 license](http://www.gnu.org/licenses/agpl-3.0.html) unless a different license for a particular library is specified in the applicable library path. diff --git a/ci_build.sh b/ci_build.sh index 54b85a56..d1c74d9e 100755 --- a/ci_build.sh +++ b/ci_build.sh @@ -1,18 +1,21 @@ #!/bin/sh -set -o nounset # Treat unset variables as an error +set -eo nounset # Treat unset variables as an error -NGINX_VERSION=1.23.0 -NGINX_URI="http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz" +BASE_DOWNLOAD_URI=http://nginx.org/download +NGINX_VERSION=`curl -L "$BASE_DOWNLOAD_URI" | + grep -oP 'href="nginx-\K[0-9]+\.[0-9]+\.[0-9]+' | + sort -t. -rn -k1,1 -k2,2 -k3,3 | head -1` +NGINX_URI="$BASE_DOWNLOAD_URI/nginx-$NGINX_VERSION.tar.gz" - -if [ ! -x "`which wget 2>/dev/null`" ];then - echo "Need to install wget." +if [ ! -x "`which curl 2>/dev/null`" ];then + echo "Need to install curl." exit 2 fi + mkdir -p /tmp/builddir/nginx-$NGINX_VERSION cp -r . /tmp/builddir/nginx-$NGINX_VERSION/nginx-vod-module cd /tmp/builddir -wget $NGINX_URI -O kaltura-nginx-$NGINX_VERSION.tar.gz +curl $NGINX_URI > kaltura-nginx-$NGINX_VERSION.tar.gz tar zxf kaltura-nginx-$NGINX_VERSION.tar.gz mv nginx-$NGINX_VERSION nginx cd nginx @@ -55,6 +58,6 @@ export LD_LIBRARY_PATH LIBRARY_PATH C_INCLUDE_PATH --with-ipv6 \ --with-debug \ --with-threads \ - --with-cc-opt="-O3 -mpopcnt" \ + --with-cc-opt="-O3 -mpopcnt" \ $* make -j $(nproc) diff --git a/config b/config index eac19257..7a627a95 100644 --- a/config +++ b/config @@ -213,7 +213,7 @@ ngx_feature_incs="#include #include " ngx_feature_path= ngx_feature_libs="$LIB_AV_FILTER" -ngx_feature_test="avfilter_register_all()" +ngx_feature_test="avfilter_get_by_name(NULL)" . auto/feature if [ $ngx_found = yes ]; then @@ -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 7d48dca1..1f916fe5 100644 --- a/ngx_http_vod_hls.c +++ b/ngx_http_vod_hls.c @@ -16,9 +16,10 @@ #endif // NGX_HAVE_OPENSSL_EVP // constants -#define SUPPORTED_CODECS \ +#define SUPPORTED_CODECS_MP4 \ (VOD_CODEC_FLAG(AVC) | \ VOD_CODEC_FLAG(HEVC) | \ + VOD_CODEC_FLAG(AV1) | \ VOD_CODEC_FLAG(AAC) | \ VOD_CODEC_FLAG(AC3) | \ VOD_CODEC_FLAG(EAC3) | \ @@ -26,6 +27,22 @@ VOD_CODEC_FLAG(DTS) | \ VOD_CODEC_FLAG(FLAC)) +#define SUPPORTED_CODECS_TS \ + (VOD_CODEC_FLAG(AVC) | \ + VOD_CODEC_FLAG(HEVC) | \ + VOD_CODEC_FLAG(AAC) | \ + VOD_CODEC_FLAG(AC3) | \ + VOD_CODEC_FLAG(EAC3) | \ + VOD_CODEC_FLAG(MP3) | \ + VOD_CODEC_FLAG(DTS)) + +#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"; static u_char encryption_key_content_type[] = "application/octet-stream"; @@ -62,12 +79,15 @@ ngx_http_vod_hls_get_container_format( ngx_http_vod_hls_loc_conf_t* conf, media_set_t* media_set) { + media_track_t* track; + if (conf->m3u8_config.container_format != HLS_CONTAINER_AUTO) { return conf->m3u8_config.container_format; } - if (media_set->filtered_tracks[0].media_info.codec_id == VOD_CODEC_ID_HEVC || + track = media_set->filtered_tracks; + if ((track->media_info.media_type == MEDIA_TYPE_VIDEO && track->media_info.codec_id != VOD_CODEC_ID_AVC) || conf->encryption_method == HLS_ENC_SAMPLE_AES_CENC) { return HLS_CONTAINER_FMP4; @@ -151,7 +171,7 @@ ngx_http_vod_hls_init_encryption_params( } encryption_params->iv = encryption_params->iv_buf; - encryption_params->return_iv = FALSE; + encryption_params->return_iv = conf->hls.output_iv; sequence = &submodule_context->media_set.sequences[0]; @@ -250,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, @@ -391,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; @@ -426,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); @@ -484,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; @@ -512,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, @@ -896,7 +1027,7 @@ static const ngx_http_vod_request_t hls_iframes_request = { REQUEST_FLAG_SINGLE_TRACK_PER_MEDIA_TYPE | REQUEST_FLAG_PARSE_ALL_CLIPS, PARSE_FLAG_FRAMES_ALL_EXCEPT_OFFSETS | PARSE_FLAG_PARSED_EXTRA_DATA_SIZE, REQUEST_CLASS_OTHER, - SUPPORTED_CODECS, + SUPPORTED_CODECS_TS, HLS_TIMESCALE, ngx_http_vod_hls_handle_iframe_playlist, NULL, @@ -916,7 +1047,7 @@ static const ngx_http_vod_request_t hls_ts_segment_request = { REQUEST_FLAG_SINGLE_TRACK_PER_MEDIA_TYPE, PARSE_FLAG_FRAMES_ALL | PARSE_FLAG_PARSED_EXTRA_DATA | PARSE_FLAG_INITIAL_PTS_DELAY, REQUEST_CLASS_SEGMENT, - SUPPORTED_CODECS, + SUPPORTED_CODECS_TS, HLS_TIMESCALE, NULL, ngx_http_vod_hls_init_ts_frame_processor, @@ -926,7 +1057,7 @@ static const ngx_http_vod_request_t hls_mp4_segment_request = { REQUEST_FLAG_SINGLE_TRACK_PER_MEDIA_TYPE, PARSE_FLAG_FRAMES_ALL | PARSE_FLAG_INITIAL_PTS_DELAY, REQUEST_CLASS_SEGMENT, - SUPPORTED_CODECS, + SUPPORTED_CODECS_MP4, HLS_TIMESCALE, NULL, ngx_http_vod_hls_init_fmp4_frame_processor, @@ -936,7 +1067,7 @@ static const ngx_http_vod_request_t hls_mp4_segment_request_cbcs = { REQUEST_FLAG_SINGLE_TRACK_PER_MEDIA_TYPE, PARSE_FLAG_FRAMES_ALL | PARSE_FLAG_EXTRA_DATA | PARSE_FLAG_INITIAL_PTS_DELAY, REQUEST_CLASS_SEGMENT, - SUPPORTED_CODECS, + SUPPORTED_CODECS_MP4, HLS_TIMESCALE, NULL, ngx_http_vod_hls_init_fmp4_frame_processor, @@ -946,7 +1077,7 @@ static const ngx_http_vod_request_t hls_mp4_segment_request_cenc = { REQUEST_FLAG_SINGLE_TRACK_PER_MEDIA_TYPE, PARSE_FLAG_FRAMES_ALL | PARSE_FLAG_PARSED_EXTRA_DATA | PARSE_FLAG_INITIAL_PTS_DELAY, REQUEST_CLASS_SEGMENT, - SUPPORTED_CODECS, + SUPPORTED_CODECS_MP4, HLS_TIMESCALE, NULL, ngx_http_vod_hls_init_fmp4_frame_processor, @@ -966,7 +1097,7 @@ static const ngx_http_vod_request_t hls_mp4_init_request = { REQUEST_FLAG_SINGLE_TRACK_PER_MEDIA_TYPE, PARSE_BASIC_METADATA_ONLY | PARSE_FLAG_SAVE_RAW_ATOMS, REQUEST_CLASS_OTHER, - SUPPORTED_CODECS, + SUPPORTED_CODECS_MP4, HLS_TIMESCALE, ngx_http_vod_hls_handle_mp4_init_segment, NULL, @@ -980,11 +1111,12 @@ 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->output_iv = NGX_CONF_UNSET; conf->m3u8_config.output_iframes_playlist = NGX_CONF_UNSET; conf->m3u8_config.force_unmuxed_segments = NGX_CONF_UNSET; conf->m3u8_config.container_format = NGX_CONF_UNSET_UINT; @@ -1000,6 +1132,7 @@ ngx_http_vod_hls_merge_loc_conf( ngx_conf_merge_value(conf->absolute_master_urls, prev->absolute_master_urls, 1); ngx_conf_merge_value(conf->absolute_index_urls, prev->absolute_index_urls, 1); ngx_conf_merge_value(conf->absolute_iframe_urls, prev->absolute_iframe_urls, 0); + ngx_conf_merge_value(conf->output_iv, prev->output_iv, 0); ngx_conf_merge_value(conf->m3u8_config.output_iframes_playlist, prev->m3u8_config.output_iframes_playlist, 1); ngx_conf_merge_str_value(conf->master_file_name_prefix, prev->master_file_name_prefix, "master"); @@ -1018,11 +1151,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..a98d6b55 100644 --- a/ngx_http_vod_hls_commands.h +++ b/ngx_http_vod_hls_commands.h @@ -35,6 +35,13 @@ NGX_HTTP_LOC_CONF_OFFSET, BASE_OFFSET + offsetof(ngx_http_vod_hls_loc_conf_t, m3u8_config.encryption_key_format_versions), NULL }, + + { ngx_string("vod_hls_encryption_output_iv"), + 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, output_iv), + NULL }, #endif // NGX_HAVE_OPENSSL_EVP { ngx_string("vod_hls_container_format"), @@ -111,28 +118,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..fd422874 100644 --- a/ngx_http_vod_hls_conf.h +++ b/ngx_http_vod_hls_conf.h @@ -12,9 +12,14 @@ 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; + bool_t output_iv; // derived fields m3u8_config_t m3u8_config; diff --git a/ngx_http_vod_module.c b/ngx_http_vod_module.c index 44ab3db3..e40b7ea8 100644 --- a/ngx_http_vod_module.c +++ b/ngx_http_vod_module.c @@ -410,21 +410,33 @@ static ngx_int_t ngx_http_vod_set_sequence_id_var(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data) { ngx_http_vod_ctx_t *ctx; + media_sequence_t* cur_sequence; ngx_str_t* value; ctx = ngx_http_get_module_ctx(r, ngx_http_vod_module); - if (ctx == NULL || - ctx->cur_sequence < ctx->submodule_context.media_set.sequences || - ctx->cur_sequence >= ctx->submodule_context.media_set.sequences_end) + if (ctx == NULL) { v->not_found = 1; return NGX_OK; } + + cur_sequence = ctx->cur_sequence; + if (cur_sequence == NULL && ctx->submodule_context.media_set.sequence_count == 1) + { + cur_sequence = ctx->submodule_context.media_set.sequences; + } - value = &ctx->cur_sequence->id; + if (cur_sequence < ctx->submodule_context.media_set.sequences || + cur_sequence >= ctx->submodule_context.media_set.sequences_end) + { + v->not_found = 1; + return NGX_OK; + } + + value = &cur_sequence->id; if (value->len == 0) { - value = &ctx->cur_sequence->stripped_uri; + value = &cur_sequence->stripped_uri; if (value->len == 0) { v->not_found = 1; @@ -444,8 +456,10 @@ ngx_http_vod_set_sequence_id_var(ngx_http_request_t *r, ngx_http_variable_value_ static ngx_int_t ngx_http_vod_set_clip_id_var(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data) { + media_clip_source_t* clip_source; ngx_http_vod_ctx_t *ctx; media_clip_t* cur_clip; + media_set_t* media_set; ngx_str_t* value; ctx = ngx_http_get_module_ctx(r, ngx_http_vod_module); @@ -457,13 +471,29 @@ ngx_http_vod_set_clip_id_var(ngx_http_request_t *r, ngx_http_variable_value_t *v cur_clip = ctx->cur_clip; if (cur_clip == NULL) { - goto not_found; + media_set = &ctx->submodule_context.media_set; + if (media_set->sequence_count == 1 && media_set->clip_count == 1) + { + cur_clip = media_set->sequences->clips[0]; + } + else + { + goto not_found; + } } switch (cur_clip->type) { case MEDIA_CLIP_SOURCE: - value = &((media_clip_source_t*)cur_clip)->mapped_uri; + clip_source = (media_clip_source_t*)cur_clip; + if (clip_source->id.len != 0) + { + value = &clip_source->id; + } + else + { + value = &clip_source->mapped_uri; + } break; case MEDIA_CLIP_DYNAMIC: @@ -593,6 +623,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 +759,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) }, }; @@ -3505,7 +3577,7 @@ ngx_http_vod_run_generators(ngx_http_vod_ctx_t *ctx) } if (parse_params.langs_mask != NULL && - !vod_is_bit_set(parse_params.langs_mask, cur_source->sequence->language)) + !vod_is_bit_set(parse_params.langs_mask, cur_source->sequence->tags.language)) { ngx_memzero(&cur_source->track_array, sizeof(cur_source->track_array)); continue; @@ -5264,9 +5336,7 @@ ngx_http_vod_map_media_set_apply(ngx_http_vod_ctx_t *ctx, ngx_str_t* mapping, in ctx->submodule_context.media_set.segmenter_conf = mapped_media_set.segmenter_conf; sequence = cur_source->sequence; sequence->mapped_uri = mapped_source->mapped_uri; - sequence->lang_str = mapped_media_set.sequences->lang_str; - sequence->language = mapped_media_set.sequences->language; - sequence->label = mapped_media_set.sequences->label; + sequence->tags = mapped_media_set.sequences->tags; sequence->id = mapped_media_set.sequences->id; ngx_memcpy(sequence->bitrate, mapped_media_set.sequences->bitrate, sizeof(sequence->bitrate)); ngx_memcpy(sequence->avg_bitrate, mapped_media_set.sequences->avg_bitrate, sizeof(sequence->avg_bitrate)); diff --git a/ngx_http_vod_request_parse.c b/ngx_http_vod_request_parse.c index 98f4c086..21eeef62 100644 --- a/ngx_http_vod_request_parse.c +++ b/ngx_http_vod_request_parse.c @@ -734,10 +734,10 @@ ngx_http_vod_parse_lang_param(ngx_str_t* value, void* output, int offset) return NGX_HTTP_BAD_REQUEST; } - sequence->lang_str.data = (u_char *)lang_get_rfc_5646_name(result); - sequence->lang_str.len = ngx_strlen(sequence->lang_str.data); - sequence->language = result; - lang_get_native_name(result, &sequence->label); + sequence->tags.lang_str.data = (u_char *)lang_get_rfc_5646_name(result); + sequence->tags.lang_str.len = ngx_strlen(sequence->tags.lang_str.data); + sequence->tags.language = result; + lang_get_native_name(result, &sequence->tags.label); return VOD_OK; } @@ -1078,9 +1078,10 @@ ngx_http_vod_parse_uri_path( } cur_sequence->id.len = 0; - cur_sequence->lang_str.len = 0; - cur_sequence->language = 0; - cur_sequence->label.len = 0; + cur_sequence->tags.lang_str.len = 0; + cur_sequence->tags.language = 0; + cur_sequence->tags.label.len = 0; + cur_sequence->tags.is_default = -1; cur_sequence->first_key_frame_offset = 0; cur_sequence->key_frame_durations = NULL; cur_sequence->drm_info = NULL; diff --git a/ngx_http_vod_thumb.c b/ngx_http_vod_thumb.c index ccda286a..8e530877 100644 --- a/ngx_http_vod_thumb.c +++ b/ngx_http_vod_thumb.c @@ -148,7 +148,7 @@ static const ngx_http_vod_request_t thumb_request = { REQUEST_FLAG_SINGLE_TRACK, PARSE_FLAG_FRAMES_ALL | PARSE_FLAG_EXTRA_DATA, REQUEST_CLASS_THUMB, - VOD_CODEC_FLAG(AVC) | VOD_CODEC_FLAG(HEVC) | VOD_CODEC_FLAG(VP8) | VOD_CODEC_FLAG(VP9), + VOD_CODEC_FLAG(AVC) | VOD_CODEC_FLAG(HEVC) | VOD_CODEC_FLAG(VP8) | VOD_CODEC_FLAG(VP9) | VOD_CODEC_FLAG(AV1), THUMB_TIMESCALE, NULL, ngx_http_vod_thumb_init_frame_processor, diff --git a/ngx_perf_counters.c b/ngx_perf_counters.c index d4c9cd64..89f5e4d9 100644 --- a/ngx_perf_counters.c +++ b/ngx_perf_counters.c @@ -43,6 +43,7 @@ ngx_perf_counters_init(ngx_shm_zone_t *shm_zone, void *data) p = ngx_sprintf(shpool->log_ctx, LOG_CONTEXT_FORMAT, &shm_zone->shm.name); // allocate the perf couonters state + p = ngx_align_ptr(p, sizeof(ngx_atomic_t)); state = (ngx_perf_counters_t*)p; ngx_memzero(state, sizeof(*state)); @@ -56,8 +57,11 @@ ngx_shm_zone_t* ngx_perf_counters_create_zone(ngx_conf_t *cf, ngx_str_t *name, void *tag) { ngx_shm_zone_t* result; + size_t size; - result = ngx_shared_memory_add(cf, name, sizeof(ngx_slab_pool_t) + sizeof(LOG_CONTEXT_FORMAT) + name->len + sizeof(ngx_perf_counters_t), tag); + size = sizeof(ngx_slab_pool_t) + sizeof(LOG_CONTEXT_FORMAT) + name->len + sizeof(ngx_atomic_t) + sizeof(ngx_perf_counters_t); + + result = ngx_shared_memory_add(cf, name, size, tag); if (result == NULL) { return NULL; diff --git a/test/http_utils.py b/test/http_utils.py index 0a682785..83e09823 100644 --- a/test/http_utils.py +++ b/test/http_utils.py @@ -11,6 +11,9 @@ import time import gzip +MAX_BODY_SIZE = 20 * 1024 * 1024 +GET_TIMEOUT = 60 + def parseHttpHeaders(headers): result = {} for header in headers: @@ -21,26 +24,31 @@ def parseHttpHeaders(headers): result[headerName].append(headerValue) return result -def readDecode(f): - body = f.read() - if f.info().get('Content-Encoding') != 'gzip': - return body +def parseResponse(url, f): + body = f.read(MAX_BODY_SIZE) - gzipFile = gzip.GzipFile(fileobj=StringIO(body)) - try: - return gzipFile.read() - except IOError as e: - return 'Error: failed to decode gzip' + contentLength = f.info().getheader('content-length') + if contentLength is not None and contentLength.isdigit() and len(body) != min(int(contentLength), MAX_BODY_SIZE): + return 0, {}, 'Error: %s content-length %s is different than the resulting file size %s' % (url, contentLength, len(body)) + + if f.info().get('Content-Encoding') == 'gzip': + gzipFile = gzip.GzipFile(fileobj=StringIO(body)) + try: + body = gzipFile.read() + except IOError as e: + return 0, {}, 'Error: failed to decode gzip' + + return f.getcode(), parseHttpHeaders(f.info().headers), body def getUrl(url, extraHeaders={}): headers = getG2OHeaderFullUrl(url) headers.update(extraHeaders) request = urllib2.Request(url, headers=headers) try: - f = urllib2.urlopen(request) - body = readDecode(f) + f = urllib2.urlopen(request, timeout=GET_TIMEOUT) + return parseResponse(url, f) except urllib2.HTTPError as e: - return e.getcode(), parseHttpHeaders(e.info().headers), readDecode(e) + return parseResponse(url, e) except urllib2.URLError as e: return 0, {}, 'Error: request failed %s %s' % (url, e) except BadStatusLine as e: @@ -52,19 +60,12 @@ def getUrl(url, extraHeaders={}): except InvalidURL as e: return 0, {}, 'Error: got invalid url error %s %s' % (url, e) - # validate content length - contentLength = f.info().getheader('content-length') - if contentLength != None and contentLength != '%s' % len(body): - return 0, {}, 'Error: %s content-length %s is different than the resulting file size %s' % (url, contentLength, len(body)) - - return f.getcode(), parseHttpHeaders(f.info().headers), body - def downloadUrl(url, fileName): r = urllib2.urlopen(urllib2.Request(url)) with file(fileName, 'wb') as w: shutil.copyfileobj(r,w) r.close() - + def getG2OHeaders(uri): if len(G2O_KEY) == 0: return {} @@ -75,7 +76,7 @@ def getG2OHeaders(uri): dig = hmac.new(G2O_KEY, msg=data + uri, digestmod=hashlib.sha256).digest() sign = base64.b64encode(dig) return { - G2O_DATA_HEADER_NAME: data, + G2O_DATA_HEADER_NAME: data, G2O_SIGN_HEADER_NAME: sign, } diff --git a/test/main.py b/test/main.py index a75fcb1d..703aa518 100644 --- a/test/main.py +++ b/test/main.py @@ -141,7 +141,7 @@ def validatePlaylistM3U8(buffer, expectedBaseUrl): assertEquals(expectedBaseUrl, baseUrl) assertStartsWith(fileName, M3U8_SEGMENT_PREFIX) assertEndsWith(fileName, M3U8_SEGMENT_POSTFIX) - expectExtInf = not expectExtInf + expectExtInf = not expectExtInf ### Misc utility functions def getHttpResponseRegular(body = '', status = '200 OK', length = None, headers = {}): @@ -178,7 +178,7 @@ def createRandomSymLink(sourcePath): ### Socket functions class SocketException(Exception): pass - + def createTcpServer(port): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # prevent Address already in use errors @@ -283,7 +283,7 @@ def serveFileHandler(s, path, mimeType, headers, maxSize = -1): if headers.startswith('HEAD'): socketSendAndShutdown(s, getHttpResponse(length=maxSize, headers={'Content-Type':mimeType})) return - + # get the request body f = file(path, 'rb') range = getHttpHeader(headers, 'range') @@ -360,7 +360,7 @@ def __init__(self, getUrl, protocolPrefix, setupServer): class DashTestSuite(ProtocolTestSuite): def __init__(self, getUrl, setupServer): super(DashTestSuite, self).__init__(getUrl, DASH_PREFIX, setupServer) - + # bad requests def testUnrecognizedRequest(self): assertRequestFails(self.getUrl('/bla'), 400) @@ -369,15 +369,15 @@ def testUnrecognizedRequest(self): def testBadStreamTypeManifest(self): assertRequestFails(self.getUrl('/manifest-a1-z1.mpd'), 400) self.logTracker.assertContains('did not consume the whole name') - + def testBadStreamIndexManifest(self): assertRequestFails(self.getUrl('/manifest-aabc.mpd'), 400) self.logTracker.assertContains('did not consume the whole name') - + def testBadStreamTypeInit(self): assertRequestFails(self.getUrl('/init-a1-z1.mp4'), 400) self.logTracker.assertContains('did not consume the whole name') - + def testBadStreamIndexInit(self): assertRequestFails(self.getUrl('/init-aabc.mp4'), 400) self.logTracker.assertContains('did not consume the whole name') @@ -389,7 +389,7 @@ def testBadFragmentIndex(self): def testBadStreamTypeFragment(self): assertRequestFails(self.getUrl('/fragment-1-a1-z1.m4s'), 400) self.logTracker.assertContains('did not consume the whole name') - + def testBadStreamIndexFragment(self): assertRequestFails(self.getUrl('/fragment-1-aabc.m4s'), 400) self.logTracker.assertContains('did not consume the whole name') @@ -406,7 +406,7 @@ def testUnrecognizedRequest(self): def testBadStreamTypeManifest(self): assertRequestFails(self.getUrl('/manifest-a1-z1.f4m'), 400) self.logTracker.assertContains('did not consume the whole name') - + def testBadStreamIndexManifest(self): assertRequestFails(self.getUrl('/manifest-aabc.f4m'), 400) self.logTracker.assertContains('did not consume the whole name') @@ -422,11 +422,11 @@ def testZeroFragmentIndex(self): def testNoSegmentFragment(self): assertRequestFails(self.getUrl('/frag-1'), 400) self.logTracker.assertContains('invalid segment / fragment requested') - + def testBadStreamTypeFragment(self): assertRequestFails(self.getUrl('/frag-a1-z1-Seg1-Frag1'), 400) self.logTracker.assertContains('did not consume the whole name') - + def testBadStreamIndexFragment(self): assertRequestFails(self.getUrl('/frag-aabc-Seg1-Frag1'), 400) self.logTracker.assertContains('did not consume the whole name') @@ -434,16 +434,16 @@ def testBadStreamIndexFragment(self): class HlsTestSuite(ProtocolTestSuite): def __init__(self, getUrl, setupServer): super(HlsTestSuite, self).__init__(getUrl, HLS_PREFIX, setupServer) - + # bad requests def testBadSegmentIndex(self): assertRequestFails(self.getUrl('/seg-abc-a1-v1.ts'), 400) self.logTracker.assertContains('failed to parse segment index') - + def testBadStreamIndex(self): assertRequestFails(self.getUrl('/seg-1-aabc-v1.ts'), 400) self.logTracker.assertContains('did not consume the whole name') - + def testBadStreamType(self): assertRequestFails(self.getUrl('/seg-1-a1-z1.ts'), 400) self.logTracker.assertContains('did not consume the whole name') @@ -468,30 +468,30 @@ def testUnrecognizedRequest(self): def testBadStreamType(self): assertRequestFails(self.getUrl('/manifest-a1-z1'), 400) self.logTracker.assertContains('did not consume the whole name') - + def testBadStreamIndex(self): assertRequestFails(self.getUrl('/manifest-aabc'), 400) self.logTracker.assertContains('did not consume the whole name') - + def testBadFragmentRequestQualityLevel(self): assertRequestFails(self.getUrl('/Quality-Levels(2364883)/Fragments(video=0)'), 400) - self.logTracker.assertContains('ngx_http_vod_parse_string failed') + self.logTracker.assertContains('ngx_http_vod_parse_string failed') def testBadFragmentRequestBitrate(self): assertRequestFails(self.getUrl('/QualityLevels(a)/Fragments(video=0)'), 400) - self.logTracker.assertContains('ngx_http_vod_parse_string failed') - + self.logTracker.assertContains('ngx_http_vod_parse_string failed') + def testBadFragmentRequestFragments(self): assertRequestFails(self.getUrl('/QualityLevels(2364883)/Frag-ments(video=0)'), 400) - self.logTracker.assertContains('ngx_http_vod_parse_string failed') + self.logTracker.assertContains('ngx_http_vod_parse_string failed') def testBadFragmentRequestNoDelim(self): assertRequestFails(self.getUrl('/QualityLevels(2364883)/Fragments(video)'), 400) - self.logTracker.assertContains('ngx_http_vod_parse_string failed') + self.logTracker.assertContains('ngx_http_vod_parse_string failed') def testBadFragmentRequestTimestamp(self): assertRequestFails(self.getUrl('/QualityLevels(2364883)/Fragments(video=a)'), 400) - self.logTracker.assertContains('ngx_http_vod_parse_string failed') + self.logTracker.assertContains('ngx_http_vod_parse_string failed') def testBadFragmentRequestMediaTypeLength(self): assertRequestFails(self.getUrl('/QualityLevels(2364883)/Fragments(videox=0)'), 400) @@ -512,11 +512,13 @@ def runChildSuites(self): HdsTestSuite(self.getUrl, self.prepareTest).run() HlsTestSuite(self.getUrl, self.prepareTest).run() MssTestSuite(self.getUrl, self.prepareTest).run() - + # sanity def testHeadRequestSanity(self): for curPrefix, curRequest, contentType in ALL_REQUESTS: url = self.getUrl(curPrefix, curRequest) + if KEEPALIVE_PREFIX in url: + continue fullResponse = urllib2.urlopen(url).read() request = urllib2.Request(url) request.get_method = lambda : 'HEAD' @@ -533,8 +535,10 @@ def testRangeRequestSanity(self): cleanupStack.resetAndDestroy() if self.prepareTest != None: self.prepareTest() - + url = self.getUrl(curPrefix, curRequest) + if KEEPALIVE_PREFIX in url: + continue response = urllib2.urlopen(url) assertEquals(response.info().getheader('Content-Type'), contentType) fullResponse = response.read() @@ -579,7 +583,7 @@ def testEncryptionSanity(self): clearSegment = urllib2.urlopen(self.getUrl(HLS_PREFIX, HLS_SEGMENT_FILE).replace(ENCRYPTED_PREFIX, '')).read() assert(clearSegment == decryptedSegment) - + def testClipToSanity(self): # index url = self.getUrl(HLS_PREFIX, '/clipTo/10000' + HLS_PLAYLIST_FILE) @@ -616,23 +620,23 @@ def testRequiredTracksOptional(self): # request must not have track specification if '-v1' in curRequest: continue - + # request must have extension if curRequest.rfind('.') < curRequest.rfind('/'): continue - + # no tracks specification url = self.getUrl(curPrefix, curRequest) noTracksResponse = urllib2.urlopen(url).read() # with tracks specification url = '-a1-v1.'.join(url.rsplit('.', 1)) # replace only the last dot - withTracksResponse = urllib2.urlopen(url).read() + withTracksResponse = urllib2.urlopen(url).read() withTracksResponse = withTracksResponse.replace('-a1-v1.f4m', '.f4m') # align the f4m id tag assert(noTracksResponse == withTracksResponse) - # bad requests + # bad requests def testPostRequest(self): assertRequestFails(self.getUrl(HLS_PREFIX, '/seg-1-a1-v1.ts'), 405, postData='abcd') self.logTracker.assertContains('unsupported method') @@ -644,7 +648,7 @@ def testSegmentIdTooBig(self): def testNonExistingTracksM3U8(self): assertRequestFails(self.getUrl(HLS_PREFIX, '/index-a10-v10.m3u8'), 400) self.logTracker.assertContains('no matching streams were found') - + def testNonExistingTracksTS(self): assertRequestFails(self.getUrl(HLS_PREFIX, '/seg-1-a10-v10.ts'), 404) self.logTracker.assertContains('no matching streams were found, probably invalid segment index') @@ -668,28 +672,28 @@ def testBadClipTo(self): def testBadClipFrom(self): assertRequestFails(self.getUrl(HLS_PREFIX, '/clipFrom/abcd' + HLS_PLAYLIST_FILE), 400) self.logTracker.assertContains('clip from parser failed') - + class UpstreamTestSuite(TestSuite): def __init__(self, baseUrl, urlFile, serverPort): super(UpstreamTestSuite, self).__init__() self.baseUrl = baseUrl self.urlFile = urlFile self.serverPort = serverPort - + def testServerNotListening(self): assertRequestFails(getUniqueUrl(self.baseUrl, self.urlFile), 502) self.logTracker.assertContains('connect() failed (111: Connection refused)') - + def testServerNotAccepting(self): s = createTcpServer(self.serverPort) - assertRequestFails(getUniqueUrl(self.baseUrl, self.urlFile), 504) + assertRequestFails(getUniqueUrl(self.baseUrl, self.urlFile), [502, 504]) self.logTracker.assertContains('upstream timed out') def testServerNotResponding(self): TcpServer(self.serverPort, lambda s: socketSendAndWait(s, 'HTTP/1.1 200 OK', 10)) - assertRequestFails(getUniqueUrl(self.baseUrl, self.urlFile), 504) + assertRequestFails(getUniqueUrl(self.baseUrl, self.urlFile), [502, 504]) self.logTracker.assertContains('upstream timed out') - + def testBadStatusLine(self): TcpServer(self.serverPort, lambda s: socketSendAndShutdown(s, 'BAD STATUS LINE\r\n')) request = urllib2.Request(getUniqueUrl(self.baseUrl, self.urlFile)) @@ -698,7 +702,7 @@ def testBadStatusLine(self): assertEquals(response.strip(), 'BAD STATUS LINE') except urllib2.HTTPError as e: assertEquals(e.getcode(), 502) # returns 502 in case the request was 'in memory' - + self.logTracker.assertContains('upstream sent no valid HTTP/1.0 header') def testBadContentLength(self): @@ -750,7 +754,7 @@ def testHideHeadersNotForwardedToUpstream(self): request = urllib2.Request(url, headers={'if-range':'ukk'}) response = urllib2.urlopen(request) self.validateResponse(response.read(), url) - + def testUpstreamHostHeader(self): TcpServer(self.serverPort, lambda s: socketExpectHttpHeaderAndHandle(s, 'Host: blabla.com', None, self.upstreamHandler)) url = getUniqueUrl(self.baseUrl, self.urlFile) @@ -803,7 +807,7 @@ def testHideHeadersForwardedToUpstream(self): url = getUniqueUrl(self.baseUrl, self.urlFile) request = urllib2.Request(url, headers={'if-range':'ukk'}) assertEquals(urllib2.urlopen(request).read(), 'abcde') - + class FallbackUpstreamTestSuite(DumpUpstreamTestSuite): def testLoopPreventionHeaderSent(self): TcpServer(self.serverPort, lambda s: socketExpectHttpHeaderAndSend(s, 'X-Kaltura-Proxy: dumpApiRequest', getHttpResponse('abcde'))) @@ -835,7 +839,7 @@ def testMetadataCache(self): logTracker = LogTracker() uncachedResponse = urllib2.urlopen(url).read() logTracker.assertContains('metadata cache miss') - + logTracker = LogTracker() cachedResponse = urllib2.urlopen(url).read() logTracker.assertContains(['metadata cache hit', 'response cache hit']) @@ -864,7 +868,7 @@ def getUrl(self, protocolPrefix, filePath): encryptionPrefix = self.encryptionPrefix return getUniqueUrl(self.getBaseUrl(filePath) + protocolPrefix + encryptionPrefix + TEST_FLAVOR_URI, filePath) -class LocalTestSuite(ModeTestSuite): +class LocalTestSuite(ModeTestSuite): def runChildSuites(self): BasicTestSuite( self.getUrl, @@ -875,14 +879,14 @@ def runChildSuites(self): def testFileNotFound(self): assertRequestFails(self.baseUrl + HLS_PREFIX + TEST_NONEXISTING_FILE + HLS_PLAYLIST_FILE, 502) # 502 is due to failing to connect to fallback self.logTracker.assertContains(['open() "%s" failed' % (TEST_FILES_ROOT + TEST_NONEXISTING_FILE), 'stat() "%s" failed' % (TEST_FILES_ROOT + TEST_NONEXISTING_FILE)]) - + def getUrl(self, protocolPrefix, filePath): # encryption is supported only for hls encryptionPrefix = '' if protocolPrefix == HLS_PREFIX: encryptionPrefix = self.encryptionPrefix return self.getBaseUrl(filePath) + protocolPrefix + encryptionPrefix + TEST_FLAVOR_FILE + filePath - + class MappedTestSuite(ModeTestSuite): def runChildSuites(self): BasicTestSuite( @@ -935,24 +939,24 @@ def testPathMappingCache(self): TcpServer(API_SERVER_PORT, lambda s: socketSendAndShutdown(s, getPathMappingResponse(TEST_FILES_ROOT + TEST_FLAVOR_FILE))) for curPrefix, curRequest, contentType in ALL_REQUESTS: url = self.getUrl(curPrefix, curRequest) - + # uncached logTracker = LogTracker() - response = urllib2.urlopen(url) - assertEquals(response.info().getheader('Content-Type'), contentType) + response = urllib2.urlopen(url) + assertEquals(response.info().getheader('Content-Type'), contentType) uncachedResponse = response.read() logTracker.assertContains('mapping cache miss') #cached logTracker = LogTracker() response = urllib2.urlopen(url) - assertEquals(response.info().getheader('Content-Type'), contentType) + assertEquals(response.info().getheader('Content-Type'), contentType) cachedResponse = response.read() logTracker.assertContains(['mapping cache hit', 'response cache hit']) assert(cachedResponse == uncachedResponse) - -class RemoteTestSuite(ModeTestSuite): + +class RemoteTestSuite(ModeTestSuite): def runChildSuites(self): BasicTestSuite( self.getUrl, @@ -960,7 +964,7 @@ def runChildSuites(self): requestHandler = lambda s,h: serveFileHandler(s, TEST_FILES_ROOT + TEST_FLAVOR_FILE, TEST_FILE_TYPE, h) MemoryUpstreamTestSuite(self.baseUrl + HLS_PREFIX + TEST_FLAVOR_URI, HLS_PLAYLIST_FILE, API_SERVER_PORT, requestHandler).run() DumpUpstreamTestSuite(self.getBaseUrl('') + TEST_FLAVOR_URI, '', API_SERVER_PORT).run() # non HLS URL will just dump to upstream - + def testMetadataCache(self): TcpServer(API_SERVER_PORT, lambda s: serveFile(s, TEST_FILES_ROOT + TEST_FLAVOR_FILE, TEST_FILE_TYPE)) for curPrefix, curRequest, _ in VOD_REQUESTS: @@ -969,7 +973,7 @@ def testMetadataCache(self): logTracker = LogTracker() uncachedResponse = urllib2.urlopen(url).read() logTracker.assertContains('metadata cache miss') - + logTracker = LogTracker() cachedResponse = urllib2.urlopen(url).read() logTracker.assertContains(['metadata cache hit', 'response cache hit']) @@ -1007,35 +1011,37 @@ def testInfoParseError(self): TcpServer(DRM_SERVER_PORT, lambda s: socketSendAndShutdown(s, getHttpResponse('null'))) assertRequestFails(self.getUrl(EDASH_PREFIX, DASH_MANIFEST_FILE), 503) self.logTracker.assertContains('invalid drm info response null') - + def testDrmInfoCache(self): TcpServer(API_SERVER_PORT, lambda s: serveFile(s, TEST_FILES_ROOT + TEST_FLAVOR_FILE, TEST_FILE_TYPE)) TcpServer(DRM_SERVER_PORT, lambda s: socketSendAndShutdown(s, getHttpResponse(DRM_SERVICE_RESPONSE))) url = self.getUrl(EDASH_PREFIX, DASH_MANIFEST_FILE) - + logTracker = LogTracker() missResponse = urllib2.urlopen(url).read() logTracker.assertContains('drm info cache miss') - + logTracker = LogTracker() hitResponse = urllib2.urlopen(url.replace(DASH_MANIFEST_FILE, '/manifest-v1-a1.mpd')).read() logTracker.assertContains('drm info cache hit') - + assert(missResponse == hitResponse) - + class MainTestSuite(TestSuite): - def runChildSuites(self): + def runChildSuites(self): DrmTestSuite(NGINX_REMOTE).run() # all combinations of (encrypted, non encrypted) x (keep alive, no keep alive) for encryptionPrefix in [ENCRYPTED_PREFIX, '']: for keepAlivePrefix in [KEEPALIVE_PREFIX, '']: + print('-- keepAlivePrefix=%s, encryptionPrefix=%s' % (keepAlivePrefix, encryptionPrefix)) LocalTestSuite(NGINX_HOST + keepAlivePrefix + '/tlocal', encryptionPrefix).run() MappedTestSuite(NGINX_HOST + keepAlivePrefix + '/tmapped', encryptionPrefix).run() RemoteTestSuite(NGINX_HOST + keepAlivePrefix + '/tremote', encryptionPrefix).run() for getHttpResponse in [getHttpResponseChunked, getHttpResponseRegular]: - socketSend = socketSendRegular - MainTestSuite().run() - socketSend = socketSendByteByByte - MainTestSuite().run() + for socketSend in [socketSendRegular, socketSendByteByByte]: + print('-- socketSend=%s, getHttpResponse=%s' % ( + 'regular' if socketSend == socketSendRegular else 'byteByByte', + 'chunked' if getHttpResponse == getHttpResponseChunked else 'regular')) + MainTestSuite().run() diff --git a/test/nginx.conf b/test/nginx.conf index b0f3aa7e..30271249 100644 --- a/test/nginx.conf +++ b/test/nginx.conf @@ -60,24 +60,24 @@ http { aio on; upstream kalapi { - server localhost:80; + server localhost:80 max_fails=0; } upstream self { - server localhost:8001; + server localhost:8001 max_fails=0; keepalive 32; } upstream testapi { - server localhost:8002; + server localhost:8002 max_fails=0; } upstream fallback { - server localhost:8003; + server localhost:8003 max_fails=0; } upstream drmservice { - server localhost:8004; + server localhost:8004 max_fails=0; } server { diff --git a/test/test_base.py b/test/test_base.py index d0fd3999..c914d6bc 100644 --- a/test/test_base.py +++ b/test/test_base.py @@ -2,7 +2,7 @@ import urllib2 import os -RUN_ONLY_PATH = '' # use for running only some tests, e.g. MainTestSuite.RemoteTestSuite.MemoryUpstreamTestSuite +RUN_ONLY_PATH = '' # use for running only some tests, e.g. MainTestSuite.RemoteTestSuite.MemoryUpstreamTestSuite NGINX_LOG_PATH = '/var/log/nginx/error.log' ### Assertions @@ -19,23 +19,23 @@ def assertIn(needle, haystack): assert(False) def assertNotIn(needle, haystack): - if not needle in haystack: - return - print('Assertion failed - %s not in %s' % (needle, haystack)) - assert(False) - + if not needle in haystack: + return + print('Assertion failed - %s not in %s' % (needle, haystack)) + assert(False) + def assertInIgnoreCase(needle, haystack): assertIn(needle.lower(), haystack.lower()) def assertNotInIgnoreCase(needle, haystack): - assertNotIn(needle.lower(), haystack.lower()) - + assertNotIn(needle.lower(), haystack.lower()) + def assertStartsWith(buffer, prefix): if buffer.startswith(prefix): return print('Assertion failed - %s.startswith(%s)' % (buffer, prefix)) assert(False) - + def assertEndsWith(buffer, postfix): if buffer.endswith(postfix): return @@ -48,7 +48,10 @@ def assertRequestFails(url, statusCode, expectedBody = None, headers = {}, postD response = urllib2.urlopen(request, data=postData) assert(False) except urllib2.HTTPError as e: - assertEquals(e.getcode(), statusCode) + if type(statusCode) == list: + assertIn(e.getcode(), statusCode) + else: + assertEquals(e.getcode(), statusCode) if expectedBody != None: assertEquals(expectedBody, e.read()) @@ -56,7 +59,7 @@ def assertRequestFails(url, statusCode, expectedBody = None, headers = {}, postD class CleanupStack: def __init__(self): self.items = [] - + def push(self, callback): self.items.append(callback) @@ -94,7 +97,7 @@ def assertContains(self, logLine): class TestSuite(object): level = 0 curPath = '' - + def __init__(self): self.prepareTest = None diff --git a/vod/common.h b/vod/common.h index 0d598b90..043a82eb 100644 --- a/vod/common.h +++ b/vod/common.h @@ -80,6 +80,9 @@ #define vod_log_debug2(level, log, err, fmt, arg1, arg2) #define vod_log_debug3(level, log, err, fmt, arg1, arg2, arg3) #define vod_log_debug4(level, log, err, fmt, arg1, arg2, arg3, arg4) +#define vod_log_debug5(level, log, err, fmt, arg1, arg2, arg3, arg4, arg5) +#define vod_log_debug6(level, log, err, fmt, arg1, arg2, arg3, arg4, arg5, arg6) +#define vod_log_debug7(level, log, err, fmt, arg1, arg2, arg3, arg4, arg5, arg6, arg7) typedef int bool_t; typedef int vod_status_t; @@ -98,6 +101,7 @@ void vod_log_error(vod_uint_t level, vod_log_t *log, int err, #define VOD_INT64_LEN NGX_INT64_LEN #define VOD_INT32_LEN NGX_INT32_LEN +#define VOD_MAX_UINT32_VALUE NGX_MAX_UINT32_VALUE #define VOD_MAX_SIZE_T_VALUE NGX_MAX_SIZE_T_VALUE #define VOD_MAX_OFF_T_VALUE NGX_MAX_OFF_T_VALUE @@ -263,6 +267,15 @@ void vod_log_error(vod_uint_t level, vod_log_t *log, int err, #define vod_log_debug4(level, log, err, fmt, arg1, arg2, arg3, arg4) \ ngx_log_debug4(level, log, err, fmt, arg1, arg2, arg3, arg4) +#define vod_log_debug5(level, log, err, fmt, arg1, arg2, arg3, arg4, arg5) \ + ngx_log_debug5(level, log, err, fmt, arg1, arg2, arg3, arg4, arg5) + +#define vod_log_debug6(level, log, err, fmt, arg1, arg2, arg3, arg4, arg5, arg6) \ + ngx_log_debug6(level, log, err, fmt, arg1, arg2, arg3, arg4, arg5, arg6) + +#define vod_log_debug7(level, log, err, fmt, arg1, arg2, arg3, arg4, arg5, arg6, arg7) \ + ngx_log_debug7(level, log, err, fmt, arg1, arg2, arg3, arg4, arg5, arg6, arg7) + #define vod_errno ngx_errno typedef intptr_t bool_t; diff --git a/vod/dash/dash_packager.c b/vod/dash/dash_packager.c index a2070847..4b48dd20 100644 --- a/vod/dash/dash_packager.c +++ b/vod/dash/dash_packager.c @@ -326,12 +326,12 @@ dash_packager_compare_tracks(uintptr_t bitrate_threshold, const media_info_t* mi (mi1->u.video.height == mi2->u.video.height); } - if (mi1->label.len == 0 || mi2->label.len == 0) + if (mi1->tags.label.len == 0 || mi2->tags.label.len == 0) { return TRUE; } - return vod_str_equals(mi1->label, mi2->label); + return vod_str_equals(mi1->tags.label, mi2->tags.label); } static void @@ -858,12 +858,12 @@ dash_packager_write_mpd_period( case MEDIA_TYPE_AUDIO: reference_track = (*adaptation_set->first) + filtered_clip_offset; - if (context->adaptation_sets.multi_audio) + if (reference_track->media_info.tags.lang_str.len > 0 || reference_track->media_info.tags.label.len > 0) { p = vod_sprintf(p, VOD_DASH_MANIFEST_ADAPTATION_HEADER_AUDIO_LANG, adapt_id++, - &reference_track->media_info.lang_str, - &reference_track->media_info.label); + &reference_track->media_info.tags.lang_str, + &reference_track->media_info.tags.label); } else { @@ -889,8 +889,8 @@ dash_packager_write_mpd_period( { reference_track = (*adaptation_set->first) + filtered_clip_offset; p = vod_sprintf(p, VOD_DASH_MANIFEST_ADAPTATION_HEADER_SUBTITLE_SMPTE_TT, - &reference_track->media_info.lang_str, - &reference_track->media_info.label); + &reference_track->media_info.tags.lang_str, + &reference_track->media_info.tags.label); break; } @@ -919,10 +919,10 @@ dash_packager_write_mpd_period( representation_id.len--; } - lang_code = lang_get_rfc_5646_name(cur_track->media_info.language); + lang_code = lang_get_rfc_5646_name(cur_track->media_info.tags.language); p = vod_sprintf(p, VOD_DASH_MANIFEST_ADAPTATION_SUBTITLE_VTT, - &cur_track->media_info.lang_str, - &cur_track->media_info.label, + &cur_track->media_info.tags.lang_str, + &cur_track->media_info.tags.label, lang_code, subtitle_adapt_id++, &cur_base_url, @@ -1205,7 +1205,7 @@ dash_packager_remove_redundant_tracks( // prefer to remove a track that doesn't have a label, so that we won't lose a language // in case of multi language manifest - if (track1->media_info.label.len == 0 || track2->media_info.label.len != 0) + if (track1->media_info.tags.label.len == 0 || track2->media_info.tags.label.len != 0) { remove = track1; } @@ -1438,7 +1438,7 @@ dash_packager_build_mpd( case MEDIA_TYPE_AUDIO: case MEDIA_TYPE_SUBTITLE: cur_track = (*adaptation_set->first) + filtered_clip_offset; - result_size += cur_track->media_info.label.len + cur_track->media_info.lang_str.len; + result_size += cur_track->media_info.tags.label.len + cur_track->media_info.tags.lang_str.len; break; } } diff --git a/vod/filters/audio_decoder.c b/vod/filters/audio_decoder.c index c81476b4..87b13d2b 100644 --- a/vod/filters/audio_decoder.c +++ b/vod/filters/audio_decoder.c @@ -55,8 +55,14 @@ audio_decoder_init_decoder( decoder->pkt_timebase = decoder->time_base; decoder->extradata = media_info->extra_data.data; decoder->extradata_size = media_info->extra_data.len; + +#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 23, 100) + av_channel_layout_from_mask(&decoder->ch_layout, media_info->u.audio.channel_layout); +#else decoder->channels = media_info->u.audio.channels; decoder->channel_layout = media_info->u.audio.channel_layout; +#endif + decoder->bits_per_coded_sample = media_info->u.audio.bits_per_sample; decoder->sample_rate = media_info->u.audio.sample_rate; diff --git a/vod/filters/audio_encoder.c b/vod/filters/audio_encoder.c index 8e8b1f6b..f27ccb93 100644 --- a/vod/filters/audio_encoder.c +++ b/vod/filters/audio_encoder.c @@ -3,7 +3,6 @@ // constants #define AUDIO_ENCODER_BITS_PER_SAMPLE (16) -#define AAC_ENCODER_NAME ("libfdk_aac") // typedefs typedef struct @@ -17,6 +16,13 @@ typedef struct static const AVCodec *encoder_codec = NULL; static bool_t initialized = FALSE; +static char* aac_encoder_names[] = { + "libfdk_aac", + "aac", + NULL +}; + + static bool_t audio_encoder_is_format_supported(const AVCodec *codec, enum AVSampleFormat sample_fmt) { @@ -36,16 +42,28 @@ audio_encoder_is_format_supported(const AVCodec *codec, enum AVSampleFormat samp void audio_encoder_process_init(vod_log_t* log) { + char** name; + #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 18, 100) avcodec_register_all(); #endif - encoder_codec = avcodec_find_encoder_by_name(AAC_ENCODER_NAME); - if (encoder_codec == NULL) + for (name = aac_encoder_names; ; name++) { - vod_log_error(VOD_LOG_WARN, log, 0, - "audio_encoder_process_init: failed to get AAC encoder, audio encoding is disabled. recompile libavcodec with libfdk_aac to enable it"); - return; + if (*name == NULL) + { + vod_log_error(VOD_LOG_WARN, log, 0, + "audio_encoder_process_init: failed to get AAC encoder, audio encoding is disabled. recompile libavcodec with an aac encoder to enable it"); + return; + } + + encoder_codec = avcodec_find_encoder_by_name(*name); + if (encoder_codec != NULL) + { + vod_log_error(VOD_LOG_INFO, log, 0, + "audio_encoder_process_init: using aac encoder \"%s\"", *name); + break; + } } if (!audio_encoder_is_format_supported(encoder_codec, AUDIO_ENCODER_INPUT_SAMPLE_FORMAT)) @@ -99,8 +117,14 @@ audio_encoder_init( encoder->time_base.num = 1; encoder->time_base.den = params->timescale; encoder->sample_rate = params->sample_rate; + +#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 23, 100) + av_channel_layout_from_mask(&encoder->ch_layout, params->channel_layout); +#else encoder->channels = params->channels; encoder->channel_layout = params->channel_layout; +#endif + encoder->bit_rate = params->bitrate; encoder->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; // make the codec generate the extra data @@ -306,8 +330,15 @@ audio_encoder_update_media_info( media_info->bitrate = encoder->bit_rate; media_info->u.audio.object_type_id = 0x40; // ffmpeg always writes 0x40 (ff_mp4_obj_type) + +#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 23, 100) + media_info->u.audio.channels = encoder->ch_layout.nb_channels; + media_info->u.audio.channel_layout = encoder->ch_layout.u.mask; +#else media_info->u.audio.channels = encoder->channels; media_info->u.audio.channel_layout = encoder->channel_layout; +#endif + media_info->u.audio.bits_per_sample = AUDIO_ENCODER_BITS_PER_SAMPLE; media_info->u.audio.packet_size = 0; // ffmpeg always writes 0 (mov_write_audio_tag) media_info->u.audio.sample_rate = encoder->sample_rate; diff --git a/vod/filters/audio_filter.c b/vod/filters/audio_filter.c index 2d9a5f64..0daeaf0e 100644 --- a/vod/filters/audio_filter.c +++ b/vod/filters/audio_filter.c @@ -315,7 +315,11 @@ audio_filter_init_source( decoder->time_base.den, decoder->sample_rate, av_get_sample_fmt_name(decoder->sample_fmt), +#if LIBAVFILTER_VERSION_INT >= AV_VERSION_INT(8, 44, 100) + decoder->ch_layout.u.mask); +#else decoder->channel_layout); +#endif avrc = avfilter_graph_create_filter( &source->buffer_src, @@ -732,8 +736,13 @@ audio_filter_alloc_state( } else { +#if LIBAVFILTER_VERSION_INT >= AV_VERSION_INT(8, 44, 100) + encoder_params.channels = sink_link->ch_layout.nb_channels; + encoder_params.channel_layout = sink_link->ch_layout.u.mask; +#else encoder_params.channels = sink_link->channels; encoder_params.channel_layout = sink_link->channel_layout; +#endif encoder_params.sample_rate = sink_link->sample_rate; encoder_params.timescale = sink_link->time_base.den; encoder_params.bitrate = output_track->media_info.bitrate; diff --git a/vod/filters/volume_map.c b/vod/filters/volume_map.c index d29c3131..acbdc729 100644 --- a/vod/filters/volume_map.c +++ b/vod/filters/volume_map.c @@ -55,13 +55,20 @@ volume_map_calc_frame( const float* end; double sum_squares; double sample; + int channels; + +#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 23, 100) + channels = frame->ch_layout.nb_channels; +#else + channels = frame->channels; +#endif switch (frame->format) { case AV_SAMPLE_FMT_FLTP: sum_squares = 0; channel_cur = (const float**)frame->extended_data; - channel_end = channel_cur + frame->channels; + channel_end = channel_cur + channels; for (; channel_cur < channel_end; channel_cur++) { cur = *channel_cur; @@ -81,7 +88,7 @@ volume_map_calc_frame( } result->sum_squares = sum_squares; - result->samples = frame->nb_samples * frame->channels; + result->samples = frame->nb_samples * channels; return VOD_OK; } diff --git a/vod/hds/hds_amf0_encoder.c b/vod/hds/hds_amf0_encoder.c index d20b79d2..f587b1c4 100644 --- a/vod/hds/hds_amf0_encoder.c +++ b/vod/hds/hds_amf0_encoder.c @@ -127,7 +127,6 @@ hds_amf0_write_metadata(u_char* p, media_set_t* media_set, media_track_t** track uint64_t duration; uint32_t timescale; uint32_t count; - uint32_t bitrate = 0; uint8_t sound_format; count = AMF0_COMMON_FIELDS_COUNT; @@ -155,7 +154,6 @@ hds_amf0_write_metadata(u_char* p, media_set_t* media_set, media_track_t** track if (tracks[MEDIA_TYPE_VIDEO] != NULL) { media_info = &tracks[MEDIA_TYPE_VIDEO]->media_info; - bitrate += media_info->bitrate; p = hds_amf0_append_array_number_value(p, &amf0_width, media_info->u.video.width); p = hds_amf0_append_array_number_value(p, &amf0_height, media_info->u.video.height); p = hds_amf0_append_array_number_value(p, &amf0_videodatarate, (double)media_info->bitrate / 1000.0); @@ -166,7 +164,6 @@ hds_amf0_write_metadata(u_char* p, media_set_t* media_set, media_track_t** track if (tracks[MEDIA_TYPE_AUDIO] != NULL) { media_info = &tracks[MEDIA_TYPE_AUDIO]->media_info; - bitrate += media_info->bitrate; p = hds_amf0_append_array_number_value(p, &amf0_audiodatarate, (double)media_info->bitrate / 1000.0); p = hds_amf0_append_array_number_value(p, &amf0_audiosamplerate, media_info->u.audio.sample_rate); p = hds_amf0_append_array_number_value(p, &amf0_audiosamplesize, media_info->u.audio.bits_per_sample); diff --git a/vod/hds/hds_manifest.c b/vod/hds/hds_manifest.c index 5d03c0d8..af37d8b6 100644 --- a/vod/hds/hds_manifest.c +++ b/vod/hds/hds_manifest.c @@ -462,14 +462,14 @@ hds_packager_build_manifest( { if (adaptation_set->first[MEDIA_TYPE_AUDIO] != NULL) { - result_size += adaptation_set->first[MEDIA_TYPE_AUDIO]->media_info.label.len + - adaptation_set->first[MEDIA_TYPE_AUDIO]->media_info.lang_str.len; + result_size += adaptation_set->first[MEDIA_TYPE_AUDIO]->media_info.tags.label.len + + adaptation_set->first[MEDIA_TYPE_AUDIO]->media_info.tags.lang_str.len; } } else { - result_size += adaptation_set->first[0]->media_info.label.len + - adaptation_set->first[0]->media_info.lang_str.len; + result_size += adaptation_set->first[0]->media_info.tags.label.len + + adaptation_set->first[0]->media_info.tags.lang_str.len; } for (cur_track_ptr = adaptation_set->first; cur_track_ptr < adaptation_set->last; cur_track_ptr += muxed_tracks) @@ -573,8 +573,8 @@ hds_packager_build_manifest( } p = vod_sprintf(p, HDS_MANIFEST_HEADER_LANG, - &track->media_info.label, - &track->media_info.lang_str); + &track->media_info.tags.label, + &track->media_info.tags.lang_str); } // bootstrap tags @@ -704,8 +704,8 @@ hds_packager_build_manifest( { p = vod_sprintf(p, HDS_MEDIA_HEADER_PREFIX_AUDIO_LANG, bitrate / 1000, - &tracks[MEDIA_TYPE_AUDIO]->media_info.label, - &tracks[MEDIA_TYPE_AUDIO]->media_info.lang_str); + &tracks[MEDIA_TYPE_AUDIO]->media_info.tags.label, + &tracks[MEDIA_TYPE_AUDIO]->media_info.tags.lang_str); } else { 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/hls/m3u8_builder.c b/vod/hls/m3u8_builder.c index 2ab9c1a2..a80b8cc2 100644 --- a/vod/hls/m3u8_builder.c +++ b/vod/hls/m3u8_builder.c @@ -855,6 +855,7 @@ m3u8_builder_closed_captions_write( { media_closed_captions_t* closed_captions; uint32_t index = 0; + bool_t is_default; for (closed_captions = media_set->closed_captions; closed_captions < media_set->closed_captions_end; closed_captions++) { @@ -869,7 +870,13 @@ m3u8_builder_closed_captions_write( p = vod_sprintf(p, M3U8_EXT_MEDIA_LANG, &closed_captions->language); } - if (closed_captions == media_set->closed_captions) + is_default = closed_captions->is_default; + if (is_default < 0) + { + is_default = closed_captions == media_set->closed_captions; + } + + if (is_default) { p = vod_copy(p, M3U8_EXT_MEDIA_DEFAULT, sizeof(M3U8_EXT_MEDIA_DEFAULT) - 1); } @@ -921,8 +928,8 @@ m3u8_builder_ext_x_media_tags_get_size( { cur_track = adaptation_set->first[0]; - label_len = cur_track->media_info.label.len; - result += vod_max(label_len, default_label.len) + cur_track->media_info.lang_str.len; + label_len = cur_track->media_info.tags.label.len; + result += vod_max(label_len, default_label.len) + cur_track->media_info.tags.lang_str.len; if (base_url->len != 0) { @@ -948,6 +955,7 @@ m3u8_builder_ext_x_media_tags_write( media_track_t* tracks[MEDIA_TYPE_COUNT]; vod_str_t* label; uint32_t group_index; + bool_t is_default; char* group_id; char* type; @@ -988,9 +996,8 @@ m3u8_builder_ext_x_media_tags_write( group_index = 0; } - label = &tracks[media_type]->media_info.label; - if (label->len == 0 || - (media_type == MEDIA_TYPE_AUDIO && !adaptation_sets->multi_audio)) + label = &tracks[media_type]->media_info.tags.label; + if (label->len == 0) { label = &default_label; } @@ -1001,13 +1008,19 @@ m3u8_builder_ext_x_media_tags_write( group_index, label); - if (tracks[media_type]->media_info.lang_str.len > 0 && (media_type != MEDIA_TYPE_AUDIO || adaptation_sets->multi_audio)) + if (tracks[media_type]->media_info.tags.lang_str.len > 0) { p = vod_sprintf(p, M3U8_EXT_MEDIA_LANG, - &tracks[media_type]->media_info.lang_str); + &tracks[media_type]->media_info.tags.lang_str); + } + + is_default = tracks[media_type]->media_info.tags.is_default; + if (is_default < 0) + { + is_default = adaptation_set == first_adaptation_set; } - if (adaptation_set == first_adaptation_set) + if (is_default) { p = vod_copy(p, M3U8_EXT_MEDIA_DEFAULT, sizeof(M3U8_EXT_MEDIA_DEFAULT) - 1); } @@ -1240,7 +1253,7 @@ m3u8_builder_write_iframe_variants( video = &tracks[MEDIA_TYPE_VIDEO]->media_info; if (conf->container_format == HLS_CONTAINER_AUTO && - video->codec_id == VOD_CODEC_ID_HEVC) + video->codec_id != VOD_CODEC_ID_AVC) { continue; } diff --git a/vod/hls/mp4_to_annexb_filter.c b/vod/hls/mp4_to_annexb_filter.c index aa31b410..d8116ac4 100644 --- a/vod/hls/mp4_to_annexb_filter.c +++ b/vod/hls/mp4_to_annexb_filter.c @@ -66,16 +66,15 @@ mp4_to_annexb_set_media_info( { mp4_to_annexb_state_t* state = get_context(context); - state->nal_packet_size_length = media_info->u.video.nal_packet_size_length; - if (state->nal_packet_size_length < 1 || state->nal_packet_size_length > 4) - { - vod_log_error(VOD_LOG_ERR, context->request_context->log, 0, - "mp4_to_annexb_set_media_info: invalid nal packet size length %uD", state->nal_packet_size_length); - return VOD_BAD_DATA; - } - switch (media_info->codec_id) { + case VOD_CODEC_ID_AVC: + state->unit_type_mask = 0x1F; + state->aud_unit_type = AVC_NAL_AUD; + state->aud_nal_packet = avc_aud_nal_packet; + state->aud_nal_packet_size = sizeof(avc_aud_nal_packet); + break; + case VOD_CODEC_ID_HEVC: if (state->sample_aes) { @@ -90,12 +89,19 @@ mp4_to_annexb_set_media_info( state->aud_nal_packet_size = sizeof(hevc_aud_nal_packet); break; - default: // AVC - state->unit_type_mask = 0x1F; - state->aud_unit_type = AVC_NAL_AUD; - state->aud_nal_packet = avc_aud_nal_packet; - state->aud_nal_packet_size = sizeof(avc_aud_nal_packet); - break; + default: + vod_log_error(VOD_LOG_ERR, context->request_context->log, 0, + "mp4_to_annexb_set_media_info: codec id %uD is not supported", + media_info->codec_id); + return VOD_BAD_REQUEST; + } + + state->nal_packet_size_length = media_info->u.video.nal_packet_size_length; + if (state->nal_packet_size_length < 1 || state->nal_packet_size_length > 4) + { + vod_log_error(VOD_LOG_ERR, context->request_context->log, 0, + "mp4_to_annexb_set_media_info: invalid nal packet size length %uD", state->nal_packet_size_length); + return VOD_BAD_DATA; } state->extra_data = media_info->extra_data.data; diff --git a/vod/input/silence_generator.c b/vod/input/silence_generator.c index 06420145..b86c2025 100644 --- a/vod/input/silence_generator.c +++ b/vod/input/silence_generator.c @@ -102,9 +102,7 @@ silence_generator_generate( track->media_info.duration_millis = parse_params->clip_to - parse_params->clip_from; track->media_info.full_duration = (uint64_t)track->media_info.duration_millis * track->media_info.timescale; track->media_info.duration = track->media_info.full_duration; - track->media_info.lang_str = sequence->lang_str; - track->media_info.language = sequence->language; - track->media_info.label = sequence->label; + track->media_info.tags = sequence->tags; rc = media_format_finalize_track( request_context, diff --git a/vod/manifest_utils.c b/vod/manifest_utils.c index d2a3bbcf..bdf7ca25 100644 --- a/vod/manifest_utils.c +++ b/vod/manifest_utils.c @@ -2,7 +2,6 @@ // internal flags #define ADAPTATION_SETS_FLAG_MULTI_AUDIO (0x1000) -#define ADAPTATION_SETS_FLAG_IGNORE_SUBTITLES (0x2000) // typedefs typedef struct { @@ -442,22 +441,21 @@ track_group_key_init( break; } - if (track->media_info.label.len == 0) + if (track->media_info.tags.label.len == 0) { return FALSE; } - key->label = track->media_info.label; + key->label = track->media_info.tags.label; break; case MEDIA_TYPE_SUBTITLE: key->codec_id = 0; - if (track->media_info.label.len == 0 || - (flags & ADAPTATION_SETS_FLAG_IGNORE_SUBTITLES) != 0) + if (track->media_info.tags.label.len == 0) { return FALSE; } - key->label = track->media_info.label; + key->label = track->media_info.tags.label; break; default: // MEDIA_TYPE_NONE @@ -798,16 +796,16 @@ manifest_utils_is_multi_audio(media_set_t* media_set) for (cur_track = media_set->filtered_tracks; cur_track < last_track; cur_track++) { if (cur_track->media_info.media_type != MEDIA_TYPE_AUDIO || - cur_track->media_info.label.len == 0) + cur_track->media_info.tags.label.len == 0) { continue; } if (label == NULL) { - label = &cur_track->media_info.label; + label = &cur_track->media_info.tags.label; } - else if (!vod_str_equals(cur_track->media_info.label, *label)) + else if (!vod_str_equals(cur_track->media_info.tags.label, *label)) { return TRUE; } @@ -950,8 +948,6 @@ manifest_utils_get_adaptation_sets( "manifest_utils_get_adaptation_sets: no audio/video tracks"); return VOD_BAD_REQUEST; } - - flags |= ADAPTATION_SETS_FLAG_IGNORE_SUBTITLES; } if (manifest_utils_is_multi_audio(media_set)) diff --git a/vod/media_clip.h b/vod/media_clip.h index 6fe34886..2a9e8f30 100644 --- a/vod/media_clip.h +++ b/vod/media_clip.h @@ -74,6 +74,7 @@ struct media_clip_source_s { // TODO: the fields below are not required for generators, consider adding another struct // input params + vod_str_t id; media_clip_source_type_t source_type; vod_str_t uri; // original uri uint64_t clip_from; diff --git a/vod/media_format.c b/vod/media_format.c index 6ef94935..39ed7226 100644 --- a/vod/media_format.c +++ b/vod/media_format.c @@ -120,17 +120,20 @@ media_format_finalize_track( break; } - rc = get_nal_units( - request_context, - &media_info->extra_data, - (parse_type & PARSE_FLAG_EXTRA_DATA) == 0, - &media_info->u.video.nal_packet_size_length, - &media_info->extra_data); - if (rc != VOD_OK) + if (get_nal_units != NULL) { - vod_log_debug1(VOD_LOG_DEBUG_LEVEL, request_context->log, 0, - "media_format_finalize_track: get_nal_units failed %i", rc); - return rc; + rc = get_nal_units( + request_context, + &media_info->extra_data, + (parse_type & PARSE_FLAG_EXTRA_DATA) == 0, + &media_info->u.video.nal_packet_size_length, + &media_info->extra_data); + if (rc != VOD_OK) + { + vod_log_debug1(VOD_LOG_DEBUG_LEVEL, request_context->log, 0, + "media_format_finalize_track: get_nal_units failed %i", rc); + return rc; + } } } else if ((parse_type & PARSE_FLAG_EXTRA_DATA) != 0) diff --git a/vod/media_format.h b/vod/media_format.h index 85af29e7..629aaf5b 100644 --- a/vod/media_format.h +++ b/vod/media_format.h @@ -225,6 +225,13 @@ typedef struct { mp4a_config_t codec_config; } audio_media_info_t; +typedef struct { + language_id_t language; + vod_str_t lang_str; + vod_str_t label; + bool_t is_default; +} media_tags_t; + typedef struct media_info_s { uint32_t media_type; uint32_t format; @@ -243,9 +250,7 @@ typedef struct media_info_s { int64_t empty_duration; // temporary during parsing int64_t start_time; // temporary during parsing uint64_t codec_delay; - language_id_t language; - vod_str_t lang_str; - vod_str_t label; + media_tags_t tags; union { video_media_info_t video; audio_media_info_t audio; 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..84133005 100644 --- a/vod/media_set.h +++ b/vod/media_set.h @@ -59,9 +59,7 @@ struct media_sequence_s { media_clip_t** clips; // [clip_count] vod_str_t stripped_uri; vod_str_t id; - vod_str_t label; - vod_str_t lang_str; - language_id_t language; + media_tags_t tags; uint32_t bitrate[MEDIA_TYPE_COUNT]; uint32_t avg_bitrate[MEDIA_TYPE_COUNT]; int64_t first_key_frame_offset; @@ -109,6 +107,7 @@ typedef struct { vod_str_t id; vod_str_t language; vod_str_t label; + bool_t is_default; } media_closed_captions_t; typedef struct { @@ -188,4 +187,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__ diff --git a/vod/media_set_parser.c b/vod/media_set_parser.c index a1a0e7ff..c46542fe 100644 --- a/vod/media_set_parser.c +++ b/vod/media_set_parser.c @@ -64,6 +64,7 @@ enum { MEDIA_CLOSED_CAPTIONS_PARAM_ID, MEDIA_CLOSED_CAPTIONS_PARAM_LANGUAGE, MEDIA_CLOSED_CAPTIONS_PARAM_LABEL, + MEDIA_CLOSED_CAPTIONS_PARAM_DEFAULT, MEDIA_CLOSED_CAPTIONS_PARAM_COUNT }; @@ -105,6 +106,7 @@ static vod_status_t media_set_parse_source(void* ctx, vod_json_object_t* element static vod_status_t media_set_parse_clips_array(void* ctx, vod_json_value_t* value, void* dest); static vod_status_t media_set_parse_bitrate(void* ctx, vod_json_value_t* value, void* dest); static vod_status_t media_set_parse_source_type(void* ctx, vod_json_value_t* value, void* dest); +static vod_status_t media_set_parse_bool(void* ctx, vod_json_value_t* value, void* dest); // constants static json_parser_union_type_def_t media_clip_union_params[] = { @@ -119,6 +121,7 @@ static json_parser_union_type_def_t media_clip_union_params[] = { }; static json_object_value_def_t media_clip_source_params[] = { + { vod_string("id"), VOD_JSON_STRING, offsetof(media_clip_source_t, id), media_set_parse_null_term_string }, { vod_string("path"), VOD_JSON_STRING, offsetof(media_clip_source_t, mapped_uri), media_set_parse_null_term_string }, { vod_string("tracks"), VOD_JSON_STRING, offsetof(media_clip_source_t, tracks_mask), media_set_parse_tracks_spec }, { vod_string("clipFrom"), VOD_JSON_INT, offsetof(media_clip_source_t, clip_from), media_set_parse_int64 }, @@ -132,8 +135,9 @@ static json_object_value_def_t media_clip_source_params[] = { static json_object_value_def_t media_sequence_params[] = { { vod_string("id"), VOD_JSON_STRING, offsetof(media_sequence_t, id), media_set_parse_null_term_string }, { vod_string("clips"), VOD_JSON_ARRAY, offsetof(media_sequence_t, unparsed_clips), media_set_parse_clips_array }, - { vod_string("language"), VOD_JSON_STRING, offsetof(media_sequence_t, lang_str), media_set_parse_null_term_string }, - { vod_string("label"), VOD_JSON_STRING, offsetof(media_sequence_t, label), media_set_parse_null_term_string }, + { vod_string("language"), VOD_JSON_STRING, offsetof(media_sequence_t, tags.lang_str), media_set_parse_null_term_string }, + { vod_string("label"), VOD_JSON_STRING, offsetof(media_sequence_t, tags.label), media_set_parse_null_term_string }, + { vod_string("default"), VOD_JSON_BOOL, offsetof(media_sequence_t, tags.is_default), media_set_parse_bool }, { vod_string("bitrate"), VOD_JSON_OBJECT, offsetof(media_sequence_t, bitrate), media_set_parse_bitrate }, { vod_string("avg_bitrate"), VOD_JSON_OBJECT, offsetof(media_sequence_t, avg_bitrate), media_set_parse_bitrate }, { vod_null_string, 0, 0, NULL } @@ -149,6 +153,7 @@ 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_string("default"), VOD_JSON_BOOL, MEDIA_CLOSED_CAPTIONS_PARAM_DEFAULT }, { vod_null_string, 0, 0 } }; @@ -522,6 +527,16 @@ media_set_parse_bitrate( return VOD_OK; } +static vod_status_t +media_set_parse_bool( + void* ctx, + vod_json_value_t* value, + void* dest) +{ + *(bool_t*)dest = value->v.boolean; + return VOD_OK; +} + vod_status_t media_set_map_source( request_context_t* request_context, @@ -866,6 +881,15 @@ media_set_parse_closed_captions( cur_output->language.len = 0; } + if (params[MEDIA_CLOSED_CAPTIONS_PARAM_DEFAULT] != NULL) + { + cur_output->is_default = params[MEDIA_CLOSED_CAPTIONS_PARAM_DEFAULT]->v.boolean; + } + else + { + cur_output->is_default = -1; + } + cur_output++; } @@ -951,9 +975,10 @@ media_set_parse_sequences( cur_output->id.len = 0; cur_output->unparsed_clips = NULL; - cur_output->lang_str.len = 0; - cur_output->language = 0; - cur_output->label.len = 0; + cur_output->tags.lang_str.len = 0; + cur_output->tags.language = 0; + cur_output->tags.label.len = 0; + cur_output->tags.is_default = -1; cur_output->first_key_frame_offset = 0; cur_output->key_frame_durations = NULL; cur_output->drm_info = NULL; @@ -983,27 +1008,27 @@ media_set_parse_sequences( continue; } - if (cur_output->lang_str.len != 0) + if (cur_output->tags.lang_str.len != 0) { - if (cur_output->lang_str.len >= LANG_ISO639_3_LEN) + if (cur_output->tags.lang_str.len >= LANG_ISO639_3_LEN) { - cur_output->language = lang_parse_iso639_3_code(iso639_3_str_to_int(cur_output->lang_str.data)); - if (cur_output->language != 0) + cur_output->tags.language = lang_parse_iso639_3_code(iso639_3_str_to_int(cur_output->tags.lang_str.data)); + if (cur_output->tags.language != 0) { - cur_output->lang_str.data = (u_char *)lang_get_rfc_5646_name(cur_output->language); - cur_output->lang_str.len = ngx_strlen(cur_output->lang_str.data); + cur_output->tags.lang_str.data = (u_char *)lang_get_rfc_5646_name(cur_output->tags.language); + cur_output->tags.lang_str.len = ngx_strlen(cur_output->tags.lang_str.data); } } - if (cur_output->label.len == 0) + if (cur_output->tags.label.len == 0) { - if (cur_output->language != 0) + if (cur_output->tags.language != 0) { - lang_get_native_name(cur_output->language, &cur_output->label); + lang_get_native_name(cur_output->tags.language, &cur_output->tags.label); } else { - cur_output->label = cur_output->lang_str; + cur_output->tags.label = cur_output->tags.lang_str; } } } diff --git a/vod/mkv/ebml.c b/vod/mkv/ebml.c index e1970249..aa1b9ec7 100644 --- a/vod/mkv/ebml.c +++ b/vod/mkv/ebml.c @@ -97,9 +97,10 @@ ebml_read_num(ebml_context_t* context, uint64_t* result, size_t max_size, int re } static vod_status_t -ebml_read_size(ebml_context_t* context, uint64_t* result) +ebml_read_size(ebml_context_t* context, uint64_t* result, bool_t truncate) { vod_status_t rc; + uint64_t left; rc = ebml_read_num(context, result, 8, 1); if (rc < 0) @@ -109,19 +110,28 @@ ebml_read_size(ebml_context_t* context, uint64_t* result) return rc; } + left = context->end_pos - context->cur_pos; if (is_unknown_size(*result, rc)) { - *result = context->end_pos - context->cur_pos; + *result = left; + return VOD_OK; } - else if (*result > (uint64_t)(context->end_pos - context->cur_pos)) + + if (*result <= left) { - vod_log_error(VOD_LOG_ERR, context->request_context->log, 0, - "ebml_read_size: size %uL greater than the remaining stream bytes %uL", - *result, (uint64_t)(context->end_pos - context->cur_pos)); - return VOD_BAD_DATA; + return VOD_OK; } - return VOD_OK; + if (truncate) + { + *result = left; + return VOD_OK; + } + + vod_log_error(VOD_LOG_ERR, context->request_context->log, 0, + "ebml_read_size: size %uL greater than the remaining stream bytes %uL", + *result, left); + return VOD_BAD_DATA; } static vod_status_t @@ -194,9 +204,10 @@ ebml_parse_element(ebml_context_t* context, ebml_spec_t* spec, void* dest) uint64_t size; void* cur_dest; vod_status_t rc; + ebml_type_t type; // size - rc = ebml_read_size(context, &size); + rc = ebml_read_size(context, &size, spec->type & EBML_TRUNCATE_SIZE); if (rc != VOD_OK) { vod_log_debug1(VOD_LOG_DEBUG_LEVEL, context->request_context->log, 0, @@ -210,7 +221,8 @@ ebml_parse_element(ebml_context_t* context, ebml_spec_t* spec, void* dest) return VOD_OK; } - max_size = ebml_max_sizes[spec->type]; + type = spec->type & ~EBML_TRUNCATE_SIZE; + max_size = ebml_max_sizes[type]; if (max_size && size > max_size) { vod_log_error(VOD_LOG_ERR, context->request_context->log, 0, @@ -220,7 +232,7 @@ ebml_parse_element(ebml_context_t* context, ebml_spec_t* spec, void* dest) cur_dest = (u_char*)dest + spec->offset; - switch (spec->type) + switch (type) { case EBML_UINT: rc = ebml_read_uint(context, size, cur_dest); @@ -249,9 +261,9 @@ ebml_parse_element(ebml_context_t* context, ebml_spec_t* spec, void* dest) break; case EBML_MASTER: - next_context.request_context = context->request_context; - next_context.cur_pos = context->cur_pos + size; - next_context.end_pos = context->end_pos; + next_context = *context; + next_context.cur_pos += size; + context->end_pos = next_context.cur_pos; rc = ebml_parse_master(context, spec->child, cur_dest); if (rc != VOD_OK) @@ -264,9 +276,9 @@ ebml_parse_element(ebml_context_t* context, ebml_spec_t* spec, void* dest) return VOD_OK; case EBML_CUSTOM: - next_context.request_context = context->request_context; - next_context.cur_pos = context->cur_pos + size; - next_context.end_pos = context->end_pos; + next_context = *context; + next_context.cur_pos += size; + context->end_pos = next_context.cur_pos; parser = spec->child; rc = parser(context, spec, cur_dest); diff --git a/vod/mkv/ebml.h b/vod/mkv/ebml.h index 907d9ad4..e3d877a4 100644 --- a/vod/mkv/ebml.h +++ b/vod/mkv/ebml.h @@ -7,6 +7,8 @@ #define ebml_read_id(context, id) ebml_read_num(context, id, 4, 0) #define is_unknown_size(num, num_bytes) ((num) + 1 == 1ULL << (7 * (num_bytes))) +#define EBML_TRUNCATE_SIZE 0x80 + // typedefs typedef enum { EBML_NONE, @@ -22,6 +24,7 @@ typedef struct { request_context_t* request_context; const u_char* cur_pos; const u_char* end_pos; + int64_t offset_delta; } ebml_context_t; typedef struct { diff --git a/vod/mkv/mkv_defs.c b/vod/mkv/mkv_defs.c index d70f7aee..c4197415 100644 --- a/vod/mkv/mkv_defs.c +++ b/vod/mkv/mkv_defs.c @@ -6,7 +6,7 @@ mkv_codec_type_t mkv_codec_types[] = { // video { vod_string("V_MPEG4/ISO/AVC"), VOD_CODEC_ID_AVC, FORMAT_AVC1, TRUE }, - { vod_string("V_MPEGH/ISO/HEVC"), VOD_CODEC_ID_HEVC, FORMAT_HEV1, TRUE }, + { vod_string("V_MPEGH/ISO/HEVC"), VOD_CODEC_ID_HEVC, FORMAT_HVC1, TRUE }, { vod_string("V_VP8"), VOD_CODEC_ID_VP8, 0, FALSE }, { vod_string("V_VP9"), VOD_CODEC_ID_VP9, 0, FALSE }, { vod_string("V_AV1"), VOD_CODEC_ID_AV1, 0, FALSE }, @@ -15,7 +15,11 @@ mkv_codec_type_t mkv_codec_types[] = { { vod_string("A_AAC"), VOD_CODEC_ID_AAC, FORMAT_MP4A, TRUE }, { vod_string("A_MPEG/L3"), VOD_CODEC_ID_MP3, FORMAT_MP4A, FALSE }, { vod_string("A_VORBIS"), VOD_CODEC_ID_VORBIS,0, TRUE }, - { vod_string("A_OPUS"), VOD_CODEC_ID_OPUS, 0, TRUE }, - + { vod_string("A_OPUS"), VOD_CODEC_ID_OPUS, FORMAT_OPUS, TRUE }, + { vod_string("A_AC3"), VOD_CODEC_ID_AC3, FORMAT_AC3, FALSE }, + { vod_string("A_EAC3"), VOD_CODEC_ID_EAC3, FORMAT_EAC3, FALSE }, + { vod_string("A_DTS"), VOD_CODEC_ID_DTS, 0, TRUE }, + { vod_string("A_FLAC"), VOD_CODEC_ID_FLAC, FORMAT_FLAC, TRUE }, + { vod_null_string, 0, 0, FALSE } }; diff --git a/vod/mkv/mkv_defs.h b/vod/mkv/mkv_defs.h index 35005595..1ffb5883 100644 --- a/vod/mkv/mkv_defs.h +++ b/vod/mkv/mkv_defs.h @@ -64,6 +64,9 @@ // cluster #define MKV_ID_CLUSTERTIMECODE (0xE7) #define MKV_ID_SIMPLEBLOCK (0xA3) +#define MKV_ID_BLOCKGROUP (0xA0) +#define MKV_ID_BLOCK (0xA1) +#define MKV_ID_REFERENCEBLOCK (0xFB) #define MKV_ID_CLUSTER (0x1F43B675) // sections diff --git a/vod/mkv/mkv_format.c b/vod/mkv/mkv_format.c index 19cbe40f..2d53092e 100644 --- a/vod/mkv/mkv_format.c +++ b/vod/mkv/mkv_format.c @@ -9,11 +9,33 @@ #define BITRATE_ESTIMATE_SEC (5) #define FRAMES_PER_PART (160) // about 4K #define MAX_GOP_FRAMES (600) // 10 sec GOP in 60 fps +#define MAX_LACES (256) // the count is stored in one byte + +/* when reading the frames, need to read some extra in order to have the headers + of the first frame of the next cluster - + + field max size + MKV_ID_CLUSTER (4) + size (8) + MKV_ID_CLUSTERTIMECODE (1) + size (1) + value (8) + MKV_BLOCKGROUP (1) + size (8) + MKV_BLOCK (1) + size (8) + track number (8) + timecode (2) + flags (1) +*/ +#define READ_FRAMES_EXTRA_SIZE (51) // prototypes static vod_status_t mkv_parse_seek_entry(ebml_context_t* context, ebml_spec_t* spec, void* dst); -static vod_status_t mkv_parse_frame(ebml_context_t* context, ebml_spec_t* spec, void* dst); +static vod_status_t mkv_simple_block(ebml_context_t* context, ebml_spec_t* spec, void* dst); static vod_status_t mkv_parse_frame_estimate_bitrate(ebml_context_t* context, ebml_spec_t* spec, void* dst); +static vod_status_t mkv_block_group(ebml_context_t* context, ebml_spec_t* spec, void* dst); +static vod_status_t mkv_reference_block(ebml_context_t* context, ebml_spec_t* spec, void* dst); // raw parsing structs typedef struct { @@ -56,13 +78,17 @@ typedef struct { uint64_t track; uint64_t time; uint64_t cluster_pos; - uint64_t relative_pos; // XXXXX needed ? + uint64_t relative_pos; } mkv_index_t; typedef struct { uint64_t timecode; } mkv_cluster_t; +typedef struct { + vod_str_t block; +} mkv_block_group_t; + // matroksa specs // seekhead @@ -111,7 +137,7 @@ static ebml_spec_t mkv_spec_track_fields[] = { { MKV_ID_TRACKDEFAULTDURATION, EBML_UINT, offsetof(mkv_track_t, default_duration), NULL }, { MKV_ID_TRACKCODECDELAY, EBML_UINT, offsetof(mkv_track_t, codec_delay), NULL }, { MKV_ID_TRACKLANGUAGE, EBML_STRING, offsetof(mkv_track_t, language), NULL }, - { MKV_ID_TRACKNAME, EBML_STRING, offsetof(mkv_track_t, name), NULL }, + { MKV_ID_TRACKNAME, EBML_STRING, offsetof(mkv_track_t, name), NULL }, { MKV_ID_TRACKVIDEO, EBML_MASTER, offsetof(mkv_track_t, u.video), mkv_spec_track_video }, { MKV_ID_TRACKAUDIO, EBML_MASTER, offsetof(mkv_track_t, u.audio), mkv_spec_track_audio }, { 0, EBML_NONE, 0, NULL } @@ -142,25 +168,38 @@ static ebml_spec_t mkv_spec_index[] = { }; // cluster +static ebml_spec_t mkv_spec_block_group[] = { + { MKV_ID_BLOCK, EBML_BINARY | EBML_TRUNCATE_SIZE, offsetof(mkv_block_group_t, block), NULL }, + { MKV_ID_REFERENCEBLOCK, EBML_CUSTOM, 0, mkv_reference_block }, + { 0, EBML_NONE, 0, NULL } +}; + static ebml_spec_t mkv_spec_cluster_fields[] = { { MKV_ID_CLUSTERTIMECODE, EBML_UINT, offsetof(mkv_cluster_t, timecode), NULL }, - { MKV_ID_SIMPLEBLOCK, EBML_CUSTOM, 0, mkv_parse_frame }, + { MKV_ID_SIMPLEBLOCK, EBML_CUSTOM | EBML_TRUNCATE_SIZE, 0, mkv_simple_block }, + { MKV_ID_BLOCKGROUP, EBML_CUSTOM | EBML_TRUNCATE_SIZE, 0, mkv_block_group }, { 0, EBML_NONE, 0, NULL } }; static ebml_spec_t mkv_spec_cluster[] = { - { MKV_ID_CLUSTER, EBML_MASTER, 0, mkv_spec_cluster_fields }, + { MKV_ID_CLUSTER, EBML_MASTER | EBML_TRUNCATE_SIZE, 0, mkv_spec_cluster_fields }, + { 0, EBML_NONE, 0, NULL } +}; + +static ebml_spec_t mkv_spec_bitrate_estimate_block_group[] = { + { MKV_ID_BLOCK, EBML_CUSTOM | EBML_TRUNCATE_SIZE, 0, mkv_parse_frame_estimate_bitrate }, { 0, EBML_NONE, 0, NULL } }; static ebml_spec_t mkv_spec_bitrate_estimate_cluster_fields[] = { { MKV_ID_CLUSTERTIMECODE, EBML_UINT, offsetof(mkv_cluster_t, timecode), NULL }, - { MKV_ID_SIMPLEBLOCK, EBML_CUSTOM, 0, mkv_parse_frame_estimate_bitrate }, + { MKV_ID_SIMPLEBLOCK, EBML_CUSTOM | EBML_TRUNCATE_SIZE, 0, mkv_parse_frame_estimate_bitrate }, + { MKV_ID_BLOCKGROUP, EBML_MASTER | EBML_TRUNCATE_SIZE, 0, mkv_spec_bitrate_estimate_block_group }, { 0, EBML_NONE, 0, NULL } }; static ebml_spec_t mkv_spec_bitrate_estimate_cluster[] = { - { MKV_ID_CLUSTER, EBML_MASTER, 0, mkv_spec_bitrate_estimate_cluster_fields }, + { MKV_ID_CLUSTER, EBML_MASTER | EBML_TRUNCATE_SIZE, 0, mkv_spec_bitrate_estimate_cluster_fields }, { 0, EBML_NONE, 0, NULL } }; @@ -214,6 +253,7 @@ typedef struct { uint64_t end_time; uint32_t max_frame_count; bool_t parse_frames; + uint64_t read_offset; } mkv_base_metadata_t; typedef struct { @@ -224,14 +264,21 @@ typedef struct { vod_str_t sections[SECTION_COUNT]; mkv_file_layout_t layout; mkv_base_metadata_t result; + uint64_t read_offset; } mkv_metadata_reader_state_t; typedef struct { input_frame_t* frame; + frame_list_part_t* part; + uint32_t laces; +} mkv_laced_frame_t; + +typedef struct { + mkv_laced_frame_t frame; uint64_t timecode; input_frame_t* unsorted_frame; uint64_t unsorted_timecode; -} frame_timecode_t; +} mkv_frame_timecode_t; typedef struct { uint64_t track_number; @@ -245,7 +292,7 @@ typedef struct { uint64_t total_frames_duration; uint64_t first_timecode; - vod_array_t gop_frames; // array of frame_timecode_t + vod_array_t gop_frames; // array of mkv_frame_timecode_t int32_t min_pts_delay; } mkv_frame_parse_track_context_t; @@ -259,6 +306,11 @@ typedef struct { mkv_frame_parse_track_context_t* last_track; } mkv_frame_parse_context_t; +typedef struct { + ebml_context_t context; + uint32_t references; +} mkv_block_group_context_t; + typedef struct { uint64_t track_number; uint64_t min_frame_timecode; @@ -278,11 +330,24 @@ static vod_str_t mkv_supported_doctypes[] = { vod_null_string }; +// XXXXX avoid using hardcoded extra data - build according to the first frame instead +static u_char mkv_extra_data_ac3[] = { + 0x50, 0x11, 0xe0 +}; + +static u_char mkv_extra_data_eac3[] = { + 0x07, 0x00, 0x20, 0x0f, 0x00, 0x00 +}; + +static u_char mkv_extra_data_opus[] = { + 0x00, 0x02, 0x01, 0x38, 0x00, 0x00, 0xbb, 0x80, 0x00, 0x00, 0x00 +}; + static bool_t mkv_is_doctype_supported(vod_str_t* doctype) { vod_str_t* cur_doctype; - + for (cur_doctype = mkv_supported_doctypes; cur_doctype->len; cur_doctype++) { if (doctype->len == cur_doctype->len && @@ -310,6 +375,7 @@ mkv_metadata_reader_init( context.request_context = request_context; context.cur_pos = buffer->data; context.end_pos = buffer->data + buffer->len; + context.offset_delta = -(intptr_t)buffer->data; rc = ebml_parse_header(&context, &header); if (rc != VOD_OK) @@ -418,6 +484,7 @@ mkv_get_file_layout( context.request_context = request_context; context.cur_pos = buffer; context.end_pos = buffer + size; + context.offset_delta = -(intptr_t)buffer; // ebml header rc = ebml_parse_header(&context, &header); @@ -534,6 +601,8 @@ mkv_metadata_reader_read( } state->state = MRS_READ_SECTION_HEADER; + state->read_offset = position->pos; + result->read_req.read_offset = position->pos; result->read_req.read_size = 0; return VOD_AGAIN; @@ -544,6 +613,7 @@ mkv_metadata_reader_read( context.request_context = state->request_context; context.cur_pos = start_pos; context.end_pos = buffer->data + buffer->len; + context.offset_delta = state->read_offset - (intptr_t)buffer->data; // section id rc = ebml_read_id(&context, &id); @@ -597,6 +667,8 @@ mkv_metadata_reader_read( } state->state = MRS_READ_SECTION_DATA; + state->read_offset = position->pos; + result->read_req.read_offset = position->pos; result->read_req.read_size = size; return VOD_AGAIN; @@ -643,6 +715,7 @@ mkv_metadata_parse( context.request_context = request_context; context.cur_pos = metadata_parts[SECTION_INFO].data; context.end_pos = context.cur_pos + metadata_parts[SECTION_INFO].len; + context.offset_delta = -1; vod_memzero(&info, sizeof(info)); rc = ebml_parse_master(&context, mkv_spec_info, &info); @@ -773,16 +846,16 @@ mkv_metadata_parse( // inherit the sequence language and label sequence = parse_params->source->sequence; - if (sequence->label.len != 0) + if (sequence->tags.label.len != 0) { - track.name = sequence->label; + track.name = sequence->tags.label; // Note: it is not possible for the sequence to have a language without a label, // since a default label will be assigned according to the language - if (sequence->lang_str.len != 0) + if (sequence->tags.lang_str.len != 0) { - track.language = sequence->lang_str; - lang_id = sequence->language; + track.language = sequence->tags.lang_str; + lang_id = sequence->tags.language; } } @@ -811,7 +884,7 @@ mkv_metadata_parse( if (metadata->base.tracks.nelts > MAX_TRACK_COUNT) { vod_log_error(VOD_LOG_ERR, request_context->log, 0, - "mkv_metadata_parse: track count exceeded the limit of %i", (ngx_int_t)MAX_TRACK_COUNT); + "mkv_metadata_parse: track count exceeded the limit of %i", (vod_int_t)MAX_TRACK_COUNT); return VOD_BAD_REQUEST; } @@ -865,19 +938,19 @@ mkv_metadata_parse( break; } - cur_track->media_info.lang_str = track.language; - cur_track->media_info.language = lang_id; + cur_track->media_info.tags.lang_str = track.language; + cur_track->media_info.tags.language = lang_id; if (track.name.len > 0) { - cur_track->media_info.label = track.name; + cur_track->media_info.tags.label = track.name; } else if (lang_id > 0) { - lang_get_native_name(lang_id, &cur_track->media_info.label); + lang_get_native_name(lang_id, &cur_track->media_info.tags.label); } else { - cur_track->media_info.label = track.language; + cur_track->media_info.tags.label = track.language; } cur_track->media_info.media_type = media_type; cur_track->media_info.codec_id = cur_codec->codec_id; @@ -896,6 +969,24 @@ mkv_metadata_parse( cur_track->media_info.duration_millis = rescale_time(cur_track->media_info.duration, timescale, 1000); cur_track->media_info.extra_data = track.codec_private; + switch (cur_track->media_info.codec_id) + { + case VOD_CODEC_ID_AC3: + cur_track->media_info.extra_data.data = mkv_extra_data_ac3; + cur_track->media_info.extra_data.len = sizeof(mkv_extra_data_ac3); + break; + + case VOD_CODEC_ID_EAC3: + cur_track->media_info.extra_data.data = mkv_extra_data_eac3; + cur_track->media_info.extra_data.len = sizeof(mkv_extra_data_eac3); + break; + + case VOD_CODEC_ID_OPUS: + cur_track->media_info.extra_data.data = mkv_extra_data_opus; + cur_track->media_info.extra_data.len = sizeof(mkv_extra_data_opus); + break; + } + cur_track->index = track_index; rc = media_format_finalize_track( @@ -931,27 +1022,46 @@ mkv_get_read_frames_request( media_format_read_request_t* read_req) { ebml_context_t context; - uint64_t prev_cluster_pos; - uint64_t end_time; + media_track_t* cur_track; + vod_uint_t i; + uint64_t segment_duration; + uint64_t seen_tracks_mask; + uint64_t done_tracks_mask; + uint64_t all_tracks_mask; + uint64_t cur_track_mask; + uint64_t initial_time; + mkv_index_t prev_index; mkv_index_t index; vod_status_t rc; + size_t extra_read_size; + bool_t align_timestamps; bool_t done = FALSE; - // Note: adding a second to the end time, to make sure we get a frame following the last frame - // this is required since there is no duration per frame - end_time = metadata->end_time + rescale_time(end_margin, 1000, metadata->base.timescale); - read_req->read_offset = ULLONG_MAX; read_req->flags = 0; - prev_cluster_pos = ULLONG_MAX; + initial_time = 0; + prev_index.cluster_pos = ULLONG_MAX; + prev_index.time = 0; + + seen_tracks_mask = 0; + done_tracks_mask = 0; context.request_context = request_context; context.cur_pos = metadata->cues.data; context.end_pos = context.cur_pos + metadata->cues.len; + context.offset_delta = -1; + + align_timestamps = TRUE; // XXXX conf param + + // XXXXX optimize this - it may be possible to use the cuetime as the cluster timestamp, and start mid-cluster + // another possible optimization is to read in fixed sizes until the segment is complete (may reduce the total read size) for (;;) { + vod_memzero(&index, sizeof(index)); + cur_track_mask = 0; + if (context.cur_pos < context.end_pos) { rc = ebml_parse_single(&context, mkv_spec_index, &index); @@ -961,6 +1071,23 @@ mkv_get_read_frames_request( "mkv_get_read_frames_request: ebml_parse_single failed %i", rc); return rc; } + + for (i = 0; i < metadata->base.tracks.nelts; i++) + { + cur_track = (media_track_t*)metadata->base.tracks.elts + i; + + if (index.track == cur_track->media_info.track_id) + { + cur_track_mask = 1 << i; + break; + } + } + + seen_tracks_mask |= cur_track_mask; + + vod_log_debug4(VOD_LOG_DEBUG_LEVEL, request_context->log, 0, + "mkv_get_read_frames_request: track=%uL, time=%uL, cluster_pos=%uL, relative_pos=%uL", + index.track, index.time, index.cluster_pos, index.relative_pos); } else { @@ -969,19 +1096,44 @@ mkv_get_read_frames_request( done = TRUE; } - if (read_req->read_offset == ULLONG_MAX && - metadata->start_time < index.time && - prev_cluster_pos != ULLONG_MAX) + if (read_req->read_offset == ULLONG_MAX) { - read_req->read_offset = prev_cluster_pos; + if (align_timestamps && + metadata->start_time <= index.time) + { + metadata->start_time = index.time; + read_req->read_offset = index.cluster_pos; + initial_time = index.time; + } + else if (metadata->start_time < index.time && + prev_index.cluster_pos != ULLONG_MAX) + { + read_req->read_offset = prev_index.cluster_pos; + initial_time = prev_index.time; + } } - if (end_time <= index.time || done) + if (done) { break; } - prev_cluster_pos = index.cluster_pos; + if (metadata->end_time <= index.time) + { + if (align_timestamps) + { + metadata->end_time = index.time; + align_timestamps = FALSE; + } + + done_tracks_mask |= cur_track_mask; + if ((seen_tracks_mask & ~done_tracks_mask) == 0) + { + break; + } + } + + prev_index = index; } if (read_req->read_offset == ULLONG_MAX) @@ -998,19 +1150,50 @@ mkv_get_read_frames_request( return VOD_BAD_DATA; } - read_req->read_size = index.cluster_pos - read_req->read_offset; + read_req->read_size = index.cluster_pos + index.relative_pos - read_req->read_offset; read_req->read_offset += metadata->base_layout.position_reference; + // since mkv does not contain a duration field per frame, we need to read the timestamp of + // one additional frame per track + all_tracks_mask = (1 << metadata->base.tracks.nelts) - 1; + if (all_tracks_mask & ~seen_tracks_mask) + { + // some needed tracks were not included in the index - increase the read size by 1 second + // in order to read another frame for each track + segment_duration = 1000; + if (index.time > initial_time + segment_duration) + { + segment_duration = index.time - initial_time; + } + extra_read_size = read_req->read_size * 1000 / segment_duration; + } + else + { + // all required tracks were found in the index, increase the read size by a fixed amount + // in order to contain the headers of the next frame + extra_read_size = READ_FRAMES_EXTRA_SIZE; + } + + read_req->read_size += extra_read_size; + + metadata->read_offset = read_req->read_offset; + + vod_log_debug6(VOD_LOG_DEBUG_LEVEL, request_context->log, 0, + "mkv_get_read_frames_request: reading offsets=%uL..%uL (size=%uz, extra=%uz), time=%uL..%uL", + read_req->read_offset, read_req->read_offset + read_req->read_size, + read_req->read_size, extra_read_size, + metadata->start_time, metadata->end_time); + return VOD_AGAIN; } static void mkv_sort_gop_frames(vod_array_t* gop_frames) { - frame_timecode_t* frames = gop_frames->elts; - frame_timecode_t* frame1; - frame_timecode_t* frame2; - input_frame_t* temp_frame; + mkv_frame_timecode_t* frames = gop_frames->elts; + mkv_frame_timecode_t* frame1; + mkv_frame_timecode_t* frame2; + mkv_laced_frame_t temp_frame; uint64_t temp_timecode; vod_uint_t index1; vod_uint_t index2; @@ -1054,11 +1237,49 @@ mkv_sort_gop_frames(vod_array_t* gop_frames) } } +static void +mkv_update_laces_duration(mkv_laced_frame_t* laced_frame, uint32_t duration) +{ + frame_list_part_t* part; + input_frame_t* frame; + uint32_t target_duration; + uint32_t prev_duration; + uint32_t laces; + uint32_t i; + + prev_duration = 0; + laces = laced_frame->laces; + + part = laced_frame->part; + frame = laced_frame->frame; + for (i = 0; i < laces; i++) + { + if (frame >= part->last_frame) + { + if (part->next == NULL) + { + // unexpected + break; + } + + part = part->next; + frame = part->first_frame; + } + + target_duration = duration * (i + 1) / laces; + frame->duration = target_duration - prev_duration; + + frame++; + prev_duration = target_duration; + } +} + static void mkv_update_frame_timestamps(mkv_frame_parse_track_context_t* context) { - frame_timecode_t* cur_frame; - frame_timecode_t* last_frame; + mkv_frame_timecode_t* cur_frame; + mkv_frame_timecode_t* last_frame; + uint32_t duration; int32_t pts_delay; // sort the frames @@ -1070,28 +1291,27 @@ mkv_update_frame_timestamps(mkv_frame_parse_track_context_t* context) cur_frame = context->gop_frames.elts; last_frame = cur_frame + (context->gop_frames.nelts - 1); - if (cur_frame->frame != NULL) + if (cur_frame->frame.frame != NULL) { // this gop is included in the parsed frames, calculate the pts delay and duration for (; cur_frame < last_frame; cur_frame++) { pts_delay = cur_frame->unsorted_timecode - cur_frame->timecode; - + cur_frame->unsorted_frame->pts_delay = pts_delay; if (pts_delay < context->min_pts_delay) { context->min_pts_delay = pts_delay; } - cur_frame->unsorted_frame->pts_delay = pts_delay; - cur_frame->frame->duration = cur_frame[1].timecode - cur_frame[0].timecode; + duration = cur_frame[1].timecode - cur_frame[0].timecode; + mkv_update_laces_duration(&cur_frame->frame, duration); + context->total_frames_duration += duration; } } else { // this gop is not included in the parsed frames, only find the min pts delay - for (cur_frame = context->gop_frames.elts; - cur_frame < last_frame; - cur_frame++) + for (; cur_frame < last_frame; cur_frame++) { pts_delay = cur_frame->unsorted_timecode - cur_frame->timecode; @@ -1106,6 +1326,47 @@ mkv_update_frame_timestamps(mkv_frame_parse_track_context_t* context) context->gop_frames.nelts = 0; } +static uint64_t +mkv_estimate_next_frame_timecode( + request_context_t* request_context, + mkv_frame_parse_track_context_t* context) +{ + mkv_frame_timecode_t* cur_frame; + mkv_frame_timecode_t* last_frame; + uint32_t laces; + uint64_t max_timecode; + uint64_t result; + + // get the number of pending laces + max timecode + laces = 0; + max_timecode = 0; + + cur_frame = context->gop_frames.elts; + last_frame = cur_frame + (context->gop_frames.nelts - 1); + for (; cur_frame < last_frame; cur_frame++) + { + laces += cur_frame->frame.laces; + if (max_timecode < cur_frame->timecode) + { + max_timecode = cur_frame->timecode; + } + } + + // estimate the next frame timecode using the average lace duration + result = max_timecode; + if (context->frame_count > laces) + { + result += context->total_frames_duration / (context->frame_count - laces) * laces; + } + + vod_log_error(VOD_LOG_WARN, request_context->log, 0, + "mkv_estimate_next_frame_timecode: estimating next frame timecode, " + "track_number=%uL, result=%uL, max=%uL, laces=%uD", + context->track_number, result, max_timecode, laces); + + return result; +} + static vod_status_t mkv_parse_frame_estimate_bitrate( ebml_context_t* context, @@ -1181,6 +1442,7 @@ mkv_parse_frames_estimate_bitrate( { mkv_estimate_bitrate_track_context_t* track_context; mkv_estimate_bitrate_context_t context; + mkv_base_metadata_t* metadata = vod_container_of(base, mkv_base_metadata_t, base); media_track_t* cur_track; mkv_cluster_t cluster; vod_uint_t i; @@ -1199,6 +1461,7 @@ mkv_parse_frames_estimate_bitrate( context.context.request_context = request_context; context.context.cur_pos = frame_data->data; context.context.end_pos = frame_data->data + frame_data->len; + context.context.offset_delta = metadata->read_offset - (intptr_t)frame_data->data; for (i = 0; i < base->tracks.nelts; i++) { @@ -1231,22 +1494,161 @@ mkv_parse_frames_estimate_bitrate( return VOD_OK; } +static vod_status_t +mkv_parse_laces(ebml_context_t* context, uint8_t flags, uint32_t* lace_sizes) +{ + vod_status_t rc; + uint64_t num; + int64_t delta; + size_t total; + uint32_t laces; + uint32_t size; + uint32_t i; + uint8_t lace_type; + u_char cur; + + lace_type = (flags & 0x06) >> 1; + if (lace_type == 0) + { + // no lacing + lace_sizes[0] = context->end_pos - context->cur_pos; + return 1; + } + + // get number of laces + if (context->cur_pos >= context->end_pos) + { + vod_log_error(VOD_LOG_ERR, context->request_context->log, 0, + "mkv_parse_laces: overflow while reading number of laces"); + return VOD_BAD_DATA; + } + + laces = *context->cur_pos + 1; + context->cur_pos++; + + total = 0; + + switch (lace_type) + { + case 0x1: // xiph + vod_memzero(lace_sizes, (laces - 1) * sizeof(lace_sizes[0])); + + for (i = 0; i < laces - 1; i++) + { + do + { + if (context->cur_pos >= context->end_pos) + { + vod_log_error(VOD_LOG_ERR, context->request_context->log, 0, + "mkv_parse_laces: overflow while reading xiph lace size"); + return VOD_BAD_DATA; + } + + cur = *context->cur_pos; + context->cur_pos++; + + lace_sizes[i] += cur; + } while (cur == 0xff); + + total += lace_sizes[i]; + } + break; + + case 0x2: // fixed size + size = context->end_pos - context->cur_pos; + if (size % laces != 0) + { + vod_log_error(VOD_LOG_ERR, context->request_context->log, 0, + "mkv_parse_laces: invalid fixed size lace, size=%uD, laces=%uD", size, laces); + return VOD_BAD_DATA; + } + + size /= laces; + for (i = 0; i < laces; i++) + { + lace_sizes[i] = size; + } + return laces; + + case 0x3: // EBML + rc = ebml_read_num(context, &num, 4, 1); + if (rc < 0) + { + vod_log_debug1(VOD_LOG_DEBUG_LEVEL, context->request_context->log, 0, + "mkv_parse_laces: ebml_read_num(initial lace size) failed %i", rc); + return rc; + } + + if (num > VOD_MAX_UINT32_VALUE) + { + vod_log_error(VOD_LOG_ERR, context->request_context->log, 0, + "mkv_parse_laces: invalid ebml lace size %uL", num); + return VOD_BAD_DATA; + } + + lace_sizes[0] = num; + total = num; + + for (i = 1; i < laces - 1; i++) + { + rc = ebml_read_num(context, &num, 4, 1); + if (rc < 0) + { + vod_log_debug1(VOD_LOG_DEBUG_LEVEL, context->request_context->log, 0, + "mkv_parse_laces: ebml_read_num(lace size delta) failed %i", rc); + return rc; + } + + delta = num - ((1LL << (7 * rc - 1)) - 1); + if (delta > (int64_t)(VOD_MAX_UINT32_VALUE - lace_sizes[i - 1])) + { + vod_log_error(VOD_LOG_ERR, context->request_context->log, 0, + "mkv_parse_laces: invalid ebml lace delta %L too big", delta); + return VOD_BAD_DATA; + } + + if (delta < -(int64_t)lace_sizes[i - 1]) + { + vod_log_error(VOD_LOG_ERR, context->request_context->log, 0, + "mkv_parse_laces: invalid ebml lace delta %L too small", delta); + return VOD_BAD_DATA; + } + + lace_sizes[i] = lace_sizes[i - 1] + delta; + total += lace_sizes[i]; + } + break; + } + + size = context->end_pos - context->cur_pos; + if (size < total) + { + vod_log_error(VOD_LOG_ERR, context->request_context->log, 0, + "mkv_parse_laces: laces total size %uz overflows size left %uD", total, size); + return VOD_BAD_DATA; + } + + lace_sizes[laces - 1] = size - total; + + return laces; +} + static vod_status_t mkv_parse_frame( + mkv_frame_parse_context_t* frame_parse_context, ebml_context_t* context, - ebml_spec_t* spec, - void* dst) + mkv_cluster_t* cluster, + int key_frame) { - mkv_frame_parse_context_t* frame_parse_context = vod_container_of(context, mkv_frame_parse_context_t, context); mkv_frame_parse_track_context_t* track_context; frame_list_part_t* last_frames_part; frame_list_part_t* new_frames_part; - frame_timecode_t* gop_frame; - mkv_cluster_t* cluster = dst; + mkv_frame_timecode_t* gop_frame; input_frame_t* cur_frame; uint64_t frame_timecode; uint64_t track_number; - uint32_t key_frame; + uint32_t lace_sizes[256]; + intptr_t laces, i; int16_t timecode; vod_status_t rc; uint8_t flags; @@ -1298,7 +1700,7 @@ mkv_parse_frame( } frame_timecode = cluster->timecode + timecode; - + gop_frame = vod_array_push(&track_context->gop_frames); if (gop_frame == NULL) { @@ -1308,32 +1710,33 @@ mkv_parse_frame( } gop_frame->timecode = frame_timecode; gop_frame->unsorted_timecode = frame_timecode; - gop_frame->frame = NULL; + gop_frame->frame.frame = NULL; gop_frame->unsorted_frame = NULL; - switch (flags) + if (key_frame == -1) + { + key_frame = (flags & 0x80) ? 1 : 0; + } + + if (!key_frame) { - case 0: - case 1: // discardable // XXXXX should not cross the clip offset if (frame_parse_context->state == FRS_WAIT_START_KEY_FRAME) { return VOD_OK; } - - key_frame = 0; - break; - - case 0x80: + } + else + { mkv_update_frame_timestamps(track_context); // repush the gop frame following the reset of the array gop_frame = vod_array_push(&track_context->gop_frames); // cant fail - + gop_frame->timecode = frame_timecode; gop_frame->unsorted_timecode = frame_timecode; - gop_frame->frame = NULL; + gop_frame->frame.frame = NULL; gop_frame->unsorted_frame = NULL; switch (frame_parse_context->state) @@ -1361,6 +1764,10 @@ mkv_parse_frame( case FRS_DONE: track_context->done = TRUE; + vod_log_debug3(VOD_LOG_DEBUG_LEVEL, context->request_context->log, 0, + "mkv_parse_frame: track=%uL, timecode=%uL, key=1, pos=%uL", + track_number, frame_timecode, (uint64_t)(uintptr_t)context->cur_pos + context->offset_delta); + // check whether all tracks are done for (track_context = frame_parse_context->first_track; track_context < frame_parse_context->last_track; @@ -1374,72 +1781,166 @@ mkv_parse_frame( return VOD_DONE; } - - key_frame = 1; - break; - - default: - vod_log_error(VOD_LOG_ERR, context->request_context->log, 0, - "mkv_parse_frame: unsupported frame flags 0x%uxD", (uint32_t)flags); - return VOD_BAD_DATA; } - // enforce frame count limit - if (track_context->frame_count >= frame_parse_context->max_frame_count) + rc = mkv_parse_laces(context, flags, lace_sizes); + if (rc < 0) { - vod_log_error(VOD_LOG_ERR, context->request_context->log, 0, - "mkv_parse_frame: frame count exceeds the limit %uD", frame_parse_context->max_frame_count); - return VOD_BAD_DATA; + vod_log_debug1(VOD_LOG_DEBUG_LEVEL, context->request_context->log, 0, + "mkv_parse_frame: failed to parse lace sizes %i", rc); + return rc; } - last_frames_part = track_context->last_frames_part; - - if (last_frames_part->last_frame >= last_frames_part->first_frame + FRAMES_PER_PART) + laces = rc; + for (i = 0; i < laces; i++) { - // allocate a new part - new_frames_part = vod_alloc(context->request_context->pool, - sizeof(*new_frames_part) + FRAMES_PER_PART * sizeof(input_frame_t)); - if (new_frames_part == NULL) + // enforce frame count limit + if (track_context->frame_count >= frame_parse_context->max_frame_count) { - vod_log_debug0(VOD_LOG_DEBUG_LEVEL, context->request_context->log, 0, - "mkv_parse_frame: vod_alloc failed"); - return VOD_ALLOC_FAILED; + vod_log_error(VOD_LOG_ERR, context->request_context->log, 0, + "mkv_parse_frame: frame count exceeds the limit %uD", frame_parse_context->max_frame_count); + return VOD_BAD_DATA; + } + + last_frames_part = track_context->last_frames_part; + + if (last_frames_part->last_frame >= last_frames_part->first_frame + FRAMES_PER_PART) + { + // allocate a new part + new_frames_part = vod_alloc(context->request_context->pool, + sizeof(*new_frames_part) + FRAMES_PER_PART * sizeof(input_frame_t)); + if (new_frames_part == NULL) + { + vod_log_debug0(VOD_LOG_DEBUG_LEVEL, context->request_context->log, 0, + "mkv_parse_frame: vod_alloc failed"); + return VOD_ALLOC_FAILED; + } + + new_frames_part->first_frame = (void*)(new_frames_part + 1); + new_frames_part->last_frame = new_frames_part->first_frame; + new_frames_part->frames_source = last_frames_part->frames_source; + new_frames_part->frames_source_context = last_frames_part->frames_source_context; + new_frames_part->clip_to = UINT_MAX; // XXXXX fix this + + last_frames_part->next = new_frames_part; + track_context->last_frames_part = new_frames_part; + last_frames_part = new_frames_part; + } + + // initialize the new frame (duration & pts delay are initialized later) + cur_frame = last_frames_part->last_frame++; + cur_frame->key_frame = key_frame; + cur_frame->duration = 0; + cur_frame->pts_delay = 0; + + cur_frame->offset = (uintptr_t)context->cur_pos; + cur_frame->size = lace_sizes[i]; + context->cur_pos += cur_frame->size; + + if (i == 0) + { + // add the frame to the gop frames + gop_frame->frame.frame = cur_frame; + gop_frame->frame.part = last_frames_part; + gop_frame->frame.laces = laces; + + gop_frame->unsorted_frame = cur_frame; + + // update the track context + if (track_context->frame_count == 0) + { + track_context->first_timecode = frame_timecode; + } } - new_frames_part->first_frame = (void*)(new_frames_part + 1); - new_frames_part->last_frame = new_frames_part->first_frame; - new_frames_part->frames_source = last_frames_part->frames_source; - new_frames_part->frames_source_context = last_frames_part->frames_source_context; - new_frames_part->clip_to = UINT_MAX; // XXXXX fix this + track_context->frame_count++; + track_context->key_frame_count += key_frame; + track_context->total_frames_size += cur_frame->size; - last_frames_part->next = new_frames_part; - track_context->last_frames_part = new_frames_part; - last_frames_part = new_frames_part; + if (laces > 1) + { + vod_log_debug7(VOD_LOG_DEBUG_LEVEL, context->request_context->log, 0, + "mkv_parse_frame: track=%uL, timecode=%uL, key=%d, pos=%uL, size=%uD, lace=%i/%i", + track_number, frame_timecode, key_frame, cur_frame->offset + context->offset_delta, cur_frame->size, i + 1, laces); + } + else + { + vod_log_debug5(VOD_LOG_DEBUG_LEVEL, context->request_context->log, 0, + "mkv_parse_frame: track=%uL, timecode=%uL, key=%d, pos=%uL, size=%uD", + track_number, frame_timecode, key_frame, cur_frame->offset + context->offset_delta, cur_frame->size); + } } - // initialize the new frame (duration & pts delay are initialized later) - cur_frame = last_frames_part->last_frame++; - cur_frame->key_frame = key_frame; - cur_frame->offset = (uintptr_t)context->cur_pos; - cur_frame->size = context->end_pos - context->cur_pos; + return VOD_OK; +} - // add the frame to the gop frames - gop_frame->frame = cur_frame; - gop_frame->unsorted_frame = cur_frame; +static vod_status_t +mkv_block_group( + ebml_context_t* context, + ebml_spec_t* spec, + void* dst) +{ + mkv_frame_parse_context_t* frame_parse_context = vod_container_of(context, mkv_frame_parse_context_t, context); + mkv_block_group_context_t block_group_context; + mkv_block_group_t block_group; + ebml_context_t block_context; + mkv_cluster_t* cluster = dst; + vod_status_t rc; + + block_group_context.context = *context; + block_group_context.references = 0; - // update the track context - if (track_context->frame_count == 0) + vod_memzero(&block_group, sizeof(block_group)); + + rc = ebml_parse_master(&block_group_context.context, mkv_spec_block_group, &block_group); + if (rc != VOD_OK) { - track_context->first_timecode = frame_timecode; + vod_log_debug0(VOD_LOG_DEBUG_LEVEL, context->request_context->log, 0, + "mkv_block_group: ebml_parse_master(block group) failed"); + return rc; } - track_context->frame_count++; - track_context->key_frame_count += key_frame; - track_context->total_frames_size += cur_frame->size; - track_context->total_frames_duration += cur_frame->duration; + + if (block_group.block.len <= 0) + { + vod_log_error(VOD_LOG_ERR, context->request_context->log, 0, + "mkv_block_group: block group without block element"); + return VOD_BAD_DATA; + } + + block_context.request_context = context->request_context; + block_context.cur_pos = block_group.block.data; + block_context.end_pos = block_group.block.data + block_group.block.len; + block_context.offset_delta = context->offset_delta; + + return mkv_parse_frame(frame_parse_context, &block_context, cluster, block_group_context.references == 0); + +} + +static vod_status_t +mkv_reference_block( + ebml_context_t* context, + ebml_spec_t* spec, + void* dst) +{ + mkv_block_group_context_t* block_group_context = vod_container_of(context, mkv_block_group_context_t, context); + + block_group_context->references++; return VOD_OK; } +static vod_status_t +mkv_simple_block( + ebml_context_t* context, + ebml_spec_t* spec, + void* dst) +{ + mkv_frame_parse_context_t* frame_parse_context = vod_container_of(context, mkv_frame_parse_context_t, context); + mkv_cluster_t* cluster = dst; + + return mkv_parse_frame(frame_parse_context, context, cluster, -1); +} + static vod_status_t mkv_parse_frames( request_context_t* request_context, @@ -1449,9 +1950,9 @@ mkv_parse_frames( { mkv_frame_parse_track_context_t* track_context; mkv_frame_parse_context_t frame_parse_context; + mkv_frame_timecode_t* gop_frame; mkv_base_metadata_t* metadata = vod_container_of(base, mkv_base_metadata_t, base); frame_list_part_t* part; - frame_timecode_t* gop_frame; input_frame_t* last_frame; input_frame_t* cur_frame; media_track_t* cur_track; @@ -1476,6 +1977,7 @@ mkv_parse_frames( frame_parse_context.context.request_context = request_context; frame_parse_context.context.cur_pos = frame_data->data; frame_parse_context.context.end_pos = frame_data->data + frame_data->len; + frame_parse_context.context.offset_delta = metadata->read_offset - (intptr_t)frame_data->data; frame_parse_context.start_time = metadata->start_time; frame_parse_context.end_time = metadata->end_time; frame_parse_context.max_frame_count = metadata->max_frame_count; @@ -1509,7 +2011,7 @@ mkv_parse_frames( } // initialize the gop frames array - if (vod_array_init(&track_context->gop_frames, request_context->pool, 60, sizeof(frame_timecode_t)) != VOD_OK) + if (vod_array_init(&track_context->gop_frames, request_context->pool, 60, sizeof(mkv_frame_timecode_t)) != VOD_OK) { vod_log_debug0(VOD_LOG_DEBUG_LEVEL, request_context->log, 0, "mkv_parse_frames: vod_array_init failed"); @@ -1547,9 +2049,9 @@ mkv_parse_frames( "mkv_parse_frames: vod_array_push failed"); return VOD_ALLOC_FAILED; } - gop_frame->timecode = base->duration; - gop_frame->unsorted_timecode = base->duration; - gop_frame->frame = NULL; + gop_frame->timecode = mkv_estimate_next_frame_timecode(request_context, track_context); + gop_frame->unsorted_timecode = gop_frame->timecode; + gop_frame->frame.frame = NULL; gop_frame->unsorted_frame = NULL; // close the last gop @@ -1643,7 +2145,7 @@ mkv_prepare_read_frames_request( metadata->end_time = rescale_time(parse_params->range->end, 1000, metadata->base.timescale); metadata->max_frame_count = parse_params->max_frame_count; metadata->parse_frames = TRUE; - end_margin = segmenter->max_segment_duration; + end_margin = 1000; } else { diff --git a/vod/mp4/mp4_cenc_encrypt.c b/vod/mp4/mp4_cenc_encrypt.c index 2bbbc500..5eaa7870 100644 --- a/vod/mp4/mp4_cenc_encrypt.c +++ b/vod/mp4/mp4_cenc_encrypt.c @@ -162,6 +162,19 @@ mp4_cenc_encrypt_start_frame(mp4_cenc_encrypt_state_t* state) static vod_status_t mp4_cenc_encrypt_video_init_track(mp4_cenc_encrypt_video_state_t* state, media_track_t* track) { + switch (track->media_info.codec_id) + { + case VOD_CODEC_ID_AVC: + case VOD_CODEC_ID_HEVC: + break; + + default: + vod_log_error(VOD_LOG_ERR, state->base.request_context->log, 0, + "mp4_cenc_encrypt_video_init_track: codec id %uD is not supported", + track->media_info.codec_id); + return VOD_BAD_REQUEST; + } + state->nal_packet_size_length = track->media_info.u.video.nal_packet_size_length; if (state->nal_packet_size_length < 1 || state->nal_packet_size_length > 4) diff --git a/vod/mp4/mp4_init_segment.c b/vod/mp4/mp4_init_segment.c index 0ee432b1..34986f72 100644 --- a/vod/mp4/mp4_init_segment.c +++ b/vod/mp4/mp4_init_segment.c @@ -488,13 +488,24 @@ mp4_init_segment_write_avcc_atom(u_char* p, media_track_t* track) return p; } +static u_char* +mp4_init_segment_write_hvcc_atom(u_char* p, media_track_t* track) +{ + size_t atom_size = ATOM_HEADER_SIZE + track->media_info.extra_data.len; + + write_atom_header(p, atom_size, 'h', 'v', 'c', 'C'); + p = vod_copy(p, track->media_info.extra_data.data, track->media_info.extra_data.len); + return p; +} + static u_char* mp4_init_segment_write_stsd_video_entry(u_char* p, media_track_t* track) { size_t atom_size = ATOM_HEADER_SIZE + sizeof(sample_entry_t) + sizeof(stsd_video_t) + ATOM_HEADER_SIZE + track->media_info.extra_data.len; - write_atom_header(p, atom_size, 'a', 'v', 'c', '1'); + write_be32(p, atom_size); + p = ngx_copy(p, &track->media_info.format, sizeof(track->media_info.format)); // sample_entry_t write_be32(p, 0); // reserved @@ -518,15 +529,24 @@ mp4_init_segment_write_stsd_video_entry(u_char* p, media_track_t* track) write_be16(p, 0x18); // depth write_be16(p, 0xffff); // pre defined - p = mp4_init_segment_write_avcc_atom(p, track); + switch (track->media_info.codec_id) + { + case VOD_CODEC_ID_AVC: + p = mp4_init_segment_write_avcc_atom(p, track); + break; + + case VOD_CODEC_ID_HEVC: + p = mp4_init_segment_write_hvcc_atom(p, track); + break; + } return p; } static u_char* -mp4_init_segment_write_esds_atom(u_char* p, media_track_t* track) +mp4_init_segment_write_esds_atom(u_char* p, media_info_t* media_info) { - size_t extra_data_len = track->media_info.extra_data.len; + size_t extra_data_len = media_info->extra_data.len; size_t atom_size = mp4_esds_atom_size(extra_data_len); write_atom_header(p, atom_size, 'e', 's', 'd', 's'); @@ -541,15 +561,15 @@ mp4_init_segment_write_esds_atom(u_char* p, media_track_t* track) *p++ = MP4DecConfigDescrTag; // tag *p++ = sizeof(config_descr_t) + // len sizeof(descr_header_t) + extra_data_len; - *p++ = track->media_info.u.audio.object_type_id; + *p++ = media_info->u.audio.object_type_id; *p++ = 0x15; // stream type write_be24(p, 0); // buffer size - write_be32(p, track->media_info.bitrate); // max bitrate - write_be32(p, track->media_info.bitrate); // avg bitrate + write_be32(p, media_info->bitrate); // max bitrate + write_be32(p, media_info->bitrate); // avg bitrate *p++ = MP4DecSpecificDescrTag; // tag *p++ = extra_data_len; // len - p = vod_copy(p, track->media_info.extra_data.data, extra_data_len); + p = vod_copy(p, media_info->extra_data.data, extra_data_len); *p++ = MP4SLDescrTag; // tag *p++ = 1; // len @@ -558,13 +578,34 @@ mp4_init_segment_write_esds_atom(u_char* p, media_track_t* track) return p; } +static vod_inline size_t +mp4_init_segment_get_stsd_audio_entry_size(media_info_t* media_info) +{ + size_t size; + + size = ATOM_HEADER_SIZE + sizeof(sample_entry_t) + sizeof(stsd_audio_t); + + if (media_info->format == FORMAT_MP4A) + { + size += mp4_esds_atom_size(media_info->extra_data.len); + } + else + { + size += ATOM_HEADER_SIZE + media_info->extra_data.len; + } + + return size; +} + static u_char* -mp4_init_segment_write_stsd_audio_entry(u_char* p, media_track_t* track) +mp4_init_segment_write_stsd_audio_entry(u_char* p, media_info_t* media_info) { - size_t atom_size = ATOM_HEADER_SIZE + sizeof(sample_entry_t) + sizeof(stsd_audio_t) + - mp4_esds_atom_size(track->media_info.extra_data.len); + size_t atom_size; - write_atom_header(p, atom_size, 'm', 'p', '4', 'a'); + atom_size = mp4_init_segment_get_stsd_audio_entry_size(media_info); + + write_be32(p, atom_size); + p = ngx_copy(p, &media_info->format, sizeof(media_info->format)); // sample_entry_t write_be32(p, 0); // reserved @@ -574,14 +615,38 @@ mp4_init_segment_write_stsd_audio_entry(u_char* p, media_track_t* track) // stsd_audio_t write_be32(p, 0); // reserved write_be32(p, 0); // reserved - write_be16(p, track->media_info.u.audio.channels); - write_be16(p, track->media_info.u.audio.bits_per_sample); + write_be16(p, media_info->u.audio.channels); + write_be16(p, media_info->u.audio.bits_per_sample); write_be16(p, 0); // pre defined write_be16(p, 0); // reserved - write_be16(p, track->media_info.u.audio.sample_rate); + write_be16(p, media_info->u.audio.sample_rate); write_be16(p, 0); - p = mp4_init_segment_write_esds_atom(p, track); + if (media_info->format == FORMAT_MP4A) + { + p = mp4_init_segment_write_esds_atom(p, media_info); + } + else + { + atom_size = ATOM_HEADER_SIZE + media_info->extra_data.len; + + switch (media_info->codec_id) + { + case VOD_CODEC_ID_AC3: + write_atom_header(p, atom_size, 'd', 'a', 'c', '3'); + break; + + case VOD_CODEC_ID_EAC3: + write_atom_header(p, atom_size, 'd', 'e', 'c', '3'); + break; + + case VOD_CODEC_ID_OPUS: + write_atom_header(p, atom_size, 'd', 'O', 'p', 's'); + break; + } + + p = vod_copy(p, media_info->extra_data.data, media_info->extra_data.len); + } return p; } @@ -594,13 +659,12 @@ mp4_init_segment_get_stsd_atom_size(media_track_t* track) switch (track->media_info.media_type) { case MEDIA_TYPE_VIDEO: - atom_size += ATOM_HEADER_SIZE + sizeof(sample_entry_t) + sizeof(stsd_video_t)+ + atom_size += ATOM_HEADER_SIZE + sizeof(sample_entry_t) + sizeof(stsd_video_t) + ATOM_HEADER_SIZE + track->media_info.extra_data.len; break; case MEDIA_TYPE_AUDIO: - atom_size += ATOM_HEADER_SIZE + sizeof(sample_entry_t) + sizeof(stsd_audio_t)+ - mp4_esds_atom_size(track->media_info.extra_data.len); + atom_size += mp4_init_segment_get_stsd_audio_entry_size(&track->media_info); break; } @@ -620,7 +684,7 @@ mp4_init_segment_write_stsd_atom(u_char* p, size_t atom_size, media_track_t* tra break; case MEDIA_TYPE_AUDIO: - p = mp4_init_segment_write_stsd_audio_entry(p, track); + p = mp4_init_segment_write_stsd_audio_entry(p, &track->media_info); break; } return p; diff --git a/vod/mp4/mp4_parser.c b/vod/mp4/mp4_parser.c index c21b8b28..9e862d6e 100644 --- a/vod/mp4/mp4_parser.c +++ b/vod/mp4/mp4_parser.c @@ -253,6 +253,7 @@ static vod_status_t mp4_parser_parse_hdlr_atom(atom_info_t* atom_info, metadata_parse_context_t* context) { const hdlr_atom_t* atom = (const hdlr_atom_t*)atom_info->ptr; + media_tags_t* tags; vod_str_t name; uint32_t type; @@ -280,7 +281,8 @@ mp4_parser_parse_hdlr_atom(atom_info_t* atom_info, metadata_parse_context_t* con } // parse the name / already set - if ((context->parse_params.parse_type & PARSE_FLAG_HDLR_NAME) == 0 || context->media_info.label.data != NULL) + tags = &context->media_info.tags; + if ((context->parse_params.parse_type & PARSE_FLAG_HDLR_NAME) == 0 || tags->label.data != NULL) { return VOD_OK; } @@ -303,19 +305,17 @@ mp4_parser_parse_hdlr_atom(atom_info_t* atom_info, metadata_parse_context_t* con return VOD_OK; } - context->media_info.label.data = vod_alloc( - context->request_context->pool, - name.len + 1); - if (context->media_info.label.data == NULL) + tags->label.data = vod_alloc(context->request_context->pool, name.len + 1); + if (tags->label.data == NULL) { vod_log_debug0(VOD_LOG_DEBUG_LEVEL, context->request_context->log, 0, "mp4_parser_parse_hdlr_atom: vod_alloc failed"); return VOD_ALLOC_FAILED; } - vod_memcpy(context->media_info.label.data, name.data, name.len); - context->media_info.label.data[name.len] = '\0'; - context->media_info.label.len = name.len; + vod_memcpy(tags->label.data, name.data, name.len); + tags->label.data[name.len] = '\0'; + tags->label.len = name.len; return VOD_OK; } @@ -475,6 +475,7 @@ mp4_parser_parse_mdhd_atom(atom_info_t* atom_info, metadata_parse_context_t* con { const mdhd_atom_t* atom = (const mdhd_atom_t*)atom_info->ptr; const mdhd64_atom_t* atom64 = (const mdhd64_atom_t*)atom_info->ptr; + media_tags_t* tags; uint64_t duration; uint32_t timescale; uint16_t language; @@ -525,15 +526,16 @@ mp4_parser_parse_mdhd_atom(atom_info_t* atom_info, metadata_parse_context_t* con context->media_info.full_duration = duration; context->media_info.duration_millis = rescale_time(duration, timescale, 1000); - context->media_info.language = lang_parse_iso639_3_code(language); - if (context->media_info.language != 0) + tags = &context->media_info.tags; + tags->language = lang_parse_iso639_3_code(language); + if (tags->language != 0) { - context->media_info.lang_str.data = (u_char *)lang_get_rfc_5646_name(context->media_info.language); - context->media_info.lang_str.len = ngx_strlen(context->media_info.lang_str.data); + tags->lang_str.data = (u_char *)lang_get_rfc_5646_name(tags->language); + tags->lang_str.len = ngx_strlen(tags->lang_str.data); - if (context->media_info.label.len == 0) + if (tags->label.len == 0) { - lang_get_native_name(context->media_info.language, &context->media_info.label); + lang_get_native_name(tags->language, &tags->label); } } @@ -543,29 +545,32 @@ mp4_parser_parse_mdhd_atom(atom_info_t* atom_info, metadata_parse_context_t* con static vod_status_t mp4_parser_parse_udta_name_atom(atom_info_t* atom_info, metadata_parse_context_t* context) { + media_tags_t* tags; vod_str_t name; + name.data = (u_char*)atom_info->ptr; name.len = atom_info->size; // atom empty/non-existent or name already set - if (name.len == 0 || context->media_info.label.data != NULL) + tags = &context->media_info.tags; + if (name.len == 0 || tags->label.data != NULL) { return VOD_OK; } - context->media_info.label.data = vod_alloc( + tags->label.data = vod_alloc( context->request_context->pool, name.len + 1); - if (context->media_info.label.data == NULL) + if (tags->label.data == NULL) { vod_log_debug0(VOD_LOG_DEBUG_LEVEL, context->request_context->log, 0, "mp4_parser_parse_udta_name_atom: vod_alloc failed"); return VOD_ALLOC_FAILED; } - vod_memcpy(context->media_info.label.data, name.data, name.len); - context->media_info.label.data[name.len] = '\0'; - context->media_info.label.len = name.len; + vod_memcpy(tags->label.data, name.data, name.len); + tags->label.data[name.len] = '\0'; + tags->label.len = name.len; return VOD_OK; } @@ -2863,16 +2868,16 @@ mp4_parser_process_moov_atom_callback(void* ctx, atom_info_t* atom_info) // inherit the sequence language and label sequence = context->parse_params.source->sequence; - if (sequence->label.len != 0) + if (sequence->tags.label.len != 0) { - metadata_parse_context.media_info.label = sequence->label; + metadata_parse_context.media_info.tags.label = sequence->tags.label; // Note: it is not possible for the sequence to have a language without a label, // since a default label will be assigned according to the language - if (sequence->lang_str.len != 0) + if (sequence->tags.lang_str.len != 0) { - metadata_parse_context.media_info.lang_str = sequence->lang_str; - metadata_parse_context.media_info.language = sequence->language; + metadata_parse_context.media_info.tags.lang_str = sequence->tags.lang_str; + metadata_parse_context.media_info.tags.language = sequence->tags.language; } } @@ -2886,7 +2891,7 @@ mp4_parser_process_moov_atom_callback(void* ctx, atom_info_t* atom_info) // filter by language if (context->parse_params.langs_mask != NULL && metadata_parse_context.media_info.media_type == MEDIA_TYPE_AUDIO && - !vod_is_bit_set(context->parse_params.langs_mask, metadata_parse_context.media_info.language)) + !vod_is_bit_set(context->parse_params.langs_mask, metadata_parse_context.media_info.tags.language)) { return VOD_OK; } diff --git a/vod/mss/mss_packager.c b/vod/mss/mss_packager.c index 5c7ed92d..ccf0e5ed 100644 --- a/vod/mss/mss_packager.c +++ b/vod/mss/mss_packager.c @@ -188,12 +188,12 @@ mss_packager_compare_tracks(uintptr_t bitrate_threshold, const media_info_t* mi1 (mi1->u.video.height == mi2->u.video.height); } - if (mi1->label.len == 0 || mi2->label.len == 0) + if (mi1->tags.label.len == 0 || mi2->tags.label.len == 0) { return TRUE; } - return vod_str_equals(mi1->label, mi2->label); + return vod_str_equals(mi1->tags.label, mi2->tags.label); } static void @@ -255,7 +255,7 @@ mss_packager_remove_redundant_tracks( // prefer to remove a track that doesn't have a label, so that we won't lose a language // in case of multi language manifest - if (track1->media_info.label.len == 0 || track2->media_info.label.len != 0) + if (track1->media_info.tags.label.len == 0 || track2->media_info.tags.label.len != 0) { remove = track1; } @@ -419,7 +419,7 @@ mss_packager_build_manifest( { cur_track = *adaptation_set->first; - result_size += cur_track->media_info.label.len + cur_track->media_info.lang_str.len; + result_size += cur_track->media_info.tags.label.len + cur_track->media_info.tags.lang_str.len; } result_size += @@ -491,8 +491,8 @@ mss_packager_build_manifest( p = vod_sprintf(p, MSS_STREAM_INDEX_HEADER_LABEL, MSS_STREAM_TYPE_AUDIO, - &cur_track->media_info.label, - &cur_track->media_info.lang_str, + &cur_track->media_info.tags.label, + &cur_track->media_info.tags.lang_str, adaptation_set->count, segment_durations[adaptation_set->type].segment_count, MSS_STREAM_TYPE_AUDIO); @@ -513,8 +513,8 @@ mss_packager_build_manifest( cur_track = *adaptation_set->first; p = vod_sprintf(p, MSS_STREAM_INDEX_HEADER_SUBTITLE, - &cur_track->media_info.label, - &cur_track->media_info.lang_str, + &cur_track->media_info.tags.label, + &cur_track->media_info.tags.lang_str, adaptation_set->count, segment_durations[adaptation_set->type].segment_count); break; diff --git a/vod/subtitle/subtitle_format.c b/vod/subtitle/subtitle_format.c index 3dab36a7..0cbe2d81 100644 --- a/vod/subtitle/subtitle_format.c +++ b/vod/subtitle/subtitle_format.c @@ -66,11 +66,8 @@ subtitle_parse( media_base_metadata_t** result) { subtitle_base_metadata_t* metadata; - media_sequence_t* sequence; media_track_t* track; - language_id_t lang_id; - vod_str_t lang_str; - vod_str_t label; + media_tags_t tags; uint64_t duration; metadata = vod_alloc(request_context->pool, sizeof(*metadata)); @@ -90,24 +87,18 @@ subtitle_parse( } // inherit the sequence language and label - sequence = parse_params->source->sequence; - if (sequence->label.len != 0) - { - lang_str = sequence->lang_str; - lang_id = sequence->language; - label = sequence->label; - } - else + tags = parse_params->source->sequence->tags; + if (tags.label.len == 0) { // no language, assume English - ngx_str_set(&lang_str, "eng"); - lang_id = VOD_LANG_EN; - lang_get_native_name(lang_id, &label); + ngx_str_set(&tags.lang_str, "eng"); + tags.language = VOD_LANG_EN; + lang_get_native_name(tags.language, &tags.label); } // filter by language if (parse_params->langs_mask != NULL && - !vod_is_bit_set(parse_params->langs_mask, lang_id)) + !vod_is_bit_set(parse_params->langs_mask, tags.language)) { metadata->base.tracks.nelts = 0; return VOD_OK; @@ -143,9 +134,7 @@ subtitle_parse( track->media_info.duration = duration; track->media_info.full_duration = full_duration; track->media_info.duration_millis = duration; - track->media_info.lang_str = lang_str; - track->media_info.language = lang_id; - track->media_info.label = label; + track->media_info.tags = tags; track->media_info.bitrate = (source->len * 1000 * 8) / full_duration; metadata->source = *source; diff --git a/vod/thumb/thumb_grabber.c b/vod/thumb/thumb_grabber.c index 22cc1a10..fe852829 100644 --- a/vod/thumb/thumb_grabber.c +++ b/vod/thumb/thumb_grabber.c @@ -55,6 +55,9 @@ static codec_id_mapping_t codec_mappings[] = { { VOD_CODEC_ID_HEVC, AV_CODEC_ID_H265, "h265" }, { VOD_CODEC_ID_VP8, AV_CODEC_ID_VP8, "vp8" }, { VOD_CODEC_ID_VP9, AV_CODEC_ID_VP9, "vp9" }, +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 89, 100) + { VOD_CODEC_ID_AV1, AV_CODEC_ID_AV1, "av1" }, +#endif }; void