/* * Copyright (C) Roman Arutyunyan */ #include #include #include #include #include #include "ngx_rtmp_mpegts.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 char * ngx_rtmp_hls_variant(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); 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_int_t ngx_rtmp_hls_flush_audio(ngx_rtmp_session_t *s); static ngx_int_t ngx_rtmp_hls_ensure_directory(ngx_rtmp_session_t *s, ngx_str_t *path); #define NGX_RTMP_HLS_BUFSIZE (1024*1024) #define NGX_RTMP_HLS_DIR_ACCESS 0744 typedef struct { uint64_t id; uint64_t key_id; double duration; unsigned active:1; unsigned discont:1; /* before */ } ngx_rtmp_hls_frag_t; typedef struct { ngx_str_t suffix; ngx_array_t args; } ngx_rtmp_hls_variant_t; typedef struct { unsigned opened:1; ngx_rtmp_mpegts_file_t file; ngx_str_t playlist; ngx_str_t playlist_bak; ngx_str_t var_playlist; ngx_str_t var_playlist_bak; ngx_str_t stream; ngx_str_t keyfile; ngx_str_t name; u_char key[16]; uint64_t frag; uint64_t frag_ts; uint64_t key_id; ngx_uint_t nfrags; ngx_rtmp_hls_frag_t *frags; /* circular 2 * winfrags + 1 */ ngx_uint_t audio_cc; ngx_uint_t video_cc; ngx_uint_t key_frags; uint64_t aframe_base; uint64_t aframe_num; ngx_buf_t *aframe; uint64_t aframe_pts; ngx_rtmp_hls_variant_t *var; } ngx_rtmp_hls_ctx_t; typedef struct { ngx_str_t path; ngx_msec_t playlen; ngx_uint_t frags_per_key; } ngx_rtmp_hls_cleanup_t; typedef struct { ngx_flag_t hls; ngx_msec_t fraglen; ngx_msec_t max_fraglen; ngx_msec_t muxdelay; ngx_msec_t sync; ngx_msec_t playlen; ngx_uint_t winfrags; ngx_flag_t continuous; ngx_flag_t nested; ngx_str_t path; ngx_uint_t naming; ngx_uint_t slicing; ngx_uint_t type; ngx_path_t *slot; ngx_msec_t max_audio_delay; size_t audio_buffer_size; ngx_flag_t cleanup; ngx_array_t *variant; ngx_str_t base_url; ngx_int_t granularity; ngx_flag_t keys; ngx_str_t key_path; ngx_str_t key_url; ngx_uint_t frags_per_key; } ngx_rtmp_hls_app_conf_t; #define NGX_RTMP_HLS_NAMING_SEQUENTIAL 1 #define NGX_RTMP_HLS_NAMING_TIMESTAMP 2 #define NGX_RTMP_HLS_NAMING_SYSTEM 3 #define NGX_RTMP_HLS_SLICING_PLAIN 1 #define NGX_RTMP_HLS_SLICING_ALIGNED 2 #define NGX_RTMP_HLS_TYPE_LIVE 1 #define NGX_RTMP_HLS_TYPE_EVENT 2 static ngx_conf_enum_t ngx_rtmp_hls_naming_slots[] = { { ngx_string("sequential"), NGX_RTMP_HLS_NAMING_SEQUENTIAL }, { ngx_string("timestamp"), NGX_RTMP_HLS_NAMING_TIMESTAMP }, { ngx_string("system"), NGX_RTMP_HLS_NAMING_SYSTEM }, { ngx_null_string, 0 } }; static ngx_conf_enum_t ngx_rtmp_hls_slicing_slots[] = { { ngx_string("plain"), NGX_RTMP_HLS_SLICING_PLAIN }, { ngx_string("aligned"), NGX_RTMP_HLS_SLICING_ALIGNED }, { ngx_null_string, 0 } }; static ngx_conf_enum_t ngx_rtmp_hls_type_slots[] = { { ngx_string("live"), NGX_RTMP_HLS_TYPE_LIVE }, { ngx_string("event"), NGX_RTMP_HLS_TYPE_EVENT }, { ngx_null_string, 0 } }; static ngx_command_t ngx_rtmp_hls_commands[] = { { ngx_string("hls"), 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, hls), NULL }, { ngx_string("hls_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_hls_app_conf_t, fraglen), NULL }, { ngx_string("hls_max_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_hls_app_conf_t, max_fraglen), NULL }, { ngx_string("hls_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_hls_app_conf_t, path), NULL }, { ngx_string("hls_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_hls_app_conf_t, playlen), NULL }, { ngx_string("hls_muxdelay"), 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_hls_app_conf_t, muxdelay), NULL }, { ngx_string("hls_sync"), 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_hls_app_conf_t, sync), NULL }, { ngx_string("hls_continuous"), 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, continuous), NULL }, { ngx_string("hls_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_hls_app_conf_t, nested), NULL }, { ngx_string("hls_fragment_naming"), 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_hls_app_conf_t, naming), &ngx_rtmp_hls_naming_slots }, { ngx_string("hls_fragment_slicing"), 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_hls_app_conf_t, slicing), &ngx_rtmp_hls_slicing_slots }, { ngx_string("hls_type"), 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_hls_app_conf_t, type), &ngx_rtmp_hls_type_slots }, { ngx_string("hls_max_audio_delay"), 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_hls_app_conf_t, max_audio_delay), NULL }, { ngx_string("hls_audio_buffer_size"), NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, ngx_conf_set_size_slot, NGX_RTMP_APP_CONF_OFFSET, offsetof(ngx_rtmp_hls_app_conf_t, audio_buffer_size), NULL }, { ngx_string("hls_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_hls_app_conf_t, cleanup), NULL }, { ngx_string("hls_variant"), NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_1MORE, ngx_rtmp_hls_variant, NGX_RTMP_APP_CONF_OFFSET, 0, NULL }, { ngx_string("hls_base_url"), 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_hls_app_conf_t, base_url), NULL }, { ngx_string("hls_fragment_naming_granularity"), 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, granularity), NULL }, { ngx_string("hls_keys"), 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, keys), NULL }, { ngx_string("hls_key_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_hls_app_conf_t, key_path), NULL }, { ngx_string("hls_key_url"), 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_hls_app_conf_t, key_url), NULL }, { ngx_string("hls_fragments_per_key"), 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, frags_per_key), NULL }, ngx_null_command }; static ngx_rtmp_module_t ngx_rtmp_hls_module_ctx = { NULL, /* preconfiguration */ ngx_rtmp_hls_postconfiguration, /* postconfiguration */ NULL, /* create main configuration */ NULL, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ ngx_rtmp_hls_create_app_conf, /* create location configuration */ ngx_rtmp_hls_merge_app_conf, /* merge location configuration */ }; ngx_module_t ngx_rtmp_hls_module = { NGX_MODULE_V1, &ngx_rtmp_hls_module_ctx, /* module context */ ngx_rtmp_hls_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_hls_frag_t * ngx_rtmp_hls_get_frag(ngx_rtmp_session_t *s, ngx_int_t n) { ngx_rtmp_hls_ctx_t *ctx; ngx_rtmp_hls_app_conf_t *hacf; hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); return &ctx->frags[(ctx->frag + n) % (hacf->winfrags * 2 + 1)]; } static void ngx_rtmp_hls_next_frag(ngx_rtmp_session_t *s) { ngx_rtmp_hls_ctx_t *ctx; ngx_rtmp_hls_app_conf_t *hacf; 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->nfrags == hacf->winfrags) { ctx->frag++; } else { ctx->nfrags++; } } static ngx_int_t ngx_rtmp_hls_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_hls_write_variant_playlist(ngx_rtmp_session_t *s) { static u_char buffer[1024]; u_char *p, *last; ssize_t rc; ngx_fd_t fd; ngx_str_t *arg; ngx_uint_t n, k; ngx_rtmp_hls_ctx_t *ctx; ngx_rtmp_hls_variant_t *var; ngx_rtmp_hls_app_conf_t *hacf; hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); fd = ngx_open_file(ctx->var_playlist_bak.data, NGX_FILE_WRONLY, NGX_FILE_TRUNCATE, NGX_FILE_DEFAULT_ACCESS); if (fd == NGX_INVALID_FILE) { ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, "hls: " ngx_open_file_n " failed: '%V'", &ctx->var_playlist_bak); return NGX_ERROR; } #define NGX_RTMP_HLS_VAR_HEADER "#EXTM3U\n#EXT-X-VERSION:3\n" rc = ngx_write_fd(fd, NGX_RTMP_HLS_VAR_HEADER, sizeof(NGX_RTMP_HLS_VAR_HEADER) - 1); if (rc < 0) { ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, "hls: " ngx_write_fd_n " failed: '%V'", &ctx->var_playlist_bak); ngx_close_file(fd); return NGX_ERROR; } var = hacf->variant->elts; for (n = 0; n < hacf->variant->nelts; n++, var++) { p = buffer; last = buffer + sizeof(buffer); p = ngx_slprintf(p, last, "#EXT-X-STREAM-INF:PROGRAM-ID=1"); arg = var->args.elts; for (k = 0; k < var->args.nelts; k++, arg++) { p = ngx_slprintf(p, last, ",%V", arg); } if (p < last) { *p++ = '\n'; } p = ngx_slprintf(p, last, "%V%*s%V", &hacf->base_url, ctx->name.len - ctx->var->suffix.len, ctx->name.data, &var->suffix); if (hacf->nested) { p = ngx_slprintf(p, last, "%s", "/index"); } p = ngx_slprintf(p, last, "%s", ".m3u8\n"); rc = ngx_write_fd(fd, buffer, p - buffer); if (rc < 0) { ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, "hls: " ngx_write_fd_n " failed '%V'", &ctx->var_playlist_bak); ngx_close_file(fd); return NGX_ERROR; } } ngx_close_file(fd); 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); return NGX_ERROR; } return NGX_OK; } static ngx_int_t ngx_rtmp_hls_write_playlist(ngx_rtmp_session_t *s) { static u_char buffer[1024]; ngx_fd_t fd; u_char *p, *end; ngx_rtmp_hls_ctx_t *ctx; ssize_t n; ngx_rtmp_hls_app_conf_t *hacf; ngx_rtmp_hls_frag_t *f; ngx_uint_t i, max_frag; ngx_str_t name_part, key_name_part; uint64_t prev_key_id; const char *sep, *key_sep; hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); 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, "hls: " ngx_open_file_n " failed: '%V'", &ctx->playlist_bak); return NGX_ERROR; } max_frag = hacf->fraglen / 1000; for (i = 0; i < ctx->nfrags; i++) { f = ngx_rtmp_hls_get_frag(s, i); if (f->duration > max_frag) { max_frag = (ngx_uint_t) (f->duration + .5); } } p = buffer; end = p + sizeof(buffer); p = ngx_slprintf(p, end, "#EXTM3U\n" "#EXT-X-VERSION:3\n" "#EXT-X-MEDIA-SEQUENCE:%uL\n" "#EXT-X-TARGETDURATION:%ui\n", ctx->frag, max_frag); if (hacf->type == NGX_RTMP_HLS_TYPE_EVENT) { p = ngx_slprintf(p, end, "#EXT-X-PLAYLIST-TYPE: EVENT\n"); } n = ngx_write_fd(fd, buffer, p - buffer); if (n < 0) { ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, "hls: " ngx_write_fd_n " failed: '%V'", &ctx->playlist_bak); ngx_close_file(fd); return NGX_ERROR; } sep = hacf->nested ? (hacf->base_url.len ? "/" : "") : "-"; key_sep = hacf->nested ? (hacf->key_url.len ? "/" : "") : "-"; name_part.len = 0; if (!hacf->nested || hacf->base_url.len) { name_part = ctx->name; } key_name_part.len = 0; if (!hacf->nested || hacf->key_url.len) { key_name_part = ctx->name; } prev_key_id = 0; for (i = 0; i < ctx->nfrags; i++) { f = ngx_rtmp_hls_get_frag(s, i); p = buffer; end = p + sizeof(buffer); if (f->discont) { p = ngx_slprintf(p, end, "#EXT-X-DISCONTINUITY\n"); } if (hacf->keys && (i == 0 || f->key_id != prev_key_id)) { p = ngx_slprintf(p, end, "#EXT-X-KEY:METHOD=AES-128," "URI=\"%V%V%s%uL.key\",IV=0x%032XL\n", &hacf->key_url, &key_name_part, key_sep, f->key_id, f->key_id); } prev_key_id = f->key_id; p = ngx_slprintf(p, end, "#EXTINF:%.3f,\n" "%V%V%s%uL.ts\n", f->duration, &hacf->base_url, &name_part, sep, f->id); ngx_log_debug5(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: fragment frag=%uL, n=%ui/%ui, duration=%.3f, " "discont=%i", ctx->frag, i + 1, ctx->nfrags, f->duration, f->discont); n = ngx_write_fd(fd, buffer, p - buffer); if (n < 0) { ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, "hls: " ngx_write_fd_n " failed '%V'", &ctx->playlist_bak); ngx_close_file(fd); return NGX_ERROR; } } ngx_close_file(fd); 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); return NGX_ERROR; } if (ctx->var) { return ngx_rtmp_hls_write_variant_playlist(s); } 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) { u_char *last; size_t pn; if (*in == NULL) { return NGX_ERROR; } 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); return NGX_ERROR; } *src = (*in)->buf->pos; } } static ngx_int_t 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; ngx_chain_t *in; ngx_rtmp_hls_ctx_t *ctx; int8_t nnals; uint16_t len, rlen; 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: * - flv fmt * - H264 CONF/PICT (0x00) * - 0 * - 0 * - 0 * - version * - profile * - compatibility * - level * - nal bytes */ if (ngx_rtmp_hls_copy(s, NULL, &p, 10, &in) != NGX_OK) { return NGX_ERROR; } /* 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); /* 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); /* AnnexB prefix */ if (out->end - out->last < 4) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "hls: too small buffer for header NAL size"); return NGX_ERROR; } *out->last++ = 0; *out->last++ = 0; *out->last++ = 0; *out->last++ = 1; /* NAL body */ if (out->end - out->last < len) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "hls: too small buffer for header NAL"); return NGX_ERROR; } if (ngx_rtmp_hls_copy(s, out->last, &p, len, &in) != NGX_OK) { return NGX_ERROR; } out->last += len; } if (n == 1) { break; } /* number of PPS NALs */ 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); } return NGX_OK; } static uint64_t ngx_rtmp_hls_get_fragment_id(ngx_rtmp_session_t *s, uint64_t ts) { ngx_rtmp_hls_ctx_t *ctx; ngx_rtmp_hls_app_conf_t *hacf; ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); switch (hacf->naming) { case NGX_RTMP_HLS_NAMING_TIMESTAMP: return ts; case NGX_RTMP_HLS_NAMING_SYSTEM: return (uint64_t) ngx_cached_time->sec * 1000 + ngx_cached_time->msec; default: /* NGX_RTMP_HLS_NAMING_SEQUENTIAL */ return ctx->frag + ctx->nfrags; } } static ngx_int_t ngx_rtmp_hls_close_fragment(ngx_rtmp_session_t *s) { ngx_rtmp_hls_ctx_t *ctx; ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); if (ctx == NULL || !ctx->opened) { return NGX_OK; } ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: close fragment n=%uL", ctx->frag); ngx_rtmp_mpegts_close_file(&ctx->file); ctx->opened = 0; ngx_rtmp_hls_next_frag(s); ngx_rtmp_hls_write_playlist(s); return NGX_OK; } static ngx_int_t ngx_rtmp_hls_open_fragment(ngx_rtmp_session_t *s, uint64_t ts, ngx_int_t discont) { uint64_t id; ngx_fd_t fd; ngx_uint_t g; ngx_rtmp_hls_ctx_t *ctx; ngx_rtmp_hls_frag_t *f; ngx_rtmp_hls_app_conf_t *hacf; ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); if (ctx->opened) { return NGX_OK; } hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); if (ngx_rtmp_hls_ensure_directory(s, &hacf->path) != NGX_OK) { return NGX_ERROR; } if (hacf->keys && ngx_rtmp_hls_ensure_directory(s, &hacf->key_path) != NGX_OK) { return NGX_ERROR; } id = ngx_rtmp_hls_get_fragment_id(s, ts); if (hacf->granularity) { g = (ngx_uint_t) hacf->granularity; id = (uint64_t) (id / g) * g; } ngx_sprintf(ctx->stream.data + ctx->stream.len, "%uL.ts%Z", id); if (hacf->keys) { if (ctx->key_frags == 0) { ctx->key_frags = hacf->frags_per_key - 1; ctx->key_id = id; if (RAND_bytes(ctx->key, 16) < 0) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "hls: failed to create key"); return NGX_ERROR; } ngx_sprintf(ctx->keyfile.data + ctx->keyfile.len, "%uL.key%Z", id); fd = ngx_open_file(ctx->keyfile.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, "hls: failed to open key file '%s'", ctx->keyfile.data); return NGX_ERROR; } if (ngx_write_fd(fd, ctx->key, 16) != 16) { ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, "hls: failed to write key file '%s'", ctx->keyfile.data); ngx_close_file(fd); return NGX_ERROR; } ngx_close_file(fd); } else { if (hacf->frags_per_key) { ctx->key_frags--; } if (ngx_set_file_time(ctx->keyfile.data, 0, ngx_cached_time->sec) != NGX_OK) { ngx_log_error(NGX_LOG_ALERT, s->connection->log, ngx_errno, ngx_set_file_time_n " '%s' failed", ctx->keyfile.data); } } } ngx_log_debug6(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: open fragment file='%s', keyfile='%s', " "frag=%uL, n=%ui, time=%uL, discont=%i", ctx->stream.data, ctx->keyfile.data ? ctx->keyfile.data : (u_char *) "", ctx->frag, ctx->nfrags, ts, discont); if (hacf->keys && ngx_rtmp_mpegts_init_encryption(&ctx->file, ctx->key, 16, ctx->key_id) != NGX_OK) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "hls: failed to initialize hls encryption"); return NGX_ERROR; } if (ngx_rtmp_mpegts_open_file(&ctx->file, ctx->stream.data, s->connection->log) != NGX_OK) { return NGX_ERROR; } ctx->opened = 1; f = ngx_rtmp_hls_get_frag(s, ctx->nfrags); ngx_memzero(f, sizeof(*f)); f->active = 1; f->discont = discont; f->id = id; f->key_id = ctx->key_id; ctx->frag_ts = ts; /* start fragment with audio to make iPhone happy */ ngx_rtmp_hls_flush_audio(s); return NGX_OK; } static void ngx_rtmp_hls_restore_stream(ngx_rtmp_session_t *s) { ngx_rtmp_hls_ctx_t *ctx; ngx_file_t file; ssize_t ret; off_t offset; u_char *p, *last, *end, *next, *pa, *pp, c; ngx_rtmp_hls_frag_t *f; double duration; ngx_int_t discont; uint64_t mag, key_id, base; static u_char buffer[4096]; ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); ngx_memzero(&file, sizeof(file)); 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; } offset = 0; ctx->nfrags = 0; f = NULL; duration = 0; discont = 0; key_id = 0; for ( ;; ) { ret = ngx_read_file(&file, buffer, sizeof(buffer), offset); if (ret <= 0) { goto done; } p = buffer; end = buffer + ret; for ( ;; ) { last = ngx_strlchr(p, end, '\n'); if (last == NULL) { if (p == buffer) { goto done; } break; } next = last + 1; offset += (next - p); if (p != last && last[-1] == '\r') { last--; } #define NGX_RTMP_MSEQ "#EXT-X-MEDIA-SEQUENCE:" #define NGX_RTMP_MSEQ_LEN (sizeof(NGX_RTMP_MSEQ) - 1) if (ngx_memcmp(p, NGX_RTMP_MSEQ, NGX_RTMP_MSEQ_LEN) == 0) { ctx->frag = (uint64_t) strtod((const char *) &p[NGX_RTMP_MSEQ_LEN], NULL); ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: restore sequence frag=%uL", ctx->frag); } #define NGX_RTMP_XKEY "#EXT-X-KEY:" #define NGX_RTMP_XKEY_LEN (sizeof(NGX_RTMP_XKEY) - 1) if (ngx_memcmp(p, NGX_RTMP_XKEY, NGX_RTMP_XKEY_LEN) == 0) { /* recover key id from initialization vector */ key_id = 0; base = 1; pp = last - 1; for ( ;; ) { if (pp < p) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "hls: failed to read key id"); break; } c = *pp; if (c == 'x') { break; } if (c >= '0' && c <= '9') { c -= '0'; goto next; } c |= 0x20; if (c >= 'a' && c <= 'f') { c -= 'a' - 10; goto next; } ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "hls: bad character in key id"); break; next: key_id += base * c; base *= 0x10; pp--; } } #define NGX_RTMP_EXTINF "#EXTINF:" #define NGX_RTMP_EXTINF_LEN (sizeof(NGX_RTMP_EXTINF) - 1) if (ngx_memcmp(p, NGX_RTMP_EXTINF, NGX_RTMP_EXTINF_LEN) == 0) { duration = strtod((const char *) &p[NGX_RTMP_EXTINF_LEN], NULL); ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: restore durarion=%.3f", duration); } #define NGX_RTMP_DISCONT "#EXT-X-DISCONTINUITY" #define NGX_RTMP_DISCONT_LEN (sizeof(NGX_RTMP_DISCONT) - 1) if (ngx_memcmp(p, NGX_RTMP_DISCONT, NGX_RTMP_DISCONT_LEN) == 0) { discont = 1; ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: discontinuity"); } /* find '.ts\r' */ if (p + 4 <= last && last[-3] == '.' && last[-2] == 't' && last[-1] == 's') { f = ngx_rtmp_hls_get_frag(s, ctx->nfrags); ngx_memzero(f, sizeof(*f)); f->duration = duration; f->discont = discont; f->active = 1; f->id = 0; discont = 0; mag = 1; for (pa = last - 4; pa >= p; pa--) { if (*pa < '0' || *pa > '9') { break; } f->id += (*pa - '0') * mag; mag *= 10; } f->key_id = key_id; ngx_rtmp_hls_next_frag(s); ngx_log_debug6(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: restore fragment '%*s' id=%uL, " "duration=%.3f, frag=%uL, nfrags=%ui", (size_t) (last - p), p, f->id, f->duration, ctx->frag, ctx->nfrags); } p = next; } } done: ngx_close_file(file.fd); } static ngx_int_t ngx_rtmp_hls_ensure_directory(ngx_rtmp_session_t *s, ngx_str_t *path) { size_t len; ngx_file_info_t fi; ngx_rtmp_hls_ctx_t *ctx; ngx_rtmp_hls_app_conf_t *hacf; static u_char zpath[NGX_MAX_PATH + 1]; hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); if (path->len + 1 > sizeof(zpath)) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "hls: too long path"); return NGX_ERROR; } ngx_snprintf(zpath, sizeof(zpath), "%V%Z", path); if (ngx_file_info(zpath, &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'", path); return NGX_ERROR; } /* ENOENT */ if (ngx_create_dir(zpath, 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'", path); return NGX_ERROR; } ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: directory '%V' created", 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", path); return NGX_ERROR; } ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: directory '%V' exists", path); } if (!hacf->nested) { return NGX_OK; } ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); len = path->len; if (path->data[len - 1] == '/') { len--; } if (len + 1 + ctx->name.len + 1 > sizeof(zpath)) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "hls: too long path"); return NGX_ERROR; } ngx_snprintf(zpath, sizeof(zpath) - 1, "%*s/%V%Z", len, path->data, &ctx->name); if (ngx_file_info(zpath, &fi) != NGX_FILE_ERROR) { if (ngx_is_dir(&fi)) { ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: directory '%s' exists", zpath); return NGX_OK; } ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "hls: '%s' exists and is not a directory", zpath); 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'", zpath); return NGX_ERROR; } /* NGX_ENOENT */ if (ngx_create_dir(zpath, 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'", zpath); return NGX_ERROR; } ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: directory '%s' created", zpath); return NGX_OK; } static ngx_int_t ngx_rtmp_hls_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) { ngx_rtmp_hls_app_conf_t *hacf; ngx_rtmp_hls_ctx_t *ctx; u_char *p, *pp; ngx_rtmp_hls_frag_t *f; ngx_buf_t *b; size_t len; ngx_rtmp_hls_variant_t *var; ngx_uint_t n; hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); if (hacf == NULL || !hacf->hls || hacf->path.len == 0) { goto next; } if (s->auto_pushed) { goto next; } ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: publish: name='%s' type='%s'", v->name, v->type); ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); if (ctx == NULL) { ctx = ngx_pcalloc(s->connection->pool, sizeof(ngx_rtmp_hls_ctx_t)); ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_hls_module); } else { f = ctx->frags; b = ctx->aframe; ngx_memzero(ctx, sizeof(ngx_rtmp_hls_ctx_t)); ctx->frags = f; ctx->aframe = b; if (b) { b->pos = b->last = b->start; } } if (ctx->frags == NULL) { ctx->frags = ngx_pcalloc(s->connection->pool, sizeof(ngx_rtmp_hls_frag_t) * (hacf->winfrags * 2 + 1)); if (ctx->frags == NULL) { return NGX_ERROR; } } if (ngx_strstr(v->name, "..")) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "hls: bad stream name: '%s'", v->name); return NGX_ERROR; } ctx->name.len = ngx_strlen(v->name); ctx->name.data = ngx_palloc(s->connection->pool, ctx->name.len + 1); if (ctx->name.data == NULL) { return NGX_ERROR; } *ngx_cpymem(ctx->name.data, v->name, ctx->name.len) = 0; len = hacf->path.len + 1 + ctx->name.len + sizeof(".m3u8"); if (hacf->nested) { len += sizeof("/index") - 1; } ctx->playlist.data = ngx_palloc(s->connection->pool, len); p = ngx_cpymem(ctx->playlist.data, hacf->path.data, hacf->path.len); if (p[-1] != '/') { *p++ = '/'; } p = ngx_cpymem(p, ctx->name.data, ctx->name.len); /* * ctx->stream holds initial part of stream file path * however the space for the whole stream path * is allocated */ ctx->stream.len = p - ctx->playlist.data + 1; ctx->stream.data = ngx_palloc(s->connection->pool, ctx->stream.len + NGX_INT64_LEN + sizeof(".ts")); ngx_memcpy(ctx->stream.data, ctx->playlist.data, ctx->stream.len - 1); ctx->stream.data[ctx->stream.len - 1] = (hacf->nested ? '/' : '-'); /* varint playlist path */ if (hacf->variant) { var = hacf->variant->elts; for (n = 0; n < hacf->variant->nelts; n++, var++) { if (ctx->name.len > var->suffix.len && ngx_memcmp(var->suffix.data, ctx->name.data + ctx->name.len - var->suffix.len, var->suffix.len) == 0) { ctx->var = var; len = (size_t) (p - ctx->playlist.data); ctx->var_playlist.len = len - var->suffix.len + sizeof(".m3u8") - 1; ctx->var_playlist.data = ngx_palloc(s->connection->pool, ctx->var_playlist.len + 1); pp = ngx_cpymem(ctx->var_playlist.data, ctx->playlist.data, len - var->suffix.len); pp = ngx_cpymem(pp, ".m3u8", sizeof(".m3u8") - 1); *pp = 0; ctx->var_playlist_bak.len = ctx->var_playlist.len + sizeof(".bak") - 1; ctx->var_playlist_bak.data = ngx_palloc(s->connection->pool, ctx->var_playlist_bak.len + 1); pp = ngx_cpymem(ctx->var_playlist_bak.data, ctx->var_playlist.data, ctx->var_playlist.len); pp = ngx_cpymem(pp, ".bak", sizeof(".bak") - 1); *pp = 0; break; } } } /* playlist path */ if (hacf->nested) { p = ngx_cpymem(p, "/index.m3u8", sizeof("/index.m3u8") - 1); } else { 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")); 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; /* key path */ if (hacf->keys) { len = hacf->key_path.len + 1 + ctx->name.len + 1 + NGX_INT64_LEN + sizeof(".key"); ctx->keyfile.data = ngx_palloc(s->connection->pool, len); if (ctx->keyfile.data == NULL) { return NGX_ERROR; } p = ngx_cpymem(ctx->keyfile.data, hacf->key_path.data, hacf->key_path.len); if (p[-1] != '/') { *p++ = '/'; } p = ngx_cpymem(p, ctx->name.data, ctx->name.len); *p++ = (hacf->nested ? '/' : '-'); ctx->keyfile.len = p - ctx->keyfile.data; } ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: playlist='%V' playlist_bak='%V' " "stream_pattern='%V' keyfile_pattern='%V'", &ctx->playlist, &ctx->playlist_bak, &ctx->stream, &ctx->keyfile); if (hacf->continuous) { ngx_rtmp_hls_restore_stream(s); } next: return next_publish(s, v); } static ngx_int_t ngx_rtmp_hls_close_stream(ngx_rtmp_session_t *s, ngx_rtmp_close_stream_t *v) { ngx_rtmp_hls_app_conf_t *hacf; 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) { goto next; } ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: close stream"); ngx_rtmp_hls_close_fragment(s); next: return next_close_stream(s, v); } 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; } if (*objtype > 4) { /* * Mark all extended profiles as LC * to make Android as happy as possible. */ *objtype = 2; } *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_update_fragment(ngx_rtmp_session_t *s, uint64_t ts, ngx_int_t boundary, ngx_uint_t flush_rate) { ngx_rtmp_hls_ctx_t *ctx; ngx_rtmp_hls_app_conf_t *hacf; ngx_rtmp_hls_frag_t *f; ngx_msec_t ts_frag_len; ngx_int_t same_frag, force,discont; ngx_buf_t *b; int64_t d; hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); f = NULL; force = 0; discont = 1; if (ctx->opened) { f = ngx_rtmp_hls_get_frag(s, ctx->nfrags); d = (int64_t) (ts - ctx->frag_ts); if (d > (int64_t) hacf->max_fraglen * 90 || d < -90000) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "hls: force fragment split: %.3f sec, ", d / 90000.); force = 1; } else { f->duration = (ts - ctx->frag_ts) / 90000.; discont = 0; } } switch (hacf->slicing) { case NGX_RTMP_HLS_SLICING_PLAIN: if (f && f->duration < hacf->fraglen / 1000.) { boundary = 0; } break; case NGX_RTMP_HLS_SLICING_ALIGNED: ts_frag_len = hacf->fraglen * 90; same_frag = ctx->frag_ts / ts_frag_len == ts / ts_frag_len; if (f && same_frag) { boundary = 0; } if (f == NULL && (ctx->frag_ts == 0 || same_frag)) { ctx->frag_ts = ts; boundary = 0; } break; } if (boundary || force) { ngx_rtmp_hls_close_fragment(s); ngx_rtmp_hls_open_fragment(s, ts, discont); } b = ctx->aframe; if (ctx->opened && b && b->last > b->pos && ctx->aframe_pts + (uint64_t) hacf->max_audio_delay * 90 / flush_rate < ts) { ngx_rtmp_hls_flush_audio(s); } } static ngx_int_t ngx_rtmp_hls_flush_audio(ngx_rtmp_session_t *s) { ngx_rtmp_hls_ctx_t *ctx; ngx_rtmp_mpegts_frame_t frame; ngx_int_t rc; ngx_buf_t *b; ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); if (ctx == NULL || !ctx->opened) { return NGX_OK; } b = ctx->aframe; if (b == NULL || b->pos == b->last) { return NGX_OK; } ngx_memzero(&frame, sizeof(frame)); frame.dts = ctx->aframe_pts; frame.pts = frame.dts; frame.cc = ctx->audio_cc; frame.pid = 0x101; frame.sid = 0xc0; ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: flush audio pts=%uL", frame.pts); rc = ngx_rtmp_mpegts_write_frame(&ctx->file, &frame, b); if (rc != NGX_OK) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "hls: audio flush failed"); } ctx->audio_cc = frame.cc; b->pos = b->last = b->start; return rc; } static ngx_int_t ngx_rtmp_hls_audio(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, ngx_chain_t *in) { ngx_rtmp_hls_app_conf_t *hacf; ngx_rtmp_hls_ctx_t *ctx; ngx_rtmp_codec_ctx_t *codec_ctx; uint64_t pts, est_pts; int64_t dpts; size_t bsize; ngx_buf_t *b; u_char *p; ngx_uint_t objtype, srindex, chconf, size; 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 < 2) { return NGX_OK; } if (codec_ctx->audio_codec_id != NGX_RTMP_AUDIO_AAC || codec_ctx->aac_header == NULL || ngx_rtmp_is_codec_header(in)) { return NGX_OK; } b = ctx->aframe; if (b == NULL) { b = ngx_pcalloc(s->connection->pool, sizeof(ngx_buf_t)); if (b == NULL) { return NGX_ERROR; } ctx->aframe = b; b->start = ngx_palloc(s->connection->pool, hacf->audio_buffer_size); if (b->start == NULL) { return NGX_ERROR; } b->end = b->start + hacf->audio_buffer_size; b->pos = b->last = b->start; } size = h->mlen - 2 + 7; pts = (uint64_t) h->timestamp * 90; if (b->start + size > b->end) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "hls: too big audio frame"); return NGX_OK; } /* * start new fragment here if * there's no video at all, otherwise * do it in video handler */ ngx_rtmp_hls_update_fragment(s, pts, codec_ctx->avc_header == NULL, 2); if (b->last + size > b->end) { ngx_rtmp_hls_flush_audio(s); } ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: audio pts=%uL", pts); if (b->last + 7 > b->end) { ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: not enough buffer for audio header"); return NGX_OK; } p = b->last; b->last += 5; /* copy payload */ for (; in && b->last < b->end; in = in->next) { bsize = in->buf->last - in->buf->pos; if (b->last + bsize > b->end) { bsize = b->end - b->last; } b->last = ngx_cpymem(b->last, in->buf->pos, bsize); } /* make up ADTS header */ 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; } /* we have 5 free bytes + 2 bytes of RTMP frame header */ p[0] = 0xff; p[1] = 0xf1; p[2] = (u_char) (((objtype - 1) << 6) | (srindex << 2) | ((chconf & 0x04) >> 2)); p[3] = (u_char) (((chconf & 0x03) << 6) | ((size >> 11) & 0x03)); p[4] = (u_char) (size >> 3); p[5] = (u_char) ((size << 5) | 0x1f); p[6] = 0xfc; if (p != b->start) { ctx->aframe_num++; return NGX_OK; } ctx->aframe_pts = pts; if (!hacf->sync || codec_ctx->sample_rate == 0) { return NGX_OK; } /* align audio frames */ /* TODO: We assume here AAC frame size is 1024 * Need to handle AAC frames with frame size of 960 */ est_pts = ctx->aframe_base + ctx->aframe_num * 90000 * 1024 / codec_ctx->sample_rate; dpts = (int64_t) (est_pts - pts); ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: audio sync dpts=%L (%.5fs)", dpts, dpts / 90000.); if (dpts <= (int64_t) hacf->sync * 90 && dpts >= (int64_t) hacf->sync * -90) { ctx->aframe_num++; ctx->aframe_pts = est_pts; return NGX_OK; } ctx->aframe_base = pts; ctx->aframe_num = 1; ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: audio sync gap dpts=%L (%.5fs)", dpts, dpts / 90000.); return NGX_OK; } static ngx_int_t ngx_rtmp_hls_video(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, ngx_chain_t *in) { ngx_rtmp_hls_app_conf_t *hacf; ngx_rtmp_hls_ctx_t *ctx; ngx_rtmp_codec_ctx_t *codec_ctx; u_char *p; uint8_t fmt, ftype, htype, nal_type, src_nal_type; uint32_t len, rlen; ngx_buf_t out, *b; uint32_t cts; ngx_rtmp_mpegts_frame_t frame; ngx_uint_t nal_bytes; ngx_int_t aud_sent, sps_pps_sent, boundary; 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 || codec_ctx->avc_header == NULL || h->mlen < 1) { return NGX_OK; } /* Only H264 is supported */ if (codec_ctx->video_codec_id != NGX_RTMP_VIDEO_H264) { return NGX_OK; } p = in->buf->pos; if (ngx_rtmp_hls_copy(s, &fmt, &p, 1, &in) != NGX_OK) { return NGX_ERROR; } /* 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); ngx_memzero(&out, sizeof(out)); out.start = buffer; out.end = buffer + sizeof(buffer); out.pos = out.start; out.last = out.pos; nal_bytes = codec_ctx->avc_nal_bytes; aud_sent = 0; sps_pps_sent = 0; while (in) { if (ngx_rtmp_hls_copy(s, &rlen, &p, nal_bytes, &in) != NGX_OK) { return NGX_OK; } 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 (nal_type >= 7 && nal_type <= 9) { if (ngx_rtmp_hls_copy(s, NULL, &p, len - 1, &in) != NGX_OK) { return NGX_ERROR; } continue; } if (!aud_sent) { switch (nal_type) { case 1: case 5: case 6: 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"); } /* fall through */ 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.end - out.last < 5) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "hls: not enough buffer for AnnexB prefix"); return NGX_OK; } /* first AnnexB prefix is long (4 bytes) */ if (out.last == out.pos) { *out.last++ = 0; } *out.last++ = 0; *out.last++ = 0; *out.last++ = 1; *out.last++ = src_nal_type; /* NAL body */ 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"); return NGX_OK; } if (ngx_rtmp_hls_copy(s, out.last, &p, len - 1, &in) != NGX_OK) { return NGX_ERROR; } out.last += (len - 1); } ngx_memzero(&frame, sizeof(frame)); frame.cc = ctx->video_cc; frame.dts = (uint64_t) h->timestamp * 90; frame.pts = frame.dts + cts * 90; frame.pid = 0x100; frame.sid = 0xe0; frame.key = (ftype == 1); /* * start new fragment if * - we have video key frame AND * - we have audio buffered or have no audio at all or stream is closed */ b = ctx->aframe; boundary = frame.key && (codec_ctx->aac_header == NULL || !ctx->opened || (b && b->last > b->pos)); ngx_rtmp_hls_update_fragment(s, frame.dts, boundary, 1); if (!ctx->opened) { return NGX_OK; } 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: video frame failed"); } ctx->video_cc = frame.cc; return NGX_OK; } static ngx_int_t ngx_rtmp_hls_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_hls_stream_eof(ngx_rtmp_session_t *s, ngx_rtmp_stream_eof_t *v) { ngx_rtmp_hls_flush_audio(s); ngx_rtmp_hls_close_fragment(s); return next_stream_eof(s, v); } static ngx_int_t ngx_rtmp_hls_cleanup_dir(ngx_str_t *ppath, ngx_msec_t playlen) { ngx_dir_t dir; time_t mtime, max_age; ngx_err_t err; ngx_str_t name, spath; u_char *p; ngx_int_t nentries, nerased; u_char path[NGX_MAX_PATH + 1]; ngx_log_debug2(NGX_LOG_DEBUG_RTMP, ngx_cycle->log, 0, "hls: 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, "hls: 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, "hls: 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, "hls: 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, "hls: cleanup " ngx_de_info_n " \"%V\" failed", &spath); continue; } if (ngx_de_is_dir(&dir)) { if (ngx_rtmp_hls_cleanup_dir(&spath, playlen) == 0) { ngx_log_debug1(NGX_LOG_DEBUG_RTMP, ngx_cycle->log, 0, "hls: 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, "hls: cleanup " ngx_delete_dir_n " failed on '%V'", &spath); } else { nerased++; } } continue; } if (!ngx_de_is_file(&dir)) { continue; } if (name.len >= 3 && name.data[name.len - 3] == '.' && name.data[name.len - 2] == 't' && name.data[name.len - 1] == 's') { max_age = playlen / 500; } else if (name.len >= 5 && name.data[name.len - 5] == '.' && name.data[name.len - 4] == 'm' && name.data[name.len - 3] == '3' && name.data[name.len - 2] == 'u' && name.data[name.len - 1] == '8') { max_age = playlen / 1000; } else if (name.len >= 4 && name.data[name.len - 4] == '.' && name.data[name.len - 3] == 'k' && name.data[name.len - 2] == 'e' && name.data[name.len - 1] == 'y') { max_age = playlen / 500; } else { ngx_log_debug1(NGX_LOG_DEBUG_RTMP, ngx_cycle->log, 0, "hls: 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, "hls: 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, "hls: cleanup " ngx_delete_file_n " failed on '%V'", &spath); continue; } nerased++; } } #if (nginx_version >= 1011005) static ngx_msec_t #else static time_t #endif ngx_rtmp_hls_cleanup(void *data) { ngx_rtmp_hls_cleanup_t *cleanup = data; ngx_rtmp_hls_cleanup_dir(&cleanup->path, cleanup->playlen); #if (nginx_version >= 1011005) return cleanup->playlen * 2; #else return cleanup->playlen / 500; #endif } static char * ngx_rtmp_hls_variant(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_rtmp_hls_app_conf_t *hacf = conf; ngx_str_t *value, *arg; ngx_uint_t n; ngx_rtmp_hls_variant_t *var; value = cf->args->elts; if (hacf->variant == NULL) { hacf->variant = ngx_array_create(cf->pool, 1, sizeof(ngx_rtmp_hls_variant_t)); if (hacf->variant == NULL) { return NGX_CONF_ERROR; } } var = ngx_array_push(hacf->variant); if (var == NULL) { return NGX_CONF_ERROR; } ngx_memzero(var, sizeof(ngx_rtmp_hls_variant_t)); var->suffix = value[1]; if (cf->args->nelts == 2) { return NGX_CONF_OK; } if (ngx_array_init(&var->args, cf->pool, cf->args->nelts - 2, sizeof(ngx_str_t)) != NGX_OK) { return NGX_CONF_ERROR; } arg = ngx_array_push_n(&var->args, cf->args->nelts - 2); if (arg == NULL) { return NGX_CONF_ERROR; } for (n = 2; n < cf->args->nelts; n++) { *arg++ = value[n]; } return NGX_CONF_OK; } static void * ngx_rtmp_hls_create_app_conf(ngx_conf_t *cf) { 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->hls = NGX_CONF_UNSET; conf->fraglen = NGX_CONF_UNSET_MSEC; conf->max_fraglen = NGX_CONF_UNSET_MSEC; conf->muxdelay = NGX_CONF_UNSET_MSEC; conf->sync = NGX_CONF_UNSET_MSEC; conf->playlen = NGX_CONF_UNSET_MSEC; conf->continuous = NGX_CONF_UNSET; conf->nested = NGX_CONF_UNSET; conf->naming = NGX_CONF_UNSET_UINT; conf->slicing = NGX_CONF_UNSET_UINT; conf->type = NGX_CONF_UNSET_UINT; conf->max_audio_delay = NGX_CONF_UNSET_MSEC; conf->audio_buffer_size = NGX_CONF_UNSET_SIZE; conf->cleanup = NGX_CONF_UNSET; conf->granularity = NGX_CONF_UNSET; conf->keys = NGX_CONF_UNSET; conf->frags_per_key = NGX_CONF_UNSET_UINT; return conf; } 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_cleanup_t *cleanup; ngx_conf_merge_value(conf->hls, prev->hls, 0); ngx_conf_merge_msec_value(conf->fraglen, prev->fraglen, 5000); ngx_conf_merge_msec_value(conf->max_fraglen, prev->max_fraglen, conf->fraglen * 10); ngx_conf_merge_msec_value(conf->muxdelay, prev->muxdelay, 700); ngx_conf_merge_msec_value(conf->sync, prev->sync, 2); ngx_conf_merge_msec_value(conf->playlen, prev->playlen, 30000); ngx_conf_merge_value(conf->continuous, prev->continuous, 1); ngx_conf_merge_value(conf->nested, prev->nested, 0); ngx_conf_merge_uint_value(conf->naming, prev->naming, NGX_RTMP_HLS_NAMING_SEQUENTIAL); ngx_conf_merge_uint_value(conf->slicing, prev->slicing, NGX_RTMP_HLS_SLICING_PLAIN); ngx_conf_merge_uint_value(conf->type, prev->type, NGX_RTMP_HLS_TYPE_LIVE); ngx_conf_merge_msec_value(conf->max_audio_delay, prev->max_audio_delay, 300); ngx_conf_merge_size_value(conf->audio_buffer_size, prev->audio_buffer_size, NGX_RTMP_HLS_BUFSIZE); ngx_conf_merge_value(conf->cleanup, prev->cleanup, 1); ngx_conf_merge_str_value(conf->base_url, prev->base_url, ""); ngx_conf_merge_value(conf->granularity, prev->granularity, 0); ngx_conf_merge_value(conf->keys, prev->keys, 0); ngx_conf_merge_str_value(conf->key_path, prev->key_path, ""); ngx_conf_merge_str_value(conf->key_url, prev->key_url, ""); ngx_conf_merge_uint_value(conf->frags_per_key, prev->frags_per_key, 0); if (conf->fraglen) { conf->winfrags = conf->playlen / conf->fraglen; } /* schedule cleanup */ if (conf->hls && conf->path.len && conf->cleanup && conf->type != NGX_RTMP_HLS_TYPE_EVENT) { 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_hls_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, ""); if (conf->keys && conf->cleanup && conf->key_path.len && ngx_strcmp(conf->key_path.data, conf->path.data) != 0 && conf->type != NGX_RTMP_HLS_TYPE_EVENT) { if (conf->key_path.data[conf->key_path.len - 1] == '/') { conf->key_path.len--; } cleanup = ngx_pcalloc(cf->pool, sizeof(*cleanup)); if (cleanup == NULL) { return NGX_CONF_ERROR; } cleanup->path = conf->key_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_hls_cleanup; conf->slot->name = conf->key_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->key_path, prev->key_path, ""); if (conf->key_path.len == 0) { conf->key_path = conf->path; } return NGX_CONF_OK; } static ngx_int_t ngx_rtmp_hls_postconfiguration(ngx_conf_t *cf) { ngx_rtmp_core_main_conf_t *cmcf; ngx_rtmp_handler_pt *h; cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module); h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_VIDEO]); *h = ngx_rtmp_hls_video; h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_AUDIO]); *h = ngx_rtmp_hls_audio; next_publish = ngx_rtmp_publish; ngx_rtmp_publish = ngx_rtmp_hls_publish; next_close_stream = ngx_rtmp_close_stream; ngx_rtmp_close_stream = ngx_rtmp_hls_close_stream; next_stream_begin = ngx_rtmp_stream_begin; ngx_rtmp_stream_begin = ngx_rtmp_hls_stream_begin; next_stream_eof = ngx_rtmp_stream_eof; ngx_rtmp_stream_eof = ngx_rtmp_hls_stream_eof; return NGX_OK; }