diff --git a/config b/config index da61214..7705e9b 100644 --- a/config +++ b/config @@ -19,6 +19,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/dash/ngx_rtmp_mp4.h \ " @@ -76,7 +78,9 @@ NGX_ADDON_SRCS="$NGX_ADDON_SRCS \ $ngx_addon_dir/ngx_rtmp_log_module.c \ $ngx_addon_dir/ngx_rtmp_limit_module.c \ $ngx_addon_dir/hls/ngx_rtmp_hls_module.c \ + $ngx_addon_dir/dash/ngx_rtmp_dash_module.c \ $ngx_addon_dir/hls/ngx_rtmp_mpegts.c \ + $ngx_addon_dir/dash/ngx_rtmp_mp4.c \ " CFLAGS="$CFLAGS -I$ngx_addon_dir" diff --git a/dash/ngx_rtmp_dash_module.c b/dash/ngx_rtmp_dash_module.c new file mode 100644 index 0000000..5d7cc66 --- /dev/null +++ b/dash/ngx_rtmp_dash_module.c @@ -0,0 +1,1527 @@ + + +#include +#include +#include +#include +#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); +static ngx_int_t ngx_rtmp_dash_write_init_segments(ngx_rtmp_session_t *s); + + +#define NGX_RTMP_DASH_BUFSIZE (1024*1024) +#define NGX_RTMP_DASH_MAX_MDAT (10*1024*1024) +#define NGX_RTMP_DASH_MAX_SAMPLES 1024 +#define NGX_RTMP_DASH_DIR_ACCESS 0744 + + +typedef struct { + uint32_t timestamp; + uint32_t duration; +} ngx_rtmp_dash_frag_t; + + +typedef struct { + ngx_uint_t id; + ngx_uint_t opened; + ngx_uint_t mdat_size; + ngx_uint_t sample_count; + ngx_uint_t sample_mask; + ngx_fd_t fd; + char type; + uint32_t earliest_pres_time; + uint32_t latest_pres_time; + ngx_rtmp_mp4_sample_t samples[NGX_RTMP_DASH_MAX_SAMPLES]; +} ngx_rtmp_dash_track_t; + + +typedef struct { + ngx_str_t playlist; + ngx_str_t playlist_bak; + ngx_str_t name; + ngx_str_t stream; + ngx_time_t start_time; + + ngx_uint_t nfrags; + ngx_uint_t frag; + ngx_rtmp_dash_frag_t *frags; /* circular 2 * winfrags + 1 */ + + unsigned opened:1; + unsigned has_video:1; + unsigned has_audio:1; + + ngx_file_t video_file; + ngx_file_t audio_file; + + ngx_uint_t id; + + ngx_rtmp_dash_track_t audio; + ngx_rtmp_dash_track_t video; +} 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_flag_t nested; + 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_string("dash_nested"), + 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, nested), + 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 *dacf; + + dacf = 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) % (dacf->winfrags * 2 + 1)]; +} + + +static void +ngx_rtmp_dash_next_frag(ngx_rtmp_session_t *s) +{ + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_dash_app_conf_t *dacf; + + dacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_dash_module); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + + if (ctx->nfrags == dacf->winfrags) { + ctx->frag++; + } else { + ctx->nfrags++; + } +} + + +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) +{ + char *sep; + u_char *p, *last; + ssize_t n; + ngx_fd_t fd; + struct tm tm; + ngx_str_t noname, *name; + ngx_uint_t i; + 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; + + static u_char buffer[NGX_RTMP_DASH_BUFSIZE]; + static u_char start_time[sizeof("1970-09-28T12:00:00+06:00")]; + static u_char end_time[sizeof("1970-09-28T12:00:00+06:00")]; + + 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; + } + + if (ctx->id == 0) { + ngx_rtmp_dash_write_init_segments(s); + } + + 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" + + +#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_MANIFEST_FOOTER \ + " \n" \ + "\n" + + ngx_libc_localtime(ctx->start_time.sec + + ngx_rtmp_dash_get_frag(s, 0)->timestamp / 1000, &tm); + + *ngx_sprintf(start_time, "%4d-%02d-%02dT%02d:%02d:%02d%c%02d:%02d", + tm.tm_year + 1900, tm.tm_mon + 1, + tm.tm_mday, tm.tm_hour, + tm.tm_min, tm.tm_sec, + ctx->start_time.gmtoff < 0 ? '-' : '+', + ngx_abs(ctx->start_time.gmtoff / 60), + ngx_abs(ctx->start_time.gmtoff % 60)) = 0; + + ngx_libc_localtime(ctx->start_time.sec + + (ngx_rtmp_dash_get_frag(s, ctx->nfrags - 1)->timestamp + + ngx_rtmp_dash_get_frag(s, ctx->nfrags - 1)->duration) / + 1000, &tm); + + *ngx_sprintf(end_time, "%4d-%02d-%02dT%02d:%02d:%02d%c%02d:%02d", + tm.tm_year + 1900, tm.tm_mon + 1, + tm.tm_mday, tm.tm_hour, + tm.tm_min, tm.tm_sec, + ctx->start_time.gmtoff < 0 ? '-' : '+', + ngx_abs(ctx->start_time.gmtoff / 60), + ngx_abs(ctx->start_time.gmtoff % 60)) = 0; + + last = buffer + sizeof(buffer); + + p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_MANIFEST_HEADER, + start_time, + end_time, + (ngx_uint_t) (dacf->fraglen / 1000), + (ngx_uint_t) (dacf->fraglen / 1000), + (ngx_uint_t) (dacf->fraglen / 500)); + + n = ngx_write_fd(fd, buffer, p - buffer); + + ngx_str_null(&noname); + + name = (dacf->nested ? &noname : &ctx->name); + sep = (dacf->nested ? "" : "-"); + + if (ctx->has_video) { + p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_MANIFEST_VIDEO, + codec_ctx->width, + codec_ctx->height, + codec_ctx->frame_rate, + &ctx->name, + codec_ctx->width, + codec_ctx->height, + codec_ctx->frame_rate, + (ngx_uint_t) (codec_ctx->video_data_rate * 1000), + 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_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_VIDEO_FOOTER); + + n = ngx_write_fd(fd, buffer, p - buffer); + } + + if (ctx->has_audio) { + p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_MANIFEST_AUDIO, + &ctx->name, + codec_ctx->audio_codec_id == NGX_RTMP_AUDIO_AAC ? + "40.2" : "6b", + codec_ctx->sample_rate, + (ngx_uint_t) (codec_ctx->audio_data_rate * 1000), + 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_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_AUDIO_FOOTER); + + n = ngx_write_fd(fd, buffer, p - buffer); + } + + 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->playlist_bak); + ngx_close_file(fd); + return NGX_ERROR; + } + + ngx_close_file(fd); + + if (ngx_rtmp_dash_rename_file(ctx->playlist_bak.data, ctx->playlist.data) + == NGX_FILE_ERROR) + { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: rename failed: '%V'->'%V'", + &ctx->playlist_bak, &ctx->playlist); + return NGX_ERROR; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_dash_write_init_segments(ngx_rtmp_session_t *s) +{ + 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_mp4_metadata_t metadata; + + static u_char buffer[NGX_RTMP_DASH_BUFSIZE]; + + 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) { + return NGX_ERROR; + } + + metadata.width = codec_ctx->width; + metadata.height = codec_ctx->height; + metadata.sample_rate = codec_ctx->sample_rate; + metadata.frame_rate = codec_ctx->frame_rate; + metadata.audio_codec = codec_ctx->audio_codec_id; + + /* init video */ + + *ngx_sprintf(ctx->stream.data + ctx->stream.len, "init.m4v") = 0; + + fd = ngx_open_file(ctx->stream.data, NGX_FILE_RDWR, NGX_FILE_TRUNCATE, + NGX_FILE_DEFAULT_ACCESS); + + if (fd == NGX_INVALID_FILE) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: error creating video init file"); + return NGX_ERROR; + } + + b.start = buffer; + b.end = b.start + sizeof(buffer); + b.pos = b.last = b.start; + + metadata.audio = 0; + metadata.video = 1; + + /*TODO: buffer control*/ + ngx_rtmp_mp4_write_ftyp(&b, NGX_RTMP_MP4_FILETYPE_INIT, &metadata); + ngx_rtmp_mp4_write_moov(s, &b, &metadata); + + rc = ngx_write_fd(fd, b.start, (size_t) (b.last - b.start)); + if (rc == NGX_ERROR) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: writing video init failed"); + } + + ngx_close_file(fd); + + /* init audio */ + + *ngx_sprintf(ctx->stream.data + ctx->stream.len, "init.m4a") = 0; + + fd = ngx_open_file(ctx->stream.data, NGX_FILE_RDWR, NGX_FILE_TRUNCATE, + NGX_FILE_DEFAULT_ACCESS); + + if (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; + } + + b.pos = b.last = b.start; + + metadata.video = 0; + metadata.audio = 1; + + /*TODO: buffer control*/ + ngx_rtmp_mp4_write_ftyp(&b, NGX_RTMP_MP4_FILETYPE_INIT, &metadata); + ngx_rtmp_mp4_write_moov(s, &b, &metadata); + + rc = ngx_write_fd(fd, b.start, (size_t) (b.last - b.start)); + if (rc == NGX_ERROR) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: writing audio init failed"); + } + + ngx_close_file(fd); + + return NGX_OK; +} + + +static void +ngx_rtmp_dash_close_fragment(ngx_rtmp_session_t *s, ngx_rtmp_dash_track_t *t) +{ + u_char *pos, *pos1; + size_t left; + ssize_t n; + ngx_fd_t fd; + ngx_buf_t b; + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_dash_frag_t *f; + + static u_char buffer[NGX_RTMP_DASH_BUFSIZE]; + + if (!t->opened) { + return; + } + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: close fragment id=%ui, type=%c, pts=%uD", + t->id, t->type, t->earliest_pres_time); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + + b.start = buffer; + b.end = buffer + sizeof(buffer); + b.pos = b.last = b.start; + + ngx_rtmp_mp4_write_ftyp(&b, NGX_RTMP_MP4_FILETYPE_SEG, NULL); + + 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); + pos1 = b.last; + b.last = pos; + + ngx_rtmp_mp4_write_sidx(&b, t->mdat_size + 8 + (pos1 - (pos + 44)), + t->earliest_pres_time, t->latest_pres_time); + b.last = pos1; + ngx_rtmp_mp4_write_mdat(&b, t->mdat_size + 8); + + /* move the data down to make room for the headers */ + + f = ngx_rtmp_dash_get_frag(s, ctx->nfrags); + + *ngx_sprintf(ctx->stream.data + ctx->stream.len, "%uD.m4%c", + f->timestamp, t->type) = 0; + + fd = ngx_open_file(ctx->stream.data, NGX_FILE_RDWR, + NGX_FILE_TRUNCATE, NGX_FILE_DEFAULT_ACCESS); + + if (fd == NGX_INVALID_FILE) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: error creating dash temp video file"); + goto done; + } + + if (ngx_write_fd(fd, b.pos, (size_t) (b.last - b.pos)) == NGX_ERROR) { + goto done; + } + + left = (size_t) t->mdat_size; + +#if (NGX_WIN32) + if (SetFilePointer(t->fd, 0, 0, FILE_BEGIN) == INVALID_SET_FILE_POINTER) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "dash: SetFilePointer error"); + goto done; + } +#else + if (lseek(t->fd, 0, SEEK_SET) == -1) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: lseek error"); + goto done; + } +#endif + + while (left > 0) { + + n = ngx_read_fd(t->fd, buffer, ngx_min(sizeof(buffer), left)); + if (n == NGX_ERROR) { + break; + } + + n = ngx_write_fd(fd, buffer, (size_t) n); + if (n == NGX_ERROR) { + break; + } + + left -= n; + } + +done: + + if (fd != NGX_INVALID_FILE) { + ngx_close_file(fd); + } + + ngx_close_file(t->fd); + + t->fd = NGX_INVALID_FILE; + t->opened = 0; +} + + +static ngx_int_t +ngx_rtmp_dash_close_fragments(ngx_rtmp_session_t *s) +{ + ngx_rtmp_dash_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + if (!ctx->opened) { + return NGX_OK; + } + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: close fragments"); + + ngx_rtmp_dash_close_fragment(s, &ctx->video); + ngx_rtmp_dash_close_fragment(s, &ctx->audio); + + ngx_rtmp_dash_next_frag(s); + + ngx_rtmp_dash_write_playlist(s); + + ctx->id++; + ctx->opened = 0; + + return NGX_OK; +} + + +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; + + if (t->opened) { + return NGX_OK; + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: open fragment id=%ui, type='%c'", id, type); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + + *ngx_sprintf(ctx->stream.data + ctx->stream.len, "raw.m4%c", type) = 0; + + t->fd = ngx_open_file(ctx->stream.data, NGX_FILE_RDWR, + NGX_FILE_TRUNCATE, NGX_FILE_DEFAULT_ACCESS); + + if (t->fd == NGX_INVALID_FILE) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: error creating fragment file"); + return NGX_ERROR; + } + + t->id = id; + t->type = type; + t->sample_count = 0; + t->earliest_pres_time = 0; + t->latest_pres_time = 0; + t->mdat_size = 0; + t->opened = 1; + + if (type == 'v') { + t->sample_mask = NGX_RTMP_MP4_SAMPLE_SIZE| + NGX_RTMP_MP4_SAMPLE_DURATION| + NGX_RTMP_MP4_SAMPLE_DELAY| + NGX_RTMP_MP4_SAMPLE_KEY; + } else { + t->sample_mask = NGX_RTMP_MP4_SAMPLE_SIZE| + NGX_RTMP_MP4_SAMPLE_DURATION; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_dash_open_fragments(ngx_rtmp_session_t *s) +{ + ngx_rtmp_dash_ctx_t *ctx; + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: open fragments"); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + + if (ctx->opened) { + return NGX_OK; + } + + ngx_rtmp_dash_open_fragment(s, &ctx->video, ctx->id, 'v'); + + ngx_rtmp_dash_open_fragment(s, &ctx->audio, ctx->id, 'a'); + + ctx->opened = 1; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_dash_ensure_directory(ngx_rtmp_session_t *s) +{ + size_t len; + ngx_file_info_t fi; + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_dash_app_conf_t *dacf; + + static u_char path[NGX_MAX_PATH + 1]; + + dacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_dash_module); + + *ngx_snprintf(path, sizeof(path) - 1, "%V", &dacf->path) = 0; + + if (ngx_file_info(path, &fi) == NGX_FILE_ERROR) { + + if (ngx_errno != NGX_ENOENT) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: " ngx_file_info_n " failed on '%V'", + &dacf->path); + return NGX_ERROR; + } + + /* ENOENT */ + + if (ngx_create_dir(path, NGX_RTMP_DASH_DIR_ACCESS) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: " ngx_create_dir_n " failed on '%V'", + &dacf->path); + return NGX_ERROR; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: directory '%V' created", &dacf->path); + + } else { + + if (!ngx_is_dir(&fi)) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "dash: '%V' exists and is not a directory", + &dacf->path); + return NGX_ERROR; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: directory '%V' exists", &dacf->path); + } + + if (!dacf->nested) { + return NGX_OK; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + + len = dacf->path.len; + if (dacf->path.data[len - 1] == '/') { + len--; + } + + *ngx_snprintf(path, sizeof(path) - 1, "%*s/%V", len, dacf->path.data, + &ctx->name) = 0; + + if (ngx_file_info(path, &fi) != NGX_FILE_ERROR) { + + if (ngx_is_dir(&fi)) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: directory '%s' exists", path); + return NGX_OK; + } + + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "dash: '%s' exists and is not a directory", path); + + return NGX_ERROR; + } + + if (ngx_errno != NGX_ENOENT) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: " ngx_file_info_n " failed on '%s'", path); + return NGX_ERROR; + } + + /* NGX_ENOENT */ + + if (ngx_create_dir(path, NGX_RTMP_DASH_DIR_ACCESS) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: " ngx_create_dir_n " failed on '%s'", path); + return NGX_ERROR; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: directory '%s' created", path); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_dash_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) +{ + u_char *p; + size_t len; + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_dash_frag_t *f; + ngx_rtmp_dash_app_conf_t *dacf; + + dacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_dash_module); + if (dacf == NULL || !dacf->dash || dacf->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)); + if (ctx == NULL) { + goto next; + } + ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_dash_module); + + } else { + if (ctx->opened) { + goto next; + } + + f = ctx->frags; + ngx_memzero(ctx, sizeof(ngx_rtmp_dash_ctx_t)); + ctx->frags = f; + } + + if (ctx->frags == NULL) { + ctx->frags = ngx_pcalloc(s->connection->pool, + sizeof(ngx_rtmp_dash_frag_t) * + (dacf->winfrags * 2 + 1)); + if (ctx->frags == NULL) { + return NGX_ERROR; + } + } + + ctx->id = 0; + + 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 = dacf->path.len + 1 + ctx->name.len + sizeof(".mpd"); + if (dacf->nested) { + len += sizeof("/index") - 1; + } + + ctx->playlist.data = ngx_palloc(s->connection->pool, len); + p = ngx_cpymem(ctx->playlist.data, dacf->path.data, dacf->path.len); + + if (p[-1] != '/') { + *p++ = '/'; + } + + p = ngx_cpymem(p, ctx->name.data, ctx->name.len); + + /* + * ctx->stream 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_INT32_LEN + + sizeof(".m4x")); + + ngx_memcpy(ctx->stream.data, ctx->playlist.data, ctx->stream.len - 1); + ctx->stream.data[ctx->stream.len - 1] = (dacf->nested ? '/' : '-'); + + if (dacf->nested) { + p = ngx_cpymem(p, "/index.mpd", sizeof("/index.mpd") - 1); + } else { + 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); + + ctx->start_time = *ngx_cached_time; + + if (ngx_rtmp_dash_ensure_directory(s) != NGX_OK) { + return NGX_ERROR; + } + +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_ctx_t *ctx; + ngx_rtmp_dash_app_conf_t *dacf; + + dacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_dash_module); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + + if (dacf == NULL || !dacf->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 timestamp) +{ + ngx_int_t hit; + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_dash_frag_t *f; + ngx_rtmp_dash_app_conf_t *dacf; + + dacf = 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); + + hit = (timestamp >= f->timestamp + dacf->fraglen); + + if (ctx->has_video && !hit) { + boundary = 0; + } + + if (!ctx->has_video && ctx->has_audio) { + boundary = hit; + } + + if (ctx->audio.mdat_size >= NGX_RTMP_DASH_MAX_MDAT) { + boundary = 1; + } + + if (ctx->video.mdat_size >= NGX_RTMP_DASH_MAX_MDAT) { + boundary = 1; + } + + if (!ctx->opened) { + boundary = 1; + } + + f->duration = (timestamp - f->timestamp); + + if (boundary) { + ngx_rtmp_dash_close_fragments(s); + ngx_rtmp_dash_open_fragments(s); + + f = ngx_rtmp_dash_get_frag(s, ctx->nfrags); + f->timestamp = timestamp; + } +} + + +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; + + static u_char buffer[NGX_RTMP_DASH_BUFSIZE]; + + p = buffer; + size = 0; + + for (; in && size < sizeof(buffer); in = in->next) { + + bsize = (size_t) (in->buf->last - in->buf->pos); + if (size + bsize > sizeof(buffer)) { + bsize = (size_t) (sizeof(buffer) - size); + } + + p = ngx_cpymem(p, in->buf->pos, bsize); + size += bsize; + } + + ngx_rtmp_dash_update_fragments(s, key, timestamp); + + if (t->sample_count == 0) { + t->earliest_pres_time = timestamp; + } + + t->latest_pres_time = timestamp; + + if (t->sample_count < NGX_RTMP_DASH_MAX_SAMPLES) { + + 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; + } + + t->samples[t->sample_count].delay = 0; + t->samples[t->sample_count].size = (uint32_t) size; + t->samples[t->sample_count].duration = 0; + t->samples[t->sample_count].timestamp = timestamp; + t->samples[t->sample_count].key = (key ? 1 : 0); + + if (t->sample_count > 0) { + t->samples[t->sample_count - 1].duration = timestamp - + t->samples[t->sample_count - 1].timestamp; + } + + t->sample_count++; + t->mdat_size += (ngx_uint_t) size; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_dash_audio(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + u_char htype; + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_codec_ctx_t *codec_ctx; + ngx_rtmp_dash_app_conf_t *dacf; + + 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 || !dacf->dash || ctx == NULL || + codec_ctx == NULL || h->mlen < 2) + { + return NGX_OK; + } + + /* Only AAC is supported */ + + if (codec_ctx->audio_codec_id != NGX_RTMP_AUDIO_AAC || + codec_ctx->aac_header == NULL) + { + return NGX_OK; + } + + if (in->buf->last - in->buf->pos < 2) { + return NGX_ERROR; + } + + /* skip AAC config */ + + htype = in->buf->pos[1]; + if (htype != 1) { + return NGX_OK; + } + + ctx->has_audio = 1; + + /* skip RTMP & AAC headers */ + + in->buf->pos += 2; + + return ngx_rtmp_dash_append(s, in, &ctx->audio, 0, h->timestamp, 0); +} + + +static ngx_int_t +ngx_rtmp_dash_video(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + u_char *p; + uint8_t ftype, htype; + uint32_t delay; + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_codec_ctx_t *codec_ctx; + ngx_rtmp_dash_app_conf_t *dacf; + + 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 || !dacf->dash || ctx == NULL || codec_ctx == NULL || + codec_ctx->avc_header == NULL || h->mlen < 5) + { + return NGX_OK; + } + + /* Only H264 is supported */ + + if (codec_ctx->video_codec_id != NGX_RTMP_VIDEO_H264) { + return NGX_OK; + } + + if (in->buf->last - in->buf->pos < 5) { + return NGX_ERROR; + } + + ftype = (in->buf->pos[0] & 0xf0) >> 4; + + /* skip AVC config */ + + htype = in->buf->pos[1]; + if (htype != 1) { + return NGX_OK; + } + + p = (u_char *) &delay; + + p[0] = in->buf->pos[4]; + p[1] = in->buf->pos[3]; + p[2] = in->buf->pos[2]; + p[3] = 0; + + ctx->has_video = 1; + + /* skip RTMP & H264 headers */ + + in->buf->pos += 5; + + return ngx_rtmp_dash_append(s, in, &ctx->video, ftype == 1, h->timestamp, + delay); +} + + +static ngx_int_t +ngx_rtmp_dash_stream_begin(ngx_rtmp_session_t *s, ngx_rtmp_stream_begin_t *v) +{ + 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_close_fragments(s); + + return next_stream_eof(s, v); +} + + +static ngx_int_t +ngx_rtmp_dash_cleanup_dir(ngx_str_t *ppath, ngx_msec_t playlen) +{ + time_t mtime, max_age; + u_char *p; + u_char path[NGX_MAX_PATH + 1], mpd_path[NGX_MAX_PATH + 1]; + ngx_dir_t dir; + ngx_err_t err; + ngx_str_t name, spath, mpd; + ngx_int_t nentries, nerased; + ngx_file_info_t fi; + + 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 (!dir.valid_info && 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); + + /* + * null-termination gets spoiled in win32 + * version of ngx_open_dir + */ + + *p = 0; + + if (ngx_delete_dir(path) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_ERR, ngx_cycle->log, ngx_errno, + "dash: cleanup " ngx_delete_dir_n + " failed on '%V'", &spath); + } else { + nerased++; + } + } + + continue; + } + + if (!ngx_de_is_file(&dir)) { + continue; + } + + if (name.len >= 8 && name.data[name.len - 8] == 'i' && + name.data[name.len - 7] == 'n' && + name.data[name.len - 6] == 'i' && + name.data[name.len - 5] == 't' && + name.data[name.len - 4] == '.' && + name.data[name.len - 3] == 'm' && + name.data[name.len - 2] == '4') + { + if (name.len == 8) { + ngx_str_set(&mpd, "index"); + } else { + mpd.data = name.data; + mpd.len = name.len - 9; + } + + p = ngx_snprintf(mpd_path, sizeof(mpd_path) - 1, "%V/%V.mpd", + ppath, &mpd); + *p = 0; + + if (ngx_file_info(mpd_path, &fi) != NGX_FILE_ERROR) { + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, ngx_cycle->log, 0, + "dash: cleanup '%V' delayed, mpd exists '%s'", + &name, mpd_path); + continue; + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, ngx_cycle->log, 0, + "dash: cleanup '%V' allowed, mpd missing '%s'", + &name, mpd_path); + + max_age = 0; + + } 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] == 'v') + { + max_age = playlen / 500; + + } 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 / 500; + + } 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 / 500; + + } else if (name.len >= 4 && name.data[name.len - 4] == '.' && + name.data[name.len - 3] == 'r' && + name.data[name.len - 2] == 'a' && + name.data[name.len - 1] == 'w') + { + max_age = playlen / 1000; + + } 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(path) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_ERR, ngx_cycle->log, ngx_errno, + "dash: cleanup " ngx_delete_file_n " failed on '%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; + conf->nested = 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); + ngx_conf_merge_value(conf->nested, prev->nested, 0); + + 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_handler_pt *h; + ngx_rtmp_core_main_conf_t *cmcf; + + 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; +} diff --git a/dash/ngx_rtmp_mp4.c b/dash/ngx_rtmp_mp4.c new file mode 100644 index 0000000..547f752 --- /dev/null +++ b/dash/ngx_rtmp_mp4.c @@ -0,0 +1,1115 @@ + + +#include +#include +#include "ngx_rtmp_mp4.h" +#include + + +/* +static ngx_int_t +ngx_rtmp_mp4_field_64(ngx_buf_t *b, uint64_t n) +{ + u_char bytes[8]; + + bytes[0] = ((uint64_t) n >> 56) & 0xFF; + bytes[1] = ((uint64_t) n >> 48) & 0xFF; + bytes[2] = ((uint64_t) n >> 40) & 0xFF; + bytes[3] = ((uint64_t) n >> 32) & 0xFF; + bytes[4] = ((uint64_t) n >> 24) & 0xFF; + bytes[5] = ((uint64_t) n >> 16) & 0xFF; + bytes[6] = ((uint64_t) n >> 8) & 0xFF; + bytes[7] = (uint64_t) n & 0xFF; + + if (b->last + sizeof(bytes) > b->end) { + return NGX_ERROR; + } + + b->last = ngx_cpymem(b->last, bytes, sizeof(bytes)); + + return NGX_OK; +} +*/ + +static ngx_int_t +ngx_rtmp_mp4_field_32(ngx_buf_t *b, uint32_t n) +{ + u_char bytes[4]; + + bytes[0] = ((uint32_t) n >> 24) & 0xFF; + bytes[1] = ((uint32_t) n >> 16) & 0xFF; + bytes[2] = ((uint32_t) n >> 8) & 0xFF; + bytes[3] = (uint32_t) n & 0xFF; + + if (b->last + sizeof(bytes) > b->end) { + return NGX_ERROR; + } + + b->last = ngx_cpymem(b->last, bytes, sizeof(bytes)); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_field_24(ngx_buf_t *b, uint32_t n) +{ + u_char bytes[3]; + + bytes[0] = ((uint32_t) n >> 16) & 0xFF; + bytes[1] = ((uint32_t) n >> 8) & 0xFF; + bytes[2] = (uint32_t) n & 0xFF; + + if (b->last + sizeof(bytes) > b->end) { + return NGX_ERROR; + } + + b->last = ngx_cpymem(b->last, bytes, sizeof(bytes)); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_field_16(ngx_buf_t *b, uint16_t n) +{ + u_char bytes[2]; + + bytes[0] = ((uint32_t) n >> 8) & 0xFF; + bytes[1] = (uint32_t) n & 0xFF; + + if (b->last + sizeof(bytes) > b->end) { + return NGX_ERROR; + } + + b->last = ngx_cpymem(b->last, bytes, sizeof(bytes)); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_field_8(ngx_buf_t *b, uint8_t n) +{ + u_char bytes[1]; + + bytes[0] = n & 0xFF; + + if (b->last + sizeof(bytes) > b->end) { + return NGX_ERROR; + } + + b->last = ngx_cpymem(b->last, bytes, sizeof(bytes)); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_put_descr(ngx_buf_t *b, int tag, unsigned int size) +{ + ngx_rtmp_mp4_field_8(b, (uint8_t) tag); + ngx_rtmp_mp4_field_8(b, size & 0x7F); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_data(ngx_buf_t *b, void *data, size_t n) +{ + if (b->last + n > b->end) { + return NGX_ERROR; + } + + b->last = ngx_cpymem(b->last, (u_char *) data, n); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_box(ngx_buf_t *b, const char box[4]) +{ + if (b->last + 4 > b->end) { + return NGX_ERROR; + } + + b->last = ngx_cpymem(b->last, (u_char *) box, 4); + + return NGX_OK; +} + + +static u_char * +ngx_rtmp_mp4_start_box(ngx_buf_t *b, const char box[4]) +{ + u_char *p; + + p = b->last; + + if (ngx_rtmp_mp4_field_32(b, 0) != NGX_OK) { + return NULL; + } + + if (ngx_rtmp_mp4_box(b, box) != NGX_OK) { + return NULL; + } + + return p; +} + + +static ngx_int_t +ngx_rtmp_mp4_update_box_size(ngx_buf_t *b, u_char *p) +{ + u_char *curpos; + + if (p == NULL) { + return NGX_ERROR; + } + + curpos = b->last; + + b->last = p; + + ngx_rtmp_mp4_field_32(b, (uint32_t) (curpos - p)); + + b->last = curpos; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_matrix(ngx_buf_t *buf, uint32_t a, uint32_t b, uint32_t c, + uint32_t d, uint32_t tx, uint32_t ty) +{ + +/* + * transformation matrix + * |a b u| + * |c d v| + * |tx ty w| + */ + + 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; + + switch (type) { + + case NGX_RTMP_MP4_FILETYPE_INIT: + + pos = ngx_rtmp_mp4_start_box(b, "ftyp"); + + ngx_rtmp_mp4_box(b, "iso5"); + ngx_rtmp_mp4_field_32(b, 1); + + if (metadata != NULL && metadata->video == 1) { + ngx_rtmp_mp4_box(b, "avc1"); + } + + ngx_rtmp_mp4_box(b, "iso5"); + ngx_rtmp_mp4_box(b, "dash"); + + break; + + default: /* NGX_RTMP_MP4_FILETYPE_SEG */ + + pos = ngx_rtmp_mp4_start_box(b, "styp"); + + ngx_rtmp_mp4_box(b, "msdh"); + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_box(b, "msdh"); + ngx_rtmp_mp4_box(b, "msix"); + + break; + } + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_mvhd(ngx_buf_t *b, ngx_rtmp_mp4_metadata_t *metadata) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "mvhd"); + + 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, 1000); /* timescale */ + ngx_rtmp_mp4_field_32(b, 0); /* duration */ + ngx_rtmp_mp4_field_32(b, 0x00010000); /* playback rate */ + ngx_rtmp_mp4_field_16(b, 0x0100); /* 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; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_tkhd(ngx_buf_t *b, ngx_rtmp_mp4_metadata_t *metadata) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "tkhd"); + + 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 */ + /* 2 16s, layer and alternate group */ + ngx_rtmp_mp4_field_32(b, metadata->audio == 1 ? 0x00000001 : 0); + 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; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_mdhd(ngx_buf_t *b) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "mdhd"); + + /* 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, 0); + + /* time scale*/ + ngx_rtmp_mp4_field_32(b, 1000); + + /* duration */ + ngx_rtmp_mp4_field_32(b, 0); + + /* lanuguage */ + ngx_rtmp_mp4_field_16(b, 0x15C7); + + /* reserved */ + ngx_rtmp_mp4_field_16(b, 0); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_hdlr(ngx_buf_t *b, ngx_rtmp_mp4_metadata_t *metadata) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "hdlr"); + + /* version and flags */ + ngx_rtmp_mp4_field_32(b, 0); + + /* pre defined */ + ngx_rtmp_mp4_field_32(b, 0); + + if (metadata->video == 1) { + ngx_rtmp_mp4_box(b, "vide"); + } else { + ngx_rtmp_mp4_box(b, "soun"); + } + + /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + + if (metadata->video == 1) { + /* video handler string, NULL-terminated */ + ngx_rtmp_mp4_data(b, "VideoHandler", sizeof("VideoHandler")); + } + else { + /* sound handler string, NULL-terminated */ + ngx_rtmp_mp4_data(b, "SoundHandler", sizeof("SoundHandler")); + } + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_vmhd(ngx_buf_t *b) +{ + /* size is always 20, apparently */ + ngx_rtmp_mp4_field_32(b, 20); + + ngx_rtmp_mp4_box(b, "vmhd"); + + /* version and flags */ + ngx_rtmp_mp4_field_32(b, 0x01); + + /* reserved (graphics mode=copy) */ + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_smhd(ngx_buf_t *b) +{ + /* size is always 16, apparently */ + ngx_rtmp_mp4_field_32(b, 16); + + ngx_rtmp_mp4_box(b, "smhd"); + + /* version and flags */ + ngx_rtmp_mp4_field_32(b, 0); + + /* reserved (balance normally=0) */ + ngx_rtmp_mp4_field_16(b, 0); + ngx_rtmp_mp4_field_16(b, 0); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_dref(ngx_buf_t *b, ngx_rtmp_mp4_metadata_t *metadata) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "dref"); + + /* version and flags */ + ngx_rtmp_mp4_field_32(b, 0); + + /* entry count */ + ngx_rtmp_mp4_field_32(b, 1); + + /* url size */ + ngx_rtmp_mp4_field_32(b, 0xc); + + ngx_rtmp_mp4_box(b, "url "); + + /* version and flags */ + ngx_rtmp_mp4_field_32(b, 0x00000001); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_dinf(ngx_buf_t *b, ngx_rtmp_mp4_metadata_t *metadata) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "dinf"); + + ngx_rtmp_mp4_write_dref(b, metadata); + + 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, + ngx_rtmp_mp4_metadata_t *metadata) +{ + u_char *pos, *p; + ngx_chain_t *in; + ngx_rtmp_codec_ctx_t *codec_ctx; + + 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 = ngx_rtmp_mp4_start_box(b, "avcC"); + + /* assume config fits one chunk (highly probable) */ + + /* check for start code */ + for (p = in->buf->pos; p <= in->buf->last; p++) { + if (*p == 0x01) { + break; + } + } + + if (in->buf->last - p > 0) { + ngx_rtmp_mp4_data(b, p, (size_t) (in->buf->last - p)); + } else { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: invalid avcc received"); + } + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static 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 = ngx_rtmp_mp4_start_box(b, "avc1"); + + /* 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) metadata->width); + ngx_rtmp_mp4_field_16(b, (uint16_t) metadata->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, metadata); + + 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, + ngx_rtmp_mp4_metadata_t *metadata) +{ + int decoder_info; + int aac_header_offset; + u_char *pos; + ngx_chain_t *aac; + ngx_rtmp_codec_ctx_t *codec_ctx; + + 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); + aac_header_offset = 2; + } + + pos = ngx_rtmp_mp4_start_box(b, "esds"); + + ngx_rtmp_mp4_field_32(b, 0); /* version */ + /* length of the rest of the box */ + ngx_rtmp_mp4_put_descr(b, 0x03, 21+decoder_info); + ngx_rtmp_mp4_field_16(b, 1); /* track id */ + ngx_rtmp_mp4_field_8(b, 0x00); /* flags */ + /* length of the rest of the box */ + ngx_rtmp_mp4_put_descr(b, 0x04, 13+decoder_info); + ngx_rtmp_mp4_field_8(b, metadata->audio_codec == NGX_RTMP_AUDIO_AAC ? 0x40 : + 0x6B); /* codec id */ + ngx_rtmp_mp4_field_8(b, 0x15); /* audio stream */ + ngx_rtmp_mp4_field_24(b, 0); /* buffersize? */ + /* Next two fields are bitrate. */ + ngx_rtmp_mp4_field_32(b, 0x0001F151); + ngx_rtmp_mp4_field_32(b, 0x0001F14D); + + if (aac) { + ngx_rtmp_mp4_put_descr(b, 0x05, decoder_info); + ngx_rtmp_mp4_data(b, aac->buf->pos + aac_header_offset, + (size_t) 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; +} + + +static 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 = ngx_rtmp_mp4_start_box(b, "mp4a"); + + 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, (uint16_t) metadata->sample_rate); + ngx_rtmp_mp4_field_16(b, 0); /* reserved */ + + ngx_rtmp_mp4_write_esds(s, b, metadata); + + 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; +} + + +static 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 = ngx_rtmp_mp4_start_box(b, "stsd"); + + 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; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_stts(ngx_buf_t *b) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "stts"); + + 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; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_stsc(ngx_buf_t *b) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "stsc"); + + 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; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_stsz(ngx_buf_t *b) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "stsz"); + + 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; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_stco(ngx_buf_t *b) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "stco"); + + 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; +} + + +static 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 = ngx_rtmp_mp4_start_box(b, "stbl"); + + 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; +} + + +static 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 = ngx_rtmp_mp4_start_box(b, "minf"); + + 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; +} + + +static 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 = ngx_rtmp_mp4_start_box(b, "mdia"); + + ngx_rtmp_mp4_write_mdhd(b); + 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; +} + +static 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 = ngx_rtmp_mp4_start_box(b, "trak"); + + 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; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_mvex(ngx_buf_t *b, ngx_rtmp_mp4_metadata_t *metadata) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "mvex"); + + /* just write the trex and mehd in here too */ +#if 0 + 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 */ +#endif + + ngx_rtmp_mp4_field_32(b, 0x20); + + ngx_rtmp_mp4_box(b, "trex"); + + /* version & flags */ + ngx_rtmp_mp4_field_32(b, 0); + + /* track id */ + ngx_rtmp_mp4_field_32(b, 1); + + /* default sample description index */ + ngx_rtmp_mp4_field_32(b, 1); + + /* default sample duration */ + ngx_rtmp_mp4_field_32(b, 0); + + /* default sample size, 1024 for AAC */ + ngx_rtmp_mp4_field_32(b, 1024); + + /* default sample flags, key on */ + ngx_rtmp_mp4_field_32(b, 0); + + 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 = ngx_rtmp_mp4_start_box(b, "moov"); + + 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; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_tfhd(ngx_buf_t *b) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "tfhd"); + + /* version & flags */ + ngx_rtmp_mp4_field_32(b, 0x00020000); + + /* track id */ + ngx_rtmp_mp4_field_32(b, 1); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_tfdt(ngx_buf_t *b, uint32_t earliest_pres_time) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "tfdt"); + + /* version == 1 aka 64 bit integer */ + ngx_rtmp_mp4_field_32(b, 0x00000000); + ngx_rtmp_mp4_field_32(b, earliest_pres_time); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +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) +{ + u_char *pos; + uint32_t i, offset, nitems, flags; + + pos = ngx_rtmp_mp4_start_box(b, "trun"); + + nitems = 0; + + /* data offset present */ + flags = 0x01; + + if (sample_mask & NGX_RTMP_MP4_SAMPLE_DURATION) { + nitems++; + flags |= 0x000100; + } + + if (sample_mask & NGX_RTMP_MP4_SAMPLE_SIZE) { + nitems++; + flags |= 0x000200; + } + + if (sample_mask & NGX_RTMP_MP4_SAMPLE_KEY) { + nitems++; + flags |= 0x000400; + } + + if (sample_mask & NGX_RTMP_MP4_SAMPLE_DELAY) { + nitems++; + flags |= 0x000800; + } + + 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); + ngx_rtmp_mp4_field_32(b, offset); + + for (i = 0; i < sample_count; i++, samples++) { + + if (sample_mask & NGX_RTMP_MP4_SAMPLE_DURATION) { + ngx_rtmp_mp4_field_32(b, samples->duration); + } + + if (sample_mask & NGX_RTMP_MP4_SAMPLE_SIZE) { + ngx_rtmp_mp4_field_32(b, samples->size); + } + + if (sample_mask & NGX_RTMP_MP4_SAMPLE_KEY) { + ngx_rtmp_mp4_field_32(b, samples->key ? 0x00000000 : 0x00010000); + } + + if (sample_mask & NGX_RTMP_MP4_SAMPLE_DELAY) { + ngx_rtmp_mp4_field_32(b, samples->delay); + } + } + + 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) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "traf"); + + 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_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_mfhd(ngx_buf_t *b, uint32_t index) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "mfhd"); + + /* don't know what this is */ + ngx_rtmp_mp4_field_32(b, 0); + + /* fragment index. */ + ngx_rtmp_mp4_field_32(b, index); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +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) +{ + u_char *pos; + uint32_t duration; + + duration = latest_pres_time - earliest_pres_time; + + pos = ngx_rtmp_mp4_start_box(b, "sidx"); + + /* version */ + ngx_rtmp_mp4_field_32(b, 0); + + /* reference id */ + ngx_rtmp_mp4_field_32(b, 1); + + /* timescale */ + ngx_rtmp_mp4_field_32(b, 1000); + + /* earliest presentation time */ + ngx_rtmp_mp4_field_32(b, earliest_pres_time); + + /* first offset */ + ngx_rtmp_mp4_field_32(b, duration); /*TODO*/ + + /* reserved */ + ngx_rtmp_mp4_field_16(b, 0); + + /* reference count = 1 */ + ngx_rtmp_mp4_field_16(b, 1); + + /* 1st bit is reference type, the rest is reference size */ + ngx_rtmp_mp4_field_32(b, reference_size); + + /* subsegment duration */ + ngx_rtmp_mp4_field_32(b, duration); + + /* first bit is startsWithSAP (=1), next 3 bits are SAP type (=001) */ + ngx_rtmp_mp4_field_8(b, 0x90); + + /* SAP delta time */ + ngx_rtmp_mp4_field_24(b, 0); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +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) +{ + 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_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); + + ngx_rtmp_mp4_box(b, "mdat"); + + return NGX_OK; +} diff --git a/dash/ngx_rtmp_mp4.h b/dash/ngx_rtmp_mp4.h new file mode 100644 index 0000000..efcfcd6 --- /dev/null +++ b/dash/ngx_rtmp_mp4.h @@ -0,0 +1,57 @@ + + +#ifndef _NGX_RTMP_MP4_H_INCLUDED_ +#define _NGX_RTMP_MP4_H_INCLUDED_ + + +#include +#include +#include + + +#define NGX_RTMP_MP4_SAMPLE_SIZE 0x01 +#define NGX_RTMP_MP4_SAMPLE_DURATION 0x02 +#define NGX_RTMP_MP4_SAMPLE_DELAY 0x04 +#define NGX_RTMP_MP4_SAMPLE_KEY 0x08 + + +typedef struct { + uint32_t size; + uint32_t duration; + uint32_t delay; + uint32_t timestamp; + unsigned key:1; +} ngx_rtmp_mp4_sample_t; + + +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_uint_t audio_codec; +} ngx_rtmp_mp4_metadata_t; + + +enum { + NGX_RTMP_MP4_FILETYPE_INIT = 0, + NGX_RTMP_MP4_FILETYPE_SEG +}; + + +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, uint32_t earliest_pres_time, + uint32_t sample_count, ngx_rtmp_mp4_sample_t *samples, + ngx_uint_t sample_mask, uint32_t index); +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); + + +#endif /* _NGX_RTMP_MP4_H_INCLUDED_ */ diff --git a/hls/ngx_rtmp_hls_module.c b/hls/ngx_rtmp_hls_module.c index cf3d5ec..4fd0e3d 100644 --- a/hls/ngx_rtmp_hls_module.c +++ b/hls/ngx_rtmp_hls_module.c @@ -295,61 +295,6 @@ ngx_module_t ngx_rtmp_hls_module = { }; -static ngx_int_t -ngx_rtmp_hls_create_parent_dir(ngx_rtmp_session_t *s) -{ - ngx_rtmp_hls_app_conf_t *hacf; - ngx_rtmp_hls_ctx_t *ctx; - ngx_err_t err; - size_t len; - static u_char path[NGX_MAX_PATH + 1]; - - hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); - - if (hacf->path.len == 0) { - return NGX_ERROR; - } - - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "hls: creating target folder: '%V'", &hacf->path); - - if (ngx_create_dir(hacf->path.data, NGX_RTMP_HLS_DIR_ACCESS) != NGX_OK) { - err = ngx_errno; - if (err != NGX_EEXIST) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, err, - "hls: error creating target folder: '%V'", - &hacf->path); - return NGX_ERROR; - } - } - - if (!hacf->nested) { - return NGX_OK; - } - - ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); - - len = hacf->path.len; - if (hacf->path.data[len - 1] == '/') { - len--; - } - - *ngx_snprintf(path, sizeof(path) - 1, "%*s/%V", len, hacf->path.data, - &ctx->name) = 0; - - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "hls: creating nested folder: '%s'", path); - - if (ngx_create_dir(path, NGX_RTMP_HLS_DIR_ACCESS) != NGX_OK) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, - "hls: error creating nested folder: '%s'", path); - return NGX_ERROR; - } - - return NGX_OK; -} - - static ngx_rtmp_hls_frag_t * ngx_rtmp_hls_get_frag(ngx_rtmp_session_t *s, ngx_int_t n) { @@ -472,10 +417,10 @@ ngx_rtmp_hls_write_variant_playlist(ngx_rtmp_session_t *s) ngx_close_file(fd); - rc = ngx_rtmp_hls_rename_file(ctx->var_playlist_bak.data, - ctx->var_playlist.data); - - if (rc != NGX_OK) { + if (ngx_rtmp_hls_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, "hls: rename failed: '%V'->'%V'", &ctx->var_playlist_bak, &ctx->var_playlist); @@ -495,7 +440,6 @@ ngx_rtmp_hls_write_playlist(ngx_rtmp_session_t *s) ngx_rtmp_hls_ctx_t *ctx; ssize_t n; ngx_rtmp_hls_app_conf_t *hacf; - ngx_int_t nretry, rc; ngx_rtmp_hls_frag_t *f; ngx_uint_t i, max_frag; ngx_str_t name_part; @@ -505,26 +449,13 @@ ngx_rtmp_hls_write_playlist(ngx_rtmp_session_t *s) hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); - nretry = 0; - -retry: - fd = ngx_open_file(ctx->playlist_bak.data, NGX_FILE_WRONLY, NGX_FILE_TRUNCATE, NGX_FILE_DEFAULT_ACCESS); if (fd == NGX_INVALID_FILE) { - - if (ngx_errno == NGX_ENOENT && nretry == 0 && - ngx_rtmp_hls_create_parent_dir(s) == NGX_OK) - { - nretry++; - goto retry; - } - ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, "hls: " ngx_open_file_n " failed: '%V'", &ctx->playlist_bak); - return NGX_ERROR; } @@ -590,9 +521,9 @@ retry: ngx_close_file(fd); - rc = ngx_rtmp_hls_rename_file(ctx->playlist_bak.data, ctx->playlist.data); - - if (rc != NGX_OK) { + if (ngx_rtmp_hls_rename_file(ctx->playlist_bak.data, ctx->playlist.data) + == NGX_FILE_ERROR) + { ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, "hls: rename failed: '%V'->'%V'", &ctx->playlist_bak, &ctx->playlist); @@ -842,7 +773,6 @@ ngx_rtmp_hls_open_fragment(ngx_rtmp_session_t *s, uint64_t ts, { ngx_rtmp_hls_ctx_t *ctx; ngx_rtmp_hls_frag_t *f; - ngx_uint_t nretry; uint64_t id; ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); @@ -865,22 +795,10 @@ ngx_rtmp_hls_open_fragment(ngx_rtmp_session_t *s, uint64_t ts, ngx_str_set(&ctx->file.name, "hls"); - nretry = 0; - -retry: - ctx->file.fd = ngx_open_file(ctx->stream.data, NGX_FILE_WRONLY, NGX_FILE_TRUNCATE, NGX_FILE_DEFAULT_ACCESS); if (ctx->file.fd == NGX_INVALID_FILE) { - - if (ngx_errno == NGX_ENOENT && nretry == 0 && - ngx_rtmp_hls_create_parent_dir(s) == NGX_OK) - { - nretry++; - goto retry; - } - ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, "hls: error creating fragment file"); return NGX_ERROR; @@ -1057,6 +975,103 @@ done: } +static ngx_int_t +ngx_rtmp_hls_ensure_directory(ngx_rtmp_session_t *s) +{ + size_t len; + ngx_file_info_t fi; + ngx_rtmp_hls_ctx_t *ctx; + ngx_rtmp_hls_app_conf_t *hacf; + + static u_char path[NGX_MAX_PATH + 1]; + + hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); + + *ngx_snprintf(path, sizeof(path) - 1, "%V", &hacf->path) = 0; + + if (ngx_file_info(path, &fi) == NGX_FILE_ERROR) { + + if (ngx_errno != NGX_ENOENT) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "hls: " ngx_file_info_n " failed on '%V'", + &hacf->path); + return NGX_ERROR; + } + + /* ENOENT */ + + if (ngx_create_dir(path, NGX_RTMP_HLS_DIR_ACCESS) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "hls: " ngx_create_dir_n " failed on '%V'", + &hacf->path); + return NGX_ERROR; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: directory '%V' created", &hacf->path); + + } else { + + if (!ngx_is_dir(&fi)) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: '%V' exists and is not a directory", + &hacf->path); + return NGX_ERROR; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: directory '%V' exists", &hacf->path); + } + + if (!hacf->nested) { + return NGX_OK; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); + + len = hacf->path.len; + if (hacf->path.data[len - 1] == '/') { + len--; + } + + *ngx_snprintf(path, sizeof(path) - 1, "%*s/%V", len, hacf->path.data, + &ctx->name) = 0; + + if (ngx_file_info(path, &fi) != NGX_FILE_ERROR) { + + if (ngx_is_dir(&fi)) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: directory '%s' exists", path); + return NGX_OK; + } + + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: '%s' exists and is not a directory", path); + + return NGX_ERROR; + } + + if (ngx_errno != NGX_ENOENT) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "hls: " ngx_file_info_n " failed on '%s'", path); + return NGX_ERROR; + } + + /* NGX_ENOENT */ + + if (ngx_create_dir(path, NGX_RTMP_HLS_DIR_ACCESS) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "hls: " ngx_create_dir_n " failed on '%s'", path); + return NGX_ERROR; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: directory '%s' created", path); + + return NGX_OK; +} + + static ngx_int_t ngx_rtmp_hls_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) { @@ -1142,9 +1157,11 @@ ngx_rtmp_hls_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) p = ngx_cpymem(p, ctx->name.data, ctx->name.len); - /* ctx->stream_path holds initial part of stream file path + /* + * ctx->stream holds initial part of stream file path * however the space for the whole stream path - * is allocated */ + * is allocated + */ ctx->stream.len = p - ctx->playlist.data + 1; ctx->stream.data = ngx_palloc(s->connection->pool, @@ -1228,6 +1245,10 @@ ngx_rtmp_hls_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) ngx_rtmp_hls_restore_stream(s); } + if (ngx_rtmp_hls_ensure_directory(s) != NGX_OK) { + return NGX_ERROR; + } + next: return next_publish(s, v); } @@ -1920,7 +1941,7 @@ ngx_rtmp_hls_cleanup_dir(ngx_str_t *ppath, ngx_msec_t playlen) ngx_log_error(NGX_LOG_CRIT, ngx_cycle->log, err, "hls: cleanup " ngx_read_dir_n - " \"%V\" failed", ppath); + " '%V' failed", ppath); return NGX_ERROR; } @@ -1939,7 +1960,7 @@ ngx_rtmp_hls_cleanup_dir(ngx_str_t *ppath, ngx_msec_t playlen) nentries++; - if (ngx_de_info(path, &dir) == NGX_FILE_ERROR) { + if (!dir.valid_info && ngx_de_info(path, &dir) == NGX_FILE_ERROR) { ngx_log_error(NGX_LOG_CRIT, ngx_cycle->log, ngx_errno, "hls: cleanup " ngx_de_info_n " \"%V\" failed", &spath); @@ -1953,9 +1974,17 @@ ngx_rtmp_hls_cleanup_dir(ngx_str_t *ppath, ngx_msec_t playlen) ngx_log_debug1(NGX_LOG_DEBUG_RTMP, ngx_cycle->log, 0, "hls: cleanup dir '%V'", &name); - if (ngx_delete_dir(spath.data) != NGX_OK) { + /* + * null-termination gets spoiled in win32 + * version of ngx_open_dir + */ + + *p = 0; + + if (ngx_delete_dir(path) == NGX_FILE_ERROR) { ngx_log_error(NGX_LOG_ERR, ngx_cycle->log, ngx_errno, - "hls: cleanup dir error '%V'", &spath); + "hls: cleanup " ngx_delete_dir_n + " failed on '%V'", &spath); } else { nerased++; } @@ -1997,9 +2026,10 @@ ngx_rtmp_hls_cleanup_dir(ngx_str_t *ppath, ngx_msec_t playlen) "hls: cleanup '%V' mtime=%T age=%T", &name, mtime, ngx_cached_time->sec - mtime); - if (ngx_delete_file(spath.data) != NGX_OK) { + if (ngx_delete_file(path) == NGX_FILE_ERROR) { ngx_log_error(NGX_LOG_ERR, ngx_cycle->log, ngx_errno, - "hls: cleanup error '%V'", &spath); + "hls: cleanup " ngx_delete_file_n " failed on '%V'", + &spath); continue; }