diff --git a/config b/config index 2bdb47f..d82c7d0 100644 --- a/config +++ b/config @@ -20,6 +20,7 @@ CORE_MODULES="$CORE_MODULES ngx_rtmp_log_module \ ngx_rtmp_limit_module \ ngx_rtmp_hls_module \ + ngx_rtmp_dash_module \ " @@ -43,6 +44,7 @@ NGX_ADDON_DEPS="$NGX_ADDON_DEPS \ $ngx_addon_dir/ngx_rtmp_relay_module.h \ $ngx_addon_dir/ngx_rtmp_streams.h \ $ngx_addon_dir/hls/ngx_rtmp_mpegts.h \ + $ngx_addon_dir/hls/ngx_rtmp_mp4.h \ " @@ -78,6 +80,7 @@ NGX_ADDON_SRCS="$NGX_ADDON_SRCS \ $ngx_addon_dir/ngx_rtmp_limit_module.c \ $ngx_addon_dir/hls/ngx_rtmp_hls_module.c \ $ngx_addon_dir/hls/ngx_rtmp_mpegts.c \ + $ngx_addon_dir/hls/ngx_rtmp_mp4.c \ " CFLAGS="$CFLAGS -I$ngx_addon_dir" diff --git a/hls/ngx_rtmp_dash_module.c b/hls/ngx_rtmp_dash_module.c new file mode 100644 index 0000000..99794d5 --- /dev/null +++ b/hls/ngx_rtmp_dash_module.c @@ -0,0 +1,1284 @@ +#include +#include +#include +#include +#include "ngx_rtmp_notify_module.h" +#include "ngx_rtmp_live_module.h" +#include "ngx_rtmp_mp4.h" + +static ngx_rtmp_publish_pt next_publish; +static ngx_rtmp_close_stream_pt next_close_stream; +static ngx_rtmp_stream_begin_pt next_stream_begin; +static ngx_rtmp_stream_eof_pt next_stream_eof; + +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); + +#define NGX_RTMP_DASH_BUFSIZE (1024*1024) +#define NGX_RTMP_DASH_DIR_ACCESS 0744 +#define NGX_RTMP_DASH_MAX_SIZE (800*1024) + +typedef struct { + ngx_uint_t video_earliest_pres_time; + ngx_uint_t video_latest_pres_time; + ngx_uint_t audio_earliest_pres_time; + ngx_uint_t audio_latest_pres_time; + unsigned SAP:1; + uint32_t id; +} ngx_rtmp_dash_frag_t; + +typedef struct { + ngx_str_t playlist; + ngx_str_t playlist_bak; + ngx_str_t name; + ngx_str_t stream; + ngx_str_t start_time; + + unsigned opened:1; + unsigned video:1; + unsigned audio:1; + + ngx_file_t video_file; + ngx_file_t audio_file; + + uint64_t frag; + uint64_t nfrags; + ngx_rtmp_dash_frag_t *frags; + + ngx_buf_t *buffer; + + ngx_uint_t video_mdat_size; + uint32_t video_sample_count; + uint32_t video_sample_sizes[128]; + ngx_str_t video_fragment; + + ngx_uint_t audio_mdat_size; + uint32_t audio_sample_count; + uint32_t audio_sample_sizes[128]; + ngx_str_t audio_fragment; +} ngx_rtmp_dash_ctx_t; + +typedef struct { + ngx_str_t path; + ngx_msec_t playlen; +} ngx_rtmp_dash_cleanup_t; + +typedef struct { + ngx_flag_t dash; + ngx_msec_t fraglen; + ngx_msec_t playlen; + ngx_str_t path; + ngx_uint_t winfrags; + ngx_flag_t cleanup; + ngx_path_t *slot; +} ngx_rtmp_dash_app_conf_t; + +static ngx_command_t ngx_rtmp_dash_commands[] = { + + { ngx_string("dash"), + 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, dash), + NULL }, + + { ngx_string("dash_fragment"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_msec_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_dash_app_conf_t, fraglen), + NULL }, + + { ngx_string("dash_path"), + 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, path), + NULL }, + + { ngx_string("dash_playlist_length"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_msec_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_dash_app_conf_t, playlen), + NULL }, + + + { ngx_string("dash_cleanup"), + 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, cleanup), + NULL }, + + ngx_null_command +}; + +static ngx_rtmp_module_t ngx_rtmp_dash_module_ctx = { + NULL, /* preconfiguration */ + ngx_rtmp_dash_postconfiguration, /* postconfiguration */ + + NULL, /* create main configuration */ + NULL, /* init main configuration */ + + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + + ngx_rtmp_dash_create_app_conf, /* create location configuration */ + ngx_rtmp_dash_merge_app_conf, /* merge location configuration */ +}; + + +ngx_module_t ngx_rtmp_dash_module = { + NGX_MODULE_V1, + &ngx_rtmp_dash_module_ctx, /* module context */ + ngx_rtmp_dash_commands, /* module directives */ + NGX_RTMP_MODULE, /* module type */ + NULL, /* init master */ + NULL, /* init module */ + NULL, /* init process */ + NULL, /* init thread */ + NULL, /* exit thread */ + NULL, /* exit process */ + NULL, /* exit master */ + NGX_MODULE_V1_PADDING +}; + +static ngx_rtmp_dash_frag_t * +ngx_rtmp_dash_get_frag(ngx_rtmp_session_t *s, ngx_int_t n) +{ + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_dash_app_conf_t *hacf; + + hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_dash_module); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + + return &ctx->frags[(ctx->frag + n) % (hacf->winfrags * 2 + 1)]; +} + +static ngx_int_t +ngx_rtmp_dash_rename_file(u_char *src, u_char *dst) +{ + /* rename file with overwrite */ + +#if (NGX_WIN32) + return MoveFileEx((LPCTSTR) src, (LPCTSTR) dst, MOVEFILE_REPLACE_EXISTING); +#else + return ngx_rename_file(src, dst); +#endif +} + +static ngx_int_t +ngx_rtmp_dash_write_playlist(ngx_rtmp_session_t *s) +{ + static u_char buffer[2048]; + int fd; + u_char *p; + ngx_rtmp_dash_app_conf_t *hacf; + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_codec_ctx_t *codec_ctx; + ngx_rtmp_live_ctx_t *live_ctx; + ssize_t n; + ngx_str_t playlist, playlist_bak; + //ngx_rtmp_dash_frag_t *f; + + hacf = 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); + live_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_live_module); + + if (hacf == NULL || ctx == NULL || codec_ctx == NULL || + live_ctx == NULL || live_ctx->stream == NULL) { + return NGX_ERROR; + } + + /* done playlists */ + + fd = ngx_open_file(ctx->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->playlist_bak); + + return NGX_ERROR; + } + +#define NGX_RTMP_DASH_MANIFEST_HEADER \ + "\n"\ + "\n"\ + " \n" +#define NGX_RTMP_DASH_MANIFEST_VIDEO \ + " \n"\ + " \n"\ + " \n"\ + " \n"\ + " \n" +#define NGX_RTMP_DASH_MANIFEST_AUDIO \ + " \n"\ + " \n"\ + " \n"\ + " \n"\ + " \n"\ + " \n" +#define NGX_RTMP_DASH_MANIFEST_FOOTER \ + " \n"\ + "\n" + + //f = ngx_rtmp_dash_get_frag(s, hacf->winfrags/2); + + p = ngx_snprintf(buffer, sizeof(buffer), NGX_RTMP_DASH_MANIFEST_HEADER, + &ctx->start_time); + n = ngx_write_fd(fd, buffer, p - buffer); + + if (ctx->video) { + p = ngx_snprintf(buffer, sizeof(buffer), NGX_RTMP_DASH_MANIFEST_VIDEO, + codec_ctx->width, + codec_ctx->height, + codec_ctx->frame_rate, + codec_ctx->width, + codec_ctx->height, + codec_ctx->frame_rate, + (uint32_t)(live_ctx->stream->bw_in.bandwidth*8), + hacf->fraglen, + &ctx->name, + &ctx->name); + n = ngx_write_fd(fd, buffer, p - buffer); + } + + if (ctx->audio) { + p = ngx_snprintf(buffer, sizeof(buffer), NGX_RTMP_DASH_MANIFEST_AUDIO, + codec_ctx->sample_rate, + codec_ctx->sample_rate, + (uint32_t)(codec_ctx->sample_rate*(hacf->fraglen/1000)), + &ctx->name, + &ctx->name); + n = ngx_write_fd(fd, buffer, p - buffer); + } + + p = ngx_snprintf(buffer, sizeof(buffer), 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->playlist_bak); + ngx_close_file(fd); + return NGX_ERROR; + } + + ngx_close_file(fd); + + if (ngx_rename_file(ctx->playlist_bak.data, ctx->playlist.data)) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: rename failed: '%V'->'%V'", + &playlist_bak, &playlist); + return NGX_ERROR; + } + + return NGX_OK; +} + +static ngx_int_t +ngx_rtmp_dash_write_init_segments(ngx_rtmp_session_t *s) +{ + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_codec_ctx_t *codec_ctx; + int rc; + ngx_file_t file; + static u_char path[1024]; + ngx_buf_t *b; + ngx_rtmp_mp4_metadata_t metadata; + + 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 || !codec_ctx) { + return NGX_ERROR; + } + + ngx_memzero(&path, sizeof(path)); + ngx_str_set(&file.name, "dash-init-video"); + ngx_snprintf(path, sizeof(path), "%Vinit-video.dash",&ctx->stream); + + file.fd = ngx_open_file(path, NGX_FILE_RDWR, + NGX_FILE_TRUNCATE, NGX_FILE_DEFAULT_ACCESS); + + if (file.fd == NGX_INVALID_FILE) { + + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: error creating video init file"); + return NGX_ERROR; + } + + file.log = s->connection->log; + + b = ngx_pcalloc(s->connection->pool, sizeof(ngx_buf_t)); + if (b == NULL) { + return NGX_ERROR; + } + b->start = ngx_palloc(s->connection->pool, 1024); + if (b->start == NULL) { + return NGX_ERROR; + } + + b->end = b->start + 1024; + b->pos = b->last = b->start; + + metadata.width = codec_ctx->width; + metadata.height = codec_ctx->height; + metadata.audio = 0; + metadata.video = 1; + metadata.sample_rate = codec_ctx->sample_rate; + metadata.frame_rate = codec_ctx->frame_rate; + + ngx_rtmp_mp4_write_ftyp(b, NGX_RTMP_MP4_FILETYPE_INIT, metadata); + ngx_rtmp_mp4_write_moov(s, b, metadata); + rc = ngx_write_file(&file, b->start, b->last-b->start, 0); + if (rc < 0) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: writing video init failed"); + } + ngx_close_file(file.fd); + + ngx_memzero(&path, sizeof(path)); + ngx_snprintf(path, sizeof(path), "%Vinit-audio.dash",&ctx->stream); + + file.fd = ngx_open_file(path, NGX_FILE_RDWR, + NGX_FILE_TRUNCATE, NGX_FILE_DEFAULT_ACCESS); + + if (file.fd == NGX_INVALID_FILE) { + + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: error creating dash audio init file"); + return NGX_ERROR; + } + + file.log = s->connection->log; + b->pos = b->last = b->start; + + metadata.video = 0; + metadata.audio = 1; + ngx_rtmp_mp4_write_ftyp(b, NGX_RTMP_MP4_FILETYPE_INIT, metadata); + ngx_rtmp_mp4_write_moov(s, b, metadata); + rc = ngx_write_file(&file, b->start, b->last-b->start, 0); + if (rc < 0) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: writing video init failed"); + } + ngx_close_file(file.fd); + + ctx->buffer = b; + + return NGX_OK; +} + +static ngx_int_t +ngx_rtmp_dash_rewrite_segments(ngx_rtmp_session_t *s) +{ + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_codec_ctx_t *codec_ctx; + ngx_buf_t *b, file_b; + ssize_t written = 0, size, write_size; + ngx_file_t file; + ngx_int_t rc; + ngx_rtmp_mp4_metadata_t metadata; + u_char *pos, *pos1; + ngx_rtmp_dash_frag_t *f; + + static u_char buffer[4096]; + + 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->opened) { + return NGX_OK; + } + + b = ctx->buffer; + + f = ngx_rtmp_dash_get_frag(s, ctx->nfrags); + + /* rewrite video segment */ + ngx_memzero(&buffer, sizeof(buffer)); + file_b.start = buffer; + file_b.end = file_b.start + sizeof(buffer); + file_b.pos = file_b.start; + file_b.last = file_b.pos; + + b->pos = b->last = b->start; + + ngx_rtmp_mp4_write_ftyp(b, NGX_RTMP_MP4_FILETYPE_SEG, metadata); + pos = b->last; + b->last += 44; /* leave room for sidx */ + ngx_rtmp_mp4_write_moof(b, f->video_earliest_pres_time, ctx->video_sample_count, + ctx->video_sample_sizes, (ctx->nfrags+ctx->frag),0); + pos1 = b->last; + b->last = pos; + ngx_rtmp_mp4_write_sidx(s, b, ctx->video_mdat_size+8+(pos1-(pos+44)), f->video_earliest_pres_time, + f->video_latest_pres_time,0); + b->last = pos1; + ngx_rtmp_mp4_write_mdat(b, ctx->video_mdat_size+8); + + /* move the data down to make room for the headers */ + size = (ssize_t) ctx->video_mdat_size; + + *ngx_sprintf(ctx->stream.data + ctx->stream.len, "v-temp") = 0; + + ngx_memzero(&file, sizeof(file)); + file.log = s->connection->log; + file.fd = ngx_open_file(ctx->stream.data, NGX_FILE_RDWR, + NGX_FILE_TRUNCATE, NGX_FILE_DEFAULT_ACCESS); + + if (file.fd == NGX_INVALID_FILE) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: error creating dash temp video file"); + return NGX_ERROR; + } + + ngx_str_set(&file.name, "temp"); + + ctx->video_file.offset = 0; + ngx_rtmp_mp4_write_data(s, &file, b); + do { + file_b.pos = file_b.last = file_b.start; + if ((ssize_t)(written + sizeof(buffer)) > size) { + ngx_read_file(&ctx->video_file, file_b.start, size-written, ctx->video_file.offset); + file_b.last += size-written; + } + else { + ngx_read_file(&ctx->video_file, file_b.start, sizeof(buffer), ctx->video_file.offset); + file_b.last += sizeof(buffer); + } + write_size = ngx_rtmp_mp4_write_data(s, &file, &file_b); + if (write_size == 0) { + break; + } + written += write_size; + } while (written < size); + + ngx_close_file(ctx->video_file.fd); + ngx_close_file(file.fd); + rc = ngx_rtmp_dash_rename_file(ctx->stream.data, ctx->video_fragment.data); + if (rc != NGX_OK) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: rename failed: '%s'->'%s'",ctx->stream.data, ctx->video_fragment.data); + return NGX_ERROR; + } + + /* rewrite audio segment */ + written = 0; + ngx_memzero(&buffer, sizeof(buffer)); + file_b.pos = file_b.last = file_b.start; + b->pos = b->last = b->start; + ngx_rtmp_mp4_write_ftyp(b, NGX_RTMP_MP4_FILETYPE_SEG, metadata); + pos = b->last; + b->last += 44; /* leave room for sidx */ + ngx_rtmp_mp4_write_moof(b, f->audio_earliest_pres_time, ctx->audio_sample_count, + ctx->audio_sample_sizes, (ctx->nfrags+ctx->frag), codec_ctx->sample_rate); + pos1 = b->last; + b->last = pos; + ngx_rtmp_mp4_write_sidx(s, b, ctx->audio_mdat_size+8+(pos1-(pos+44)), f->audio_earliest_pres_time, + f->audio_latest_pres_time, codec_ctx->sample_rate); + b->last = pos1; + ngx_rtmp_mp4_write_mdat(b, ctx->audio_mdat_size+8); + + /* move the data down to make room for the headers */ + size = (ssize_t) ctx->audio_mdat_size; + + *ngx_sprintf(ctx->stream.data + ctx->stream.len, "a-temp") = 0; + + ngx_memzero(&file, sizeof(file)); + file.log = s->connection->log; + file.fd = ngx_open_file(ctx->stream.data, NGX_FILE_RDWR, + NGX_FILE_TRUNCATE, NGX_FILE_DEFAULT_ACCESS); + + if (file.fd == NGX_INVALID_FILE) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: error creating temp audio file"); + return NGX_ERROR; + } + + ngx_str_set(&file.name, "temp"); + + ctx->audio_file.offset = 0; + ngx_rtmp_mp4_write_data(s, &file, b); + do { + file_b.pos = file_b.last = file_b.start; + if ((ssize_t)(written + sizeof(buffer)) > size) { + ngx_read_file(&ctx->audio_file, file_b.start, size-written, ctx->audio_file.offset); + file_b.last += size-written; + } + else { + ngx_read_file(&ctx->audio_file, file_b.start, sizeof(buffer), ctx->audio_file.offset); + file_b.last += sizeof(buffer); + } + write_size = ngx_rtmp_mp4_write_data(s, &file, &file_b); + if (write_size == 0) { + break; + } + written += write_size; + } while (written < size); + + ngx_close_file(ctx->audio_file.fd); + ngx_close_file(file.fd); + rc = ngx_rtmp_dash_rename_file(ctx->stream.data, ctx->audio_fragment.data); + if (rc != NGX_OK) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: rename failed: '%s'->'%s'",ctx->stream.data, ctx->video_fragment.data); + return NGX_ERROR; + } + + return NGX_OK; +} + +static ngx_int_t +ngx_rtmp_dash_close_fragments(ngx_rtmp_session_t *s) +{ + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_dash_app_conf_t *hacf; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_dash_module); + if (!ctx->opened) { + return NGX_OK; + } + + ngx_rtmp_dash_rewrite_segments(s); + + ctx->opened = 0; + + if (ctx->nfrags == hacf->winfrags) { + ctx->frag++; + } else { + ctx->nfrags++; + } + + ngx_rtmp_dash_write_playlist(s); + + return NGX_OK; +} + +static ngx_int_t +ngx_rtmp_dash_open_fragments(ngx_rtmp_session_t *s) +{ + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_dash_frag_t *f; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + if (ctx->opened) { + return NGX_OK; + } + + f = ngx_rtmp_dash_get_frag(s, ctx->nfrags); + f->id = (ctx->frag+ctx->nfrags); + + ngx_memzero(&ctx->video_file, sizeof(ctx->video_file)); + ngx_memzero(&ctx->audio_file, sizeof(ctx->audio_file)); + + ctx->video_file.log = s->connection->log; + ctx->audio_file.log = s->connection->log; + + *ngx_sprintf(ctx->stream.data + ctx->stream.len, "%uL.m4v", f->id) = 0; + *ngx_sprintf(ctx->video_fragment.data + ctx->video_fragment.len, "%uL.m4v", f->id) = 0; + ngx_str_set(&ctx->video_file.name, "dash-v"); + ctx->video_file.fd = ngx_open_file(ctx->stream.data, NGX_FILE_RDWR, + NGX_FILE_TRUNCATE, NGX_FILE_DEFAULT_ACCESS); + + if (ctx->video_file.fd == NGX_INVALID_FILE) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: error creating video fragment file"); + return NGX_ERROR; + } + + *ngx_sprintf(ctx->stream.data + ctx->stream.len, "%uL.m4a", f->id) = 0; + *ngx_sprintf(ctx->audio_fragment.data + ctx->audio_fragment.len, "%uL.m4a", f->id) = 0; + ngx_str_set(&ctx->audio_file.name, "dash-a"); + ctx->audio_file.fd = ngx_open_file(ctx->stream.data, NGX_FILE_RDWR, + NGX_FILE_TRUNCATE, NGX_FILE_DEFAULT_ACCESS); + + if (ctx->audio_file.fd == NGX_INVALID_FILE) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: error creating audio fragment file"); + return NGX_ERROR; + } + + ctx->video_sample_count = 0; + f->video_earliest_pres_time = 0; + ctx->video_mdat_size = 0; + + ctx->audio_sample_count = 0; + f->audio_earliest_pres_time = 0; + ctx->audio_mdat_size = 0; + + ctx->opened = 1; + + return NGX_OK; +} + +static ngx_int_t +ngx_rtmp_dash_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) +{ + ngx_rtmp_dash_app_conf_t *hacf; + ngx_rtmp_dash_ctx_t *ctx; + u_char *p; + size_t len; + + hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_dash_module); + if (hacf == NULL || !hacf->dash || hacf->path.len == 0) { + goto next; + } + + if (s->auto_pushed) { + goto next; + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: publish: name='%s' type='%s'", + v->name, v->type); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + + if (ctx == NULL) { + + ctx = ngx_pcalloc(s->connection->pool, sizeof(ngx_rtmp_dash_ctx_t)); + ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_dash_module); + + } + + if (ctx->frags == NULL) { + ctx->frags = ngx_pcalloc(s->connection->pool, + sizeof(ngx_rtmp_dash_frag_t) * + (hacf->winfrags * 2 + 1)); + if (ctx->frags == NULL) { + return NGX_ERROR; + } + } + + if (ngx_strstr(v->name, "..")) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "dash: bad stream name: '%s'", v->name); + return NGX_ERROR; + } + + ctx->name.len = ngx_strlen(v->name); + ctx->name.data = ngx_palloc(s->connection->pool, ctx->name.len + 1); + + if (ctx->name.data == NULL) { + return NGX_ERROR; + } + + *ngx_cpymem(ctx->name.data, v->name, ctx->name.len) = 0; + + len = hacf->path.len + 1 + ctx->name.len + sizeof(".mpd"); + + ctx->playlist.data = ngx_palloc(s->connection->pool, len); + p = ngx_cpymem(ctx->playlist.data, hacf->path.data, hacf->path.len); + + if (p[-1] != '/') { + *p++ = '/'; + } + + p = ngx_cpymem(p, ctx->name.data, ctx->name.len); + + /* ctx->stream_path holds initial part of stream file path + * however the space for the whole stream path + * is allocated */ + + ctx->stream.len = p - ctx->playlist.data + 1; + ctx->stream.data = ngx_palloc(s->connection->pool, + ctx->stream.len + NGX_INT64_LEN + + sizeof(".mp4")); + + ngx_memcpy(ctx->stream.data, ctx->playlist.data, ctx->stream.len - 1); + ctx->stream.data[ctx->stream.len - 1] = '-'; + + ctx->video_fragment.len = p - ctx->playlist.data + 1; + ctx->video_fragment.data = ngx_palloc(s->connection->pool, + ctx->video_fragment.len + NGX_INT64_LEN + + sizeof(".m4v")); + ctx->audio_fragment.len = p - ctx->playlist.data + 1; + ctx->audio_fragment.data = ngx_palloc(s->connection->pool, + ctx->audio_fragment.len + NGX_INT64_LEN + + sizeof(".m4a")); + + ngx_memcpy(ctx->video_fragment.data, ctx->playlist.data, ctx->video_fragment.len - 1); + ctx->video_fragment.data[ctx->video_fragment.len - 1] = '-'; + ngx_memcpy(ctx->audio_fragment.data, ctx->playlist.data, ctx->audio_fragment.len - 1); + ctx->audio_fragment.data[ctx->audio_fragment.len - 1] = '-'; + + + /* playlist path */ + p = ngx_cpymem(p, ".mpd", sizeof(".mpd") - 1); + + ctx->playlist.len = p - ctx->playlist.data; + + *p = 0; + + /* playlist bak (new playlist) path */ + + ctx->playlist_bak.data = ngx_palloc(s->connection->pool, + ctx->playlist.len + sizeof(".bak")); + p = ngx_cpymem(ctx->playlist_bak.data, ctx->playlist.data, + ctx->playlist.len); + p = ngx_cpymem(p, ".bak", sizeof(".bak") - 1); + + ctx->playlist_bak.len = p - ctx->playlist_bak.data; + + *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); + + /* start time for mpd */ + ctx->start_time.data = ngx_palloc(s->connection->pool, + ngx_cached_http_log_iso8601.len); + ngx_memcpy(ctx->start_time.data, ngx_cached_http_log_iso8601.data, + ngx_cached_http_log_iso8601.len); + ctx->start_time.len = ngx_cached_http_log_iso8601.len; + +next: + return next_publish(s, v); +} + +static ngx_int_t +ngx_rtmp_dash_close_stream(ngx_rtmp_session_t *s, ngx_rtmp_close_stream_t *v) +{ + ngx_rtmp_dash_app_conf_t *hacf; + ngx_rtmp_dash_ctx_t *ctx; + + hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_dash_module); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + + if (hacf == NULL || !hacf->dash || ctx == NULL) { + goto next; + } + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: delete stream"); + + ngx_rtmp_dash_close_fragments(s); + +next: + return next_close_stream(s, v); +} + +static void +ngx_rtmp_dash_update_fragments(ngx_rtmp_session_t *s, ngx_int_t boundary, uint32_t ts) +{ + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_dash_app_conf_t *hacf; + int32_t duration; + ngx_rtmp_dash_frag_t *f; + + hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_dash_module); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + + f = ngx_rtmp_dash_get_frag(s, ctx->nfrags); + + duration = ctx->video ? (f->video_latest_pres_time-f->video_earliest_pres_time) : + (f->audio_latest_pres_time-f->audio_earliest_pres_time); + + if ((ctx->video) && ((int32_t)(hacf->fraglen - duration) > 150)) { + boundary = 0; + } + if (!ctx->video && ctx->audio) { + if ((int32_t)(hacf->fraglen - duration) > 0) { + boundary = 0; + } + else { + boundary = 1; + } + } + + if (ctx->nfrags == 0) { + ngx_rtmp_dash_write_init_segments(s); + boundary = 1; + } + + if (boundary) { + f->SAP = 1; + } + + if (ctx->audio_mdat_size >= NGX_RTMP_DASH_MAX_SIZE) { + boundary = 1; + } + if (ctx->video_mdat_size >= NGX_RTMP_DASH_MAX_SIZE) { + boundary = 1; + } + + if (boundary) { + ngx_rtmp_dash_close_fragments(s); + ngx_rtmp_dash_open_fragments(s); + } +} + + +static ngx_int_t +ngx_rtmp_dash_audio(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_rtmp_dash_app_conf_t *hacf; + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_codec_ctx_t *codec_ctx; + size_t bsize; + ngx_buf_t out; + ngx_rtmp_dash_frag_t *f; + static u_char buffer[NGX_RTMP_DASH_BUFSIZE]; + + hacf = 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 (hacf == NULL || !hacf->dash || ctx == NULL || + codec_ctx == NULL || h->mlen < 2) + { + return NGX_OK; + } + + if (codec_ctx->audio_codec_id != NGX_RTMP_AUDIO_AAC || + codec_ctx->aac_header == NULL) + { + return NGX_OK; + } + + ngx_memzero(&out, sizeof(out)); + + out.start = buffer; + out.end = buffer + sizeof(buffer); + out.pos = out.start; + out.last = out.pos; + + /* copy payload */ + + ctx->audio = 1; + + for (; in && out.last < out.end; in = in->next) { + bsize = in->buf->last - in->buf->pos; + if (bsize < 4) { + continue; + } + if (out.last + bsize > out.end) { + bsize = out.end - out.last; + } + if (*in->buf->pos == 0xAF) { /* rtmp frame header */ + if (*(in->buf->pos+1) == 0x00) { /* rtmp audio frame number--skip 0 */ + break; + } + else { + if (bsize > 2) { + in->buf->pos += 2; /* skip two bytes of audio frame header */ + } + } + } + if (!in->next) { + bsize -= 2; /* chop 2 bytes off the end of the frame */ + } + out.last = ngx_cpymem(out.last, in->buf->pos, bsize); + } + + if (out.last-out.pos > 0) { + ngx_rtmp_dash_update_fragments(s, 0, h->timestamp); + + f = ngx_rtmp_dash_get_frag(s, ctx->nfrags); + + /* Set Presentation Times */ + if (ctx->audio_sample_count == 0 ) { + f->audio_earliest_pres_time = h->timestamp; + } + f->audio_latest_pres_time = h->timestamp; + + ctx->audio_sample_count += 1; + if ((ctx->audio_sample_count <= sizeof(ctx->audio_sample_sizes))) { + ctx->audio_sample_sizes[ctx->audio_sample_count] = ngx_rtmp_mp4_write_data(s, &ctx->audio_file, &out); + ctx->audio_mdat_size += ctx->audio_sample_sizes[ctx->audio_sample_count]; + } + else { + ctx->audio_sample_count = sizeof(ctx->audio_sample_count); + } + } + + return NGX_OK; +} + +static ngx_int_t +ngx_rtmp_dash_video(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_rtmp_dash_app_conf_t *hacf; + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_codec_ctx_t *codec_ctx; + u_char *p; + uint8_t htype, fmt, ftype; + uint32_t i = 1; + ngx_buf_t out; + size_t bsize; + ngx_rtmp_dash_frag_t *f; + static u_char buffer[NGX_RTMP_DASH_BUFSIZE]; + + hacf = 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 (hacf == NULL || !hacf->dash || ctx == NULL || codec_ctx == NULL || + codec_ctx->avc_header == NULL || h->mlen < 1) + { + return NGX_OK; + } + + /* Only H264 is supported */ + if (codec_ctx->video_codec_id != NGX_RTMP_VIDEO_H264) { + return NGX_OK; + } + + ctx->video = 1; + + if (in->buf->last-in->buf->pos < 2) { + return NGX_ERROR; + } + + /* 1: keyframe (IDR) + * 2: inter frame + * 3: disposable inter frame */ + + ngx_memcpy(&fmt, in->buf->pos, 1); + ftype = (fmt & 0xf0) >> 4; + + /* proceed only with PICT */ + + ngx_memcpy(&htype, in->buf->pos+1, 1); + if (htype != 1) { + return NGX_OK; + } + + ngx_memzero(&out, sizeof(out)); + out.start = buffer; + out.end = buffer + sizeof(buffer); + out.pos = out.start; + out.last = out.pos; + + ngx_rtmp_dash_update_fragments(s, (ftype ==1), h->timestamp); + + f = ngx_rtmp_dash_get_frag(s, ctx->nfrags); + + if (!ctx->opened) { + return NGX_OK; + } + + /* Set presentation times */ + if (ctx->video_sample_count == 0) { + f->video_earliest_pres_time = h->timestamp; + } + f->video_latest_pres_time = h->timestamp; + + for (; in && out.last < out.end; in = in->next) { + p = in->buf->pos; + if (i == 1) { + i = 2; + p += 5; + } + bsize = in->buf->last - p; + if (out.last + bsize > out.end) { + bsize = out.end - out.last; + } + + out.last = ngx_cpymem(out.last, p, bsize); + } + + ctx->video_sample_count += 1; + if (ctx->video_sample_count <= sizeof(ctx->video_sample_sizes)) { + ctx->video_sample_sizes[ctx->video_sample_count] = ngx_rtmp_mp4_write_data(s, &ctx->video_file, &out); + ctx->video_mdat_size += ctx->video_sample_sizes[ctx->video_sample_count]; + } + else { + ctx->video_sample_count = sizeof(ctx->video_sample_count); + } + + return NGX_OK; +} + +static void +ngx_rtmp_dash_discontinue(ngx_rtmp_session_t *s) +{ + ngx_rtmp_dash_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + + + if (ctx != NULL && ctx->opened) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: discontinue"); + + ngx_close_file(ctx->video_file.fd); + ngx_close_file(ctx->audio_file.fd); + ctx->opened = 0; + } +} + +static ngx_int_t +ngx_rtmp_dash_stream_begin(ngx_rtmp_session_t *s, ngx_rtmp_stream_begin_t *v) +{ + ngx_rtmp_dash_discontinue(s); + + return next_stream_begin(s, v); +} + + +static ngx_int_t +ngx_rtmp_dash_stream_eof(ngx_rtmp_session_t *s, ngx_rtmp_stream_eof_t *v) +{ + ngx_rtmp_dash_discontinue(s); + + return next_stream_eof(s, v); +} + +static ngx_int_t +ngx_rtmp_dash_cleanup_dir(ngx_str_t *ppath, ngx_msec_t playlen) +{ + ngx_dir_t dir; + time_t mtime, max_age; + ngx_err_t err; + ngx_str_t name, spath; + u_char *p; + ngx_int_t nentries, nerased; + u_char path[NGX_MAX_PATH + 1]; + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, ngx_cycle->log, 0, + "dash: cleanup path='%V' playlen=%M", + ppath, playlen); + + if (ngx_open_dir(ppath, &dir) != NGX_OK) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, ngx_cycle->log, ngx_errno, + "dash: cleanup open dir failed '%V'", ppath); + return NGX_ERROR; + } + + nentries = 0; + nerased = 0; + + for ( ;; ) { + ngx_set_errno(0); + + if (ngx_read_dir(&dir) == NGX_ERROR) { + err = ngx_errno; + + if (ngx_close_dir(&dir) == NGX_ERROR) { + ngx_log_error(NGX_LOG_CRIT, ngx_cycle->log, ngx_errno, + "dash: cleanup " ngx_close_dir_n " \"%V\" failed", + ppath); + } + + if (err == NGX_ENOMOREFILES) { + return nentries - nerased; + } + + ngx_log_error(NGX_LOG_CRIT, ngx_cycle->log, err, + "dash: cleanup " ngx_read_dir_n + " \"%V\" failed", ppath); + return NGX_ERROR; + } + + name.data = ngx_de_name(&dir); + if (name.data[0] == '.') { + continue; + } + + name.len = ngx_de_namelen(&dir); + + p = ngx_snprintf(path, sizeof(path) - 1, "%V/%V", ppath, &name); + *p = 0; + + spath.data = path; + spath.len = p - path; + + nentries++; + + if (ngx_de_info(path, &dir) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_CRIT, ngx_cycle->log, ngx_errno, + "dash: cleanup " ngx_de_info_n " \"%V\" failed", + &spath); + + continue; + } + + if (ngx_de_is_dir(&dir)) { + + if (ngx_rtmp_dash_cleanup_dir(&spath, playlen) == 0) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, ngx_cycle->log, 0, + "dash: cleanup dir '%V'", &name); + + if (ngx_delete_dir(spath.data) != NGX_OK) { + ngx_log_error(NGX_LOG_ERR, ngx_cycle->log, ngx_errno, + "dash: cleanup dir error '%V'", &spath); + } else { + nerased++; + } + } + + continue; + } + + if (!ngx_de_is_file(&dir)) { + continue; + } + + if (name.len >= 4 && name.data[name.len - 4] == '.' && + name.data[name.len - 3] == 'm' && + name.data[name.len - 2] == '4' && + name.data[name.len - 1] == 'v') + { + max_age = playlen / 166; + + } else if (name.len >= 4 && name.data[name.len - 4] == '.' && + name.data[name.len - 3] == 'm' && + name.data[name.len - 2] == '4' && + name.data[name.len - 1] == 'a') + { + max_age = playlen / 166; + + } else if (name.len >= 4 && name.data[name.len - 4] == '.' && + name.data[name.len - 3] == 'm' && + name.data[name.len - 2] == 'p' && + name.data[name.len - 1] == 'd') + { + max_age = playlen / 166; + } else { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, ngx_cycle->log, 0, + "dash: cleanup skip unknown file type '%V'", &name); + continue; + } + + mtime = ngx_de_mtime(&dir); + if (mtime + max_age > ngx_cached_time->sec) { + continue; + } + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, ngx_cycle->log, 0, + "dash: cleanup '%V' mtime=%T age=%T", + &name, mtime, ngx_cached_time->sec - mtime); + + if (ngx_delete_file(spath.data) != NGX_OK) { + ngx_log_error(NGX_LOG_ERR, ngx_cycle->log, ngx_errno, + "dash: cleanup error '%V'", &spath); + continue; + } + + nerased++; + } +} + +static time_t +ngx_rtmp_dash_cleanup(void *data) +{ + ngx_rtmp_dash_cleanup_t *cleanup = data; + + ngx_rtmp_dash_cleanup_dir(&cleanup->path, cleanup->playlen); + + return cleanup->playlen / 500; +} + +static void * +ngx_rtmp_dash_create_app_conf(ngx_conf_t *cf) +{ + ngx_rtmp_dash_app_conf_t *conf; + + conf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_dash_app_conf_t)); + if (conf == NULL) { + return NULL; + } + + conf->dash = NGX_CONF_UNSET; + conf->fraglen = NGX_CONF_UNSET_MSEC; + conf->playlen = NGX_CONF_UNSET_MSEC; + conf->cleanup = NGX_CONF_UNSET; + + return conf; +} + +static char * +ngx_rtmp_dash_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) +{ + ngx_rtmp_dash_app_conf_t *prev = parent; + ngx_rtmp_dash_app_conf_t *conf = child; + ngx_rtmp_dash_cleanup_t *cleanup; + + ngx_conf_merge_value(conf->dash, prev->dash, 0); + ngx_conf_merge_msec_value(conf->fraglen, prev->fraglen, 5000); + ngx_conf_merge_msec_value(conf->playlen, prev->playlen, 30000); + ngx_conf_merge_str_value(conf->path, prev->path, ""); + ngx_conf_merge_value(conf->cleanup, prev->cleanup, 1); + + if (conf->fraglen) { + conf->winfrags = conf->playlen / conf->fraglen; + } + + /* schedule cleanup */ + + if (conf->path.len == 0 || !conf->cleanup) { + return NGX_CONF_OK; + } + + if (conf->path.data[conf->path.len - 1] == '/') { + conf->path.len--; + } + + cleanup = ngx_pcalloc(cf->pool, sizeof(*cleanup)); + if (cleanup == NULL) { + return NGX_CONF_ERROR; + } + + cleanup->path = conf->path; + cleanup->playlen = conf->playlen; + + conf->slot = ngx_pcalloc(cf->pool, sizeof(*conf->slot)); + if (conf->slot == NULL) { + return NGX_CONF_ERROR; + } + + conf->slot->manager = ngx_rtmp_dash_cleanup; + conf->slot->name = conf->path; + conf->slot->data = cleanup; + conf->slot->conf_file = cf->conf_file->file.name.data; + conf->slot->line = cf->conf_file->line; + + if (ngx_add_path(cf, &conf->slot) != NGX_OK) { + return NGX_CONF_ERROR; + } + + return NGX_CONF_OK; +} + +static ngx_int_t +ngx_rtmp_dash_postconfiguration(ngx_conf_t *cf) +{ + ngx_rtmp_core_main_conf_t *cmcf; + ngx_rtmp_handler_pt *h; + + cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module); + + h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_VIDEO]); + *h = ngx_rtmp_dash_video; + + h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_AUDIO]); + *h = ngx_rtmp_dash_audio; + + next_publish = ngx_rtmp_publish; + ngx_rtmp_publish = ngx_rtmp_dash_publish; + + next_close_stream = ngx_rtmp_close_stream; + ngx_rtmp_close_stream = ngx_rtmp_dash_close_stream; + + next_stream_begin = ngx_rtmp_stream_begin; + ngx_rtmp_stream_begin = ngx_rtmp_dash_stream_begin; + + next_stream_eof = ngx_rtmp_stream_eof; + ngx_rtmp_stream_eof = ngx_rtmp_dash_stream_eof; + + return NGX_OK; +} \ No newline at end of file diff --git a/hls/ngx_rtmp_mp4.c b/hls/ngx_rtmp_mp4.c new file mode 100644 index 0000000..13fef5a --- /dev/null +++ b/hls/ngx_rtmp_mp4.c @@ -0,0 +1,1064 @@ + +#include +#include +#include "ngx_rtmp_mp4.h" +#include + +static u_char compressor_name[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +ngx_int_t +ngx_rtmp_mp4_field_64(ngx_buf_t *b, uint64_t n) +{ + u_char bytes[8]; + + bytes[0] = (n >> 56) & 0xFF; + bytes[1] = (n >> 48) & 0xFF; + bytes[2] = (n >> 40) & 0xFF; + bytes[3] = (n >> 32) & 0xFF; + bytes[4] = (n >> 24) & 0xFF; + bytes[5] = (n >> 16) & 0xFF; + bytes[6] = (n >> 8) & 0xFF; + bytes[7] = n & 0xFF; + + b->last = ngx_cpymem(b->last, bytes, sizeof(bytes)); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_field_32(ngx_buf_t *b, unsigned long n) +{ + u_char bytes[4]; + + bytes[0] = (n >> 24) & 0xFF; + bytes[1] = (n >> 16) & 0xFF; + bytes[2] = (n >> 8) & 0xFF; + bytes[3] = n & 0xFF; + + b->last = ngx_cpymem(b->last, bytes, sizeof(bytes)); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_field_24(ngx_buf_t *b, unsigned long n) +{ + u_char bytes[3]; + + bytes[0] = (n >> 16) & 0xFF; + bytes[1] = (n >> 8) & 0xFF; + bytes[2] = n & 0xFF; + + b->last = ngx_cpymem(b->last, bytes, sizeof(bytes)); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_field_16(ngx_buf_t *b, unsigned long n) +{ + u_char bytes[2]; + + bytes[0] = (n >> 8) & 0xFF; + bytes[1] = n & 0xFF; + + b->last = ngx_cpymem(b->last, bytes, sizeof(bytes)); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_field_8(ngx_buf_t *b, unsigned int n) +{ + u_char bytes[1]; + + bytes[0] = n & 0xFF; + + b->last = ngx_cpymem(b->last, bytes, sizeof(bytes)); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_put_descr(ngx_buf_t *b, int tag, unsigned int size) { + //int i = 3; + + /* initially stolen from ffmpeg, but most of it doesnt appear to be necessary */ + + ngx_rtmp_mp4_field_8(b, tag); + //for (; i > 0; i--) { + // ngx_rtmp_mp4_field_8(b, (size >> (7 * i)) | 0x80); + //} + ngx_rtmp_mp4_field_8(b, size & 0x7F); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_update_box_size(ngx_buf_t *b, u_char *pos) +{ + u_char *curpos; + + curpos = b->last; + b->last = pos; + + ngx_rtmp_mp4_field_32(b, (curpos-pos)); + + b->last = curpos; + + return NGX_OK; +} + +/* transformation matrix + |a b u| + |c d v| + |tx ty w| */ +ngx_int_t +ngx_rtmp_mp4_write_matrix(ngx_buf_t *buf, int16_t a, int16_t b, int16_t c, + int16_t d, int16_t tx, int16_t ty) +{ + ngx_rtmp_mp4_field_32(buf, a << 16); /* 16.16 format */ + ngx_rtmp_mp4_field_32(buf, b << 16); /* 16.16 format */ + ngx_rtmp_mp4_field_32(buf, 0); /* u in 2.30 format */ + ngx_rtmp_mp4_field_32(buf, c << 16); /* 16.16 format */ + ngx_rtmp_mp4_field_32(buf, d << 16); /* 16.16 format */ + ngx_rtmp_mp4_field_32(buf, 0); /* v in 2.30 format */ + ngx_rtmp_mp4_field_32(buf, tx << 16); /* 16.16 format */ + ngx_rtmp_mp4_field_32(buf, ty << 16); /* 16.16 format */ + ngx_rtmp_mp4_field_32(buf, 1 << 30); /* w in 2.30 format */ + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_ftyp(ngx_buf_t *b, int type, ngx_rtmp_mp4_metadata_t metadata) +{ + u_char *pos; + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + if (type == NGX_RTMP_MP4_FILETYPE_INIT) { + b->last = ngx_cpymem(b->last, "ftypiso5", sizeof("ftypiso5")-1); + + ngx_rtmp_mp4_field_32(b, 1); + if (metadata.video == 1) { + b->last = ngx_cpymem(b->last, "avc1iso5dash", sizeof("avc1iso5dash")-1); + } + else { + b->last = ngx_cpymem(b->last, "iso5dash", sizeof("iso5dash")-1); + } + + } + if (type == NGX_RTMP_MP4_FILETYPE_SEG) { + b->last = ngx_cpymem(b->last, "stypmsdh", sizeof("stypmsdh")-1); + + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "msdhmsix", sizeof("msdhmsix")-1); + } + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_mvhd(ngx_buf_t *b, ngx_rtmp_mp4_metadata_t metadata) +{ + u_char *pos; + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "mvhd", sizeof("mvhd")-1); + + ngx_rtmp_mp4_field_32(b, 0); /* version */ + ngx_rtmp_mp4_field_32(b, 0x00000000); /* creation time */ + ngx_rtmp_mp4_field_32(b, 0); /* modification time */ + ngx_rtmp_mp4_field_32(b, NGX_RTMP_MP4_TIMESCALE); /* timescale */ + ngx_rtmp_mp4_field_32(b, 0); /* duration */ + ngx_rtmp_mp4_field_32(b, NGX_RTMP_MP4_PREFERRED_RATE); /* preferred playback rate */ + ngx_rtmp_mp4_field_16(b, NGX_RTMP_MP4_PREFERRED_VOLUME); /* preferred volume rate */ + ngx_rtmp_mp4_field_16(b, 0); /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + + ngx_rtmp_mp4_write_matrix(b, 1, 0, 0, 1, 0, 0); + + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + + ngx_rtmp_mp4_field_32(b, 1); /* track id */ + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_tkhd(ngx_buf_t *b, ngx_rtmp_mp4_metadata_t metadata) +{ + u_char *pos; + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "tkhd", sizeof("tkhd")-1); + + ngx_rtmp_mp4_field_8(b, 0); /* version */ + ngx_rtmp_mp4_field_24(b, 0x0000000F); /* flags */ + ngx_rtmp_mp4_field_32(b, 0); /* creation time */ + ngx_rtmp_mp4_field_32(b, 0); /* modification time */ + ngx_rtmp_mp4_field_32(b, 1); /* track id */ + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); /* duration */ + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + ngx_rtmp_mp4_field_32(b, metadata.audio == 1 ? 0x00000001 : 0); /* 2 16s, layer and alternate group */ + ngx_rtmp_mp4_field_16(b, metadata.audio == 1 ? 0x0100 : 0); + ngx_rtmp_mp4_field_16(b, 0); /* reserved */ + + ngx_rtmp_mp4_write_matrix(b, 1, 0, 0, 1, 0, 0); + + if (metadata.video == 1) { + ngx_rtmp_mp4_field_32(b, metadata.width << 16); /* width */ + ngx_rtmp_mp4_field_32(b, metadata.height << 16); /* height */ + } + else { + ngx_rtmp_mp4_field_32(b, 0); /* not relevant for audio */ + ngx_rtmp_mp4_field_32(b, 0); /* not relevant for audio */ + } + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_mdhd(ngx_buf_t *b, ngx_rtmp_mp4_metadata_t metadata) +{ + u_char *pos; + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "mdhd", sizeof("mdhd")-1); + + ngx_rtmp_mp4_field_32(b, 0); /* version */ + ngx_rtmp_mp4_field_32(b, 0); /* creation time */ + ngx_rtmp_mp4_field_32(b, 0); /* modification time */ + ngx_rtmp_mp4_field_32(b, metadata.audio == 1 ? metadata.sample_rate : NGX_RTMP_MP4_TIMESCALE); /* time scale*/ + ngx_rtmp_mp4_field_32(b, 0); /* duration */ + ngx_rtmp_mp4_field_16(b, 0x15C7); /* language */ + ngx_rtmp_mp4_field_16(b, 0); /* reserved */ + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_hdlr(ngx_buf_t *b, ngx_rtmp_mp4_metadata_t metadata) +{ + u_char *pos; + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "hdlr", sizeof("hdlr")-1); + + ngx_rtmp_mp4_field_32(b, 0); /* version and flags */ + ngx_rtmp_mp4_field_32(b, 0); /* pre defined (=0) */ + if (metadata.video == 1) { + b->last = ngx_cpymem(b->last, "vide", sizeof("vide")-1); /* video handler */ + } + else { + b->last = ngx_cpymem(b->last, "soun", sizeof("soun")-1); /* sound handler */ + } + + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + + if (metadata.video == 1) { + b->last = ngx_cpymem(b->last, "VideoHandler", sizeof("VideoHandler")); /* video handler string--NULL TERMINATED */ + } + else { + b->last = ngx_cpymem(b->last, "SoundHandler", sizeof("SoundHandler")); /* sound handler string--NULL TERMINATED */ + } + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_vmhd(ngx_buf_t *b) +{ + /* size is always 0x14, apparently */ + ngx_rtmp_mp4_field_32(b, 0x14); + + b->last = ngx_cpymem(b->last, "vmhd", sizeof("vmhd")-1); + + ngx_rtmp_mp4_field_32(b, 0x01); /* version and flags */ + ngx_rtmp_mp4_field_32(b, 0); /* reserved (graphics mode = copy) */ + ngx_rtmp_mp4_field_32(b, 0); /* reserved (graphics mode = copy) */ + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_smhd(ngx_buf_t *b) +{ + /* size is always 16, apparently */ + ngx_rtmp_mp4_field_32(b, 16); + + b->last = ngx_cpymem(b->last, "smhd", sizeof("smhd")-1); + + ngx_rtmp_mp4_field_32(b, 0); /* version and flags */ + ngx_rtmp_mp4_field_16(b, 0); /* reserved (balance, normally = 0) */ + ngx_rtmp_mp4_field_16(b, 0); /* reserved */ + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_dref(ngx_buf_t *b, ngx_rtmp_mp4_metadata_t metadata) +{ + u_char *pos; + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "dref", sizeof("dref")-1); + + ngx_rtmp_mp4_field_32(b, 0); /* version & flags */ + ngx_rtmp_mp4_field_32(b, 1); /* entry count */ + ngx_rtmp_mp4_field_32(b, 0xc); /* size of url */ + + b->last = ngx_cpymem(b->last, "url ", sizeof("url ")-1); + + ngx_rtmp_mp4_field_32(b, 0x00000001); /* version & flags */ + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_dinf(ngx_buf_t *b, ngx_rtmp_mp4_metadata_t metadata) +{ + u_char *pos; + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "dinf", sizeof("dinf")-1); + + ngx_rtmp_mp4_write_dref(b, metadata); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_avcc(ngx_rtmp_session_t *s, ngx_buf_t *b, ngx_rtmp_mp4_metadata_t metadata) +{ + u_char *pos, *p; + ngx_rtmp_codec_ctx_t *codec_ctx; + ngx_chain_t *in; + + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + if (codec_ctx == NULL) { + return NGX_ERROR; + } + + in = codec_ctx->avc_header; + if (in == NULL) { + return NGX_ERROR; + } + + pos = b->last; + + for (p=in->buf->pos;p<=in->buf->last;p++) { + if (*p == 0x01) { /* check for start code */ + break; + } + } + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "avcC", sizeof("avcC")-1); + + if (in->buf->last-p > 0) { + b->last = ngx_cpymem(b->last, p, in->buf->last-p); + } + else { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "hls: dash: invalid avcc received"); + } + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_video(ngx_rtmp_session_t *s, ngx_buf_t *b, ngx_rtmp_mp4_metadata_t metadata) +{ + u_char *pos; + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "avc1", sizeof("avc1")-1); + + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + ngx_rtmp_mp4_field_16(b, 0); /* reserved */ + ngx_rtmp_mp4_field_16(b, 1); /* data reference index */ + ngx_rtmp_mp4_field_16(b, 0); /* codec stream version */ + ngx_rtmp_mp4_field_16(b, 0); /* codec stream revision (=0) */ + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + ngx_rtmp_mp4_field_16(b, metadata.width); + ngx_rtmp_mp4_field_16(b, metadata.height); + ngx_rtmp_mp4_field_32(b, 0x00480000); /* Horizontal resolution 72dpi */ + ngx_rtmp_mp4_field_32(b, 0x00480000); /* Vertical resolution 72dpi */ + ngx_rtmp_mp4_field_32(b, 0); /* Data size (= 0) */ + ngx_rtmp_mp4_field_16(b, 1); /* Frame count (= 1) */ + ngx_rtmp_mp4_field_8(b, 0); /* compressor name len */ + + b->last = ngx_cpymem(b->last, compressor_name, sizeof(compressor_name)+1); + + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + ngx_rtmp_mp4_field_16(b, 0x18); /* reserved */ + ngx_rtmp_mp4_field_16(b, 0xffff); /* reserved */ + + ngx_rtmp_mp4_write_avcc(s, b, metadata); + + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_esds(ngx_rtmp_session_t *s, ngx_buf_t *b) +{ + /* SCREW THIS FUNCTION */ + + u_char *pos; + ngx_rtmp_codec_ctx_t *codec_ctx; + ngx_chain_t *aac; + int decoder_info; + int aac_header_offset; + + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + if (codec_ctx == NULL) { + return NGX_ERROR; + } + + aac = codec_ctx->aac_header; + if (aac == NULL) { + decoder_info = 0; + aac_header_offset = 0; + } + else { + decoder_info = (aac->buf->last-aac->buf->pos)-2; + aac_header_offset = 2; + } + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "esds", sizeof("esds")-1); + + ngx_rtmp_mp4_field_32(b, 0); /* version */ + ngx_rtmp_mp4_put_descr(b, 0x03, 23+decoder_info); /* length of the rest of the box */ + ngx_rtmp_mp4_field_16(b, 1); /* track id */ + ngx_rtmp_mp4_field_8(b, 0x00); /* flags */ + ngx_rtmp_mp4_put_descr(b, 0x04, 15+decoder_info); /* length of the rest of the box */ + ngx_rtmp_mp4_field_8(b, 0x40); /* codec id */ + ngx_rtmp_mp4_field_8(b, 0x15); /* audio stream */ + ngx_rtmp_mp4_field_24(b, 0); /* buffersize? */ + ngx_rtmp_mp4_field_32(b, 0x0001F151); /* bitrate TODO: should probably set it dynamically*/ + ngx_rtmp_mp4_field_32(b, 0x0001F14D); /* I really dont know */ + + if (aac) { + ngx_rtmp_mp4_put_descr(b, 0x05, decoder_info); + b->last = ngx_cpymem(b->last, aac->buf->pos+aac_header_offset, decoder_info); + } + + ngx_rtmp_mp4_put_descr(b, 0x06, 1); + ngx_rtmp_mp4_field_8(b, 0x02); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_audio(ngx_rtmp_session_t *s, ngx_buf_t *b, ngx_rtmp_mp4_metadata_t metadata) +{ + u_char *pos; + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "mp4a", sizeof("mp4a")-1); + + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + ngx_rtmp_mp4_field_16(b, 0); /* reserved */ + ngx_rtmp_mp4_field_16(b, 1); /* Data-reference index, XXX == 1 */ + ngx_rtmp_mp4_field_16(b, 0); /* Version */ + ngx_rtmp_mp4_field_16(b, 0); /* Revision level */ + ngx_rtmp_mp4_field_32(b, 0); /* reserved */ + ngx_rtmp_mp4_field_16(b, 2); /* something mp4 specific */ + ngx_rtmp_mp4_field_16(b, 16); /* something mp4 specific */ + ngx_rtmp_mp4_field_16(b, 0); /* something mp4 specific */ + ngx_rtmp_mp4_field_16(b, 0); /* packet size (=0) */ + ngx_rtmp_mp4_field_16(b, metadata.sample_rate); /* sample rate */ + ngx_rtmp_mp4_field_16(b, 0); /* reserved */ + + ngx_rtmp_mp4_write_esds(s, b); + + ngx_rtmp_mp4_field_32(b, 8); /* size */ + ngx_rtmp_mp4_field_32(b, 0); /* null tag */ + + ngx_rtmp_mp4_update_box_size(b, pos); + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_stsd(ngx_rtmp_session_t *s, ngx_buf_t *b, ngx_rtmp_mp4_metadata_t metadata) +{ + u_char *pos; + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "stsd", sizeof("stsd")-1); + + ngx_rtmp_mp4_field_32(b, 0); /* version & flags */ + ngx_rtmp_mp4_field_32(b, 1); /* entry count */ + + if (metadata.video == 1) { + ngx_rtmp_mp4_write_video(s,b,metadata); + } + else { + ngx_rtmp_mp4_write_audio(s,b,metadata); + } + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_stts(ngx_buf_t *b) +{ + u_char *pos; + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "stts", sizeof("stts")-1); + + ngx_rtmp_mp4_field_32(b, 0); /* version */ + ngx_rtmp_mp4_field_32(b, 0); /* entry count */ + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_stsc(ngx_buf_t *b) +{ + u_char *pos; + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "stsc", sizeof("stsc")-1); + + ngx_rtmp_mp4_field_32(b, 0); /* version */ + ngx_rtmp_mp4_field_32(b, 0); /* entry count */ + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_stsz(ngx_buf_t *b) +{ + u_char *pos; + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "stsz", sizeof("stsz")-1); + + ngx_rtmp_mp4_field_32(b, 0); /* version */ + ngx_rtmp_mp4_field_32(b, 0); /* entry count */ + ngx_rtmp_mp4_field_32(b, 0); /* moar zeros */ + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_stco(ngx_buf_t *b) +{ + u_char *pos; + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "stco", sizeof("stco")-1); + + ngx_rtmp_mp4_field_32(b, 0); /* version */ + ngx_rtmp_mp4_field_32(b, 0); /* entry count */ + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_stbl(ngx_rtmp_session_t *s, ngx_buf_t *b, ngx_rtmp_mp4_metadata_t metadata) +{ + u_char *pos; + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "stbl", sizeof("stbl")-1); + + ngx_rtmp_mp4_write_stsd(s, b, metadata); + ngx_rtmp_mp4_write_stts(b); + ngx_rtmp_mp4_write_stsc(b); + ngx_rtmp_mp4_write_stsz(b); + ngx_rtmp_mp4_write_stco(b); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_minf(ngx_rtmp_session_t *s, ngx_buf_t *b, ngx_rtmp_mp4_metadata_t metadata) +{ + u_char *pos; + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "minf", sizeof("minf")-1); + + if (metadata.video == 1) { + ngx_rtmp_mp4_write_vmhd(b); + } + else { + ngx_rtmp_mp4_write_smhd(b); + } + + ngx_rtmp_mp4_write_dinf(b, metadata); + ngx_rtmp_mp4_write_stbl(s, b, metadata); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_mdia(ngx_rtmp_session_t *s, ngx_buf_t *b, ngx_rtmp_mp4_metadata_t metadata) +{ + u_char *pos; + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "mdia", sizeof("mdia")-1); + + ngx_rtmp_mp4_write_mdhd(b, metadata); + ngx_rtmp_mp4_write_hdlr(b, metadata); + ngx_rtmp_mp4_write_minf(s, b, metadata); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_trak(ngx_rtmp_session_t *s, ngx_buf_t *b, ngx_rtmp_mp4_metadata_t metadata) +{ + u_char *pos; + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "trak", sizeof("trak")-1); + + ngx_rtmp_mp4_write_tkhd(b, metadata); + ngx_rtmp_mp4_write_mdia(s, b, metadata); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_mvex(ngx_buf_t *b, ngx_rtmp_mp4_metadata_t metadata) +{ + u_char *pos; + uint32_t sample_dur; + + if (metadata.video == 1) { + sample_dur = metadata.frame_rate > 0 ? NGX_RTMP_MP4_TIMESCALE/metadata.frame_rate : NGX_RTMP_MP4_TIMESCALE; + } + else { + sample_dur = 1024; + } + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "mvex", sizeof("mvex")-1); + + /* just write the trex and mehd in here too */ + + ngx_rtmp_mp4_field_32(b, 16); + + b->last = ngx_cpymem(b->last, "mehd", sizeof("mehd")-1); + + ngx_rtmp_mp4_field_32(b, 0); /* version & flags */ + ngx_rtmp_mp4_field_32(b, 0x000D8D2A); /* frag duration */ + + ngx_rtmp_mp4_field_32(b, 0x20); + + b->last = ngx_cpymem(b->last, "trex", sizeof("trex")-1); + + ngx_rtmp_mp4_field_32(b, 0); /* version & flags */ + ngx_rtmp_mp4_field_32(b, 1); /* track id */ + ngx_rtmp_mp4_field_32(b, 1); /* default sample description index */ + ngx_rtmp_mp4_field_32(b, sample_dur); /* default sample duration */ + ngx_rtmp_mp4_field_32(b, 0); /* default sample size */ + ngx_rtmp_mp4_field_32(b, metadata.audio == 1 ? 0 : 0x00010000); /* default sample flags */ + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_moov(ngx_rtmp_session_t *s, ngx_buf_t *b, ngx_rtmp_mp4_metadata_t metadata) +{ + u_char *pos; + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "moov", sizeof("moov")-1); + + ngx_rtmp_mp4_write_mvhd(b, metadata); + ngx_rtmp_mp4_write_mvex(b, metadata); + ngx_rtmp_mp4_write_trak(s, b, metadata); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_tfhd(ngx_buf_t *b, ngx_uint_t sample_rate) +{ + u_char *pos; + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "tfhd", sizeof("tfhd")-1); + + ngx_rtmp_mp4_field_32(b, sample_rate > 0 ? 0x00020020 : 0x00020000); /* version & flags */ + ngx_rtmp_mp4_field_32(b, 1); /* track id */ + if (sample_rate > 0) { + ngx_rtmp_mp4_field_32(b, 0x02000000); + } + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_tfdt(ngx_buf_t *b, ngx_uint_t earliest_pres_time, ngx_uint_t sample_rate) +{ + u_char *pos; + float multiplier; + + if (sample_rate > 0) { + multiplier = (float)sample_rate/(float)NGX_RTMP_MP4_TIMESCALE; + } + else { + multiplier = 1; + } + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "tfdt", sizeof("tfdt")-1); + + ngx_rtmp_mp4_field_32(b, 0x00000000); /* version == 1 aka 64 bit integer */ + ngx_rtmp_mp4_field_32(b, (uint32_t)((float)earliest_pres_time*multiplier)); /* no idea */ + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_trun(ngx_buf_t *b, uint32_t sample_count, uint32_t sample_sizes[128], + u_char *moof_pos, ngx_uint_t sample_rate) +{ + u_char *pos; + uint32_t i, offset; + + pos = b->last; + + if (sample_rate > 0) { + offset = (pos-moof_pos) + 20 + (sample_count*4) + 8; /* moof stuff + trun stuff + sample entries + mdat header */ + } + else { + offset = (pos-moof_pos) + 24 + (sample_count*4) + 8; /* moof stuff + trun stuff + sample entries + mdat header */ + } + + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "trun", sizeof("trun")-1); + + ngx_rtmp_mp4_field_32(b, sample_rate > 0 ? 0x00000201 : 0x00000205); /* version and flags */ + ngx_rtmp_mp4_field_32(b, sample_count); /* sample count */ + ngx_rtmp_mp4_field_32(b, offset); /* data offset */ + if (sample_rate == 0) { + ngx_rtmp_mp4_field_32(b, 0); /* first sample flags */ + } + + for (i = 1; i <= sample_count; i++) { + ngx_rtmp_mp4_field_32(b, sample_sizes[i]); + } + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_traf(ngx_buf_t *b, ngx_uint_t earliest_pres_time, uint32_t sample_count, uint32_t sample_sizes[128], + u_char *moof_pos, ngx_uint_t sample_rate) +{ + u_char *pos; + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "traf", sizeof("traf")-1); + + ngx_rtmp_mp4_write_tfhd(b, sample_rate); + ngx_rtmp_mp4_write_tfdt(b, earliest_pres_time, sample_rate); + ngx_rtmp_mp4_write_trun(b, sample_count, sample_sizes, moof_pos, sample_rate); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_mfhd(ngx_buf_t *b, uint32_t index) +{ + u_char *pos; + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "mfhd", sizeof("mfhd")-1); + + ngx_rtmp_mp4_field_32(b, 0); /* don't know what this is */ + ngx_rtmp_mp4_field_32(b, index); /* fragment index. */ + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_sidx(ngx_rtmp_session_t *s, ngx_buf_t *b, ngx_uint_t reference_size, + ngx_uint_t earliest_pres_time, ngx_uint_t latest_pres_time, ngx_uint_t sample_rate) +{ + u_char *pos; + uint32_t ept, dur; + + if (sample_rate > 0) { + ept = (uint32_t)((float)earliest_pres_time*((float)sample_rate/(float)NGX_RTMP_MP4_TIMESCALE)); + dur = (uint32_t)((float)(latest_pres_time-earliest_pres_time)*((float)sample_rate/(float)NGX_RTMP_MP4_TIMESCALE)); + } + else { + ept = earliest_pres_time; + dur = (latest_pres_time-earliest_pres_time); + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: buffered dash range start: %uL, duration: %uL", + ept, dur); + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "sidx", sizeof("sidx")-1); + + ngx_rtmp_mp4_field_32(b, 0); /* version */ + ngx_rtmp_mp4_field_32(b, 1); /* reference id */ + ngx_rtmp_mp4_field_32(b, sample_rate > 0 ? sample_rate : NGX_RTMP_MP4_TIMESCALE); /* timescale */ + ngx_rtmp_mp4_field_32(b, ept); /* earliest presentation time */ + ngx_rtmp_mp4_field_32(b, 0); /* first offset */ + ngx_rtmp_mp4_field_16(b, 0); /* reserved */ + ngx_rtmp_mp4_field_16(b, 1); /* reference count (=1) */ + ngx_rtmp_mp4_field_32(b, reference_size); /* 1st bit is reference type, the rest is reference size */ + ngx_rtmp_mp4_field_32(b, dur); /* subsegment duration */ + ngx_rtmp_mp4_field_8(b, 0x90); /* first bit is startsWithSAP (=1), next 3 bits are SAP type (=001) */ + ngx_rtmp_mp4_field_24(b, 0); /* SAP delta time */ + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_int_t +ngx_rtmp_mp4_write_moof(ngx_buf_t *b, ngx_uint_t earliest_pres_time, uint32_t sample_count, + uint32_t sample_sizes[128], uint32_t index, ngx_uint_t sample_rate) +{ + u_char *pos; + + pos = b->last; + + /* box size placeholder */ + ngx_rtmp_mp4_field_32(b, 0); + + b->last = ngx_cpymem(b->last, "moof", sizeof("moof")-1); + + ngx_rtmp_mp4_write_mfhd(b, index); + ngx_rtmp_mp4_write_traf(b, earliest_pres_time, sample_count, sample_sizes, pos, sample_rate); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +ngx_uint_t +ngx_rtmp_mp4_write_mdat(ngx_buf_t *b, ngx_uint_t size) +{ + ngx_rtmp_mp4_field_32(b, size); + + b->last = ngx_cpymem(b->last, "mdat", sizeof("mdat")-1); + + return NGX_OK; +} + +uint32_t +ngx_rtmp_mp4_write_data(ngx_rtmp_session_t *s, ngx_file_t *file, ngx_buf_t *b) +{ + ngx_int_t rc; + uint32_t size; + + if (b == NULL) { + return 0; //error + } + + size = b->last-b->start; + if (size < 1) { + return 0; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: data written: %d", + (int) size); + + rc = ngx_write_file(file, b->start, size, file->offset); + if (rc < 0) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: writing file failed"); + return 0; //error + } + + return size; +} \ No newline at end of file diff --git a/hls/ngx_rtmp_mp4.h b/hls/ngx_rtmp_mp4.h new file mode 100644 index 0000000..0cd2b51 --- /dev/null +++ b/hls/ngx_rtmp_mp4.h @@ -0,0 +1,38 @@ + +#ifndef _NGX_RTMP_MP4_H_INCLUDED_ +#define _NGX_RTMP_MP4_H_INCLUDED_ + + +#include +#include +#include + +typedef struct { + ngx_uint_t width; + ngx_uint_t height; + ngx_uint_t audio; + ngx_uint_t video; + ngx_uint_t sample_rate; + ngx_uint_t frame_rate; +} ngx_rtmp_mp4_metadata_t; + +enum { + NGX_RTMP_MP4_FILETYPE_INIT = 0, + NGX_RTMP_MP4_FILETYPE_SEG = 1 +}; + +#define NGX_RTMP_MP4_TIMESCALE 1000 /* divide all times by this value. this is the same resolution as RTMP so it is convenient */ +#define NGX_RTMP_MP4_PREFERRED_RATE 0x00010000 /* normal forward playback as defined by spec */ +#define NGX_RTMP_MP4_PREFERRED_VOLUME 0x0100 /* full volume as defined by spec */ + +ngx_int_t ngx_rtmp_mp4_write_ftyp(ngx_buf_t *b, int type, ngx_rtmp_mp4_metadata_t metadata); +ngx_int_t ngx_rtmp_mp4_write_moov(ngx_rtmp_session_t *s, ngx_buf_t *b, ngx_rtmp_mp4_metadata_t metadata); +ngx_int_t ngx_rtmp_mp4_write_moof(ngx_buf_t *b, ngx_uint_t earliest_pres_time, uint32_t sample_count, + uint32_t sample_sizes[128], uint32_t index, ngx_uint_t sample_rate); +ngx_int_t ngx_rtmp_mp4_write_sidx(ngx_rtmp_session_t *s, ngx_buf_t *b, ngx_uint_t reference_size, + ngx_uint_t earliest_pres_time, ngx_uint_t latest_pres_time, ngx_uint_t sample_rate); +ngx_uint_t ngx_rtmp_mp4_write_mdat(ngx_buf_t *b, ngx_uint_t size); +uint32_t ngx_rtmp_mp4_write_data(ngx_rtmp_session_t *s, ngx_file_t *file, ngx_buf_t *b); + + +#endif /* _NGX_RTMP_MP4_H_INCLUDED_ */ \ No newline at end of file