diff --git a/AUTHORS b/AUTHORS index 151f456f1..27a3e775c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -13,3 +13,10 @@ Fork author: Contacts: sergey.dryabzhinsky@gmail.com + +Fork ot the fork author: + Raphael Mazelier + Paris, France + + Contacts: + raph@futomaki.net diff --git a/DRM.md b/DRM.md new file mode 100644 index 000000000..932653a34 --- /dev/null +++ b/DRM.md @@ -0,0 +1,79 @@ + +## DRM Common Encryption + +This fork give the possibility of packaging dash "protected" stream. +Concretely it implement the minimal requirement of "common-encryption" as described in ISO/IEC 23001-7:2015, Information technology — MPEG systems technologies — Part 7: Common encryption in ISO Base Media File Format files - 2nd Edition. +You can read a brief description here : "https://w3c.github.io/encrypted-media/format-registry/stream/mp4.html#bib-CENC" + +### How to use it : + +You need at least to enable common_encryption and provide one key and one key id with the following directives : + +``` +dash_cenc on; # enable common encryption on all stream in this block +dash_cenc_kid XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX; # 16 bytes KEY-ID in hex +dash_cenc_key XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX; # 16 bytes KEY in hex +``` + +It enable automatically Clear-Key pseudo DRM. (use it only for testing purpose) + +Currently the are two real DRM supported : Widevine and Microsoft Playready. + +For widevine you need the following directives in addition : + +``` +dash_wdv on; # enable widevine signalling +dash_wdv_data AAAAbHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAA... ; # base64 encoded widevine pssh +``` + +For playready you need the following directives in addition (you can use both widevine and playready together with the same kid:key pair): + +``` +dash_mspr on; # enable playready signalling +dash_mspr_data AAACsHBzc2gAAAAAmgTweZhAQoarkuZb4Ih...; # base64 encoded playready pssh +dash_mspr_kid AAATH/7xxxfUbpB8mhqA==; # base64 encoded playready kid +dash_mspr_pro kAIAAAEAAQCGAjwAVwBSAE0ASABFAEEARA...; # base64 encoded playready PRO (Playready Object) +``` + +### Implementation : + +_TLDR;_ This was quite an adventure + +The implementation is based on the ISO_IEC_23001-7_2016 normative document. +I also took lot of inspiration on kaltura nginx-vod module. + +It implement the minimal requirement of the norm, the 'cenc' scheme, AES-CTR mode full sample and video NAL Subsample encryption. + +Audio track are encrypted in full sample mode with AES-CTR. + +Video track are encrypted in sub sample mode, assuming one NALU per frame, using enough clear text size at the beginning of the frame to keep the NAL header in clear. (the module does not analyse NAL Headers). + +The clear size is rounded to make encrypted size of data a multiple of the AES-CRT block size. + +The implementation allow only one KID:KEY couple used for all tracks. + +The implementation use 64bits IVs. + +### Conformity : + +This implementation have been tested with and known working : + +Clearkey : + - Firefox: dashjs/shakaplayer + - Chrome: dashjs/shakaplayer + +Widevine : +- Firefox: dashjs/shakaplayer +- Chrome: dashjs/shakaplayer + +Playready: + - Edge : dashjs/shakaplayer + +Bitmovin player seem also to work. + +Thanks: + +Special thanks to Eran Kornblau from Kaltura, Joey Parrish, and Jacob Timble. + + + diff --git a/README.md b/README.md index d53d0e6b9..77ef9cabe 100644 --- a/README.md +++ b/README.md @@ -1,336 +1,76 @@ # NGINX-based Media Streaming Server -## nginx-rtmp-module - -### Project blog - - http://nginx-rtmp.blogspot.com - -### Documentation - -* [Home](doc/README.md) -* [Control module](doc/control_modul.md) -* [Debug log](doc/debug_log.md) -* [Directives](doc/directives.md) -* [Examples](doc/examples.md) -* [Exec wrapper in bash](doc/exec_wrapper_in_bash.md) -* [FAQ](doc/faq.md) -* [Getting number of subscribers](doc/getting_number_of_subscribers.md) -* [Getting started with nginx rtmp](doc/getting_started.md) -* [Installing in Gentoo](doc/installing_in_gentoo.md) -* [Installing on Ubuntu using PPAs](doc/installing_ubuntu_using_ppas.md) -* [Tutorial](doc/tutorial.md) - -*Source: https://github.com/arut/nginx-rtmp-module/wiki* - -* [Latest updates](doc/README.md#updates) - -### Google group - - https://groups.google.com/group/nginx-rtmp - - https://groups.google.com/group/nginx-rtmp-ru (Russian) - -### Donation page (Paypal etc) - - http://arut.github.com/nginx-rtmp-module/ - -### Features - -* RTMP/HLS/MPEG-DASH live streaming - -* RTMP Video on demand FLV/MP4, - playing from local filesystem or HTTP - -* Stream relay support for distributed - streaming: push & pull models - -* Recording streams in multiple FLVs - -* H264/AAC support - -* Online transcoding with FFmpeg - -* HTTP callbacks (publish/play/record/update etc) - -* Running external programs on certain events (exec) - -* HTTP control module for recording audio/video and dropping clients - -* Advanced buffering techniques - to keep memory allocations at a minimum - level for faster streaming and low - memory footprint - -* Proved to work with Wirecast, FMS, Wowza, - JWPlayer, FlowPlayer, StrobeMediaPlayback, - ffmpeg, avconv, rtmpdump, flvstreamer - and many more - -* Statistics in XML/XSL in machine- & human- - readable form - -* Linux/FreeBSD/MacOS/Windows - -### Build - -cd to NGINX source directory & run this: - - ./configure --add-module=/path/to/nginx-rtmp-module - make - make install - -Several versions of nginx (1.3.14 - 1.5.0) require http_ssl_module to be -added as well: - - ./configure --add-module=/path/to/nginx-rtmp-module --with-http_ssl_module - -For building debug version of nginx add `--with-debug` - - ./configure --add-module=/path/to-nginx/rtmp-module --with-debug - -[Read more about debug log](https://github.com/arut/nginx-rtmp-module/wiki/Debug-log) - -### Contributing and Branch Policy - -The "dev" branch is the one where all contributions will be merged before reaching "master". -If you plan to propose a patch, please commit into the "dev" branch or its own feature branch. -Direct commit to "master" are not permitted. - -### Windows limitations - -Windows support is limited. These features are not supported - -* execs -* static pulls -* auto_push - -### RTMP URL format - - rtmp://rtmp.example.com/app[/name] - -app - should match one of application {} - blocks in config - -name - interpreted by each application - can be empty - - -### Multi-worker live streaming - -This NGINX-RTMP module does not support multi-worker live -streaming. While this feature can be enabled through rtmp_auto_push on|off directive, it is ill advised because it is incompatible with NGINX versions starting 1.7.2 and up, there for it should not be used. - - -### Example nginx.conf - - rtmp { - - server { - - listen 1935; - - chunk_size 4000; - - # TV mode: one publisher, many subscribers - application mytv { - - # enable live streaming - live on; - - # record first 1K of stream - record all; - record_path /tmp/av; - record_max_size 1K; - - # append current timestamp to each flv - record_unique on; - - # publish only from localhost - allow publish 127.0.0.1; - deny publish all; - - #allow play all; - } - - # Transcoding (ffmpeg needed) - application big { - live on; - - # On every pusblished stream run this command (ffmpeg) - # with substitutions: $app/${app}, $name/${name} for application & stream name. - # - # This ffmpeg call receives stream from this application & - # reduces the resolution down to 32x32. The stream is the published to - # 'small' application (see below) under the same name. - # - # ffmpeg can do anything with the stream like video/audio - # transcoding, resizing, altering container/codec params etc - # - # Multiple exec lines can be specified. - - exec ffmpeg -re -i rtmp://localhost:1935/$app/$name -vcodec flv -acodec copy -s 32x32 - -f flv rtmp://localhost:1935/small/${name}; - } - - application small { - live on; - # Video with reduced resolution comes here from ffmpeg - } - - application webcam { - live on; - - # Stream from local webcam - exec_static ffmpeg -f video4linux2 -i /dev/video0 -c:v libx264 -an - -f flv rtmp://localhost:1935/webcam/mystream; - } - - application mypush { - live on; - - # Every stream published here - # is automatically pushed to - # these two machines - push rtmp1.example.com; - push rtmp2.example.com:1934; - } - - application mypull { - live on; - - # Pull all streams from remote machine - # and play locally - pull rtmp://rtmp3.example.com pageUrl=www.example.com/index.html; - } - - application mystaticpull { - live on; - - # Static pull is started at nginx start - pull rtmp://rtmp4.example.com pageUrl=www.example.com/index.html name=mystream static; - } - - # video on demand - application vod { - play /var/flvs; - } - - application vod2 { - play /var/mp4s; - } - - # Many publishers, many subscribers - # no checks, no recording - application videochat { - - live on; - - # The following notifications receive all - # the session variables as well as - # particular call arguments in HTTP POST - # request - - # Make HTTP request & use HTTP retcode - # to decide whether to allow publishing - # from this connection or not - on_publish http://localhost:8080/publish; - - # Same with playing - on_play http://localhost:8080/play; - - # Publish/play end (repeats on disconnect) - on_done http://localhost:8080/done; - - # All above mentioned notifications receive - # standard connect() arguments as well as - # play/publish ones. If any arguments are sent - # with GET-style syntax to play & publish - # these are also included. - # Example URL: - # rtmp://localhost/myapp/mystream?a=b&c=d - - # record 10 video keyframes (no audio) every 2 minutes - record keyframes; - record_path /tmp/vc; - record_max_frames 10; - record_interval 2m; - - # Async notify about an flv recorded - on_record_done http://localhost:8080/record_done; - - } - - - # HLS - - # For HLS to work please create a directory in tmpfs (/tmp/hls here) - # for the fragments. The directory contents is served via HTTP (see - # http{} section in config) - # - # Incoming stream must be in H264/AAC. For iPhones use baseline H264 - # profile (see ffmpeg example). - # This example creates RTMP stream from movie ready for HLS: - # - # ffmpeg -loglevel verbose -re -i movie.avi -vcodec libx264 - # -vprofile baseline -acodec libmp3lame -ar 44100 -ac 1 - # -f flv rtmp://localhost:1935/hls/movie - # - # If you need to transcode live stream use 'exec' feature. - # - application hls { - live on; - hls on; - hls_path /tmp/hls; - } - - # MPEG-DASH is similar to HLS - - application dash { - live on; - dash on; - dash_path /tmp/dash; - } - } +## nginx-rtmp-module (dash enhanced version) + +Forked from https://github.com/sergey-dryabzhinsky/ which was the most up to date version (until now) + +Notable new features : + + - add the possibility to have dash variant like (show below configuration, using ffmpeg to trancode in 3 variants). + note the "max" flag which indicate which representation should have max witdh and height and so use it to create the variant manifest. + you can also use any encoder to directly push the variant. + - add the support of using repetition in manifest to shorten them (optopn dash_repetition) + - add the support of common-encryption; currently working DRM are ClearKey/Widevine/Playready (see specific doc Here) + - add the support of inband scte event, from rtmp AMF event to dash (InbandEvent in manifest and emsg box in mp4 fragment) + + +See original doc here for full list of options. + +``` + rtmp { + server { + listen 1935; + + application ingest { + live on; + exec /usr/bin/ffmpeg -i rtmp://localhost/$app/$name \ + -c:a libfdk_aac -b:a 64k -c:v libx264 -preset fast -profile:v baseline -vsync cfr -s 1024x576 -b:v 1024K -bufsize 1024k \ + -f flv rtmp://localhost/dash/$name_hi \ + -c:a libfdk_aac -b:a 64k -c:v libx264 -preset fast -profile:v baseline -vsync cfr -s 640x360 -b:v 832K -bufsize 832k \ + -f flv rtmp://localhost/dash/$name_med \ + -c:a libfdk_aac -b:a 64k -c:v libx264 -preset fast -profile:v baseline -vsync cfr -s 320x180 -b:v 256K -bufsize 256k \ + -f flv rtmp://localhost/dash/$name_low } + + application dash { + live on; + dash on; + dash_nested on; + dash_repetition on; + dash_path /dev/shm/dash; + dash_fragment 4; # 4 second is generaly a good choice for live + dash_playlist_length 120; # keep 120s of tail + dash_cleanup on; + dash_variant _low bandwidth="256000" width="320" height="180"; + dash_variant _med bandwidth="832000" width="640" height="360"; + dash_variant _hi bandwidth="1024000" width="1024" height="576" max; + } + } + + server { + listen 443 ssl; + location / { + root /var/www; + add_header Cache-Control no-cache; + add_header 'Access-Control-Allow-Origin' '*'; + } + location /dash/live/index.mpd { + alias /dev/shm/dash/live/index.mpd; + add_header 'Access-Control-Allow-Origin' '*'; + add_header Cache-Control 'public, max-age=0, s-maxage=2'; + } + location /dash/live { + alias /dev/shm/dash/live; + add_header 'Access-Control-Allow-Origin' '*'; + add_header Cache-Control 'public, max-age=600, s-maxage=600'; + } + + server_name live.site.net; + ssl_certificate /etc/letsencrypt/live/live.site.net/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/live.sit.net/privkey.pem; - # HTTP can be used for accessing RTMP stats - http { - - server { - - listen 8080; - - # This URL provides RTMP statistics in XML - location /stat { - rtmp_stat all; - - # Use this stylesheet to view XML as web page - # in browser - rtmp_stat_stylesheet stat.xsl; - } - - location /stat.xsl { - # XML stylesheet to view RTMP stats. - # Copy stat.xsl wherever you want - # and put the full directory path here - root /path/to/stat.xsl/; - } + } +} +``` - location /hls { - # Serve HLS fragments - types { - application/vnd.apple.mpegurl m3u8; - video/mp2t ts; - } - root /tmp; - add_header Cache-Control no-cache; - } - location /dash { - # Serve DASH fragments - root /tmp; - add_header Cache-Control no-cache; - } - } - } diff --git a/TODO.md b/TODO.md new file mode 100644 index 000000000..fa5a5c934 --- /dev/null +++ b/TODO.md @@ -0,0 +1,20 @@ +## TODO + +- doc // OK +- need re-upstream // wait response +- rewritte the variant code for dash, using memory ? + +- test common encryption code (OK clearkey, Widevine both on chrome/firefox!) +- need to write some doc about cenc/drm implem +- refacto code for writing content protection in manifest // OK +- clarify the use of %V in ngx_printf // OK +- refacto code for init / kid // OK +- correct pssh in manifest // OK +- add pssh / cenc in variant mpd // OK +- add real base64 encrypt pssh data // OK +- add support for sub sample encryption //OK +- add sig for wdv //OK +- add struct for drm info // OK +- add pssh in init file for wdv // OK +- add msplayready support // OK need pssh in init file + diff --git a/config b/config index 13f00e83c..32f6d551e 100644 --- a/config +++ b/config @@ -43,6 +43,8 @@ RTMP_DEPS=" \ $ngx_addon_dir/ngx_rtmp_proxy_protocol.h \ $ngx_addon_dir/hls/ngx_rtmp_mpegts.h \ $ngx_addon_dir/dash/ngx_rtmp_mp4.h \ + $ngx_addon_dir/dash/ngx_rtmp_cenc.h \ + $ngx_addon_dir/dash/ngx_rtmp_dash_templates.h \ " RTMP_CORE_SRCS=" \ $ngx_addon_dir/ngx_rtmp.c \ @@ -78,6 +80,7 @@ RTMP_CORE_SRCS=" \ $ngx_addon_dir/hls/ngx_rtmp_mpegts.c \ $ngx_addon_dir/hls/ngx_rtmp_mpegts_crc.c \ $ngx_addon_dir/dash/ngx_rtmp_mp4.c \ + $ngx_addon_dir/dash/ngx_rtmp_cenc.c \ " RTMP_HTTP_SRCS=" \ $ngx_addon_dir/ngx_rtmp_stat_module.c \ diff --git a/dash/ngx_rtmp_cenc.c b/dash/ngx_rtmp_cenc.c new file mode 100644 index 000000000..f0b81e757 --- /dev/null +++ b/dash/ngx_rtmp_cenc.c @@ -0,0 +1,190 @@ + + +#include +#include +#include +#include +#include +#include +#include "ngx_rtmp_cenc.h" + + +void +debug_counter(ngx_rtmp_session_t *s, uint8_t *c, uint8_t *k, size_t l) +{ + u_char hexc[AES_BLOCK_SIZE*2+1]; + u_char hexk[AES_BLOCK_SIZE*2+1]; + + ngx_hex_dump(hexc, c, AES_BLOCK_SIZE); + ngx_hex_dump(hexk, k, AES_BLOCK_SIZE); + hexc[AES_BLOCK_SIZE*2] = '\0'; + hexk[AES_BLOCK_SIZE*2] = '\0'; + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash cenc_counter: %ui %s %s", l, hexc, hexk); +} + + +ngx_int_t +ngx_rtmp_cenc_read_hex(ngx_str_t src, u_char* dst) +{ + u_char l, h; + size_t i; + + if (src.len != NGX_RTMP_CENC_KEY_SIZE*2) { + return NGX_ERROR; + } + + for (i = 0; i < NGX_RTMP_CENC_KEY_SIZE; i++) { + l = ngx_tolower(src.data[i*2]); + l = l >= 'a' ? l - 'a' + 10 : l - '0'; + h = ngx_tolower(src.data[i*2+1]); + h = h >= 'a' ? h - 'a' + 10 : h - '0'; + dst[i] = (l << 4) | h; + } + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_cenc_rand_iv(u_char* iv) +{ + if(RAND_bytes(iv, NGX_RTMP_CENC_IV_SIZE) != 1) { + return NGX_ERROR; + } + + return NGX_OK; +} + + +void +ngx_rtmp_cenc_increment_iv(u_char* iv) +{ + int i; + + for (i = NGX_RTMP_CENC_IV_SIZE - 1; i >= 0; i--) { + iv[i]++; + if (iv[i]) + break; + } +} + + +ngx_int_t +ngx_rtmp_cenc_aes_ctr_encrypt(ngx_rtmp_session_t *s, uint8_t *key, uint8_t *iv, + uint8_t *data, size_t data_len) +{ + /* aes-ctr implementation */ + + EVP_CIPHER_CTX* ctx; + size_t j, len, left = data_len; + int i, w; + uint8_t *pos = data; + uint8_t counter[AES_BLOCK_SIZE], buf[AES_BLOCK_SIZE]; + + ngx_memset(counter + NGX_RTMP_CENC_IV_SIZE, 0, NGX_RTMP_CENC_IV_SIZE); + ngx_memcpy(counter, iv, NGX_RTMP_CENC_IV_SIZE); + + ctx = EVP_CIPHER_CTX_new(); + if (ctx == NULL) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "dash rtmp_cenc_encrypt: evp_cipher_ctx failed"); + return NGX_ERROR; + } + + if (EVP_EncryptInit_ex(ctx, EVP_aes_128_ecb(), NULL, key, NULL) != 1) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "dash rtmp_cenc_encrypt: evp_encrypt_init failed"); + return NGX_ERROR; + } + + while (left > 0) { + + if (EVP_EncryptUpdate(ctx, buf, &w, counter, AES_BLOCK_SIZE) != 1) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "dash rtmp_cenc_encrypt: evp_encrypt_update failed"); + return NGX_ERROR; + } + + len = (left < AES_BLOCK_SIZE) ? left : AES_BLOCK_SIZE; + for (j = 0; j < len; j++) + pos[j] ^= buf[j]; + pos += len; + left -= len; + + for (i = AES_BLOCK_SIZE - 1; i >= 0; i--) { + counter[i]++; + if (counter[i]) + break; + } + } + + EVP_CIPHER_CTX_free(ctx); + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_cenc_encrypt_full_sample(ngx_rtmp_session_t *s, uint8_t *key, uint8_t *iv, + uint8_t *data, size_t data_len) +{ + return ngx_rtmp_cenc_aes_ctr_encrypt(s, key, iv, data, data_len); +} + + +ngx_int_t +ngx_rtmp_cenc_encrypt_sub_sample(ngx_rtmp_session_t *s, uint8_t *key, uint8_t *iv, + uint8_t *data, size_t data_len, size_t *clear_data_len) +{ + size_t crypted_data_len; + + /* small sample : leave it in clear */ + if (data_len <= NGX_RTMP_CENC_MIN_CLEAR_SIZE) { + *clear_data_len = data_len; + return NGX_OK; + } + + /* skip sufficient amount of data to leave nalu header/infos + * in clear to conform to the norm */ + crypted_data_len = + ((data_len - NGX_RTMP_CENC_MIN_CLEAR_SIZE) / AES_BLOCK_SIZE) * AES_BLOCK_SIZE; + *clear_data_len = data_len - crypted_data_len; + + data += *clear_data_len; + return ngx_rtmp_cenc_aes_ctr_encrypt(s, key, iv, data, crypted_data_len); + +} + + +ngx_int_t +ngx_rtmp_cenc_content_protection_pssh(u_char* kid, ngx_str_t *dest_pssh) +{ + ngx_str_t src_pssh; + u_char dest[NGX_RTMP_CENC_MAX_PSSH_SIZE]; + + u_char pssh[] = { + 0x00, 0x00, 0x00, 0x34, 0x70, 0x73, 0x73, 0x68, // pssh box header + 0x01, 0x00, 0x00, 0x00, // header + 0x10, 0x77, 0xef, 0xec, 0xc0, 0xb2, 0x4d, 0x02, // systemID + 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b, + 0x00, 0x00, 0x00, 0x01, // kid count + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // kid + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00 // data size + }; + + ngx_memcpy(pssh+32, kid, NGX_RTMP_CENC_KEY_SIZE); + + src_pssh.len = sizeof(pssh); + src_pssh.data = pssh; + + dest_pssh->len = ngx_base64_encoded_length(src_pssh.len); + dest_pssh->data = dest; + + ngx_encode_base64(dest_pssh, &src_pssh); + + return NGX_OK; +} + diff --git a/dash/ngx_rtmp_cenc.h b/dash/ngx_rtmp_cenc.h new file mode 100644 index 000000000..a04c002e7 --- /dev/null +++ b/dash/ngx_rtmp_cenc.h @@ -0,0 +1,44 @@ +#ifndef _NGX_RTMP_CENC_H_INCLUDED_ +#define _NGX_RTMP_CENC_H_INCLUDED_ + + +#define NGX_RTMP_CENC_IV_SIZE (8) +#define NGX_RTMP_CENC_KEY_SIZE (16) +#define NGX_RTMP_CENC_MIN_CLEAR_SIZE (100) +#define NGX_RTMP_CENC_MAX_PSSH_SIZE (1024) + + +typedef struct { + u_char kid[NGX_RTMP_CENC_KEY_SIZE]; + unsigned wdv:1; + ngx_str_t wdv_data; + unsigned mspr:1; + ngx_str_t mspr_data; + ngx_str_t mspr_kid; + ngx_str_t mspr_pro; +} ngx_rtmp_cenc_drm_info_t; + + +ngx_int_t +ngx_rtmp_cenc_read_hex(ngx_str_t src, u_char* dst); + +ngx_int_t +ngx_rtmp_cenc_rand_iv(u_char* iv); + +void +ngx_rtmp_cenc_increment_iv(u_char* iv); + +ngx_int_t +ngx_rtmp_cenc_encrypt_full_sample(ngx_rtmp_session_t *s, + uint8_t *key, uint8_t *iv, uint8_t *data, size_t data_len); + +ngx_int_t +ngx_rtmp_cenc_encrypt_sub_sample(ngx_rtmp_session_t *s, + uint8_t *key, uint8_t *iv, uint8_t *data, + size_t data_len, size_t *clear_data_len); + +ngx_int_t +ngx_rtmp_cenc_content_protection_pssh(u_char* kid, + ngx_str_t *dest_pssh); + +#endif /* _NGX_RTMP_CENC_H_INCLUDED_ */ diff --git a/dash/ngx_rtmp_dash_module.c b/dash/ngx_rtmp_dash_module.c index 1038ae285..f98a3a2e7 100644 --- a/dash/ngx_rtmp_dash_module.c +++ b/dash/ngx_rtmp_dash_module.c @@ -6,6 +6,8 @@ #include #include "ngx_rtmp_live_module.h" #include "ngx_rtmp_mp4.h" +#include "ngx_rtmp_dash_templates.h" +#include "ngx_rtmp_cenc.h" static ngx_rtmp_publish_pt next_publish; @@ -15,11 +17,14 @@ static ngx_rtmp_stream_eof_pt next_stream_eof; static ngx_rtmp_playlist_pt next_playlist; +static char * ngx_rtmp_dash_variant(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); static ngx_int_t ngx_rtmp_dash_postconfiguration(ngx_conf_t *cf); static void * ngx_rtmp_dash_create_app_conf(ngx_conf_t *cf); static char * ngx_rtmp_dash_merge_app_conf(ngx_conf_t *cf, void *parent, void *child); -static ngx_int_t ngx_rtmp_dash_write_init_segments(ngx_rtmp_session_t *s); +static ngx_int_t ngx_rtmp_dash_write_init_segments(ngx_rtmp_session_t *s, + ngx_rtmp_cenc_drm_info_t *drmi); static ngx_int_t ngx_rtmp_dash_ensure_directory(ngx_rtmp_session_t *s); @@ -46,14 +51,28 @@ typedef struct { char type; uint32_t earliest_pres_time; uint32_t latest_pres_time; + unsigned is_protected:1; + u_char key[NGX_RTMP_CENC_KEY_SIZE]; + u_char iv[NGX_RTMP_CENC_IV_SIZE]; ngx_rtmp_mp4_sample_t samples[NGX_RTMP_DASH_MAX_SAMPLES]; } ngx_rtmp_dash_track_t; typedef struct { + ngx_str_t suffix; + ngx_array_t args; +} ngx_rtmp_dash_variant_t; + + +typedef struct { + ngx_str_t segments; + ngx_str_t segments_bak; ngx_str_t playlist; ngx_str_t playlist_bak; + ngx_str_t var_playlist; + ngx_str_t var_playlist_bak; ngx_str_t name; + ngx_str_t varname; ngx_str_t stream; ngx_time_t start_time; @@ -64,6 +83,13 @@ typedef struct { unsigned opened:1; unsigned has_video:1; unsigned has_audio:1; + unsigned start_cuepoint:1; + unsigned end_cuepoint:1; + + uint32_t cuepoint_starttime; + uint32_t cuepoint_endtime; + uint32_t cuepoint_duration; + uint32_t cuepoint_id; ngx_file_t video_file; ngx_file_t audio_file; @@ -72,6 +98,10 @@ typedef struct { ngx_rtmp_dash_track_t audio; ngx_rtmp_dash_track_t video; + ngx_rtmp_dash_variant_t *var; + + ngx_rtmp_cenc_drm_info_t drm_info; + } ngx_rtmp_dash_ctx_t; @@ -94,11 +124,32 @@ static ngx_conf_enum_t ngx_rtmp_dash_clock_compensation_type_sl { ngx_null_string, 0 } }; +#define NGX_RTMP_DASH_AD_MARKERS_OFF 1 +#define NGX_RTMP_DASH_AD_MARKERS_ON_CUEPOINT 2 +#define NGX_RTMP_DASH_AD_MARKERS_ON_CUEPOINT_SCTE35 3 + +static ngx_conf_enum_t ngx_rtmp_dash_ad_markers_type_slots[] = { + { ngx_string("off"), NGX_RTMP_DASH_AD_MARKERS_OFF }, + { ngx_string("on_cuepoint"), NGX_RTMP_DASH_AD_MARKERS_ON_CUEPOINT }, + { ngx_string("on_cuepoint_scte35"), NGX_RTMP_DASH_AD_MARKERS_ON_CUEPOINT_SCTE35 }, + { ngx_null_string, 0 } +}; + typedef struct { ngx_flag_t dash; ngx_msec_t fraglen; ngx_msec_t playlen; ngx_flag_t nested; + ngx_flag_t cenc; + ngx_str_t cenc_key; + ngx_str_t cenc_kid; + ngx_flag_t wdv; + ngx_str_t wdv_data; + ngx_flag_t mspr; + ngx_str_t mspr_data; + ngx_str_t mspr_kid; + ngx_str_t mspr_pro; + ngx_flag_t repetition; ngx_uint_t clock_compensation; // Try to compensate clock drift // between client and server (on client side) ngx_str_t clock_helper_uri; // Use uri to static file on HTTP server @@ -108,6 +159,8 @@ typedef struct { ngx_uint_t winfrags; ngx_flag_t cleanup; ngx_path_t *slot; + ngx_array_t *variant; + ngx_uint_t ad_markers; } ngx_rtmp_dash_app_conf_t; @@ -155,6 +208,76 @@ static ngx_command_t ngx_rtmp_dash_commands[] = { offsetof(ngx_rtmp_dash_app_conf_t, nested), NULL }, + { ngx_string("dash_repetition"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_dash_app_conf_t, repetition), + NULL }, + + { ngx_string("dash_cenc"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_dash_app_conf_t, cenc), + NULL }, + + { ngx_string("dash_cenc_key"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_dash_app_conf_t, cenc_key), + NULL }, + + { ngx_string("dash_cenc_kid"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_dash_app_conf_t, cenc_kid), + NULL }, + + { ngx_string("dash_wdv"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_dash_app_conf_t, wdv), + NULL }, + + { ngx_string("dash_wdv_data"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_dash_app_conf_t, wdv_data), + NULL }, + + { ngx_string("dash_mspr"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_dash_app_conf_t, mspr), + NULL }, + + { ngx_string("dash_mspr_data"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_dash_app_conf_t, mspr_data), + NULL }, + + { ngx_string("dash_mspr_kid"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_dash_app_conf_t, mspr_kid), + NULL }, + + { ngx_string("dash_mspr_pro"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_dash_app_conf_t, mspr_pro), + NULL }, + { ngx_string("dash_clock_compensation"), NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, ngx_conf_set_enum_slot, @@ -169,6 +292,20 @@ static ngx_command_t ngx_rtmp_dash_commands[] = { offsetof(ngx_rtmp_dash_app_conf_t, clock_helper_uri), NULL }, + { ngx_string("dash_variant"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_1MORE, + ngx_rtmp_dash_variant, + NGX_RTMP_APP_CONF_OFFSET, + 0, + NULL }, + + { ngx_string("dash_ad_markers"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_enum_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_dash_app_conf_t, ad_markers), + &ngx_rtmp_dash_ad_markers_type_slots }, + ngx_null_command }; @@ -247,19 +384,436 @@ ngx_rtmp_dash_rename_file(u_char *src, u_char *dst) } -static ngx_uint_t -ngx_rtmp_dash_gcd(ngx_uint_t m, ngx_uint_t n) -{ - /* greatest common divisor */ +static ngx_uint_t +ngx_rtmp_dash_gcd(ngx_uint_t m, ngx_uint_t n) +{ + /* greatest common divisor */ + + ngx_uint_t temp; + + while (n) { + temp=n; + n=m % n; + m=temp; + } + return m; +} + + +static u_char * +ngx_rtmp_dash_write_segment(u_char *p, u_char *last, ngx_uint_t t, + ngx_uint_t d, ngx_uint_t r) +{ + if (r == 0) { + p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_TIME, t, d); + } else { + p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_TIME_WITH_REPETITION, t, d, r); + } + + return p; +} + + +static u_char * +ngx_rtmp_dash_write_segment_timeline(ngx_rtmp_session_t *s, ngx_rtmp_dash_ctx_t *ctx, + ngx_rtmp_dash_app_conf_t *dacf, u_char *p, u_char *last) +{ + ngx_uint_t i, t, d, r; + ngx_rtmp_dash_frag_t *f; + + for (i = 0; i < ctx->nfrags; i++) { + f = ngx_rtmp_dash_get_frag(s, i); + + if (dacf->repetition) { + if (i == 0) { + t = f->timestamp; + d = f->duration; + r = 0; + } else { + if (f->duration == d) { + r++; + } else { + p = ngx_rtmp_dash_write_segment(p, last, t, d, r); + t = f->timestamp; + d = f->duration; + r = 0; + } + } + if (i == ctx->nfrags - 1) { + p = ngx_rtmp_dash_write_segment(p, last, t, d, r); + } + } else { + t = f->timestamp; + d = f->duration; + p = ngx_rtmp_dash_write_segment(p, last, t, d, 0); + } + } + + return p; +} + + +static u_char * +ngx_rtmp_dash_write_content_protection(ngx_rtmp_session_t *s, + ngx_rtmp_cenc_drm_info_t *drmi, u_char *p, u_char *last) +{ + u_char *k; + ngx_str_t cenc_pssh; + + k = drmi->kid; + + ngx_rtmp_cenc_content_protection_pssh(k, &cenc_pssh); + + p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_CONTENT_PROTECTION_CENC, + k[0], k[1], k[2], k[3], + k[4], k[5], k[6], k[7], + k[8], k[9], k[10], k[11], k[12], k[13], k[14], k[15]); + + + p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_CONTENT_PROTECTION_PSSH_CENC, + &cenc_pssh); + + if (drmi->wdv) { + p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_CONTENT_PROTECTION_PSSH_WDV, + &drmi->wdv_data); + } + + if (drmi->mspr) { + p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_CONTENT_PROTECTION_PSSH_MSPR, + &drmi->mspr_data, + &drmi->mspr_kid, + &drmi->mspr_pro); + } + + return p; +} + + +static ngx_int_t +ngx_rtmp_dash_write_variant_playlist(ngx_rtmp_session_t *s) +{ + char *sep; + u_char *p, *last; + ssize_t n; + ngx_fd_t fd, fds; + struct tm tm; + ngx_uint_t i, j, k, frame_rate_num, frame_rate_denom; + ngx_uint_t depth_msec, depth_sec; + ngx_uint_t update_period, update_period_msec; + ngx_uint_t start_time, buffer_time, buffer_time_msec; + ngx_uint_t presentation_delay, presentation_delay_msec; + ngx_uint_t gcd, par_x, par_y; + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_codec_ctx_t *codec_ctx; + ngx_rtmp_dash_frag_t *f; + ngx_rtmp_dash_app_conf_t *dacf; + ngx_rtmp_dash_variant_t *var; + ngx_str_t *arg; + + ngx_rtmp_playlist_t v; + + static u_char buffer[NGX_RTMP_DASH_BUFSIZE]; + static u_char available_time[NGX_RTMP_DASH_GMT_LENGTH]; + static u_char publish_time[NGX_RTMP_DASH_GMT_LENGTH]; + static u_char buffer_depth[sizeof("P00Y00M00DT00H00M00.000S")]; + static u_char frame_rate[(NGX_INT_T_LEN * 2) + 2]; + static u_char seg_path[NGX_MAX_PATH + 1]; + + dacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_dash_module); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + if (dacf == NULL || ctx == NULL || codec_ctx == NULL) { + return NGX_ERROR; + } + + fd = ngx_open_file(ctx->var_playlist_bak.data, NGX_FILE_WRONLY, + NGX_FILE_TRUNCATE, NGX_FILE_DEFAULT_ACCESS); + + if (fd == NGX_INVALID_FILE) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: open failed: '%V'", &ctx->var_playlist_bak); + return NGX_ERROR; + } + + /* availabity and publish time should be relative to peer epoch */ + start_time = ctx->start_time.sec - (s->peer_epoch/1000); + ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "Fixing start_time=%uD %uD epoch=%uD new_start_time=%uD", + (uint32_t)ctx->start_time.sec, (uint32_t)ctx->start_time.msec, + (uint32_t)s->peer_epoch, + (uint32_t)start_time); + + /** + * Availability time must be equal stream start time + * Cos segments time counting from it + */ + ngx_libc_gmtime(start_time, &tm); + *ngx_sprintf(available_time, "%4d-%02d-%02dT%02d:%02d:%02dZ", + tm.tm_year + 1900, tm.tm_mon + 1, + tm.tm_mday, tm.tm_hour, + tm.tm_min, tm.tm_sec + ) = 0; + + /* Stream publish time */ + *ngx_sprintf(publish_time, "%s", available_time) = 0; + + depth_sec = (ngx_uint_t) ( + ngx_rtmp_dash_get_frag(s, ctx->nfrags - 1)->timestamp + + ngx_rtmp_dash_get_frag(s, ctx->nfrags - 1)->duration - + ngx_rtmp_dash_get_frag(s, 0)->timestamp); + + depth_msec = depth_sec % 1000; + depth_sec -= depth_msec; + depth_sec /= 1000; + + ngx_libc_gmtime(depth_sec, &tm); + + *ngx_sprintf(buffer_depth, "P%dY%02dM%02dDT%dH%02dM%02d.%03dS", + tm.tm_year - 70, tm.tm_mon, + tm.tm_mday - 1, tm.tm_hour, + tm.tm_min, tm.tm_sec, + depth_msec) = 0; + + last = buffer + sizeof(buffer); + + /** + * Calculate playlist minimal update period + * This should be more than biggest segment duration + * Cos segments rounded by keyframe/GOP. + * And that time not always equals to fragment length. + */ + update_period = dacf->fraglen; + + for (i = 0; i < ctx->nfrags; i++) { + f = ngx_rtmp_dash_get_frag(s, i); + if (f->duration > update_period) { + update_period = f->duration; + } + } + + // Reasonable delay for streaming + presentation_delay = update_period * 2 + 1000; + presentation_delay_msec = presentation_delay % 1000; + presentation_delay -= presentation_delay_msec; + presentation_delay /= 1000; + + // Calculate msec part and seconds + update_period_msec = update_period % 1000; + update_period -= update_period_msec; + update_period /= 1000; + + // Buffer length by default fragment length + buffer_time = dacf->fraglen; + buffer_time_msec = buffer_time % 1000; + buffer_time -= buffer_time_msec; + buffer_time /= 1000; + + // Fill DASH header + p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_MANIFEST_HEADER, + // availabilityStartTime + available_time, + // publishTime + publish_time, + // minimumUpdatePeriod + update_period, update_period_msec, + // minBufferTime + buffer_time, buffer_time_msec, + // timeShiftBufferDepth + buffer_depth, + // suggestedPresentationDelay + presentation_delay, presentation_delay_msec + ); + + p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_PERIOD); + + n = ngx_write_fd(fd, buffer, p - buffer); + + sep = (dacf->nested ? "/" : "-"); + var = dacf->variant->elts; + + if (ctx->has_video) { + frame_rate_num = (ngx_uint_t) (codec_ctx->frame_rate * 1000.); + + if (frame_rate_num % 1000 == 0) { + *ngx_sprintf(frame_rate, "%ui", frame_rate_num / 1000) = 0; + } else { + frame_rate_denom = 1000; + switch (frame_rate_num) { + case 23976: + frame_rate_num = 24000; + frame_rate_denom = 1001; + break; + case 29970: + frame_rate_num = 30000; + frame_rate_denom = 1001; + break; + case 59940: + frame_rate_num = 60000; + frame_rate_denom = 1001; + break; + } + + *ngx_sprintf(frame_rate, "%ui/%ui", frame_rate_num, frame_rate_denom) = 0; + } + + gcd = ngx_rtmp_dash_gcd(codec_ctx->width, codec_ctx->height); + par_x = codec_ctx->width / gcd; + par_y = codec_ctx->height / gcd; + + p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_MANIFEST_ADAPTATIONSET_VIDEO, + codec_ctx->width, + codec_ctx->height, + frame_rate, + par_x, par_y); + + switch (dacf->ad_markers) { + case NGX_RTMP_DASH_AD_MARKERS_ON_CUEPOINT: + case NGX_RTMP_DASH_AD_MARKERS_ON_CUEPOINT_SCTE35: + p = ngx_slprintf(p, last, NGX_RTMP_DASH_INBAND_EVENT); + } + + if (dacf->cenc) { + p = ngx_rtmp_dash_write_content_protection(s, &ctx->drm_info, p, last); + } + + n = ngx_write_fd(fd, buffer, p - buffer); + + for (j = 0; j < dacf->variant->nelts; j++, var++) { + + /* read segments file */ + if (dacf->nested) { + *ngx_sprintf(seg_path, "%V/%V%V/index.seg", + &dacf->path, &ctx->varname, &var->suffix) = 0; + } else { + *ngx_sprintf(seg_path, "%V/%V%V.seg", + &dacf->path, &ctx->varname, &var->suffix) = 0; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: read segments file for variant '%s'", seg_path); + + fds = ngx_open_file(seg_path, NGX_FILE_RDONLY, NGX_FILE_OPEN, 0); + + if (fds == NGX_INVALID_FILE) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: open failed: segments '%s'", seg_path); + continue; + } + + p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_MANIFEST_REPRESENTATION_VARIANT_VIDEO, + &ctx->varname, &var->suffix, + codec_ctx->avc_profile, + codec_ctx->avc_compat, + codec_ctx->avc_level); + + arg = var->args.elts; + for (k = 0; k < var->args.nelts && k < 3 ; k++, arg++) { + p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_VARIANT_ARG, arg); + } + + p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_VARIANT_ARG_FOOTER); + + p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_SEGMENTTPL_VARIANT_VIDEO, + &ctx->varname, &var->suffix, sep, + &ctx->varname, &var->suffix, sep); + + n = ngx_write_fd(fd, buffer, p - buffer); + + while ((n = ngx_read_fd(fds, buffer, sizeof(buffer)))) { + n = ngx_write_fd(fd, buffer, n); + } + + ngx_close_file(fds); + + p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_MANIFEST_REPRESENTATION_VIDEO_FOOTER); + n = ngx_write_fd(fd, buffer, p - buffer); + + } + + p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_MANIFEST_ADAPTATIONSET_VIDEO_FOOTER); + n = ngx_write_fd(fd, buffer, p - buffer); + } + + if (ctx->has_audio) { + p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_MANIFEST_ADAPTATIONSET_AUDIO); + + if (dacf->cenc) { + p = ngx_rtmp_dash_write_content_protection(s, &ctx->drm_info, p, last); + } + + p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_REPRESENTATION_AUDIO, + &ctx->name, + codec_ctx->audio_codec_id == NGX_RTMP_AUDIO_AAC ? + (codec_ctx->aac_sbr ? "40.5" : "40.2") : "6b", + codec_ctx->sample_rate, + (ngx_uint_t) (codec_ctx->audio_data_rate * 1000), + &ctx->name, sep, + &ctx->name, sep); + + p = ngx_rtmp_dash_write_segment_timeline(s, ctx, dacf, p, last); + + p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_REPRESENTATION_AUDIO_FOOTER); + + p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_ADAPTATIONSET_AUDIO_FOOTER); + + n = ngx_write_fd(fd, buffer, p - buffer); + } + + p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_PERIOD_FOOTER); + n = ngx_write_fd(fd, buffer, p - buffer); + + /* UTCTiming value */ + switch (dacf->clock_compensation) { + case NGX_RTMP_DASH_CLOCK_COMPENSATION_NTP: + p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_MANIFEST_CLOCK, + "ntp", + &dacf->clock_helper_uri + ); + n = ngx_write_fd(fd, buffer, p - buffer); + break; + case NGX_RTMP_DASH_CLOCK_COMPENSATION_HTTP_HEAD: + p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_MANIFEST_CLOCK, + "http-head", + &dacf->clock_helper_uri + ); + n = ngx_write_fd(fd, buffer, p - buffer); + break; + case NGX_RTMP_DASH_CLOCK_COMPENSATION_HTTP_ISO: + p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_MANIFEST_CLOCK, + "http-iso", + &dacf->clock_helper_uri + ); + n = ngx_write_fd(fd, buffer, p - buffer); + break; + } + + p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_MANIFEST_FOOTER); + n = ngx_write_fd(fd, buffer, p - buffer); + + if (n < 0) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: write failed: '%V'", &ctx->var_playlist_bak); + ngx_close_file(fd); + return NGX_ERROR; + } - ngx_uint_t temp; + ngx_close_file(fd); - while (n) { - temp=n; - n=m % n; - m=temp; + if (ngx_rtmp_dash_rename_file(ctx->var_playlist_bak.data, ctx->var_playlist.data) + == NGX_FILE_ERROR) + { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: rename failed: '%V'->'%V'", + &ctx->var_playlist_bak, &ctx->var_playlist); + return NGX_ERROR; } - return m; + + ngx_memzero(&v, sizeof(v)); + ngx_str_set(&(v.module), "dash"); + v.playlist.data = ctx->playlist.data; + v.playlist.len = ctx->playlist.len; + return next_playlist(s, &v); } @@ -269,13 +823,13 @@ ngx_rtmp_dash_write_playlist(ngx_rtmp_session_t *s) char *sep; u_char *p, *last; ssize_t n; - ngx_fd_t fd; + ngx_fd_t fd, fds; struct tm tm; ngx_str_t noname, *name; ngx_uint_t i, frame_rate_num, frame_rate_denom; ngx_uint_t depth_msec, depth_sec; ngx_uint_t update_period, update_period_msec; - ngx_uint_t buffer_time, buffer_time_msec; + ngx_uint_t start_time, buffer_time, buffer_time_msec; ngx_uint_t presentation_delay, presentation_delay_msec; ngx_uint_t gcd, par_x, par_y; ngx_rtmp_dash_ctx_t *ctx; @@ -286,7 +840,7 @@ ngx_rtmp_dash_write_playlist(ngx_rtmp_session_t *s) ngx_rtmp_playlist_t v; static u_char buffer[NGX_RTMP_DASH_BUFSIZE]; - static u_char avaliable_time[NGX_RTMP_DASH_GMT_LENGTH]; + static u_char available_time[NGX_RTMP_DASH_GMT_LENGTH]; static u_char publish_time[NGX_RTMP_DASH_GMT_LENGTH]; static u_char buffer_depth[sizeof("P00Y00M00DT00H00M00.000S")]; static u_char frame_rate[(NGX_INT_T_LEN * 2) + 2]; @@ -300,7 +854,7 @@ ngx_rtmp_dash_write_playlist(ngx_rtmp_session_t *s) } if (ctx->id == 0) { - ngx_rtmp_dash_write_init_segments(s); + ngx_rtmp_dash_write_init_segments(s, &ctx->drm_info); } fd = ngx_open_file(ctx->playlist_bak.data, NGX_FILE_WRONLY, @@ -311,121 +865,38 @@ ngx_rtmp_dash_write_playlist(ngx_rtmp_session_t *s) "dash: open failed: '%V'", &ctx->playlist_bak); return NGX_ERROR; } + + /* write segments file */ + fds = ngx_open_file(ctx->segments_bak.data, NGX_FILE_WRONLY, + NGX_FILE_TRUNCATE, NGX_FILE_DEFAULT_ACCESS); + + if (fds == NGX_INVALID_FILE) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: open failed: '%V'", &ctx->segments_bak); + return NGX_ERROR; + } + /* availabity and publish time should be relative to peer epoch */ + start_time = ctx->start_time.sec - (s->peer_epoch/1000); + ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "Fixing start_time=%uD %uD epoch=%uD new_start_time=%uD", + (uint32_t)ctx->start_time.sec, (uint32_t)ctx->start_time.msec, + (uint32_t)s->peer_epoch, + (uint32_t)start_time); -#define NGX_RTMP_DASH_MANIFEST_HEADER \ - "\n" \ - "\n" - -#define NGX_RTMP_DASH_MANIFEST_PERIOD \ - " \n" - - -#define NGX_RTMP_DASH_MANIFEST_VIDEO \ - " \n" \ - " \n" \ - " \n" \ - " \n" - - -#define NGX_RTMP_DASH_MANIFEST_VIDEO_FOOTER \ - " \n" \ - " \n" \ - " \n" \ - " \n" - - -#define NGX_RTMP_DASH_MANIFEST_TIME \ - " \n" - - -#define NGX_RTMP_DASH_MANIFEST_AUDIO \ - " \n" \ - " \n" \ - " \n" \ - " \n" \ - " \n" - - -#define NGX_RTMP_DASH_MANIFEST_AUDIO_FOOTER \ - " \n" \ - " \n" \ - " \n" \ - " \n" - - -#define NGX_RTMP_DASH_PERIOD_FOOTER \ - " \n" - - -#define NGX_RTMP_DASH_MANIFEST_CLOCK \ - " \n" - - -#define NGX_RTMP_DASH_MANIFEST_FOOTER \ - "\n" - - -/** - * Availability time must be equal stream start time - * Cos segments time counting from it - */ - ngx_libc_gmtime(ctx->start_time.sec, &tm); - *ngx_sprintf(avaliable_time, "%4d-%02d-%02dT%02d:%02d:%02dZ", + /** + * Availability time must be equal stream start time + * Cos segments time counting from it + */ + ngx_libc_gmtime(start_time, &tm); + *ngx_sprintf(available_time, "%4d-%02d-%02dT%02d:%02d:%02dZ", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec ) = 0; /* Stream publish time */ - *ngx_sprintf(publish_time, "%s", avaliable_time) = 0; + *ngx_sprintf(publish_time, "%s", available_time) = 0; depth_sec = (ngx_uint_t) ( ngx_rtmp_dash_get_frag(s, ctx->nfrags - 1)->timestamp + @@ -481,7 +952,7 @@ ngx_rtmp_dash_write_playlist(ngx_rtmp_session_t *s) // Fill DASH header p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_MANIFEST_HEADER, // availabilityStartTime - avaliable_time, + available_time, // publishTime publish_time, // minimumUpdatePeriod @@ -532,11 +1003,23 @@ ngx_rtmp_dash_write_playlist(ngx_rtmp_session_t *s) par_x = codec_ctx->width / gcd; par_y = codec_ctx->height / gcd; - p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_MANIFEST_VIDEO, + p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_MANIFEST_ADAPTATIONSET_VIDEO, codec_ctx->width, codec_ctx->height, frame_rate, - par_x, par_y, + par_x, par_y); + + switch (dacf->ad_markers) { + case NGX_RTMP_DASH_AD_MARKERS_ON_CUEPOINT: + case NGX_RTMP_DASH_AD_MARKERS_ON_CUEPOINT_SCTE35: + p = ngx_slprintf(p, last, NGX_RTMP_DASH_INBAND_EVENT); + } + + if (dacf->cenc) { + p = ngx_rtmp_dash_write_content_protection(s, &ctx->drm_info, p, last); + } + + p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_REPRESENTATION_VIDEO, &ctx->name, codec_ctx->avc_profile, codec_ctx->avc_compat, @@ -548,19 +1031,28 @@ ngx_rtmp_dash_write_playlist(ngx_rtmp_session_t *s) name, sep, name, sep); - for (i = 0; i < ctx->nfrags; i++) { - f = ngx_rtmp_dash_get_frag(s, i); - p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_TIME, - f->timestamp, f->duration); - } + n = ngx_write_fd(fd, buffer, p - buffer); + + p = buffer; + p = ngx_rtmp_dash_write_segment_timeline(s, ctx, dacf, p, last); - p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_VIDEO_FOOTER); + ngx_write_fd(fds, buffer, p - buffer); + + p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_REPRESENTATION_VIDEO_FOOTER); + + p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_ADAPTATIONSET_VIDEO_FOOTER); n = ngx_write_fd(fd, buffer, p - buffer); } if (ctx->has_audio) { - p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_MANIFEST_AUDIO, + p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_MANIFEST_ADAPTATIONSET_AUDIO); + + if (dacf->cenc) { + p = ngx_rtmp_dash_write_content_protection(s, &ctx->drm_info, p, last); + } + + p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_REPRESENTATION_AUDIO, &ctx->name, codec_ctx->audio_codec_id == NGX_RTMP_AUDIO_AAC ? (codec_ctx->aac_sbr ? "40.5" : "40.2") : "6b", @@ -569,13 +1061,11 @@ ngx_rtmp_dash_write_playlist(ngx_rtmp_session_t *s) name, sep, name, sep); - for (i = 0; i < ctx->nfrags; i++) { - f = ngx_rtmp_dash_get_frag(s, i); - p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_TIME, - f->timestamp, f->duration); - } + p = ngx_rtmp_dash_write_segment_timeline(s, ctx, dacf, p, last); - p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_AUDIO_FOOTER); + p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_REPRESENTATION_AUDIO_FOOTER); + + p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_ADAPTATIONSET_AUDIO_FOOTER); n = ngx_write_fd(fd, buffer, p - buffer); } @@ -619,6 +1109,7 @@ ngx_rtmp_dash_write_playlist(ngx_rtmp_session_t *s) } ngx_close_file(fd); + ngx_close_file(fds); if (ngx_rtmp_dash_rename_file(ctx->playlist_bak.data, ctx->playlist.data) == NGX_FILE_ERROR) @@ -629,6 +1120,20 @@ ngx_rtmp_dash_write_playlist(ngx_rtmp_session_t *s) return NGX_ERROR; } + if (ngx_rtmp_dash_rename_file(ctx->segments_bak.data, ctx->segments.data) + == NGX_FILE_ERROR) + { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: rename failed: '%V'->'%V'", + &ctx->segments_bak, &ctx->segments); + return NGX_ERROR; + } + + /* try to write the variant file only once, check the max flag */ + if (ctx->var && ctx->var->args.nelts > 3) { + return ngx_rtmp_dash_write_variant_playlist(s); + } + ngx_memzero(&v, sizeof(v)); ngx_str_set(&(v.module), "dash"); v.playlist.data = ctx->playlist.data; @@ -638,20 +1143,22 @@ ngx_rtmp_dash_write_playlist(ngx_rtmp_session_t *s) static ngx_int_t -ngx_rtmp_dash_write_init_segments(ngx_rtmp_session_t *s) +ngx_rtmp_dash_write_init_segments(ngx_rtmp_session_t *s, ngx_rtmp_cenc_drm_info_t *drmi) { - ngx_fd_t fd; - ngx_int_t rc; - ngx_buf_t b; - ngx_rtmp_dash_ctx_t *ctx; - ngx_rtmp_codec_ctx_t *codec_ctx; + ngx_fd_t fd; + ngx_int_t rc; + ngx_buf_t b; + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_codec_ctx_t *codec_ctx; + ngx_rtmp_dash_app_conf_t *dacf; static u_char buffer[NGX_RTMP_DASH_BUFSIZE]; + dacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_dash_module); ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); - if (ctx == NULL || codec_ctx == NULL) { + if (dacf == NULL || ctx == NULL || codec_ctx == NULL) { return NGX_ERROR; } @@ -673,7 +1180,11 @@ ngx_rtmp_dash_write_init_segments(ngx_rtmp_session_t *s) b.pos = b.last = b.start; ngx_rtmp_mp4_write_ftyp(&b); - ngx_rtmp_mp4_write_moov(s, &b, NGX_RTMP_MP4_VIDEO_TRACK); + if (dacf->cenc) { + ngx_rtmp_mp4_write_moov(s, &b, NGX_RTMP_MP4_EVIDEO_TRACK, drmi); + } else { + ngx_rtmp_mp4_write_moov(s, &b, NGX_RTMP_MP4_VIDEO_TRACK, NULL); + } rc = ngx_write_fd(fd, b.start, (size_t) (b.last - b.start)); if (rc == NGX_ERROR) { @@ -699,7 +1210,11 @@ ngx_rtmp_dash_write_init_segments(ngx_rtmp_session_t *s) b.pos = b.last = b.start; ngx_rtmp_mp4_write_ftyp(&b); - ngx_rtmp_mp4_write_moov(s, &b, NGX_RTMP_MP4_AUDIO_TRACK); + if (dacf->cenc) { + ngx_rtmp_mp4_write_moov(s, &b, NGX_RTMP_MP4_EAUDIO_TRACK, drmi); + } else { + ngx_rtmp_mp4_write_moov(s, &b, NGX_RTMP_MP4_AUDIO_TRACK, NULL); + } rc = ngx_write_fd(fd, b.start, (size_t) (b.last - b.start)); if (rc == NGX_ERROR) { @@ -740,13 +1255,58 @@ ngx_rtmp_dash_close_fragment(ngx_rtmp_session_t *s, ngx_rtmp_dash_track_t *t) b.end = buffer + sizeof(buffer); b.pos = b.last = b.start; + if (ctx->start_cuepoint) { + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "dash : onCuepoint write start emsg : epts='%uD', lpts='%uD', cpts='%uD', "\ + "ecpts='%uD', duration='%uD', prid='%uD'", + t->earliest_pres_time, t->latest_pres_time, ctx->cuepoint_starttime, + ctx->cuepoint_endtime, ctx->cuepoint_duration, ctx->cuepoint_id); + + /* should be ngx_rtmp_mp4_write_emsg(&b, t->earliest_pres_time, + thanks to dashjs + */ + ngx_rtmp_mp4_write_emsg(&b, 0, + ctx->cuepoint_starttime, + ctx->cuepoint_duration, + ctx->cuepoint_id); + + pos = b.last; + b.last = pos; + ctx->start_cuepoint = 0; + ctx->cuepoint_duration = 0; + ctx->end_cuepoint = 1; + + } else if (ctx->end_cuepoint && ctx->cuepoint_endtime >= t->earliest_pres_time + && ctx->cuepoint_endtime <= t->latest_pres_time) { + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "dash : onCuepoint write end emsg : epts='%uD', lpts='%uD', cpts='%uD', "\ + "ecpts='%uD', duration='%uD', prid='%uD'", + t->earliest_pres_time, t->latest_pres_time, ctx->cuepoint_starttime, + ctx->cuepoint_endtime, ctx->cuepoint_duration, ctx->cuepoint_id); + + /* end marker have duration set to zero and prid set to zero */ + ngx_rtmp_mp4_write_emsg(&b, 0, + ctx->cuepoint_endtime, + 0, + 0); + + pos = b.last; + b.last = pos; + ctx->end_cuepoint = 0; + } else if (ctx->end_cuepoint && ctx->cuepoint_endtime < t->earliest_pres_time ) { + /* fallback */ + ctx->end_cuepoint = 0; + } + ngx_rtmp_mp4_write_styp(&b); pos = b.last; b.last += 44; /* leave room for sidx */ - ngx_rtmp_mp4_write_moof(&b, t->earliest_pres_time, t->sample_count, - t->samples, t->sample_mask, t->id); + ngx_rtmp_mp4_write_moof(&b, t->earliest_pres_time, t->type, t->sample_count, + t->samples, t->sample_mask, t->id, t->is_protected); pos1 = b.last; b.last = pos; @@ -850,7 +1410,8 @@ static ngx_int_t ngx_rtmp_dash_open_fragment(ngx_rtmp_session_t *s, ngx_rtmp_dash_track_t *t, ngx_uint_t id, char type) { - ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_dash_app_conf_t *dacf; if (t->opened) { return NGX_OK; @@ -860,6 +1421,7 @@ ngx_rtmp_dash_open_fragment(ngx_rtmp_session_t *s, ngx_rtmp_dash_track_t *t, "dash: open fragment id=%ui, type='%c'", id, type); ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + dacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_dash_module); *ngx_sprintf(ctx->stream.data + ctx->stream.len, "raw.m4%c", type) = 0; @@ -879,6 +1441,17 @@ ngx_rtmp_dash_open_fragment(ngx_rtmp_session_t *s, ngx_rtmp_dash_track_t *t, t->latest_pres_time = 0; t->mdat_size = 0; t->opened = 1; + + if (dacf->cenc) { + + if (ngx_rtmp_cenc_read_hex(dacf->cenc_key, t->key) == NGX_ERROR) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "dash: error cenc key is invalid"); + return NGX_ERROR; + } + + t->is_protected = 1; + } if (type == 'v') { t->sample_mask = NGX_RTMP_MP4_SAMPLE_SIZE| @@ -1022,11 +1595,13 @@ ngx_rtmp_dash_ensure_directory(ngx_rtmp_session_t *s) static ngx_int_t ngx_rtmp_dash_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) { - u_char *p; + u_char *p, *pp; size_t len; ngx_rtmp_dash_ctx_t *ctx; ngx_rtmp_dash_frag_t *f; ngx_rtmp_dash_app_conf_t *dacf; + ngx_rtmp_dash_variant_t *var; + ngx_uint_t n; dacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_dash_module); if (dacf == NULL || !dacf->dash || dacf->path.len == 0) { @@ -1113,6 +1688,53 @@ ngx_rtmp_dash_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) ngx_memcpy(ctx->stream.data, ctx->playlist.data, ctx->stream.len - 1); ctx->stream.data[ctx->stream.len - 1] = (dacf->nested ? '/' : '-'); + if (dacf->variant) { + var = dacf->variant->elts; + for (n = 0; n < dacf->variant->nelts; n++, var++) { + if (ctx->name.len > var->suffix.len && + ngx_memcmp(var->suffix.data, + ctx->name.data + ctx->name.len - var->suffix.len, + var->suffix.len) + == 0) + { + len = (size_t) (ctx->name.len - var->suffix.len); + + ctx->varname.len = len; + ctx->varname.data = ngx_palloc(s->connection->pool, + ctx->varname.len + 1); + pp = ngx_cpymem(ctx->varname.data, + ctx->name.data, len); + + *pp = 0; + + ctx->var = var; + + len = (size_t) (p - ctx->playlist.data); + + ctx->var_playlist.len = len - var->suffix.len + sizeof(".mpd") + -1; + ctx->var_playlist.data = ngx_palloc(s->connection->pool, + ctx->var_playlist.len + 1); + pp = ngx_cpymem(ctx->var_playlist.data, + ctx->playlist.data, len - var->suffix.len); + pp = ngx_cpymem(pp, ".mpd", sizeof(".mpd") - 1); + *pp = 0; + + ctx->var_playlist_bak.len = ctx->var_playlist.len + + sizeof(".bak") - 1; + ctx->var_playlist_bak.data = ngx_palloc(s->connection->pool, + ctx->var_playlist_bak.len + 1); + pp = ngx_cpymem(ctx->var_playlist_bak.data, + ctx->var_playlist.data, + ctx->var_playlist.len); + pp = ngx_cpymem(pp, ".bak", sizeof(".bak") - 1); + *pp = 0; + + break; + } + } + } + if (dacf->nested) { p = ngx_cpymem(p, "/index.mpd", sizeof("/index.mpd") - 1); } else { @@ -1135,15 +1757,57 @@ ngx_rtmp_dash_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) *p = 0; - ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "dash: playlist='%V' playlist_bak='%V' stream_pattern='%V'", - &ctx->playlist, &ctx->playlist_bak, &ctx->stream); + /* segments path */ + + ctx->segments.data = ngx_palloc(s->connection->pool, + ctx->playlist.len - 4 + sizeof(".seg")); + p = ngx_cpymem(ctx->segments.data, ctx->playlist.data, + ctx->playlist.len - 4); + p = ngx_cpymem(p, ".seg", sizeof(".seg") - 1); + + ctx->segments.len = p - ctx->segments.data; + + *p = 0; + + /* segments bak (new segments) path */ + + ctx->segments_bak.data = ngx_palloc(s->connection->pool, + ctx->playlist.len - 4 + sizeof(".sbk")); + p = ngx_cpymem(ctx->segments_bak.data, ctx->playlist.data, + ctx->playlist.len - 4); + p = ngx_cpymem(p, ".sbk", sizeof(".sbk") - 1); + + ctx->segments_bak.len = p - ctx->segments_bak.data; + + *p = 0; + + ngx_log_debug5(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: playlist='%V' playlist_bak='%V' segments='%V' segments_bak='%V' stream_pattern='%V'", + &ctx->playlist, &ctx->playlist_bak, &ctx->segments, &ctx->segments_bak, &ctx->stream); ctx->start_time = *ngx_cached_time; if (ngx_rtmp_dash_ensure_directory(s) != NGX_OK) { return NGX_ERROR; } + + /* drm info */ + if (dacf->cenc) { + if (ngx_rtmp_cenc_read_hex(dacf->cenc_kid, ctx->drm_info.kid) == NGX_ERROR) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "dash: error cenc kid is invalid"); + return NGX_ERROR; + } + if (dacf->wdv) { + ctx->drm_info.wdv = 1; + ctx->drm_info.wdv_data = dacf->wdv_data; + } + if (dacf->mspr) { + ctx->drm_info.mspr = 1; + ctx->drm_info.mspr_data = dacf->mspr_data; + ctx->drm_info.mspr_kid = dacf->mspr_kid; + ctx->drm_info.mspr_pro = dacf->mspr_pro; + } + } next: return next_publish(s, v); @@ -1173,7 +1837,6 @@ ngx_rtmp_dash_close_stream(ngx_rtmp_session_t *s, ngx_rtmp_close_stream_t *v) return next_close_stream(s, v); } - static void ngx_rtmp_dash_update_fragments(ngx_rtmp_session_t *s, ngx_int_t boundary, uint32_t timestamp) @@ -1202,7 +1865,6 @@ ngx_rtmp_dash_update_fragments(ngx_rtmp_session_t *s, ngx_int_t boundary, } else { /* sometimes clients generate slightly unordered frames */ - hit = (-d > 1000); } @@ -1258,11 +1920,11 @@ static ngx_int_t ngx_rtmp_dash_append(ngx_rtmp_session_t *s, ngx_chain_t *in, ngx_rtmp_dash_track_t *t, ngx_int_t key, uint32_t timestamp, uint32_t delay) { - u_char *p; - size_t size, bsize; - ngx_rtmp_mp4_sample_t *smpl; + u_char *p; + size_t size, bsize, csize; + ngx_rtmp_mp4_sample_t *smpl; - static u_char buffer[NGX_RTMP_DASH_BUFSIZE]; + static u_char buffer[NGX_RTMP_DASH_BUFSIZE]; p = buffer; size = 0; @@ -1282,18 +1944,35 @@ ngx_rtmp_dash_append(ngx_rtmp_session_t *s, ngx_chain_t *in, if (t->sample_count == 0) { t->earliest_pres_time = timestamp; + if (t->is_protected) { + ngx_rtmp_cenc_rand_iv(t->iv); + } } t->latest_pres_time = timestamp; if (t->sample_count < NGX_RTMP_DASH_MAX_SAMPLES) { + if (t->is_protected) { + if (t->type == 'v') { + ngx_rtmp_cenc_encrypt_sub_sample(s, t->key, t->iv, buffer, size, &csize); + ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: cenc crypt video sample: count=%ui, key=%ui, size=%ui, csize=%ui", + t->sample_count, key, size, csize); + } else { + ngx_rtmp_cenc_encrypt_full_sample(s, t->key, t->iv, buffer, size); + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: cenc crypt audio sample: count=%ui, key=%ui, size=%ui", + t->sample_count, key, size); + } + } + if (ngx_write_fd(t->fd, buffer, size) == NGX_ERROR) { ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, "dash: " ngx_write_fd_n " failed"); return NGX_ERROR; - } - + } + smpl = &t->samples[t->sample_count]; smpl->delay = delay; @@ -1302,6 +1981,15 @@ ngx_rtmp_dash_append(ngx_rtmp_session_t *s, ngx_chain_t *in, smpl->timestamp = timestamp; smpl->key = (key ? 1 : 0); + if (t->is_protected) { + smpl->is_protected = 1; + ngx_memcpy(smpl->iv, t->iv, NGX_RTMP_CENC_IV_SIZE); + ngx_rtmp_cenc_increment_iv(t->iv); + if (t->type == 'v') { + smpl->clear_size = (uint32_t) csize; + } + } + if (t->sample_count > 0) { smpl = &t->samples[t->sample_count - 1]; smpl->duration = timestamp - smpl->timestamp; @@ -1394,6 +2082,7 @@ ngx_rtmp_dash_video(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, return NGX_ERROR; } + /* check what header it is */ ftype = (in->buf->pos[0] & 0xf0) >> 4; /* skip AVC config */ @@ -1621,6 +2310,7 @@ ngx_rtmp_dash_cleanup_dir(ngx_str_t *ppath, ngx_msec_t playlen) } } + #if (nginx_version >= 1011005) static ngx_msec_t #else @@ -1641,12 +2331,365 @@ ngx_rtmp_dash_cleanup(void *data) #endif } + +static char * +ngx_rtmp_dash_variant(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_rtmp_dash_app_conf_t *dacf = conf; + + ngx_str_t *value, *arg; + ngx_uint_t n; + ngx_rtmp_dash_variant_t *var; + + value = cf->args->elts; + + if (dacf->variant == NULL) { + dacf->variant = ngx_array_create(cf->pool, 1, + sizeof(ngx_rtmp_dash_variant_t)); + if (dacf->variant == NULL) { + return NGX_CONF_ERROR; + } + } + + var = ngx_array_push(dacf->variant); + if (var == NULL) { + return NGX_CONF_ERROR; + } + + ngx_memzero(var, sizeof(ngx_rtmp_dash_variant_t)); + + var->suffix = value[1]; + + if (cf->args->nelts == 2) { + return NGX_CONF_OK; + } + + if (ngx_array_init(&var->args, cf->pool, cf->args->nelts - 2, + sizeof(ngx_str_t)) + != NGX_OK) + { + return NGX_CONF_ERROR; + } + + arg = ngx_array_push_n(&var->args, cf->args->nelts - 2); + if (arg == NULL) { + return NGX_CONF_ERROR; + } + + for (n = 2; n < cf->args->nelts; n++) { + *arg++ = value[n]; + } + + return NGX_CONF_OK; +} + + static ngx_int_t ngx_rtmp_dash_playlist(ngx_rtmp_session_t *s, ngx_rtmp_playlist_t *v) { return next_playlist(s, v); } + +static ngx_int_t +ngx_rtmp_dash_on_cuepoint(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_int_t res; + ngx_rtmp_dash_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + + static struct { + double time; + double duration; + u_char name[128]; + u_char type[128]; + u_char ptype[128]; + } v; + + static ngx_rtmp_amf_elt_t in_pr_elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_string("type"), + v.ptype, sizeof(v.ptype) }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("duration"), + &v.duration, sizeof(v.duration) }, + + }; + + static ngx_rtmp_amf_elt_t in_dt_elts[] = { + + { NGX_RTMP_AMF_NUMBER, + ngx_string("time"), + &v.time, sizeof(v.time) }, + + { NGX_RTMP_AMF_STRING, + ngx_string("name"), + v.name, sizeof(v.name) }, + + { NGX_RTMP_AMF_STRING, + ngx_string("type"), + v.type, sizeof(v.type) }, + + { NGX_RTMP_AMF_OBJECT, + ngx_string("parameters"), + in_pr_elts, sizeof(in_pr_elts) }, + + }; + + static ngx_rtmp_amf_elt_t in_elts[] = { + + { NGX_RTMP_AMF_OBJECT, + ngx_null_string, + in_dt_elts, sizeof(in_dt_elts) }, + + }; + + ngx_memzero(&v, sizeof(v)); + res = ngx_rtmp_receive_amf(s, in, in_elts, + sizeof(in_elts) / sizeof(in_elts[0])); + + if (res == NGX_OK && v.duration > 0) { + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "dash : onCuepoint : ts='%ui', time='%f', name='%s' type='%s' ptype='%s' duration='%f'", + h->timestamp, v.time, v.name, v.type, v.ptype, v.duration); + ctx->start_cuepoint = 1; + ctx->cuepoint_starttime = h->timestamp; + ctx->cuepoint_endtime = h->timestamp+(v.duration*1000); + ctx->cuepoint_duration = v.duration; + ctx->cuepoint_id = 0; + } else { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash : onCuepoint : amf not understood"); + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_dash_on_cuepoint_scte35(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_int_t res; + ngx_rtmp_dash_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + + static struct { + unsigned ooni; + unsigned splice_event_ci; + unsigned splice_imd; + double avail_num; + double avail_expected; + double duration; + double prtime; + double prgid; + double sctype; + double sevtid; + u_char type[128]; + u_char mtype[128]; + } v; + + static ngx_rtmp_amf_elt_t in_pr_elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_string("messageType"), + v.mtype, sizeof(v.mtype) }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("splice_command_type"), + &v.sctype, sizeof(v.sctype) }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("splice_event_id"), + &v.sevtid, sizeof(v.sevtid) }, + + { NGX_RTMP_AMF_BOOLEAN, + ngx_string("splice_event_cancel_indicator"), + &v.splice_event_ci, sizeof(v.splice_event_ci) }, + + { NGX_RTMP_AMF_BOOLEAN, + ngx_string("out_of_network_indicator"), + &v.ooni, sizeof(v.ooni) }, + + { NGX_RTMP_AMF_BOOLEAN, + ngx_string("splice_immediate"), + &v.splice_imd, sizeof(v.splice_imd) }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("pre_roll_time"), + &v.prtime, sizeof(v.prtime) }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("break_duration"), + &v.duration, sizeof(v.duration) }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("unique_program_id"), + &v.prgid, sizeof(v.prgid) }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("avail_num"), + &v.avail_num, sizeof(v.avail_num) }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("avail_expected"), + &v.avail_expected, sizeof(v.avail_expected) }, + + }; + + static ngx_rtmp_amf_elt_t in_dt_elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_string("type"), + v.type, sizeof(v.type) }, + + { NGX_RTMP_AMF_OBJECT, + ngx_string("parameters"), + in_pr_elts, sizeof(in_pr_elts) }, + + }; + + static ngx_rtmp_amf_elt_t in_elts[] = { + + { NGX_RTMP_AMF_OBJECT, + ngx_null_string, + in_dt_elts, sizeof(in_dt_elts) }, + + }; + + ngx_memzero(&v, sizeof(v)); + res = ngx_rtmp_receive_amf(s, in, in_elts, + sizeof(in_elts) / sizeof(in_elts[0])); + + if (res == NGX_OK && v.duration > 0) { + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "dash : onCuepoint_scte35 : ts='%ui', type='%s', mtype='%s', sctype='%f', "\ + "scid='%f', prgid='%f', duration='%f', avail_num='%f', avail_expected='%f'", + h->timestamp, v.type, v.mtype, v.sctype, + v.sevtid, v.prgid, v.duration, v.avail_num, v.avail_expected); + ctx->start_cuepoint = 1; + ctx->cuepoint_starttime = h->timestamp; + ctx->cuepoint_endtime = h->timestamp + v.duration; + ctx->cuepoint_duration = v.duration; + ctx->cuepoint_id = v.prgid; + } else { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash : onCuepoint_scte35 : amf not understood"); + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_dash_on_cuepoint_cont_scte35(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_int_t res; + + static struct { + double segupid; + double segduration; + double segrduration; + u_char type[128]; + u_char mtype[128]; + } v; + + static ngx_rtmp_amf_elt_t in_pr_elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_string("messageType"), + v.mtype, sizeof(v.mtype) }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("segmentation_upid"), + &v.segupid, sizeof(v.segupid) }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("segmentation_duration"), + &v.segduration, sizeof(v.segduration) }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("segmentation_remaining_duration"), + &v.segrduration, sizeof(v.segrduration) }, + + }; + + static ngx_rtmp_amf_elt_t in_dt_elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_string("type"), + v.type, sizeof(v.type) }, + + { NGX_RTMP_AMF_OBJECT, + ngx_string("parameters"), + in_pr_elts, sizeof(in_pr_elts) }, + + }; + + static ngx_rtmp_amf_elt_t in_elts[] = { + + { NGX_RTMP_AMF_OBJECT, + ngx_null_string, + in_dt_elts, sizeof(in_dt_elts) }, + + }; + + ngx_memzero(&v, sizeof(v)); + res = ngx_rtmp_receive_amf(s, in, in_elts, + sizeof(in_elts) / sizeof(in_elts[0])); + + if (res == NGX_OK && v.segrduration > 0) { + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "dash : onCuepoint_cont_scte35 : ts='%ui', type='%s', "\ + "segupid='%f', segduration='%f', segrduration='%f'", + h->timestamp, v.mtype, v.segupid, v.segduration, v.segrduration); + } else { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash : onCuepoint_cont_scte35 : amf not understood"); + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_dash_ad_markers(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_rtmp_dash_app_conf_t *dacf; + ngx_rtmp_dash_ctx_t *ctx; + + dacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_dash_module); + if (dacf == NULL || !dacf->dash || dacf->path.len == 0) { + return NGX_OK; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + + switch (dacf->ad_markers) { + case NGX_RTMP_DASH_AD_MARKERS_ON_CUEPOINT: + return ngx_rtmp_dash_on_cuepoint(s, h, in); + break; + case NGX_RTMP_DASH_AD_MARKERS_ON_CUEPOINT_SCTE35: + if (ctx->end_cuepoint) + return ngx_rtmp_dash_on_cuepoint_cont_scte35(s, h, in); + else + return ngx_rtmp_dash_on_cuepoint_scte35(s, h, in); + break; + default: + return NGX_OK; + } + + return NGX_OK; +} + + static void * ngx_rtmp_dash_create_app_conf(ngx_conf_t *cf) { @@ -1662,7 +2705,12 @@ ngx_rtmp_dash_create_app_conf(ngx_conf_t *cf) conf->playlen = NGX_CONF_UNSET_MSEC; conf->cleanup = NGX_CONF_UNSET; conf->nested = NGX_CONF_UNSET; + conf->repetition = NGX_CONF_UNSET; + conf->cenc = NGX_CONF_UNSET; + conf->wdv = NGX_CONF_UNSET; + conf->mspr = NGX_CONF_UNSET; conf->clock_compensation = NGX_CONF_UNSET; + conf->ad_markers = NGX_CONF_UNSET; return conf; } @@ -1680,9 +2728,21 @@ ngx_rtmp_dash_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_msec_value(conf->playlen, prev->playlen, 30000); ngx_conf_merge_value(conf->cleanup, prev->cleanup, 1); ngx_conf_merge_value(conf->nested, prev->nested, 0); + ngx_conf_merge_value(conf->repetition, prev->repetition, 0); + ngx_conf_merge_value(conf->cenc, prev->cenc, 0); + ngx_conf_merge_str_value(conf->cenc_key, prev->cenc_key, ""); + ngx_conf_merge_str_value(conf->cenc_kid, prev->cenc_kid, ""); + ngx_conf_merge_value(conf->wdv, prev->wdv, 0); + ngx_conf_merge_str_value(conf->wdv_data, prev->wdv_data, ""); + ngx_conf_merge_value(conf->mspr, prev->mspr, 0); + ngx_conf_merge_str_value(conf->mspr_data, prev->mspr_data, ""); + ngx_conf_merge_str_value(conf->mspr_kid, prev->mspr_kid, ""); + ngx_conf_merge_str_value(conf->mspr_pro, prev->mspr_pro, ""); ngx_conf_merge_uint_value(conf->clock_compensation, prev->clock_compensation, NGX_RTMP_DASH_CLOCK_COMPENSATION_OFF); ngx_conf_merge_str_value(conf->clock_helper_uri, prev->clock_helper_uri, ""); + ngx_conf_merge_uint_value(conf->ad_markers, prev->ad_markers, + NGX_RTMP_DASH_AD_MARKERS_OFF); if (conf->fraglen) { conf->winfrags = conf->playlen / conf->fraglen; @@ -1730,6 +2790,7 @@ ngx_rtmp_dash_postconfiguration(ngx_conf_t *cf) { ngx_rtmp_handler_pt *h; ngx_rtmp_core_main_conf_t *cmcf; + ngx_rtmp_amf_handler_t *ch; cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module); @@ -1754,5 +2815,9 @@ ngx_rtmp_dash_postconfiguration(ngx_conf_t *cf) next_playlist = ngx_rtmp_playlist; ngx_rtmp_playlist = ngx_rtmp_dash_playlist; + ch = ngx_array_push(&cmcf->amf); + ngx_str_set(&ch->name, "onCuePoint"); + ch->handler = ngx_rtmp_dash_ad_markers; + return NGX_OK; } diff --git a/dash/ngx_rtmp_dash_templates.h b/dash/ngx_rtmp_dash_templates.h new file mode 100644 index 000000000..cad90d466 --- /dev/null +++ b/dash/ngx_rtmp_dash_templates.h @@ -0,0 +1,193 @@ +#ifndef _NGX_RTMP_DASH_TEMPLATES +#define _NGX_RTMP_DASH_TEMPLATES + + +#define NGX_RTMP_DASH_MANIFEST_HEADER \ + "\n" \ + "\n" + + +#define NGX_RTMP_DASH_MANIFEST_PERIOD \ + " \n" + + +#define NGX_RTMP_DASH_MANIFEST_ADAPTATIONSET_VIDEO \ + " \n" + + +#define NGX_RTMP_DASH_INBAND_EVENT \ + " \n" + + +#define NGX_RTMP_DASH_MANIFEST_CONTENT_PROTECTION_CENC \ + " \n" + + +#define NGX_RTMP_DASH_MANIFEST_CONTENT_PROTECTION_PSSH_CENC \ + " \n" \ + " %V\n" \ + " \n" + + +#define NGX_RTMP_DASH_MANIFEST_CONTENT_PROTECTION_PSSH_WDV \ + " \n" \ + " %V\n" \ + " \n" + + +#define NGX_RTMP_DASH_MANIFEST_CONTENT_PROTECTION_PSSH_MSPR \ + " \n" \ + " %V\n" \ + " 1\n" \ + " 8\n" \ + " %V\n" \ + " %V\n" \ + " \n" + + +#define NGX_RTMP_DASH_MANIFEST_REPRESENTATION_VIDEO \ + " \n" \ + " \n" \ + " \n" + + +#define NGX_RTMP_DASH_MANIFEST_REPRESENTATION_VARIANT_VIDEO \ + " \n" + + +#define NGX_RTMP_DASH_MANIFEST_SEGMENTTPL_VARIANT_VIDEO \ + " \n" \ + " \n" + + +#define NGX_RTMP_DASH_MANIFEST_REPRESENTATION_VIDEO_FOOTER \ + " \n" \ + " \n" \ + " \n" + + +#define NGX_RTMP_DASH_MANIFEST_ADAPTATIONSET_VIDEO_FOOTER \ + " \n" + + +#define NGX_RTMP_DASH_MANIFEST_TIME \ + " \n" + + +#define NGX_RTMP_DASH_MANIFEST_TIME_WITH_REPETITION \ + " \n" + + +#define NGX_RTMP_DASH_MANIFEST_ADAPTATIONSET_AUDIO \ + " \n" \ + " \n" + + +#define NGX_RTMP_DASH_MANIFEST_REPRESENTATION_AUDIO \ + " \n" \ + " \n" \ + " \n" + + +#define NGX_RTMP_DASH_MANIFEST_REPRESENTATION_AUDIO_FOOTER \ + " \n" \ + " \n" \ + " \n" + + +#define NGX_RTMP_DASH_MANIFEST_ADAPTATIONSET_AUDIO_FOOTER \ + " \n" + + +#define NGX_RTMP_DASH_PERIOD_FOOTER \ + " \n" + + +#define NGX_RTMP_DASH_MANIFEST_CLOCK \ + " \n" + + +#define NGX_RTMP_DASH_MANIFEST_FOOTER \ + "\n" + + +#endif diff --git a/dash/ngx_rtmp_mp4.c b/dash/ngx_rtmp_mp4.c index dd680c9cd..e1cf0fbc4 100644 --- a/dash/ngx_rtmp_mp4.c +++ b/dash/ngx_rtmp_mp4.c @@ -316,20 +316,27 @@ ngx_rtmp_mp4_write_tkhd(ngx_rtmp_session_t *s, ngx_buf_t *b, ngx_rtmp_mp4_field_32(b, 0); /* reserved */ - ngx_rtmp_mp4_field_16(b, ttype == NGX_RTMP_MP4_VIDEO_TRACK ? 0 : 0x0100); + if (ttype == NGX_RTMP_MP4_VIDEO_TRACK || + ttype == NGX_RTMP_MP4_EVIDEO_TRACK) { + ngx_rtmp_mp4_field_16(b, 0); + } else { + /* reserved */ + ngx_rtmp_mp4_field_16(b, 0x0100); + } /* reserved */ ngx_rtmp_mp4_field_16(b, 0); ngx_rtmp_mp4_write_matrix(b, 1, 0, 0, 1, 0, 0); - if (ttype == NGX_RTMP_MP4_VIDEO_TRACK) { + if (ttype == NGX_RTMP_MP4_VIDEO_TRACK || + ttype == NGX_RTMP_MP4_EVIDEO_TRACK) { ngx_rtmp_mp4_field_32(b, (uint32_t) codec_ctx->width << 16); ngx_rtmp_mp4_field_32(b, (uint32_t) codec_ctx->height << 16); } else { ngx_rtmp_mp4_field_32(b, 0); ngx_rtmp_mp4_field_32(b, 0); - } + } ngx_rtmp_mp4_update_box_size(b, pos); @@ -384,7 +391,8 @@ ngx_rtmp_mp4_write_hdlr(ngx_buf_t *b, ngx_rtmp_mp4_track_type_t ttype) /* pre defined */ ngx_rtmp_mp4_field_32(b, 0); - if (ttype == NGX_RTMP_MP4_VIDEO_TRACK) { + if (ttype == NGX_RTMP_MP4_VIDEO_TRACK || + ttype == NGX_RTMP_MP4_EVIDEO_TRACK) { ngx_rtmp_mp4_box(b, "vide"); } else { ngx_rtmp_mp4_box(b, "soun"); @@ -395,7 +403,8 @@ ngx_rtmp_mp4_write_hdlr(ngx_buf_t *b, ngx_rtmp_mp4_track_type_t ttype) ngx_rtmp_mp4_field_32(b, 0); ngx_rtmp_mp4_field_32(b, 0); - if (ttype == NGX_RTMP_MP4_VIDEO_TRACK) { + if (ttype == NGX_RTMP_MP4_VIDEO_TRACK || + ttype == NGX_RTMP_MP4_EVIDEO_TRACK) { /* video handler string, NULL-terminated */ ngx_rtmp_mp4_data(b, "VideoHandler", sizeof("VideoHandler")); } else { @@ -489,6 +498,216 @@ ngx_rtmp_mp4_write_dinf(ngx_buf_t *b) } +static ngx_int_t +ngx_rtmp_mp4_write_frma(ngx_buf_t *b, const char format[4]) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "frma"); + + /* original_format */ + ngx_rtmp_mp4_box(b, format); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_schm(ngx_buf_t *b) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "schm"); + + /* version and flags */ + ngx_rtmp_mp4_field_32(b, 0); + + /* scheme_type */ + ngx_rtmp_mp4_box(b, "cenc"); + + /* scheme_version */ + ngx_rtmp_mp4_field_32(b, 65536); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_pssh_cenc(ngx_buf_t *b, + ngx_rtmp_cenc_drm_info_t *drmi) +{ + u_char *pos; + u_char sid[] = { + 0x10, 0x77, 0xef, 0xec, 0xc0, 0xb2, 0x4d, 0x02, + 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b + }; + + pos = ngx_rtmp_mp4_start_box(b, "pssh"); + + /* version and flags */ + ngx_rtmp_mp4_field_32(b, 0x01000000); + + /* system ID : org.w3.clearkey */ + ngx_rtmp_mp4_data(b, sid, NGX_RTMP_CENC_KEY_SIZE); + + /* kid count */ + ngx_rtmp_mp4_field_32(b, 1); + + /* default KID */ + ngx_rtmp_mp4_data(b, drmi->kid, NGX_RTMP_CENC_KEY_SIZE); + + /* data size */ + ngx_rtmp_mp4_field_32(b, 0); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_pssh_wdv(ngx_buf_t *b, + ngx_rtmp_cenc_drm_info_t *drmi) +{ + ngx_str_t dest, src; + u_char *pos; + u_char buf[NGX_RTMP_CENC_MAX_PSSH_SIZE]; + + u_char sid[] = { + 0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, + 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed + }; + + pos = ngx_rtmp_mp4_start_box(b, "pssh"); + + /* assuming v0 pssh for widevine */ + ngx_rtmp_mp4_field_32(b, 0); + + /* system ID : com.widevine.alpha */ + ngx_rtmp_mp4_data(b, sid, NGX_RTMP_CENC_KEY_SIZE); + + /* decode base64 wdv_data */ + dest.data = buf; + src.len = ngx_base64_decoded_length(drmi->wdv_data.len) - 32; + ngx_decode_base64(&dest, &drmi->wdv_data); + + /* data size */ + ngx_rtmp_mp4_field_32(b, src.len); + + /* data */ + ngx_rtmp_mp4_data(b, dest.data + 32, src.len); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_pssh_mspr(ngx_buf_t *b, + ngx_rtmp_cenc_drm_info_t *drmi) +{ + ngx_str_t dest, src; + u_char *pos; + u_char buf[NGX_RTMP_CENC_MAX_PSSH_SIZE]; + + u_char sid[] = { + 0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86, + 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95 + }; + + pos = ngx_rtmp_mp4_start_box(b, "pssh"); + + /* assuming v0 pssh for playready */ + ngx_rtmp_mp4_field_32(b, 0); + + /* system ID : com.microsoft.playready */ + ngx_rtmp_mp4_data(b, sid, NGX_RTMP_CENC_KEY_SIZE); + + /* decode base64 mspr_data */ + dest.data = buf; + src.len = ngx_base64_decoded_length(drmi->mspr_data.len) - 32; + ngx_decode_base64(&dest, &drmi->mspr_data); + + /* data size */ + ngx_rtmp_mp4_field_32(b, src.len); + + /* data */ + ngx_rtmp_mp4_data(b, dest.data + 32, src.len); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + + +static ngx_int_t +ngx_rtmp_mp4_write_tenc(ngx_buf_t *b, ngx_rtmp_cenc_drm_info_t *drmi) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "tenc"); + + /* version and flags */ + ngx_rtmp_mp4_field_32(b, 0); + + /* reserved */ + ngx_rtmp_mp4_field_8(b, 0); + ngx_rtmp_mp4_field_8(b, 0); + + /* default isProtected */ + ngx_rtmp_mp4_field_8(b, 1); + + /* default per_sample_iv_size */ + ngx_rtmp_mp4_field_8(b, NGX_RTMP_CENC_IV_SIZE); + + /* default KID */ + ngx_rtmp_mp4_data(b, drmi->kid, NGX_RTMP_CENC_KEY_SIZE); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_schi(ngx_buf_t *b, ngx_rtmp_cenc_drm_info_t *drmi) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "schi"); + + ngx_rtmp_mp4_write_tenc(b, drmi); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_sinf(ngx_buf_t *b, + const char format[4], ngx_rtmp_cenc_drm_info_t *drmi) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "sinf"); + + ngx_rtmp_mp4_write_frma(b, format); + ngx_rtmp_mp4_write_schm(b); + ngx_rtmp_mp4_write_schi(b, drmi); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + static ngx_int_t ngx_rtmp_mp4_write_avcc(ngx_rtmp_session_t *s, ngx_buf_t *b) { @@ -597,6 +816,71 @@ ngx_rtmp_mp4_write_video(ngx_rtmp_session_t *s, ngx_buf_t *b) } +static ngx_int_t +ngx_rtmp_mp4_write_evideo(ngx_rtmp_session_t *s, + ngx_buf_t *b, ngx_rtmp_cenc_drm_info_t *drmi) +{ + u_char *pos; + ngx_rtmp_codec_ctx_t *codec_ctx; + + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + pos = ngx_rtmp_mp4_start_box(b, "encv"); + + /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_16(b, 0); + + /* data reference index */ + ngx_rtmp_mp4_field_16(b, 1); + + /* codec stream version & revision */ + ngx_rtmp_mp4_field_16(b, 0); + ngx_rtmp_mp4_field_16(b, 0); + + /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + + /* width & height */ + ngx_rtmp_mp4_field_16(b, (uint16_t) codec_ctx->width); + ngx_rtmp_mp4_field_16(b, (uint16_t) codec_ctx->height); + + /* horizontal & vertical resolutions 72 dpi */ + ngx_rtmp_mp4_field_32(b, 0x00480000); + ngx_rtmp_mp4_field_32(b, 0x00480000); + + /* data size */ + ngx_rtmp_mp4_field_32(b, 0); + + /* frame count */ + ngx_rtmp_mp4_field_16(b, 1); + + /* compressor name */ + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + + /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_16(b, 0x18); + ngx_rtmp_mp4_field_16(b, 0xffff); + + ngx_rtmp_mp4_write_avcc(s, b); + + ngx_rtmp_mp4_write_sinf(b, "avc1", drmi); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + static ngx_int_t ngx_rtmp_mp4_write_esds(ngx_rtmp_session_t *s, ngx_buf_t *b) { @@ -727,9 +1011,63 @@ ngx_rtmp_mp4_write_audio(ngx_rtmp_session_t *s, ngx_buf_t *b) } +static ngx_int_t +ngx_rtmp_mp4_write_eaudio(ngx_rtmp_session_t *s, + ngx_buf_t *b, ngx_rtmp_cenc_drm_info_t *drmi) +{ + u_char *pos; + ngx_rtmp_codec_ctx_t *codec_ctx; + + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + pos = ngx_rtmp_mp4_start_box(b, "enca"); + + /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_16(b, 0); + + /* data reference index */ + ngx_rtmp_mp4_field_16(b, 1); + + /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + + /* channel count */ + ngx_rtmp_mp4_field_16(b, (uint16_t) codec_ctx->audio_channels); + + /* sample size */ + ngx_rtmp_mp4_field_16(b, (uint16_t) (codec_ctx->sample_size * 8)); + + /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); + + /* time scale */ + ngx_rtmp_mp4_field_16(b, 1000); + + /* sample rate */ + ngx_rtmp_mp4_field_16(b, (uint16_t) codec_ctx->sample_rate); + + ngx_rtmp_mp4_write_esds(s, b); +#if 0 + /* tag size*/ + ngx_rtmp_mp4_field_32(b, 8); + + /* null tag */ + ngx_rtmp_mp4_field_32(b, 0); +#endif + + ngx_rtmp_mp4_write_sinf(b, "mp4a", drmi); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + static ngx_int_t ngx_rtmp_mp4_write_stsd(ngx_rtmp_session_t *s, ngx_buf_t *b, - ngx_rtmp_mp4_track_type_t ttype) + ngx_rtmp_mp4_track_type_t ttype, ngx_rtmp_cenc_drm_info_t *drmi) { u_char *pos; @@ -741,10 +1079,14 @@ ngx_rtmp_mp4_write_stsd(ngx_rtmp_session_t *s, ngx_buf_t *b, /* entry count */ ngx_rtmp_mp4_field_32(b, 1); - if (ttype == NGX_RTMP_MP4_VIDEO_TRACK) { + if (ttype == NGX_RTMP_MP4_VIDEO_TRACK) { ngx_rtmp_mp4_write_video(s, b); - } else { + } else if (ttype == NGX_RTMP_MP4_EVIDEO_TRACK){ + ngx_rtmp_mp4_write_evideo(s, b, drmi); + } else if (ttype == NGX_RTMP_MP4_AUDIO_TRACK){ ngx_rtmp_mp4_write_audio(s, b); + } else if (ttype == NGX_RTMP_MP4_EAUDIO_TRACK){ + ngx_rtmp_mp4_write_eaudio(s, b, drmi); } ngx_rtmp_mp4_update_box_size(b, pos); @@ -820,13 +1162,13 @@ ngx_rtmp_mp4_write_stco(ngx_buf_t *b) static ngx_int_t ngx_rtmp_mp4_write_stbl(ngx_rtmp_session_t *s, ngx_buf_t *b, - ngx_rtmp_mp4_track_type_t ttype) + ngx_rtmp_mp4_track_type_t ttype, ngx_rtmp_cenc_drm_info_t *drmi) { u_char *pos; pos = ngx_rtmp_mp4_start_box(b, "stbl"); - ngx_rtmp_mp4_write_stsd(s, b, ttype); + ngx_rtmp_mp4_write_stsd(s, b, ttype, drmi); ngx_rtmp_mp4_write_stts(b); ngx_rtmp_mp4_write_stsc(b); ngx_rtmp_mp4_write_stsz(b); @@ -840,20 +1182,21 @@ ngx_rtmp_mp4_write_stbl(ngx_rtmp_session_t *s, ngx_buf_t *b, static ngx_int_t ngx_rtmp_mp4_write_minf(ngx_rtmp_session_t *s, ngx_buf_t *b, - ngx_rtmp_mp4_track_type_t ttype) + ngx_rtmp_mp4_track_type_t ttype, ngx_rtmp_cenc_drm_info_t *drmi) { u_char *pos; pos = ngx_rtmp_mp4_start_box(b, "minf"); - if (ttype == NGX_RTMP_MP4_VIDEO_TRACK) { + if (ttype == NGX_RTMP_MP4_VIDEO_TRACK || + ttype == NGX_RTMP_MP4_EVIDEO_TRACK) { ngx_rtmp_mp4_write_vmhd(b); } else { ngx_rtmp_mp4_write_smhd(b); } ngx_rtmp_mp4_write_dinf(b); - ngx_rtmp_mp4_write_stbl(s, b, ttype); + ngx_rtmp_mp4_write_stbl(s, b, ttype, drmi); ngx_rtmp_mp4_update_box_size(b, pos); @@ -863,7 +1206,7 @@ ngx_rtmp_mp4_write_minf(ngx_rtmp_session_t *s, ngx_buf_t *b, static ngx_int_t ngx_rtmp_mp4_write_mdia(ngx_rtmp_session_t *s, ngx_buf_t *b, - ngx_rtmp_mp4_track_type_t ttype) + ngx_rtmp_mp4_track_type_t ttype, ngx_rtmp_cenc_drm_info_t *drmi) { u_char *pos; @@ -871,7 +1214,7 @@ ngx_rtmp_mp4_write_mdia(ngx_rtmp_session_t *s, ngx_buf_t *b, ngx_rtmp_mp4_write_mdhd(b); ngx_rtmp_mp4_write_hdlr(b, ttype); - ngx_rtmp_mp4_write_minf(s, b, ttype); + ngx_rtmp_mp4_write_minf(s, b, ttype, drmi); ngx_rtmp_mp4_update_box_size(b, pos); @@ -880,14 +1223,14 @@ ngx_rtmp_mp4_write_mdia(ngx_rtmp_session_t *s, ngx_buf_t *b, static ngx_int_t ngx_rtmp_mp4_write_trak(ngx_rtmp_session_t *s, ngx_buf_t *b, - ngx_rtmp_mp4_track_type_t ttype) + ngx_rtmp_mp4_track_type_t ttype, ngx_rtmp_cenc_drm_info_t *drmi) { u_char *pos; pos = ngx_rtmp_mp4_start_box(b, "trak"); ngx_rtmp_mp4_write_tkhd(s, b, ttype); - ngx_rtmp_mp4_write_mdia(s, b, ttype); + ngx_rtmp_mp4_write_mdia(s, b, ttype, drmi); ngx_rtmp_mp4_update_box_size(b, pos); @@ -932,7 +1275,7 @@ ngx_rtmp_mp4_write_mvex(ngx_buf_t *b) ngx_int_t ngx_rtmp_mp4_write_moov(ngx_rtmp_session_t *s, ngx_buf_t *b, - ngx_rtmp_mp4_track_type_t ttype) + ngx_rtmp_mp4_track_type_t ttype, ngx_rtmp_cenc_drm_info_t *drmi) { u_char *pos; @@ -940,7 +1283,18 @@ ngx_rtmp_mp4_write_moov(ngx_rtmp_session_t *s, ngx_buf_t *b, ngx_rtmp_mp4_write_mvhd(b); ngx_rtmp_mp4_write_mvex(b); - ngx_rtmp_mp4_write_trak(s, b, ttype); + ngx_rtmp_mp4_write_trak(s, b, ttype, drmi); + + if (ttype == NGX_RTMP_MP4_EVIDEO_TRACK || + ttype == NGX_RTMP_MP4_EAUDIO_TRACK) { + ngx_rtmp_mp4_write_pssh_cenc(b, drmi); + if (drmi->wdv) { + ngx_rtmp_mp4_write_pssh_wdv(b, drmi); + } + if (drmi->mspr) { + ngx_rtmp_mp4_write_pssh_mspr(b, drmi); + } + } ngx_rtmp_mp4_update_box_size(b, pos); @@ -985,8 +1339,9 @@ ngx_rtmp_mp4_write_tfdt(ngx_buf_t *b, uint32_t earliest_pres_time) static ngx_int_t -ngx_rtmp_mp4_write_trun(ngx_buf_t *b, uint32_t sample_count, - ngx_rtmp_mp4_sample_t *samples, ngx_uint_t sample_mask, u_char *moof_pos) +ngx_rtmp_mp4_write_trun(ngx_buf_t *b, char type, + uint32_t sample_count, ngx_rtmp_mp4_sample_t *samples, + ngx_uint_t sample_mask, u_char *moof_pos, ngx_flag_t is_protected) { u_char *pos; uint32_t i, offset, nitems, flags; @@ -1018,7 +1373,20 @@ ngx_rtmp_mp4_write_trun(ngx_buf_t *b, uint32_t sample_count, flags |= 0x000800; } - offset = (pos - moof_pos) + 20 + (sample_count * nitems * 4) + 8; + if (is_protected) { + /* if cenc is enabled we neeed to add + * saiz saiz senc size to the data offset */ + offset = (pos - moof_pos) + 20 + (sample_count * nitems * 4); + if (type == 'v') { + /* video use sub sample senc */ + offset += 17 + 20 + 16 + (sample_count * (NGX_RTMP_CENC_IV_SIZE + 8)) + 8; + } else { + /* audio use full sample senc */ + offset += 17 + 20 + 16 + (sample_count * NGX_RTMP_CENC_IV_SIZE) + 8; + } + } else { + offset = (pos - moof_pos) + 20 + (sample_count * nitems * 4) + 8; + } ngx_rtmp_mp4_field_32(b, flags); ngx_rtmp_mp4_field_32(b, sample_count); @@ -1049,10 +1417,109 @@ ngx_rtmp_mp4_write_trun(ngx_buf_t *b, uint32_t sample_count, } +static ngx_int_t +ngx_rtmp_mp4_write_saiz(ngx_buf_t *b, char type, uint32_t sample_count) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "saiz"); + + /* version & flag */ + ngx_rtmp_mp4_field_32(b, 0); + + /* defaut sample info size */ + if (type == 'v') { + /* sub sample */ + ngx_rtmp_mp4_field_8(b, NGX_RTMP_CENC_IV_SIZE + 8); + } else { + /* full sample */ + ngx_rtmp_mp4_field_8(b, NGX_RTMP_CENC_IV_SIZE); + } + + /* sample count */ + ngx_rtmp_mp4_field_32(b, sample_count); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_saio(ngx_buf_t *b, u_char *moof_pos) +{ + u_char *pos; + uint32_t offset; + + pos = ngx_rtmp_mp4_start_box(b, "saio"); + + /* version & flag */ + ngx_rtmp_mp4_field_32(b, 0); + + /* entry count */ + ngx_rtmp_mp4_field_32(b, 1); + + /* entry 0 is offset to the first IV in senc box */ + offset = (pos - moof_pos) + 20 + 16; + ngx_rtmp_mp4_field_32(b, offset); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_senc(ngx_buf_t *b, char type, + uint32_t sample_count, ngx_rtmp_mp4_sample_t *samples) +{ + u_char *pos; + uint32_t i; + + pos = ngx_rtmp_mp4_start_box(b, "senc"); + + /* version & flag */ + if (type == 'v') { + /* video use sub_sample flag 0x02 */ + ngx_rtmp_mp4_field_32(b, 0x02); + } else { + /* audio use full_sample flag 0x00 */ + ngx_rtmp_mp4_field_32(b, 0); + } + + /* sample count */ + ngx_rtmp_mp4_field_32(b, sample_count); + + for (i = 0; i < sample_count; i++, samples++) { + + /* IV per sample */ + ngx_rtmp_mp4_data(b, samples->iv, NGX_RTMP_CENC_IV_SIZE); + + /* subsample informations */ + if (type == 'v') { + + /* sub sample count */ + ngx_rtmp_mp4_field_16(b, 1); + + /* sub sample clear data size */ + ngx_rtmp_mp4_field_16(b, samples->clear_size); + + /* sub sample protected data size */ + ngx_rtmp_mp4_field_32(b, samples->size - samples->clear_size); + + } + } + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + static ngx_int_t ngx_rtmp_mp4_write_traf(ngx_buf_t *b, uint32_t earliest_pres_time, - uint32_t sample_count, ngx_rtmp_mp4_sample_t *samples, - ngx_uint_t sample_mask, u_char *moof_pos) + char type, uint32_t sample_count, ngx_rtmp_mp4_sample_t *samples, + ngx_uint_t sample_mask, u_char *moof_pos, ngx_flag_t is_protected) { u_char *pos; @@ -1060,7 +1527,14 @@ ngx_rtmp_mp4_write_traf(ngx_buf_t *b, uint32_t earliest_pres_time, ngx_rtmp_mp4_write_tfhd(b); ngx_rtmp_mp4_write_tfdt(b, earliest_pres_time); - ngx_rtmp_mp4_write_trun(b, sample_count, samples, sample_mask, moof_pos); + ngx_rtmp_mp4_write_trun(b, type, sample_count, samples, sample_mask, + moof_pos, is_protected); + + if (is_protected) { + ngx_rtmp_mp4_write_saiz(b, type, sample_count); + ngx_rtmp_mp4_write_saio(b, moof_pos); + ngx_rtmp_mp4_write_senc(b, type, sample_count, samples); + } ngx_rtmp_mp4_update_box_size(b, pos); @@ -1139,16 +1613,16 @@ ngx_rtmp_mp4_write_sidx(ngx_buf_t *b, ngx_uint_t reference_size, ngx_int_t ngx_rtmp_mp4_write_moof(ngx_buf_t *b, uint32_t earliest_pres_time, - uint32_t sample_count, ngx_rtmp_mp4_sample_t *samples, - ngx_uint_t sample_mask, uint32_t index) + char type, uint32_t sample_count, ngx_rtmp_mp4_sample_t *samples, + ngx_uint_t sample_mask, uint32_t index, ngx_flag_t is_protected) { u_char *pos; pos = ngx_rtmp_mp4_start_box(b, "moof"); ngx_rtmp_mp4_write_mfhd(b, index); - ngx_rtmp_mp4_write_traf(b, earliest_pres_time, sample_count, samples, - sample_mask, pos); + ngx_rtmp_mp4_write_traf(b, earliest_pres_time, type, sample_count, + samples, sample_mask, pos, is_protected); ngx_rtmp_mp4_update_box_size(b, pos); @@ -1156,7 +1630,7 @@ ngx_rtmp_mp4_write_moof(ngx_buf_t *b, uint32_t earliest_pres_time, } -ngx_uint_t +ngx_int_t ngx_rtmp_mp4_write_mdat(ngx_buf_t *b, ngx_uint_t size) { ngx_rtmp_mp4_field_32(b, size); @@ -1165,3 +1639,53 @@ ngx_rtmp_mp4_write_mdat(ngx_buf_t *b, ngx_uint_t size) return NGX_OK; } + +ngx_int_t +ngx_rtmp_mp4_write_emsg(ngx_buf_t *b, + uint32_t earliest_pres_time, uint32_t cuepoint_time, uint32_t duration_time, uint32_t id) +{ + u_char *pos; + uint32_t delta_time; + uint32_t timescale = 1000; + + delta_time = cuepoint_time - earliest_pres_time; + + pos = ngx_rtmp_mp4_start_box(b, "emsg"); + + /* version & flag */ + ngx_rtmp_mp4_field_32(b, 0); + + /* scheme_id_uri */ + ngx_rtmp_mp4_data(b, "urn:scte:scte35:2013:xml", sizeof("urn:scte:scte35:2013:xml")); + + /* value */ + ngx_rtmp_mp4_data(b, "1", sizeof("1")); + + /* timescale */ + ngx_rtmp_mp4_field_32(b, timescale); + + /* presentation_time_delta */ + ngx_rtmp_mp4_field_32(b, delta_time); + + /* duration */ + ngx_rtmp_mp4_field_32(b, duration_time); + + /* id */ + ngx_rtmp_mp4_field_32(b, id); + +#define SCTE_EVENT "\ + \ + \ + \ +" + + /* data */ + ngx_rtmp_mp4_data(b, SCTE_EVENT, sizeof(SCTE_EVENT)); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; + +} + diff --git a/dash/ngx_rtmp_mp4.h b/dash/ngx_rtmp_mp4.h index 697b6c87c..455cd1040 100644 --- a/dash/ngx_rtmp_mp4.h +++ b/dash/ngx_rtmp_mp4.h @@ -7,6 +7,7 @@ #include #include #include +#include "ngx_rtmp_cenc.h" #define NGX_RTMP_MP4_SAMPLE_SIZE 0x01 @@ -17,10 +18,13 @@ typedef struct { uint32_t size; + uint32_t clear_size; uint32_t duration; uint32_t delay; uint32_t timestamp; unsigned key:1; + unsigned is_protected:1; + u_char iv[NGX_RTMP_CENC_IV_SIZE]; } ngx_rtmp_mp4_sample_t; @@ -32,21 +36,24 @@ typedef enum { typedef enum { NGX_RTMP_MP4_VIDEO_TRACK, - NGX_RTMP_MP4_AUDIO_TRACK + NGX_RTMP_MP4_AUDIO_TRACK, + NGX_RTMP_MP4_EVIDEO_TRACK, + NGX_RTMP_MP4_EAUDIO_TRACK } ngx_rtmp_mp4_track_type_t; ngx_int_t ngx_rtmp_mp4_write_ftyp(ngx_buf_t *b); ngx_int_t ngx_rtmp_mp4_write_styp(ngx_buf_t *b); ngx_int_t ngx_rtmp_mp4_write_moov(ngx_rtmp_session_t *s, ngx_buf_t *b, - ngx_rtmp_mp4_track_type_t ttype); + ngx_rtmp_mp4_track_type_t ttype, ngx_rtmp_cenc_drm_info_t *drmi); ngx_int_t ngx_rtmp_mp4_write_moof(ngx_buf_t *b, uint32_t earliest_pres_time, - uint32_t sample_count, ngx_rtmp_mp4_sample_t *samples, - ngx_uint_t sample_mask, uint32_t index); + char type, uint32_t sample_count, ngx_rtmp_mp4_sample_t *samples, + ngx_uint_t sample_mask, uint32_t index, ngx_flag_t is_protected); ngx_int_t ngx_rtmp_mp4_write_sidx(ngx_buf_t *b, ngx_uint_t reference_size, uint32_t earliest_pres_time, uint32_t latest_pres_time); -ngx_uint_t ngx_rtmp_mp4_write_mdat(ngx_buf_t *b, ngx_uint_t size); - +ngx_int_t ngx_rtmp_mp4_write_mdat(ngx_buf_t *b, ngx_uint_t size); +ngx_int_t ngx_rtmp_mp4_write_emsg(ngx_buf_t *b, + uint32_t earliest_pres_time, uint32_t cuepoint_time, uint32_t duration_time, uint32_t id); #endif /* _NGX_RTMP_MP4_H_INCLUDED_ */ diff --git a/ngx_rtmp_handler.c b/ngx_rtmp_handler.c index a8bef611a..c6b43c8bc 100644 --- a/ngx_rtmp_handler.c +++ b/ngx_rtmp_handler.c @@ -398,24 +398,37 @@ ngx_rtmp_recv(ngx_event_t *rev) if (b->last - p < 4) continue; pp = (u_char*)×tamp; + /* extented time stamp: + * big-endian 4b -> little-endian 4b */ pp[3] = *p++; pp[2] = *p++; pp[1] = *p++; pp[0] = *p++; + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, c->log, 0, "RTMP extended timestamp %uD", (uint32_t)timestamp); } - if (st->len == 0) { - /* Messages with type=3 should - * never have ext timestamp field - * according to standard. - * However that's not always the case - * in real life */ - st->ext = (ext && cscf->publish_time_fix); + /* type 0,1,2 */ + if (fmt <= 2) { + /* The specification states that ext timestamp + * is present in Type 3 chunks when the most recent Type 0, + * 1, or 2 chunk for the same chunk stream ID have the presence of + * an extended timestamp field. */ + st->ext = ext; if (fmt) { + /* type 1,2 => timestamp delta */ + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, c->log, 0, "RTMP timestamp delta on fmt type %d", (int)fmt); st->dtime = timestamp; } else { - h->timestamp = timestamp; - st->dtime = 0; + /* type */ + /* fix elemental live server sending garbage ts ?! */ + if ((int)h->type == 18) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, c->log, 0, "RTMP FIX fmt type %d type 18", (int)fmt); + st->dtime = 0; + } else { + h->timestamp = timestamp; + st->dtime = 0; + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, c->log, 0, "RTMP absolute timestamp on fmt type 0 : %uD", h->timestamp); + } } } @@ -430,7 +443,7 @@ ngx_rtmp_recv(ngx_event_t *rev) if (h->mlen > cscf->max_message) { ngx_log_error(NGX_LOG_INFO, c->log, 0, - "too big message: %uz", cscf->max_message); + "too big message: %uz > %uz ", h->mlen, cscf->max_message); ngx_rtmp_finalize_session(s); return; } @@ -774,8 +787,8 @@ ngx_rtmp_receive_message(ngx_rtmp_session_t *s, ch = ch->next, ++nbufs); ngx_log_debug7(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "RTMP recv %s (%d) csid=%D timestamp=%D " - "mlen=%D msid=%D nbufs=%d", + "RTMP recv %s (%d) csid=%D timestamp=%uD " + "mlen=%uD msid=%D nbufs=%d", ngx_rtmp_message_type(h->type), (int)h->type, h->csid, h->timestamp, h->mlen, h->msid, nbufs); } diff --git a/ngx_rtmp_live_module.c b/ngx_rtmp_live_module.c index 8380d94c5..f4295f866 100644 --- a/ngx_rtmp_live_module.c +++ b/ngx_rtmp_live_module.c @@ -1253,7 +1253,7 @@ ngx_rtmp_live_on_fi(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, if (res == NGX_OK) { - ngx_log_error(NGX_LOG_DEBUG, s->connection->log, 0, + ngx_log_debug2(NGX_LOG_DEBUG, s->connection->log, 0, "live: onFi: date='%s', time='%s'", v.date, v.time);