/* * Copyright (c) 2013 Roman Arutyunyan */ #include #include #include #include "ngx_rtmp_mpegts.h" static ngx_rtmp_publish_pt next_publish; 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); #define NGX_RTMP_HLS_BUFSIZE (1024*1024) #define NGX_RTMP_HLS_DIR_ACCESS 0744 typedef struct { ngx_uint_t flags; ngx_msec_t frag_start; unsigned publishing:1; unsigned opened: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; ngx_uint_t audio_cc; ngx_uint_t video_cc; int64_t aframe_base; int64_t aframe_num; int8_t nal_bytes; } ngx_rtmp_hls_ctx_t; typedef struct { ngx_flag_t hls; ngx_msec_t fraglen; ngx_msec_t muxdelay; ngx_msec_t sync; ngx_msec_t playlen; size_t nfrags; ngx_flag_t continuous; ngx_rtmp_hls_ctx_t **ctx; ngx_uint_t nbuckets; ngx_str_t path; } ngx_rtmp_hls_app_conf_t; 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_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_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 void ngx_rtmp_hls_chain2buffer(ngx_buf_t *out, ngx_chain_t *in, size_t skip) { size_t size; for (; in; in = in->next) { size = in->buf->last - in->buf->pos; if (size < skip) { skip -= size; continue; } out->last = ngx_cpymem(out->last, in->buf->pos + skip, ngx_min(size - skip, (size_t) (out->end - out->last))); skip = 0; } } static ngx_int_t ngx_rtmp_hls_update_playlist(ngx_rtmp_session_t *s) { static u_char buffer[1024]; int fd; u_char *p; ngx_rtmp_hls_ctx_t *ctx; ssize_t n; ngx_int_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); if (fd == NGX_INVALID_FILE) { ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, "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); ++nretry; goto retry; } return NGX_ERROR; } ffrag = ctx->frag - hacf->nfrags; if (ffrag < 1) { ffrag = 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", (ngx_int_t) (hacf->fraglen / 1000), 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); 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", (ngx_int_t) (hacf->fraglen / 1000), &ctx->name, 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); ngx_close_file(fd); return NGX_ERROR; } } ngx_close_file(fd); if (ngx_rename_file(ctx->playlist_bak.data, ctx->playlist.data)) { ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, "hls: rename failed: '%V'->'%V'", &ctx->playlist_bak, &ctx->playlist); return NGX_ERROR; } 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_avc_header(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 */ if (ngx_rtmp_hls_copy(s, NULL, &p, 9, &in) != NGX_OK) { return NGX_ERROR; } /* NAL size length (1,2,4) */ if (ngx_rtmp_hls_copy(s, &ctx->nal_bytes, &p, 1, &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, "h264: 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, "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, "h264: header NAL length: %uz", (size_t) len); /* AnnexB prefix */ if (out->last - out->pos < 4) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "h264: too small buffer for header NAL size"); return NGX_ERROR; } *out->pos++ = 0; *out->pos++ = 0; *out->pos++ = 0; *out->pos++ = 1; /* NAL body */ if (out->last - out->pos < len) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "h264: too small buffer for header NAL"); return NGX_ERROR; } if (ngx_rtmp_hls_copy(s, out->pos, &p, len, &in) != NGX_OK) { return NGX_ERROR; } out->pos += 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, "h264: PPS number: %uz", nnals); } return NGX_OK; } static void ngx_rtmp_hls_restart(ngx_rtmp_session_t *s) { 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_close_file(ctx->file.fd); ctx->opened = 0; } /* * 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_delete_file(ctx->stream.data); ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: delete stream file '%s'", ctx->stream.data); } ++ctx->frag; ctx->frag_start = ngx_current_msec; /* we have preallocated memory in ctx->stream */ *ngx_sprintf(ctx->stream.data + ctx->stream.len, "-%i.ts", ctx->frag) = 0; 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_mpegts_write_header(&ctx->file) != NGX_OK) { ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, "hls: error writing fragment header"); return; } ctx->opened = 1; ngx_rtmp_hls_update_playlist(s); } static void ngx_rtmp_hls_restore_frag(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 */ ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); ngx_memzero(&file, sizeof(file)); file.log = s->connection->log; 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); if (ret <= 0) { goto done; } /* last line example: * mystream-14.ts\r\n */ if (ret < (ssize_t) sizeof(".ts\r\n")) { goto done; } ret -= (sizeof(".ts\r\n") - 1); last = buffer + ret; p = last; while (p > buffer && *(p - 1) != '-') { --p; } if (p == buffer) { goto done; } ctx->frag = ngx_atoi(p, (size_t) (last - p)) + 1; ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: restored frag=%i", ctx->frag); done: ngx_close_file(file.fd); } 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; size_t len; u_char *p; 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_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)); /*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); 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")); 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")); 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")); p = ngx_cpymem(ctx->playlist_bak.data, ctx->playlist.data, ctx->playlist.len); p = ngx_cpymem(p, ".bak", sizeof(".bak") - 1); ctx->playlist_bak.len = p - ctx->playlist_bak.data; *p = 0; ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "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); } /* schedule restart event */ ctx->publishing = 1; ctx->frag_start = ngx_current_msec - hacf->fraglen - 1; next: return next_publish(s, v); } static ngx_int_t ngx_rtmp_hls_delete_stream(ngx_rtmp_session_t *s, ngx_rtmp_delete_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 || !ctx->publishing) { goto next; } ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: delete"); ctx->publishing = 0; if (ctx->opened) { ngx_close_file(ctx->file.fd); ctx->opened = 0; } next: return next_delete_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; } (*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 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; int64_t dts, 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 < 2) { return NGX_OK; } if (codec_ctx->audio_codec_id != NGX_RTMP_AUDIO_AAC || codec_ctx->aac_header == NULL) { return NGX_OK; } /* Fragment is restarted in video handler. * However if video stream is missing then do it here */ if (codec_ctx->avc_header == NULL && ngx_current_msec - ctx->frag_start > hacf->fraglen) { ngx_rtmp_hls_restart(s); } if (!ctx->opened) { return NGX_OK; } ngx_memzero(&frame, sizeof(frame)); frame.dts = h->timestamp * 90L; 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 - frame.dts; ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: sync stat ddts=%L (%.5fs)", ddts, ddts / 90000.); if (ddts > (int64_t) hacf->sync * 90 || ddts < (int64_t) hacf->sync * -90) { 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 { frame.dts = dts; } ctx->aframe_num++; } frame.pts = frame.dts; ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: audio dts=%uL, timestamp=%uD", frame.dts, h->timestamp); if (ngx_rtmp_mpegts_write_frame(&ctx->file, &frame, &out) != NGX_OK) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "hls: audio frame failed"); } ctx->audio_cc = frame.cc; 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, llen; uint32_t len, rlen; ngx_buf_t out; int32_t cts; ngx_rtmp_mpegts_frame_t frame; 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) { 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.pos = buffer; out.last = buffer + sizeof(buffer); /* 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 || codec_ctx->avc_header == NULL) { return NGX_OK; } if (ctx->nal_bytes == 0) { 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; } while (in) { llen = ctx->nal_bytes; if (ngx_rtmp_hls_copy(s, &rlen, &p, llen, &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: h264 NAL type=%i llen=%i len=%uD unit_type=%i", (ngx_int_t) ftype, (ngx_int_t) llen, len, (ngx_int_t) (*p & 0x1f)); /* AnnexB prefix */ if (out.last - out.pos < 4) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "hls: not enough buffer for AnnexB prefix"); return NGX_OK; } /*TODO: add AUD NAL*/ /* first AnnexB prefix is long (4 bytes) */ if (out.pos == buffer) { *out.pos++ = 0; } *out.pos++ = 0; *out.pos++ = 0; *out.pos++ = 1; /* NAL body */ if (out.last - out.pos < (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.pos, &p, len, &in) != NGX_OK) { return NGX_ERROR; } out.pos += len; } /*TODO*/ out.last = out.pos; out.pos = buffer; ngx_memzero(&frame, sizeof(frame)); frame.cc = ctx->video_cc; frame.dts = h->timestamp * 90L; frame.pts = frame.dts + cts * 90; frame.pid = 0x100; frame.sid = 0xe0; frame.key = (ftype == 1); 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 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; conf->muxdelay = NGX_CONF_UNSET; conf->sync = NGX_CONF_UNSET; conf->playlen = NGX_CONF_UNSET; conf->continuous = NGX_CONF_UNSET; conf->nbuckets = 1024; 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_conf_merge_value(conf->hls, prev->hls, 0); ngx_conf_merge_msec_value(conf->fraglen, prev->fraglen, 5000); ngx_conf_merge_msec_value(conf->muxdelay, prev->muxdelay, 700); 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; } if (conf->fraglen) { conf->nfrags = conf->playlen / conf->fraglen; } 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_delete_stream = ngx_rtmp_delete_stream; ngx_rtmp_delete_stream = ngx_rtmp_hls_delete_stream; return NGX_OK; }