diff --git a/config b/config index 7f8a372..2bdb47f 100644 --- a/config +++ b/config @@ -19,6 +19,7 @@ CORE_MODULES="$CORE_MODULES ngx_rtmp_notify_module \ ngx_rtmp_log_module \ ngx_rtmp_limit_module \ + ngx_rtmp_hls_module \ " @@ -41,6 +42,7 @@ NGX_ADDON_DEPS="$NGX_ADDON_DEPS \ $ngx_addon_dir/ngx_rtmp_record_module.h \ $ngx_addon_dir/ngx_rtmp_relay_module.h \ $ngx_addon_dir/ngx_rtmp_streams.h \ + $ngx_addon_dir/hls/ngx_rtmp_mpegts.h \ " @@ -74,6 +76,8 @@ NGX_ADDON_SRCS="$NGX_ADDON_SRCS \ $ngx_addon_dir/ngx_rtmp_notify_module.c \ $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/hls/ngx_rtmp_mpegts.c \ " CFLAGS="$CFLAGS -I$ngx_addon_dir" diff --git a/hls/README.md b/hls/README.md index df3915f..385c5d5 100644 --- a/hls/README.md +++ b/hls/README.md @@ -1,9 +1 @@ -# HLS (HTTP Live Streaming) module - -This module should be added explicitly when building NGINX: - - ./configure ... --add-module=/path/to/nginx-rtmp-module/hls ... - -## Requirement - -The module requires ffmpeg version>= 53.31.100 from ffmpeg (ffmpeg.org) +HLS is now a part of rtmp module diff --git a/hls/config b/hls/config deleted file mode 100644 index 24624cb..0000000 --- a/hls/config +++ /dev/null @@ -1,13 +0,0 @@ -ngx_addon_name="ngx_rtmp_hls_module" - -CORE_MODULES="$CORE_MODULES - ngx_rtmp_hls_module \ - " - - -NGX_ADDON_SRCS="$NGX_ADDON_SRCS \ - $ngx_addon_dir/ngx_rtmp_hls_module.c \ - " - -CORE_LIBS="$CORE_LIBS -lavformat -lavcodec -lavutil" - diff --git a/hls/ngx_rtmp_hls_module.c b/hls/ngx_rtmp_hls_module.c index c5d2030..1923e6c 100644 --- a/hls/ngx_rtmp_hls_module.c +++ b/hls/ngx_rtmp_hls_module.c @@ -1,16 +1,12 @@ /* - * Copyright (c) 2012 Roman Arutyunyan + * Copyright (c) 2013 Roman Arutyunyan */ #include #include #include - - -#include -#include -#include +#include "ngx_rtmp_mpegts.h" static ngx_rtmp_publish_pt next_publish; @@ -20,37 +16,7 @@ static ngx_rtmp_delete_stream_pt next_delete_stream; static ngx_int_t ngx_rtmp_hls_postconfiguration(ngx_conf_t *cf); static void * ngx_rtmp_hls_create_app_conf(ngx_conf_t *cf); static char * ngx_rtmp_hls_merge_app_conf(ngx_conf_t *cf, - void *parent, void *child); - - -static ngx_log_t *ngx_rtmp_hls_log; - -static void -ngx_rtmp_hls_av_log_callback(void* avcl, int level, const char* fmt, - va_list args) -{ - char *p; - static char buf[1024]; - int n; - - - n = vsnprintf(buf, sizeof(buf), fmt, args); - buf[n] = 0; - - for (p = buf; *p; ++p) { - if (*p == (u_char)'\n' || *p == (u_char)'\r') { - *p = ' '; - } - } - - if (level <= AV_LOG_ERROR) { - ngx_log_error_core(NGX_LOG_ERR, ngx_rtmp_hls_log, 0, "hls: av: %s", - buf); - return; - } - - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, ngx_rtmp_hls_log, 0, "hls: av: %s", buf); -} + void *parent, void *child); #define NGX_RTMP_HLS_BUFSIZE (1024*1024) @@ -59,31 +25,25 @@ ngx_rtmp_hls_av_log_callback(void* avcl, int level, const char* fmt, typedef struct { - ngx_uint_t flags; - ngx_msec_t frag_start; - unsigned publishing:1; unsigned opened:1; - unsigned audio:1; - unsigned video:1; - unsigned header_sent:1; + + ngx_file_t file; ngx_str_t playlist; ngx_str_t playlist_bak; ngx_str_t stream; ngx_str_t name; - ngx_int_t frag; + uint64_t frag; - ngx_int_t out_vstream; - ngx_int_t out_astream; - int8_t nal_bytes; + ngx_uint_t audio_cc; + ngx_uint_t video_cc; - int64_t aframe_base; - int64_t aframe_num; - - AVFormatContext *out_format; + uint64_t aframe_base; + uint64_t aframe_num; + uint64_t offset; } ngx_rtmp_hls_ctx_t; @@ -93,10 +53,11 @@ typedef struct { ngx_msec_t muxdelay; ngx_msec_t sync; ngx_msec_t playlen; - size_t nfrags; + ngx_int_t factor; + ngx_uint_t winfrags; + ngx_uint_t nfrags; ngx_flag_t continuous; - ngx_rtmp_hls_ctx_t **ctx; - ngx_uint_t nbuckets; + ngx_flag_t nodelete; ngx_str_t path; } ngx_rtmp_hls_app_conf_t; @@ -152,6 +113,20 @@ static ngx_command_t ngx_rtmp_hls_commands[] = { offsetof(ngx_rtmp_hls_app_conf_t, continuous), NULL }, + { ngx_string("hls_nodelete"), + 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_hls_app_conf_t, nodelete), + NULL }, + + { ngx_string("hls_playlist_factor"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_num_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_hls_app_conf_t, factor), + NULL }, + ngx_null_command }; @@ -187,201 +162,27 @@ ngx_module_t ngx_rtmp_hls_module = { }; -/* convert Flash codec id to FFmpeg codec id */ -#if 0 -static enum CodecID -ngx_rtmp_hls_get_video_codec(ngx_int_t cid) +#define ngx_rtmp_hls_frag(hacf, f) (hacf->nfrags ? (f) % hacf->nfrags : (f)) + + +static void +ngx_rtmp_hls_chain2buffer(ngx_buf_t *out, ngx_chain_t *in, size_t skip) { - switch (cid) { - case 1: - return CODEC_ID_JPEG2000; /* JPEG */ - case 2: - return CODEC_ID_FLV1; /* Sorensen H.263 */ - case 3: - return CODEC_ID_FLASHSV; /* Screen video */ - case 4: - return CODEC_ID_VP6F; /* On2 VP6 */ - case 5: - return CODEC_ID_VP6A; /* On2 VP6 Alpha */ - case 6: - return CODEC_ID_FLASHSV2; /* Screen Video 2 */ - case 7: - return CODEC_ID_H264; /* H264 / MPEG4-AVC */ - default: - return CODEC_ID_NONE; - } -} -#endif - -static enum CodecID -ngx_rtmp_hls_get_audio_codec(ngx_int_t cid) -{ - switch (cid) { - case 0: - return CODEC_ID_NONE; /* Uncompressed */ - case 1: - return CODEC_ID_ADPCM_SWF; /* ADPCM */ - case 2: - case 14: - return CODEC_ID_MP3; /* Mp3 */ - case 4: - case 5: - case 6: - return CODEC_ID_NELLYMOSER; /* Nellymoser */ - case 7: - return CODEC_ID_PCM_ALAW; /* G711 */ - case 8: - return CODEC_ID_PCM_MULAW; /* G711Mu */ - case 10: - return CODEC_ID_AAC; /* AAC */ - case 11: - return CODEC_ID_SPEEX; /* Speex */ - default: - return CODEC_ID_NONE; - } -} - - -static size_t -ngx_rtmp_hls_chain2buffer(u_char *buffer, size_t size, ngx_chain_t *in, - size_t skip) -{ - ngx_buf_t out; - - out.pos = buffer; - out.last = buffer + size - FF_INPUT_BUFFER_PADDING_SIZE; + size_t size; for (; in; in = in->next) { + size = in->buf->last - in->buf->pos; if (size < skip) { skip -= size; continue; } - out.pos = ngx_cpymem(out.pos, in->buf->pos + skip, ngx_min( - size - skip, (size_t)(out.last - out.pos))); + + out->last = ngx_cpymem(out->last, in->buf->pos + skip, + ngx_min(size - skip, + (size_t) (out->end - out->last))); skip = 0; } - - return out.pos - buffer; -} - - -static ngx_int_t -ngx_rtmp_hls_init_video(ngx_rtmp_session_t *s) -{ - AVStream *stream; - ngx_rtmp_hls_ctx_t *ctx; - ngx_rtmp_codec_ctx_t *codec_ctx; - - - ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); - codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); - - if (codec_ctx == NULL - || codec_ctx->video_codec_id != NGX_RTMP_VIDEO_H264 - || ctx->video == 1 - || ctx->out_format == NULL) - { - return NGX_OK; - } - - ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "hls: adding video stream"); - - stream = avformat_new_stream(ctx->out_format, NULL); - if (stream == NULL) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "hls: av_new_stream failed (video)"); - return NGX_ERROR; - } - - stream->codec->codec_id = CODEC_ID_H264; - stream->codec->codec_type = AVMEDIA_TYPE_VIDEO; - stream->codec->pix_fmt = PIX_FMT_YUV420P; - stream->codec->time_base.den = 25; - stream->codec->time_base.num = 1; - stream->codec->width = 100; - stream->codec->height = 100; - - if (ctx->out_format->oformat->flags & AVFMT_GLOBALHEADER) { - stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER; - } - - ctx->out_vstream = stream->index; - ctx->video = 1; - - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "hls: video stream: %i", ctx->out_vstream); - - /*if (ctx->header_sent) { - if (av_write_trailer(ctx->out_format) < 0) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "hls: av_write_trailer failed"); - } - ctx->header_sent = 0; - }*/ - - return NGX_OK; -} - - -static ngx_int_t -ngx_rtmp_hls_init_audio(ngx_rtmp_session_t *s) -{ - AVStream *stream; - ngx_rtmp_hls_ctx_t *ctx; - ngx_rtmp_codec_ctx_t *codec_ctx; - enum CodecID cid; - - - ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); - codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); - - if (codec_ctx == NULL || ctx->audio == 1 || ctx->out_format == NULL) { - return NGX_OK; - } - - ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "hls: adding audio stream"); - - cid = ngx_rtmp_hls_get_audio_codec(codec_ctx->audio_codec_id); - if (cid == CODEC_ID_NONE) { - ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "hls: no audio"); - return NGX_OK; - } - - stream = avformat_new_stream(ctx->out_format, NULL); - if (stream == NULL) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "hls: av_new_stream failed (audio)"); - return NGX_ERROR; - } - - stream->codec->codec_id = cid; - stream->codec->codec_type = AVMEDIA_TYPE_AUDIO; - stream->codec->sample_fmt = (codec_ctx->sample_size == 1 ? - AV_SAMPLE_FMT_U8 : AV_SAMPLE_FMT_S16); - stream->codec->sample_rate = 48000;/*codec_ctx->sample_rate;*/ - stream->codec->bit_rate = 2000000; - stream->codec->channels = codec_ctx->audio_channels; - - ctx->out_astream = stream->index; - ctx->audio = 1; - - ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "hls: audio stream: %i %iHz", - ctx->out_astream, codec_ctx->sample_rate); -/* - if (ctx->header_sent) { - if (av_write_trailer(ctx->out_format) < 0) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "hls: av_write_trailer failed"); - } - ctx->header_sent = 0; - } -*/ - return NGX_OK; } @@ -393,67 +194,68 @@ ngx_rtmp_hls_update_playlist(ngx_rtmp_session_t *s) u_char *p; ngx_rtmp_hls_ctx_t *ctx; ssize_t n; - ngx_int_t ffrag; + uint64_t ffrag; ngx_rtmp_hls_app_conf_t *hacf; ngx_int_t nretry; 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); + NGX_FILE_TRUNCATE, NGX_FILE_DEFAULT_ACCESS); + if (fd == NGX_INVALID_FILE) { ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, - "hls: open failed: '%V'", - &ctx->playlist_bak); + "hls: open failed: '%V'", &ctx->playlist_bak); + /* try to create parent folder */ + if (nretry == 0 && ngx_create_dir(hacf->path.data, NGX_RTMP_HLS_DIR_ACCESS) != NGX_INVALID_FILE) { ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "hls: creating target folder: '%V'", &hacf->path); + "hls: creating target folder: '%V'", &hacf->path); ++nretry; goto retry; } + return NGX_ERROR; } - ffrag = ctx->frag - hacf->nfrags; - if (ffrag < 1) { - ffrag = 1; - } + ffrag = ctx->frag > hacf->winfrags ? + ctx->frag - (uint64_t) hacf->winfrags : 1; p = ngx_snprintf(buffer, sizeof(buffer), - "#EXTM3U\r\n" - "#EXT-X-TARGETDURATION:%i\r\n" - "#EXT-X-ALLOW-CACHE:NO\r\n" - "#EXT-X-MEDIA-SEQUENCE:%i\r\n\r\n", - /*TODO: float*/(ngx_int_t)(hacf->fraglen / 1000), ffrag); + "#EXTM3U\r\n" + "#EXT-X-MEDIA-SEQUENCE:%uL\r\n" + "#EXT-X-TARGETDURATION:%ui\r\n" + "#EXT-X-ALLOW-CACHE:NO\r\n\r\n", + ctx->frag, (ngx_uint_t) (hacf->fraglen / 1000)); + n = write(fd, buffer, p - buffer); if (n < 0) { ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, - "hls: write failed: '%V'", - &ctx->playlist_bak); + "hls: write failed: '%V'", &ctx->playlist_bak); ngx_close_file(fd); return NGX_ERROR; } for (; ffrag < ctx->frag; ++ffrag) { p = ngx_snprintf(buffer, sizeof(buffer), - "#EXTINF:%i,\r\n" - "%V-%i.ts\r\n", - /*TODO:float*/(ngx_int_t)(hacf->fraglen / 1000), - &ctx->name, ffrag); + "#EXTINF:%i,\r\n" + "%V-%uL.ts\r\n", + (ngx_int_t) (hacf->fraglen / 1000), &ctx->name, + ngx_rtmp_hls_frag(hacf, ffrag)); + n = write(fd, buffer, p - buffer); if (n < 0) { ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, - "hls: write failed: '%V'", - &ctx->playlist_bak); + "hls: write failed '%V'", &ctx->playlist_bak); ngx_close_file(fd); return NGX_ERROR; } @@ -463,8 +265,8 @@ retry: if (ngx_rename_file(ctx->playlist_bak.data, ctx->playlist.data)) { ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, - "hls: rename failed: '%V'->'%V'", - &ctx->playlist_bak, &ctx->playlist); + "hls: rename failed: '%V'->'%V'", + &ctx->playlist_bak, &ctx->playlist); return NGX_ERROR; } @@ -472,113 +274,12 @@ retry: } -static ngx_int_t -ngx_rtmp_hls_initialize(ngx_rtmp_session_t *s) -{ - ngx_rtmp_hls_ctx_t *ctx; - ngx_rtmp_hls_app_conf_t *hacf; - AVOutputFormat *format; - - hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); - ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); - if (ctx == NULL || ctx->out_format || ctx->publishing == 0) { - return NGX_OK; - } - - ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "hls: initialize stream"); - - /* create output format */ - format = av_guess_format("mpegts", NULL, NULL); - if (format == NULL) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "hls: av_guess_format failed"); - return NGX_ERROR; - } - ctx->out_format = avformat_alloc_context(); - if (ctx->out_format == NULL) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "hls: avformat_alloc_context failed"); - return NGX_ERROR; - } - ctx->out_format->oformat = format; - ctx->out_format->max_delay = (int64_t)hacf->muxdelay * AV_TIME_BASE / 1000; - - return NGX_ERROR; -} - - -static ngx_int_t -ngx_rtmp_hls_open_file(ngx_rtmp_session_t *s, u_char *fpath) -{ - ngx_rtmp_hls_ctx_t *ctx; - ngx_rtmp_codec_ctx_t *codec_ctx; - AVStream *astream; - static u_char buffer[NGX_RTMP_HLS_BUFSIZE]; - - ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); - codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); - if (ctx == NULL || ctx->out_format == NULL) { - return NGX_OK; - } - - if (!ctx->video && !ctx->audio) { - return NGX_OK; - } - - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "hls: open stream file '%s'", fpath); - - /* open file */ - if (avio_open(&ctx->out_format->pb, (char *)fpath, AVIO_FLAG_WRITE) < 0) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "hls: avio_open failed"); - return NGX_ERROR; - } - - astream = NULL; - if (codec_ctx && codec_ctx->audio_codec_id == NGX_RTMP_AUDIO_AAC - && codec_ctx->aac_header && codec_ctx->aac_header->buf->last - - codec_ctx->aac_header->buf->pos > 2 - && ctx->audio) - { - astream = ctx->out_format->streams[ctx->out_astream]; - astream->codec->extradata = buffer; - astream->codec->extradata_size = ngx_rtmp_hls_chain2buffer(buffer, - sizeof(buffer), codec_ctx->aac_header, 2); - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "hls: setting AAC extradata %i bytes", - (ngx_int_t)astream->codec->extradata_size); - } - - /* write header */ - if (!ctx->header_sent && - avformat_write_header(ctx->out_format, NULL) < 0) - { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "hls: avformat_write_header failed"); - return NGX_ERROR; - } - ctx->header_sent = 1; - - if (astream) { - astream->codec->extradata = NULL; - astream->codec->extradata_size = 0; - } - - ctx->opened = 1; - ctx->nal_bytes = -1; - - return NGX_OK; -} - - static ngx_int_t ngx_rtmp_hls_copy(ngx_rtmp_session_t *s, void *dst, u_char **src, size_t n, - ngx_chain_t **in) + ngx_chain_t **in) { - u_char *last; - size_t pn; + u_char *last; + size_t pn; if (*in == NULL) { return NGX_ERROR; @@ -586,39 +287,62 @@ ngx_rtmp_hls_copy(ngx_rtmp_session_t *s, void *dst, u_char **src, size_t n, for ( ;; ) { last = (*in)->buf->last; + if ((size_t)(last - *src) >= n) { if (dst) { ngx_memcpy(dst, *src, n); } + *src += n; + while (*in && *src == (*in)->buf->last) { *in = (*in)->next; if (*in) { *src = (*in)->buf->pos; } } + return NGX_OK; } pn = last - *src; + if (dst) { ngx_memcpy(dst, *src, pn); dst = (u_char *)dst + pn; } + n -= pn; *in = (*in)->next; + if (*in == NULL) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "hls: failed to read %uz byte(s)", n); + "hls: failed to read %uz byte(s)", n); return NGX_ERROR; } + *src = (*in)->buf->pos; } } static ngx_int_t -ngx_rtmp_hls_append_avc_header(ngx_rtmp_session_t *s, ngx_buf_t *out) +ngx_rtmp_hls_append_aud(ngx_rtmp_session_t *s, ngx_buf_t *out) +{ + static u_char aud_nal[] = { 0x00, 0x00, 0x00, 0x01, 0x09, 0xf0 }; + + if (out->last + sizeof(aud_nal) > out->end) { + return NGX_ERROR; + } + + out->last = ngx_cpymem(out->last, aud_nal, sizeof(aud_nal)); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_hls_append_sps_pps(ngx_rtmp_session_t *s, ngx_buf_t *out) { ngx_rtmp_codec_ctx_t *codec_ctx; u_char *p; @@ -629,20 +353,22 @@ ngx_rtmp_hls_append_avc_header(ngx_rtmp_session_t *s, ngx_buf_t *out) ngx_int_t n; ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + if (ctx == NULL || codec_ctx == NULL) { return NGX_ERROR; } - in = codec_ctx->avc_header; - if (in == NULL) { return NGX_ERROR; } + p = in->buf->pos; - /* skip bytes: + /* + * Skip bytes: * - flv fmt * - H264 CONF/PICT (0x00) * - 0 @@ -651,60 +377,62 @@ ngx_rtmp_hls_append_avc_header(ngx_rtmp_session_t *s, ngx_buf_t *out) * - version * - profile * - compatibility - * - level */ - if (ngx_rtmp_hls_copy(s, NULL, &p, 9, &in) != NGX_OK) { - return NGX_ERROR; - } + * - level + * - nal bytes + */ - /* NAL size length (1,2,4) */ - if (ngx_rtmp_hls_copy(s, &ctx->nal_bytes, &p, 1, &in) != NGX_OK) { + if (ngx_rtmp_hls_copy(s, NULL, &p, 10, &in) != NGX_OK) { return NGX_ERROR; } - ctx->nal_bytes &= 0x03; /* 2 lsb */ - ++ctx->nal_bytes; - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "hls: NAL size bytes: %uz", ctx->nal_bytes); /* number of SPS NALs */ if (ngx_rtmp_hls_copy(s, &nnals, &p, 1, &in) != NGX_OK) { return NGX_ERROR; } + nnals &= 0x1f; /* 5lsb */ + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "hls: SPS number: %uz", nnals); + "h264: SPS number: %uz", nnals); /* SPS */ for (n = 0; ; ++n) { for (; nnals; --nnals) { + /* NAL length */ if (ngx_rtmp_hls_copy(s, &rlen, &p, 2, &in) != NGX_OK) { return NGX_ERROR; } + ngx_rtmp_rmemcpy(&len, &rlen, 2); + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "hls: header NAL length: %uz", (size_t)len); + "h264: header NAL length: %uz", (size_t) len); /* AnnexB prefix */ - if (out->last - out->pos < 4) { + if (out->end - out->last < 4) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "hls: too small buffer for header NAL length"); + "h264: too small buffer for header NAL size"); return NGX_ERROR; } - *out->pos++ = 0; - *out->pos++ = 0; - *out->pos++ = 0; - *out->pos++ = 1; + + *out->last++ = 0; + *out->last++ = 0; + *out->last++ = 0; + *out->last++ = 1; /* NAL body */ - if (out->last - out->pos < len) { + if (out->end - out->last < len) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "hls: too small buffer for header NAL"); + "h264: too small buffer for header NAL"); return NGX_ERROR; } - if (ngx_rtmp_hls_copy(s, out->pos, &p, len, &in) != NGX_OK) { + + if (ngx_rtmp_hls_copy(s, out->last, &p, len, &in) != NGX_OK) { return NGX_ERROR; } - out->pos += len; + + out->last += len; } if (n == 1) { @@ -715,113 +443,96 @@ ngx_rtmp_hls_append_avc_header(ngx_rtmp_session_t *s, ngx_buf_t *out) if (ngx_rtmp_hls_copy(s, &nnals, &p, 1, &in) != NGX_OK) { return NGX_ERROR; } + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "hls: PPS number: %uz", nnals); + "h264: PPS number: %uz", nnals); } return NGX_OK; } -static ngx_int_t -ngx_rtmp_hls_close_file(ngx_rtmp_session_t *s) -{ - ngx_rtmp_hls_ctx_t *ctx; - - - ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); - - if (av_write_trailer(ctx->out_format) < 0) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "hls: av_write_trailer failed"); - } - ctx->header_sent = 0; - - avio_flush(ctx->out_format->pb); - - if (avio_close(ctx->out_format->pb) < 0) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "hls: avio_close failed"); - } - - ctx->opened = 0; - - return NGX_OK; -} - - static void -ngx_rtmp_hls_restart(ngx_rtmp_session_t *s) +ngx_rtmp_hls_next_frag(ngx_rtmp_session_t *s) { - ngx_rtmp_hls_app_conf_t *hacf; - ngx_rtmp_hls_ctx_t *ctx; - + ngx_rtmp_hls_app_conf_t *hacf; + ngx_rtmp_hls_ctx_t *ctx; ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); + hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); + if (ctx == NULL || hacf == NULL) { return; } - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "hls: restart frag=%i", ctx->frag); - if (ctx->opened) { - ngx_rtmp_hls_close_file(s); + ngx_close_file(ctx->file.fd); + ctx->opened = 0; } - if (ngx_rtmp_hls_initialize(s) != NGX_OK) { + if (hacf->nfrags == 0 && ctx->frag > 2 * hacf->winfrags && + !hacf->nodelete) + { + *ngx_sprintf(ctx->stream.data + ctx->stream.len, "-%uL.ts", + ngx_rtmp_hls_frag(hacf, ctx->frag - 2 * hacf->winfrags)) + = 0; + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: delete frag '%s'", ctx->stream.data); + + ngx_delete_file(ctx->stream.data); + + } + + ctx->frag++; + + *ngx_sprintf(ctx->stream.data + ctx->stream.len, "-%uL.ts", + ngx_rtmp_hls_frag(hacf, ctx->frag)) = 0; + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: open frag '%s', frag=%uL", + ctx->stream.data, ctx->frag); + + ngx_memzero(&ctx->file, sizeof(ctx->file)); + + ctx->file.log = s->connection->log; + + ngx_str_set(&ctx->file.name, "hls"); + + 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) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "hls: error creating fragment file"); return; } - if (ngx_rtmp_hls_init_video(s) != NGX_OK) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "hls: video init failed"); + if (ngx_rtmp_mpegts_write_header(&ctx->file) != NGX_OK) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "hls: error writing fragment header"); + ngx_close_file(ctx->file.fd); + return; } - if (ngx_rtmp_hls_init_audio(s) != NGX_OK) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "hls: audio init failed"); - } + ctx->opened = 1; - /* remember we have preallocated memory in ctx->stream */ - - /* erase old file; - * we should keep old fragments available - * whole next cycle */ - if (ctx->frag > (ngx_int_t)hacf->nfrags * 2) { - *ngx_sprintf(ctx->stream.data + ctx->stream.len, "-%i.ts", - ctx->frag - hacf->nfrags * 2) = 0; - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "hls: delete stream file '%s'", ctx->stream.data); - ngx_delete_file(ctx->stream.data); - } - - ++ctx->frag; - ctx->frag_start = ngx_current_msec; - - /* create new one */ - *ngx_sprintf(ctx->stream.data + ctx->stream.len, "-%i.ts", - ctx->frag) = 0; - ngx_rtmp_hls_open_file(s, ctx->stream.data); - - /* update playlist */ ngx_rtmp_hls_update_playlist(s); } +#define NGX_RTMP_HLS_RESTORE_PREFIX "#EXTM3U\r\n#EXT-X-MEDIA-SEQUENCE:" + + static void -ngx_rtmp_hls_restore_frag(ngx_rtmp_session_t *s) +ngx_rtmp_hls_restore_stream(ngx_rtmp_session_t *s) { ngx_rtmp_hls_ctx_t *ctx; ngx_file_t file; - ngx_file_info_t fi; ssize_t ret; - u_char *p, *last; - u_char buffer[sizeof("-.ts\r\n") + - NGX_OFF_T_LEN]; - - /* try to restore frag from previously stored playlist */ + u_char buffer[sizeof(NGX_RTMP_HLS_RESTORE_PREFIX) - + 1 + NGX_INT64_LEN]; ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); @@ -829,46 +540,38 @@ ngx_rtmp_hls_restore_frag(ngx_rtmp_session_t *s) file.log = s->connection->log; + ngx_str_set(&file.name, "m3u8"); + file.fd = ngx_open_file(ctx->playlist.data, NGX_FILE_RDONLY, NGX_FILE_OPEN, 0); if (file.fd == NGX_INVALID_FILE) { return; } - if (ngx_fd_info(file.fd, &fi)) { - goto done; - } - - ret = ngx_read_file(&file, buffer, sizeof(buffer), - fi.st_size > (off_t) sizeof(buffer) ? - fi.st_size - sizeof(buffer) : 0); + ret = ngx_read_file(&file, buffer, sizeof(buffer), 0); if (ret <= 0) { goto done; } - /* last line example: - * mystream-14.ts\r\n */ - - if (ret < (ssize_t) sizeof(".ts\r\n")) { + if ((size_t) ret < sizeof(NGX_RTMP_HLS_RESTORE_PREFIX)) { goto done; } - ret -= (sizeof(".ts\r\n") - 1); - - last = buffer + ret; - p = last; - while (p > buffer && *(p - 1) != '-') { - --p; - } - - if (p == buffer) { + if (ngx_strncmp(buffer, NGX_RTMP_HLS_RESTORE_PREFIX, + sizeof(NGX_RTMP_HLS_RESTORE_PREFIX) - 1)) + { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: failed to restore stream"); goto done; } - ctx->frag = ngx_atoi(p, (size_t) (last - p)) + 1; + buffer[ret] = 0; + + ctx->frag = strtod((const char *) + &buffer[sizeof(NGX_RTMP_HLS_RESTORE_PREFIX) - 1], NULL); ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "hls: restored frag=%i", ctx->frag); + "hls: restored frag=%uL", ctx->frag); done: ngx_close_file(file.fd); @@ -893,65 +596,74 @@ ngx_rtmp_hls_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) } ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "hls: publish: name='%s' type='%s'", - v->name, v->type); + "hls: publish: name='%s' type='%s'", + v->name, v->type); - /* create context */ ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); if (ctx == NULL) { ctx = ngx_palloc(s->connection->pool, sizeof(ngx_rtmp_hls_ctx_t)); ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_hls_module); } + ngx_memzero(ctx, sizeof(ngx_rtmp_hls_ctx_t)); - /* init names & paths */ + /*TODO: escaping does not solve the problem*/ + len = ngx_strlen(v->name); - ctx->name.len = len + (ngx_uint_t)ngx_escape_uri(NULL, v->name, len, - NGX_ESCAPE_URI_COMPONENT); - ctx->name.data = ngx_palloc(s->connection->pool, - ctx->name.len); + ctx->name.len = len + (ngx_uint_t) ngx_escape_uri(NULL, v->name, len, + NGX_ESCAPE_URI_COMPONENT); + ctx->name.data = ngx_palloc(s->connection->pool, ctx->name.len); + ngx_escape_uri(ctx->name.data, v->name, len, NGX_ESCAPE_URI_COMPONENT); + ctx->playlist.data = ngx_palloc(s->connection->pool, - hacf->path.len + 1 + ctx->name.len + sizeof(".m3u8")); + hacf->path.len + 1 + ctx->name.len + + sizeof(".m3u8")); p = ngx_cpymem(ctx->playlist.data, hacf->path.data, hacf->path.len); + if (p[-1] != '/') { *p++ = '/'; } + p = ngx_cpymem(p, ctx->name.data, ctx->name.len); /* ctx->stream_path holds initial part of stream file path * however the space for the whole stream path * is allocated */ + ctx->stream.len = p - ctx->playlist.data; ctx->stream.data = ngx_palloc(s->connection->pool, - ctx->stream.len + 1 + NGX_OFF_T_LEN + sizeof(".ts")); + ctx->stream.len + 1 + NGX_INT64_LEN + sizeof(".ts")); + ngx_memcpy(ctx->stream.data, ctx->playlist.data, ctx->stream.len); /* playlist path */ + p = ngx_cpymem(p, ".m3u8", sizeof(".m3u8") - 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")); + ctx->playlist.len + sizeof(".bak")); p = ngx_cpymem(ctx->playlist_bak.data, ctx->playlist.data, - ctx->playlist.len); + 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, - "hls: playlist='%V' playlist_bak='%V' stream_pattern='%V'", - &ctx->playlist, &ctx->playlist_bak, &ctx->stream); + "hls: playlist='%V' playlist_bak='%V' stream_pattern='%V'", + &ctx->playlist, &ctx->playlist_bak, &ctx->stream); if (hacf->continuous) { - ngx_rtmp_hls_restore_frag(s); + ngx_rtmp_hls_restore_stream(s); } - /* schedule restart event */ ctx->publishing = 1; - ctx->frag_start = ngx_current_msec - hacf->fraglen - 1; next: return next_publish(s, v); @@ -965,29 +677,21 @@ ngx_rtmp_hls_delete_stream(ngx_rtmp_session_t *s, ngx_rtmp_delete_stream_t *v) ngx_rtmp_hls_ctx_t *ctx; hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); - if (hacf == NULL || !hacf->hls || ctx == NULL - || ctx->publishing == 0) - { + + if (hacf == NULL || !hacf->hls || ctx == NULL || !ctx->publishing) { goto next; } ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "hls: delete"); + "hls: delete"); ctx->publishing = 0; - if (ctx->out_format == NULL) { - goto next; - } - if (ctx->opened) { - ngx_rtmp_hls_close_file(s); - } - - if (ctx->out_format) { - avformat_free_context(ctx->out_format); - ctx->out_format = NULL; + ngx_close_file(ctx->file.fd); + ctx->opened = 0; } next: @@ -995,53 +699,163 @@ next: } +static ngx_int_t +ngx_rtmp_hls_parse_aac_header(ngx_rtmp_session_t *s, ngx_uint_t *objtype, + ngx_uint_t *srindex, ngx_uint_t *chconf) +{ + ngx_rtmp_codec_ctx_t *codec_ctx; + ngx_chain_t *cl; + u_char *p, b0, b1; + + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + cl = codec_ctx->aac_header; + + p = cl->buf->pos; + + if (ngx_rtmp_hls_copy(s, NULL, &p, 2, &cl) != NGX_OK) { + return NGX_ERROR; + } + + if (ngx_rtmp_hls_copy(s, &b0, &p, 1, &cl) != NGX_OK) { + return NGX_ERROR; + } + + if (ngx_rtmp_hls_copy(s, &b1, &p, 1, &cl) != NGX_OK) { + return NGX_ERROR; + } + + *objtype = b0 >> 3; + if (*objtype == 0 || *objtype == 0x1f) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: unsupported adts object type:%ui", *objtype); + return NGX_ERROR; + } + + (*objtype)--; + + *srindex = ((b0 << 1) & 0x0f) | ((b1 & 0x80) >> 7); + if (*srindex == 0x0f) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: unsupported adts sample rate:%ui", *srindex); + return NGX_ERROR; + } + + *chconf = (b1 >> 3) & 0x0f; + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: aac object_type:%ui, sample_rate_index:%ui, " + "channel_config:%ui", *objtype, *srindex, *chconf); + + return NGX_OK; +} + + +static void +ngx_rtmp_hls_set_frag(ngx_rtmp_session_t *s, uint64_t ts) +{ + ngx_rtmp_hls_app_conf_t *hacf; + ngx_rtmp_hls_ctx_t *ctx; + uint64_t frag; + + hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); + + frag = ts / hacf->fraglen / 90; + + if (frag == ctx->frag && ctx->opened) { + return; + } + + if (frag != ctx->frag + 1) { + ctx->offset = (ctx->frag + 1) * (uint64_t) hacf->fraglen * 90 - ts; + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: time gap offset=%uL", ctx->offset); + } + + ngx_rtmp_hls_next_frag(s); +} + + static ngx_int_t ngx_rtmp_hls_audio(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, - ngx_chain_t *in) + ngx_chain_t *in) { ngx_rtmp_hls_app_conf_t *hacf; ngx_rtmp_hls_ctx_t *ctx; ngx_rtmp_codec_ctx_t *codec_ctx; - AVPacket packet; - int64_t dts, ddts; + uint64_t dts; + int64_t ddts; + ngx_rtmp_mpegts_frame_t frame; + ngx_buf_t out; + u_char *p; + ngx_uint_t objtype, srindex, chconf, size; static u_char buffer[NGX_RTMP_HLS_BUFSIZE]; hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); - if (hacf == NULL || !hacf->hls || ctx == NULL || codec_ctx == NULL || - h->mlen < 1) + + if (hacf == NULL || !hacf->hls || ctx == NULL || + codec_ctx == NULL || h->mlen < 2) { return NGX_OK; } - /* fragment is restarted in video handler; - * however if video stream is missing then do it here */ - if (ctx->video == 0 - && ngx_current_msec - ctx->frag_start > hacf->fraglen) + if (codec_ctx->audio_codec_id != NGX_RTMP_AUDIO_AAC || + codec_ctx->aac_header == NULL) { - ngx_rtmp_hls_restart(s); - } - - if (!ctx->opened || !ctx->audio) { return NGX_OK; } - /* write to file */ - av_init_packet(&packet); - packet.dts = h->timestamp * 90L; - packet.stream_index = ctx->out_astream; - packet.data = buffer; - packet.size = ngx_rtmp_hls_chain2buffer(buffer, sizeof(buffer), in, 1); + ngx_memzero(&frame, sizeof(frame)); + + frame.dts = (uint64_t) h->timestamp * 90 + ctx->offset; + frame.cc = ctx->audio_cc; + frame.pid = 0x101; + frame.sid = 0xc0; + + ngx_memzero(&out, sizeof(out)); + + out.start = buffer; + out.end = buffer + sizeof(buffer); + out.pos = out.start; + out.last = out.pos; + + p = out.last; + out.last += 7; + + if (ngx_rtmp_hls_parse_aac_header(s, &objtype, &srindex, &chconf) + != NGX_OK) + { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: aac header error"); + return NGX_OK; + } + + size = h->mlen - 2 + 7; + + p[0] = 0xff; + p[1] = 0xf1; + p[2] = (objtype << 6) | (srindex << 2) | (chconf & 0x04); + p[3] = ((chconf & 0x03) << 6) | ((size >> 11) & 0x03); + p[4] = (size >> 3); + p[5] = (size << 5) | 0x1f; + p[6] = 0xfc; + + ngx_rtmp_hls_chain2buffer(&out, in, 2); if (hacf->sync && codec_ctx->sample_rate) { - + /* TODO: We assume here AAC frame size is 1024 * Need to handle AAC frames with frame size of 960 */ dts = ctx->aframe_base + ctx->aframe_num * 90000 * 1024 / codec_ctx->sample_rate; - ddts = dts - packet.dts; + ddts = (int64_t) (dts - frame.dts); ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: sync stat ddts=%L (%.5fs)", @@ -1050,63 +864,105 @@ ngx_rtmp_hls_audio(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, if (ddts > (int64_t) hacf->sync * 90 || ddts < (int64_t) hacf->sync * -90) { - ctx->aframe_base = packet.dts; + ctx->aframe_base = frame.dts; ctx->aframe_num = 0; ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: sync breakup ddts=%L (%.5fs)", ddts, ddts / 90000.); } else { - packet.dts = dts; + frame.dts = dts; } ctx->aframe_num++; } - packet.pts = packet.dts; + frame.pts = frame.dts; - if (codec_ctx->audio_codec_id == NGX_RTMP_AUDIO_AAC) { - if (packet.size == 0) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "hls: malformed AAC packet"); - return NGX_OK; - } - ++packet.data; - --packet.size; + /* Fragment is restarted in video handler. + * However if video stream is missing then do it here */ + + if (codec_ctx->avc_header == NULL) { + ngx_rtmp_hls_set_frag(s, frame.dts); } - ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "hls: audio dts=%L timestamp=%uD", (int64_t)packet.dts, - h->timestamp); + if (!ctx->opened) { + return NGX_OK; + } - if (av_interleaved_write_frame(ctx->out_format, &packet) < 0) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: audio dts=%uL", frame.dts); + + if (ngx_rtmp_mpegts_write_frame(&ctx->file, &frame, &out) != NGX_OK) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "hls: av_interleaved_write_frame failed"); + "hls: audio frame failed"); } + + ctx->audio_cc = frame.cc; + return NGX_OK; } +static ngx_int_t +ngx_rtmp_hls_get_nal_bytes(ngx_rtmp_session_t *s) +{ + ngx_rtmp_codec_ctx_t *codec_ctx; + ngx_chain_t *cl; + u_char *p; + uint8_t nal_bytes; + + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + cl = codec_ctx->avc_header; + + p = cl->buf->pos; + + if (ngx_rtmp_hls_copy(s, NULL, &p, 9, &cl) != NGX_OK) { + return NGX_ERROR; + } + + /* NAL size length (1,2,4) */ + if (ngx_rtmp_hls_copy(s, &nal_bytes, &p, 1, &cl) != NGX_OK) { + return NGX_ERROR; + } + + nal_bytes &= 0x03; /* 2 lsb */ + + ++nal_bytes; + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: NAL bytes: %ui", (ngx_uint_t) nal_bytes); + + return nal_bytes; +} + + static ngx_int_t ngx_rtmp_hls_video(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, - ngx_chain_t *in) + ngx_chain_t *in) { ngx_rtmp_hls_app_conf_t *hacf; ngx_rtmp_hls_ctx_t *ctx; ngx_rtmp_codec_ctx_t *codec_ctx; - AVPacket packet; u_char *p; - uint8_t fmt, ftype, htype, llen; + uint8_t fmt, ftype, htype, nal_type, src_nal_type; uint32_t len, rlen; ngx_buf_t out; + uint32_t cts; + ngx_rtmp_mpegts_frame_t frame; + ngx_uint_t nal_bytes; + ngx_int_t aud_sent, sps_pps_sent, rc; static u_char buffer[NGX_RTMP_HLS_BUFSIZE]; - int32_t cts; hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); - if (hacf == NULL || !hacf->hls || ctx == NULL || codec_ctx == NULL - || h->mlen < 1) + + if (hacf == NULL || !hacf->hls || ctx == NULL || codec_ctx == NULL || + codec_ctx->avc_header == NULL || h->mlen < 1) { return NGX_OK; } @@ -1124,110 +980,161 @@ ngx_rtmp_hls_video(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, /* 1: keyframe (IDR) * 2: inter frame * 3: disposable inter frame */ + ftype = (fmt & 0xf0) >> 4; /* H264 HDR/PICT */ + if (ngx_rtmp_hls_copy(s, &htype, &p, 1, &in) != NGX_OK) { return NGX_ERROR; } + /* proceed only with PICT */ + if (htype != 1) { return NGX_OK; } /* 3 bytes: decoder delay */ + if (ngx_rtmp_hls_copy(s, &cts, &p, 3, &in) != NGX_OK) { return NGX_ERROR; } - cts = ((cts & 0x00FF0000) >> 16) | ((cts & 0x000000FF) << 16) - | (cts & 0x0000FF00); - out.pos = buffer; - out.last = buffer + sizeof(buffer) - FF_INPUT_BUFFER_PADDING_SIZE; + cts = ((cts & 0x00FF0000) >> 16) | ((cts & 0x000000FF) << 16) | + (cts & 0x0000FF00); + + ngx_memzero(&out, sizeof(out)); + + out.start = buffer; + out.end = buffer + sizeof(buffer); + out.pos = out.start; + out.last = out.pos; - /* keyframe? */ - if (ftype == 1) { - if (ngx_current_msec - ctx->frag_start > hacf->fraglen) { - ngx_rtmp_hls_restart(s); - } - - /* Prepend IDR frame with H264 header for random seeks */ - if (ngx_rtmp_hls_append_avc_header(s, &out) != NGX_OK) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "hls: error appenging H264 header"); - } - } - - if (!ctx->opened || !ctx->video) { + rc = ngx_rtmp_hls_get_nal_bytes(s); + if (rc < 0) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: failed to parse NAL bytes"); return NGX_OK; } - if (ctx->nal_bytes == -1) { - ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "hls: nal length size is unknown, " - "waiting for IDR to parse header"); - return NGX_OK; - } - - ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "hls: parsing NALs"); + nal_bytes = rc; + aud_sent = 0; + sps_pps_sent = 0; while (in) { - llen = ctx->nal_bytes; - if (ngx_rtmp_hls_copy(s, &rlen, &p, llen, &in) != NGX_OK) { + if (ngx_rtmp_hls_copy(s, &rlen, &p, nal_bytes, &in) != NGX_OK) { return NGX_OK; } - len = 0; - ngx_rtmp_rmemcpy(&len, &rlen, llen); - ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "hls: NAL type=%i llen=%i len=%uD unit_type=%i", - (ngx_int_t)ftype, (ngx_int_t)llen, len, (ngx_int_t)(*p & 0x1f)); + len = 0; + ngx_rtmp_rmemcpy(&len, &rlen, nal_bytes); + + if (len == 0) { + continue; + } + + if (ngx_rtmp_hls_copy(s, &src_nal_type, &p, 1, &in) != NGX_OK) { + return NGX_OK; + } + + nal_type = src_nal_type & 0x1f; + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: h264 NAL type=%ui, len=%uD", + (ngx_uint_t) nal_type, len); + + if (!aud_sent) { + switch (nal_type) { + case 1: + case 5: + if (ngx_rtmp_hls_append_aud(s, &out) != NGX_OK) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: error appending AUD NAL"); + } + case 9: + aud_sent = 1; + break; + } + } + + switch (nal_type) { + case 1: + sps_pps_sent = 0; + break; + case 5: + if (sps_pps_sent) { + break; + } + if (ngx_rtmp_hls_append_sps_pps(s, &out) != NGX_OK) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: error appenging SPS/PPS NALs"); + } + sps_pps_sent = 1; + break; + } /* AnnexB prefix */ - if (out.last - out.pos < 4) { + + if (out.end - out.last < 5) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "hls: not enough buffer for AnnexB prefix"); + "hls: not enough buffer for AnnexB prefix"); return NGX_OK; } /* first AnnexB prefix is long (4 bytes) */ - if (out.pos == buffer) { - *out.pos++ = 0; + + if (out.last == out.pos) { + *out.last++ = 0; } - *out.pos++ = 0; - *out.pos++ = 0; - *out.pos++ = 1; + + *out.last++ = 0; + *out.last++ = 0; + *out.last++ = 1; + *out.last++ = src_nal_type; /* NAL body */ - if (out.last - out.pos < (ngx_int_t) len) { + + if (out.end - out.last < (ngx_int_t) len) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "hls: not enough buffer for NAL"); + "hls: not enough buffer for NAL"); return NGX_OK; } - if (ngx_rtmp_hls_copy(s, out.pos, &p, len, &in) != NGX_OK) { + + if (ngx_rtmp_hls_copy(s, out.last, &p, len - 1, &in) != NGX_OK) { return NGX_ERROR; } - out.pos += len; + + out.last += (len - 1); } - av_init_packet(&packet); - packet.dts = h->timestamp * 90L; - packet.pts = packet.dts + cts * 90; - packet.stream_index = ctx->out_vstream; + ngx_memzero(&frame, sizeof(frame)); - if (ftype == 1) { - packet.flags |= AV_PKT_FLAG_KEY; + frame.cc = ctx->video_cc; + frame.dts = (uint64_t) h->timestamp * 90 + ctx->offset; + frame.pts = frame.dts + cts * 90; + frame.pid = 0x100; + frame.sid = 0xe0; + frame.key = (ftype == 1); + + if (frame.key) { + ngx_rtmp_hls_set_frag(s, frame.dts); } - packet.data = buffer; - packet.size = out.pos - buffer; + if (!ctx->opened) { + return NGX_OK; + } - if (av_interleaved_write_frame(ctx->out_format, &packet) < 0) { + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: video pts=%uL, dts=%uL", frame.pts, frame.dts); + + if (ngx_rtmp_mpegts_write_frame(&ctx->file, &frame, &out) != NGX_OK) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "hls: av_interleaved_write_frame failed"); + "hls: video frame failed"); } + ctx->video_cc = frame.cc; + return NGX_OK; } @@ -1235,12 +1142,12 @@ ngx_rtmp_hls_video(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, static void * ngx_rtmp_hls_create_app_conf(ngx_conf_t *cf) { - ngx_rtmp_hls_app_conf_t *conf; + ngx_rtmp_hls_app_conf_t *conf; - conf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_hls_app_conf_t)); - if (conf == NULL) { - return NULL; - } + conf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_hls_app_conf_t)); + if (conf == NULL) { + return NULL; + } conf->hls = NGX_CONF_UNSET; conf->fraglen = NGX_CONF_UNSET; @@ -1248,7 +1155,8 @@ ngx_rtmp_hls_create_app_conf(ngx_conf_t *cf) conf->sync = NGX_CONF_UNSET; conf->playlen = NGX_CONF_UNSET; conf->continuous = NGX_CONF_UNSET; - conf->nbuckets = 1024; + conf->nodelete = NGX_CONF_UNSET; + conf->factor = NGX_CONF_UNSET; return conf; } @@ -1257,8 +1165,8 @@ ngx_rtmp_hls_create_app_conf(ngx_conf_t *cf) static char * ngx_rtmp_hls_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) { - ngx_rtmp_hls_app_conf_t *prev = parent; - ngx_rtmp_hls_app_conf_t *conf = child; + ngx_rtmp_hls_app_conf_t *prev = parent; + ngx_rtmp_hls_app_conf_t *conf = child; ngx_conf_merge_value(conf->hls, prev->hls, 0); ngx_conf_merge_msec_value(conf->fraglen, prev->fraglen, 5000); @@ -1266,14 +1174,13 @@ ngx_rtmp_hls_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_msec_value(conf->sync, prev->sync, 300); ngx_conf_merge_msec_value(conf->playlen, prev->playlen, 30000); ngx_conf_merge_str_value(conf->path, prev->path, ""); - ngx_conf_merge_value(conf->continuous, prev->continuous, 0); - conf->ctx = ngx_pcalloc(cf->pool, - sizeof(ngx_rtmp_hls_ctx_t *) * conf->nbuckets); - if (conf->ctx == NULL) { - return NGX_CONF_ERROR; - } + ngx_conf_merge_value(conf->continuous, prev->continuous, 1); + ngx_conf_merge_value(conf->nodelete, prev->nodelete, 0); + ngx_conf_merge_value(conf->factor, prev->factor, 2); + if (conf->fraglen) { - conf->nfrags = conf->playlen / conf->fraglen; + conf->winfrags = conf->playlen / conf->fraglen; + conf->nfrags = conf->winfrags * conf->factor; } return NGX_CONF_OK; @@ -1283,10 +1190,9 @@ ngx_rtmp_hls_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) static ngx_int_t ngx_rtmp_hls_postconfiguration(ngx_conf_t *cf) { - ngx_rtmp_core_main_conf_t *cmcf; - ngx_rtmp_handler_pt *h; + ngx_rtmp_core_main_conf_t *cmcf; + ngx_rtmp_handler_pt *h; - /* av handler */ cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module); h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_VIDEO]); @@ -1295,18 +1201,11 @@ ngx_rtmp_hls_postconfiguration(ngx_conf_t *cf) h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_AUDIO]); *h = ngx_rtmp_hls_audio; - /* chain handlers */ next_publish = ngx_rtmp_publish; ngx_rtmp_publish = ngx_rtmp_hls_publish; next_delete_stream = ngx_rtmp_delete_stream; ngx_rtmp_delete_stream = ngx_rtmp_hls_delete_stream; - /* register all ffmpeg stuff */ - av_register_all(); - ngx_rtmp_hls_log = &cf->cycle->new_log; - av_log_set_callback(ngx_rtmp_hls_av_log_callback); - return NGX_OK; } - diff --git a/hls/ngx_rtmp_mpegts.c b/hls/ngx_rtmp_mpegts.c new file mode 100644 index 0000000..72eb047 --- /dev/null +++ b/hls/ngx_rtmp_mpegts.c @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2013 Roman Arutyunyan + */ + + +#include "ngx_rtmp_mpegts.h" + + +static u_char ngx_rtmp_mpegts_header[] = { + + /* TS */ + 0x47, 0x40, 0x00, 0x10, 0x00, + /* PSI */ + 0x00, 0xb0, 0x0d, 0x00, 0x01, 0xc1, 0x00, 0x00, + /* PAT */ + 0x00, 0x01, 0xf0, 0x01, + /* CRC */ + 0x2e, 0x70, 0x19, 0x05, + /* stuffing 167 bytes */ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + + /* TS */ + 0x47, 0x50, 0x01, 0x10, 0x00, + /* PSI */ + 0x02, 0xb0, 0x17, 0x00, 0x01, 0xc1, 0x00, 0x00, + /* PMT */ + 0xe1, 0x00, + 0xf0, 0x00, + 0x1b, 0xe1, 0x00, 0xf0, 0x00, /* h264 */ + 0x0f, 0xe1, 0x01, 0xf0, 0x00, /* aac */ + /*0x03, 0xe1, 0x01, 0xf0, 0x00,*/ /* mp3 */ + /* CRC */ + 0x2f, 0x44, 0xb9, 0x9b, /* crc for aac */ + /*0x4e, 0x59, 0x3d, 0x1e,*/ /* crc for mp3 */ + /* stuffing 157 bytes */ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; + + +/* 700 ms PCR delay */ +#define NGX_RTMP_HLS_DELAY 63000 + + +ngx_int_t +ngx_rtmp_mpegts_write_header(ngx_file_t *file) +{ + ssize_t rc; + + rc = ngx_write_file(file, ngx_rtmp_mpegts_header, + sizeof(ngx_rtmp_mpegts_header), 0); + + return rc > 0 ? NGX_OK : rc; +} + + +static u_char * +ngx_rtmp_mpegts_write_pcr(u_char *p, uint64_t pcr) +{ + *p++ = pcr >> 25; + *p++ = pcr >> 17; + *p++ = pcr >> 9; + *p++ = pcr >> 1; + *p++ = pcr << 7 | 0x7e; + *p++ = 0; + + return p; +} + + +static u_char * +ngx_rtmp_mpegts_write_pts(u_char *p, ngx_uint_t fb, uint64_t pts) +{ + ngx_uint_t val; + + val = fb << 4 | (((pts >> 30) & 0x07) << 1) | 1; + *p++ = val; + + val = (((pts >> 15) & 0x7fff) << 1) | 1; + *p++ = val >> 8; + *p++ = val; + + val = (((pts) & 0x7fff) << 1) | 1; + *p++ = val >> 8; + *p++ = val; + + return p; +} + + +ngx_int_t +ngx_rtmp_mpegts_write_frame(ngx_file_t *file, ngx_rtmp_mpegts_frame_t *f, + ngx_buf_t *b) +{ + ngx_uint_t pes_size, header_size, body_size, in_size, stuff_size, flags; + u_char packet[188], *p; + ngx_int_t first, rc; + + ngx_log_debug6(NGX_LOG_DEBUG_HTTP, file->log, 0, + "mpegts: pid=%ui, sid=%ui, pts=%uL, " + "dts=%uL, key=%ui, size=%ui", + f->pid, f->sid, f->pts, f->dts, + (ngx_uint_t) f->key, (size_t) (b->last - b->pos)); + + first = 1; + + while (b->pos < b->last) { + p = packet; + + f->cc++; + + *p++ = 0x47; + *p++ = (f->pid >> 8); + + if (first) { + p[-1] |= 0x40; + } + + *p++ = f->pid; + *p++ = 0x10 | (f->cc & 0x0f); /* payload */ + + if (first) { + + if (f->key) { + packet[3] |= 0x20; /* adaptation */ + + *p++ = 7; /* size */ + *p++ = 0x50; /* random access + PCR */ + + p = ngx_rtmp_mpegts_write_pcr(p, f->dts - NGX_RTMP_HLS_DELAY); + } + + /* PES header */ + + *p++ = 0x00; + *p++ = 0x00; + *p++ = 0x01; + *p++ = f->sid; + + header_size = 5; + flags = 0x80; /* PTS */ + + if (f->dts != f->pts) { + header_size += 5; + flags |= 0x40; /* DTS */ + } + + pes_size = (b->last - b->pos) + header_size + 3; + + *p++ = (pes_size >> 8); + *p++ = pes_size; + *p++ = 0x80; /* H222 */ + *p++ = flags; + *p++ = header_size; + + p = ngx_rtmp_mpegts_write_pts(p, flags >> 6, f->pts + + NGX_RTMP_HLS_DELAY); + + if (f->dts != f->pts) { + p = ngx_rtmp_mpegts_write_pts(p, 1, f->dts + + NGX_RTMP_HLS_DELAY); + } + + first = 0; + } + + body_size = (ngx_uint_t) (packet + sizeof(packet) - p); + in_size = (ngx_uint_t) (b->last - b->pos); + + if (body_size <= in_size) { + ngx_memcpy(p, b->pos, body_size); + b->pos += body_size; + + } else { + stuff_size = (body_size - in_size); + + if (packet[3] & 0x20) { + packet[4] += stuff_size; + p = ngx_movemem(&packet[6] + stuff_size, &packet[6], p - &packet[4]); + ngx_memset(&packet[6], 0xff, stuff_size); + + } else { + packet[3] |= 0x20; + if (stuff_size == 1) { + p = ngx_movemem(&packet[5], &packet[4], p - &packet[4]); + packet[4] = 0; + } else { + p = ngx_movemem(&packet[4] + stuff_size, &packet[4], + p - &packet[4]); + packet[4] = stuff_size - 1; + packet[5] = 0; + ngx_memset(&packet[6], 0xff, stuff_size - 2); + } + } + + ngx_memcpy(p, b->pos, in_size); + b->pos = b->last; + } + + rc = ngx_write_file(file, packet, sizeof(packet), file->offset); + if (rc < 0) { + return rc; + } + } + + return NGX_OK; +} diff --git a/hls/ngx_rtmp_mpegts.h b/hls/ngx_rtmp_mpegts.h new file mode 100644 index 0000000..011ba62 --- /dev/null +++ b/hls/ngx_rtmp_mpegts.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2013 Roman Arutyunyan + */ + + +#ifndef _NGX_RTMP_MPEGTS_H_INCLUDED_ +#define _NGX_RTMP_MPEGTS_H_INCLUDED_ + + +#include + + +typedef struct { + uint64_t pts; + uint64_t dts; + ngx_uint_t pid; + ngx_uint_t sid; + ngx_uint_t cc; + unsigned key:1; +} ngx_rtmp_mpegts_frame_t; + + +ngx_int_t ngx_rtmp_mpegts_write_header(ngx_file_t *file); +ngx_int_t ngx_rtmp_mpegts_write_frame(ngx_file_t *file, + ngx_rtmp_mpegts_frame_t *f, ngx_buf_t *b); + + +#endif /* _NGX_RTMP_MPEGTS_H_INCLUDED_ */