nginx-mod-rtmp/dash/ngx_rtmp_dash_module.c
2016-06-20 14:53:09 +03:00

1718 lines
54 KiB
C

#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_rtmp.h>
#include <ngx_rtmp_codec_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_rtmp_playlist_pt next_playlist;
static ngx_int_t ngx_rtmp_dash_postconfiguration(ngx_conf_t *cf);
static void * ngx_rtmp_dash_create_app_conf(ngx_conf_t *cf);
static char * ngx_rtmp_dash_merge_app_conf(ngx_conf_t *cf,
void *parent, void *child);
static ngx_int_t ngx_rtmp_dash_write_init_segments(ngx_rtmp_session_t *s);
static ngx_int_t ngx_rtmp_dash_ensure_directory(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
#define NGX_RTMP_DASH_GMT_LENGTH sizeof("1970-09-28T12:00:00+06:00")
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;
#define NGX_RTMP_DASH_CLOCK_COMPENSATION_OFF 1
#define NGX_RTMP_DASH_CLOCK_COMPENSATION_NTP 2
#define NGX_RTMP_DASH_CLOCK_COMPENSATION_HTTP_HEAD 3
#define NGX_RTMP_DASH_CLOCK_COMPENSATION_HTTP_ISO 4
static ngx_conf_enum_t ngx_rtmp_dash_clock_compensation_type_slots[] = {
{ ngx_string("off"), NGX_RTMP_DASH_CLOCK_COMPENSATION_OFF },
{ ngx_string("ntp"), NGX_RTMP_DASH_CLOCK_COMPENSATION_NTP },
{ ngx_string("http_head"), NGX_RTMP_DASH_CLOCK_COMPENSATION_HTTP_HEAD },
{ ngx_string("http_iso"), NGX_RTMP_DASH_CLOCK_COMPENSATION_HTTP_ISO },
{ ngx_null_string, 0 }
};
typedef struct {
ngx_flag_t dash;
ngx_msec_t fraglen;
ngx_msec_t playlen;
ngx_flag_t nested;
ngx_uint_t clock_compensation; // Try to compensate clock drift
// between client and server (on client side)
ngx_str_t clock_helper_uri; // Use uri to static file on HTTP server
// - same machine as RTMP/DASH)
// - or NTP server address
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_string("dash_clock_compensation"),
NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1,
ngx_conf_set_enum_slot,
NGX_RTMP_APP_CONF_OFFSET,
offsetof(ngx_rtmp_dash_app_conf_t, clock_compensation),
&ngx_rtmp_dash_clock_compensation_type_slots },
{ ngx_string("dash_clock_helper_uri"),
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, clock_helper_uri),
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, frame_rate_num, frame_rate_denom;
ngx_uint_t depth_msec, depth_sec;
ngx_uint_t update_period, update_period_msec;
ngx_uint_t buffer_time, buffer_time_msec;
ngx_uint_t presentation_delay, presentation_delay_msec;
ngx_rtmp_dash_ctx_t *ctx;
ngx_rtmp_codec_ctx_t *codec_ctx;
ngx_rtmp_dash_frag_t *f;
ngx_rtmp_dash_app_conf_t *dacf;
ngx_rtmp_playlist_t v;
static u_char buffer[NGX_RTMP_DASH_BUFSIZE];
static u_char avaliable_time[NGX_RTMP_DASH_GMT_LENGTH];
static u_char publish_time[NGX_RTMP_DASH_GMT_LENGTH];
static u_char buffer_depth[sizeof("P00Y00M00DT00H00M00.000S")];
static u_char frame_rate[(NGX_INT_T_LEN * 2) + 2];
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 \
"<?xml version=\"1.0\"?>\n" \
"<MPD\n" \
" type=\"dynamic\"\n" \
" xmlns=\"urn:mpeg:dash:schema:mpd:2011\"\n" \
" availabilityStartTime=\"%s\"\n" \
" publishTime=\"%s\"\n" \
" minimumUpdatePeriod=\"PT%ui.%03uiS\"\n" \
" minBufferTime=\"PT%ui.%03uiS\"\n" \
" timeShiftBufferDepth=\"%s\"\n" \
" suggestedPresentationDelay=\"PT%ui.%03uiS\"\n" \
" profiles=\"urn:hbbtv:dash:profile:isoff-live:2012," \
"urn:mpeg:dash:profile:isoff-live:2011\"\n" \
" xmlns:xsi=\"http://www.w3.org/2011/XMLSchema-instance\"\n" \
" xsi:schemaLocation=\"urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd\">\n"
#define NGX_RTMP_DASH_MANIFEST_CLOCK \
" <UTCTiming schemeIdUri=\"urn:mpeg:dash:utc:%s:2014\"\n" \
" value=\"%V\" />\n" \
#define NGX_RTMP_DASH_MANIFEST_PERIOD \
" <Period start=\"PT0S\" id=\"dash\">\n"
#define NGX_RTMP_DASH_MANIFEST_VIDEO \
" <AdaptationSet\n" \
" id=\"1\"\n" \
" segmentAlignment=\"true\"\n" \
" startWithSAP=\"1\"\n" \
" maxWidth=\"%ui\"\n" \
" maxHeight=\"%ui\"\n" \
" maxFrameRate=\"%s\">\n" \
" <Representation\n" \
" id=\"%V_H264\"\n" \
" mimeType=\"video/mp4\"\n" \
" codecs=\"avc1.%02uxi%02uxi%02uxi\"\n" \
" width=\"%ui\"\n" \
" height=\"%ui\"\n" \
" frameRate=\"%s\"\n" \
" sar=\"1:1\"\n" \
" bandwidth=\"%ui\">\n" \
" <SegmentTemplate\n" \
" presentationTimeOffset=\"0\"\n" \
" timescale=\"1000\"\n" \
" media=\"%V%s$Time$.m4v\"\n" \
" initialization=\"%V%sinit.m4v\">\n" \
" <SegmentTimeline>\n"
#define NGX_RTMP_DASH_MANIFEST_VIDEO_FOOTER \
" </SegmentTimeline>\n" \
" </SegmentTemplate>\n" \
" </Representation>\n" \
" </AdaptationSet>\n"
#define NGX_RTMP_DASH_MANIFEST_TIME \
" <S t=\"%uD\" d=\"%uD\"/>\n"
#define NGX_RTMP_DASH_MANIFEST_AUDIO \
" <AdaptationSet\n" \
" id=\"2\"\n" \
" startWithSAP=\"1\"\n" \
" segmentAlignment=\"true\">\n" \
" <AudioChannelConfiguration\n" \
" schemeIdUri=\"urn:mpeg:dash:" \
"23003:3:audio_channel_configuration:2011\"\n" \
" value=\"1\"/>\n" \
" <Representation\n" \
" id=\"%V_AAC\"\n" \
" mimeType=\"audio/mp4\"\n" \
" codecs=\"mp4a.%s\"\n" \
" audioSamplingRate=\"%ui\"\n" \
" bandwidth=\"%ui\">\n" \
" <SegmentTemplate\n" \
" presentationTimeOffset=\"0\"\n" \
" timescale=\"1000\"\n" \
" media=\"%V%s$Time$.m4a\"\n" \
" initialization=\"%V%sinit.m4a\">\n" \
" <SegmentTimeline>\n"
#define NGX_RTMP_DASH_MANIFEST_AUDIO_FOOTER \
" </SegmentTimeline>\n" \
" </SegmentTemplate>\n" \
" </Representation>\n" \
" </AdaptationSet>\n"
#define NGX_RTMP_DASH_MANIFEST_FOOTER \
" </Period>\n" \
"</MPD>\n"
/**
* Availability time must be equal stream start time
* Cos segments time counting from it
*/
ngx_libc_gmtime(ctx->start_time.sec, &tm);
*ngx_sprintf(avaliable_time, "%4d-%02d-%02dT%02d:%02d:%02dZ",
tm.tm_year + 1900, tm.tm_mon + 1,
tm.tm_mday, tm.tm_hour,
tm.tm_min, tm.tm_sec
) = 0;
/* Stream publish time */
*ngx_sprintf(publish_time, "%s", avaliable_time) = 0;
depth_sec = (ngx_uint_t) (
ngx_rtmp_dash_get_frag(s, ctx->nfrags - 1)->timestamp +
ngx_rtmp_dash_get_frag(s, ctx->nfrags - 1)->duration -
ngx_rtmp_dash_get_frag(s, 0)->timestamp);
depth_msec = depth_sec % 1000;
depth_sec -= depth_msec;
depth_sec /= 1000;
ngx_libc_gmtime(depth_sec, &tm);
*ngx_sprintf(buffer_depth, "P%dY%02dM%02dDT%dH%02dM%02d.%03dS",
tm.tm_year - 70, tm.tm_mon,
tm.tm_mday - 1, tm.tm_hour,
tm.tm_min, tm.tm_sec,
depth_msec) = 0;
last = buffer + sizeof(buffer);
/**
* Calculate playlist minimal update period
* This should be more than biggest segment duration
* Cos segments rounded by keyframe/GOP.
* And that time not always equals to fragment length.
*/
update_period = dacf->fraglen;
for (i = 0; i < ctx->nfrags; i++) {
f = ngx_rtmp_dash_get_frag(s, i);
if (f->duration > update_period) {
update_period = f->duration;
}
}
// Reasonable delay for streaming
presentation_delay = update_period * 2 + 1000;
presentation_delay_msec = presentation_delay % 1000;
presentation_delay -= presentation_delay_msec;
presentation_delay /= 1000;
// Calculate msec part and seconds
update_period_msec = update_period % 1000;
update_period -= update_period_msec;
update_period /= 1000;
// Buffer length by default fragment length
buffer_time = dacf->fraglen;
buffer_time_msec = buffer_time % 1000;
buffer_time -= buffer_time_msec;
buffer_time /= 1000;
// Fill DASH header
p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_MANIFEST_HEADER,
// availabilityStartTime
avaliable_time,
// publishTime
publish_time,
// minimumUpdatePeriod
update_period, update_period_msec,
// minBufferTime
buffer_time, buffer_time_msec,
// timeShiftBufferDepth
buffer_depth,
// suggestedPresentationDelay
presentation_delay, presentation_delay_msec
);
/* UTCTiming value */
switch (dacf->clock_compensation) {
case NGX_RTMP_DASH_CLOCK_COMPENSATION_NTP:
p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_CLOCK,
"ntp",
&dacf->clock_helper_uri
);
break;
case NGX_RTMP_DASH_CLOCK_COMPENSATION_HTTP_HEAD:
p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_CLOCK,
"http-head",
&dacf->clock_helper_uri
);
break;
case NGX_RTMP_DASH_CLOCK_COMPENSATION_HTTP_ISO:
p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_CLOCK,
"http-iso",
&dacf->clock_helper_uri
);
break;
}
p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_PERIOD);
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) {
frame_rate_num = (ngx_uint_t) (codec_ctx->frame_rate * 1000.);
if (frame_rate_num % 1000 == 0) {
*ngx_sprintf(frame_rate, "%ui", frame_rate_num / 1000) = 0;
} else {
frame_rate_denom = 1000;
switch (frame_rate_num) {
case 23976:
frame_rate_num = 24000;
frame_rate_denom = 1001;
break;
case 29970:
frame_rate_num = 30000;
frame_rate_denom = 1001;
break;
case 59940:
frame_rate_num = 60000;
frame_rate_denom = 1001;
break;
}
*ngx_sprintf(frame_rate, "%ui/%ui", frame_rate_num, frame_rate_denom) = 0;
}
p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_MANIFEST_VIDEO,
codec_ctx->width,
codec_ctx->height,
frame_rate,
&ctx->name,
codec_ctx->avc_profile,
codec_ctx->avc_compat,
codec_ctx->avc_level,
codec_ctx->width,
codec_ctx->height,
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 ?
(codec_ctx->aac_sbr ? "40.5" : "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;
}
ngx_memzero(&v, sizeof(v));
ngx_str_set(&(v.module), "dash");
v.playlist.data = ctx->playlist.data;
v.playlist.len = ctx->playlist.len;
return next_playlist(s, &v);
}
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;
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;
}
/* 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;
ngx_rtmp_mp4_write_ftyp(&b);
ngx_rtmp_mp4_write_moov(s, &b, NGX_RTMP_MP4_VIDEO_TRACK);
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;
ngx_rtmp_mp4_write_ftyp(&b);
ngx_rtmp_mp4_write_moov(s, &b, NGX_RTMP_MP4_AUDIO_TRACK);
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_styp(&b);
pos = b.last;
b.last += 44; /* leave room for sidx */
ngx_rtmp_mp4_write_moof(&b, t->earliest_pres_time, t->sample_count,
t->samples, t->sample_mask, t->id);
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 == NULL || !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;
}
if (ngx_rtmp_dash_ensure_directory(s) != NGX_OK) {
return NGX_ERROR;
}
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)
{
int32_t d;
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);
ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"dash: update_fragments: timestamp=%ui, f-timestamp=%ui, boundary=%i, dacf-fraglen=%ui",
timestamp, f->timestamp, boundary, dacf->fraglen);
d = (int32_t) (timestamp - f->timestamp);
if (d >= 0) {
f->duration = timestamp - f->timestamp;
hit = (f->duration >= dacf->fraglen);
} else {
/* sometimes clients generate slightly unordered frames */
hit = (-d > 1000);
}
ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"dash: update_fragments: d=%i, f-duration=%ui, hit=%i",
d, f->duration, hit);
if (ctx->has_video && !hit) {
boundary = 0;
ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"dash: update_fragments: boundary=0 cos has_video && !hit");
}
if (!ctx->has_video && ctx->has_audio) {
boundary = hit;
ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"dash: update_fragments: boundary=hit cos !has_video && has_audio");
}
if (ctx->audio.mdat_size >= NGX_RTMP_DASH_MAX_MDAT) {
boundary = 1;
ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"dash: update_fragments: boundary=1 cos audio max mdat");
}
if (ctx->video.mdat_size >= NGX_RTMP_DASH_MAX_MDAT) {
boundary = 1;
ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"dash: update_fragments: boundary=1 cos video max mdat");
}
if (!ctx->opened) {
boundary = 1;
ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"dash: update_fragments: boundary=1 cos !opened");
}
ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"dash: update_fragments: boundary=%i",
boundary);
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;
ngx_rtmp_mp4_sample_t *smpl;
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;
}
smpl = &t->samples[t->sample_count];
smpl->delay = delay;
smpl->size = (uint32_t) size;
smpl->duration = 0;
smpl->timestamp = timestamp;
smpl->key = (key ? 1 : 0);
if (t->sample_count > 0) {
smpl = &t->samples[t->sample_count - 1];
smpl->duration = timestamp - smpl->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 / 500;
} 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);
// Next callback in doubled playlist length time to make sure what all
// players read all segments
return cleanup->playlen / 500;
}
static ngx_int_t
ngx_rtmp_dash_playlist(ngx_rtmp_session_t *s, ngx_rtmp_playlist_t *v)
{
return next_playlist(s, v);
}
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;
conf->clock_compensation = 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_value(conf->cleanup, prev->cleanup, 1);
ngx_conf_merge_value(conf->nested, prev->nested, 0);
ngx_conf_merge_uint_value(conf->clock_compensation, prev->clock_compensation,
NGX_RTMP_DASH_CLOCK_COMPENSATION_OFF);
ngx_conf_merge_str_value(conf->clock_helper_uri, prev->clock_helper_uri, "");
if (conf->fraglen) {
conf->winfrags = conf->playlen / conf->fraglen;
}
/* schedule cleanup */
if (conf->dash && conf->path.len && conf->cleanup) {
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;
}
}
ngx_conf_merge_str_value(conf->path, prev->path, "");
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;
next_playlist = ngx_rtmp_playlist;
ngx_rtmp_playlist = ngx_rtmp_dash_playlist;
return NGX_OK;
}