IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> Nginx RTMP源码分析--record module -> 正文阅读

[系统运维]Nginx RTMP源码分析--record module

简介

本章介绍RTMP的录制模块。Nginx RTMP中,直播流可以被录制为FLV格式的文件。

录制模块配置解析

record指令指定了应该被准确录制的内容:
  • off:不开启录制;
  • all:音频和视频录制;
  • audio:只录制音频
  • video:只录制视频;
  • keyframes:只录制视频关键帧;
  • manual:从不自动开启录制,使用录制接口控制开始/结束录制。
record_path

指定录制的FLV文件的路径

record_path /tmp/rec;
record_suffix

设置录制文件后缀名,默认是“.flv”

record_suffix _recorded.flv;

录制的后缀也可以是strftime格式的模式。以下指令将产生格式为mystream-24-Apr-13-18:23:38.flv的文件。

record_suffix -%d-%b-%y-%T.flv;
record_unique

如果开启record_unique,则将当前的时间戳添加到录制文件的文件名中。否则,每次进行新的录制的时候都将写入相同的录制文件。默认是关闭状态。

record_append

切换文件追加模式。打开录制后,会将新数据追加到旧文件中,或者在丢失时创建新数据。 文件中的旧数据和新数据之间没有时间间隔。默认为关闭。

record_lock

打开时,当前录制的文件将通过fcntl调用锁定。可以从其他地方进行检查以找出正在录制的文件。 默认为关闭。

在FreeBSD上,您可以使用flock工具进行检查。在Linux上,flock和fcntl是无关的,因此您只需要编写一个简单的脚本来检查文件锁定状态即可。下面是一个简单的python检测脚本。

#!/usr/bin/python

import fcntl, sys

sys.stderr.close()
fcntl.lockf(open(sys.argv[1], "a"), fcntl.LOCK_EX|fcntl.LOCK_NB)
record_max_size

设置最大录制文件大小。

record_max_frames

设置每个录制文件的最大视频帧数。

record_interval

在此配置的时间后重新开始录制。默认情况下关闭。零表示两次录制之间没有延迟。 如果record_unique关闭,则所有记录片段都将写入同一文件。 否则,将附加时间戳,这会使文件不同(给定record_interval大于1秒)。

recorder

创建录制块。 可以在单个application块中创建多个录制块。可以在recorder {}块中指定所有上述与记录有关的指令。所有设置都继承自更高级别。

record_notify

当特定的录制块开始或停止录制文件时,切换向发布者发送NetStream.Record.Start和NetStream.Record.Stop状态消息(onStatus)。状态描述字段保存录制块名称(默认录制块为空)。默认情况下关闭。

FLV格式分析

由于RTMP的录制是保存为FLV格式的文件,所有我们首先需要对FLV的封装格式有所了解。本节中我们将知悉FLV是如何封装音视频数据的。

源码分析

ngx_rtmp_record_postconfiguration

首先来看ngx_rtmp_record_postconfiguration函数。

    h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_AUDIO]);
    *h = ngx_rtmp_record_av;

    h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_VIDEO]);
    *h = ngx_rtmp_record_av;

在这个函数里面,我们可以看到录制模块中对于接收到音视频数据的处理都是交由ngx_rtmp_record_av来完成的,也意味着对录制的处理是在ngx_rtmp_record_av函数中。

    next_publish = ngx_rtmp_publish;
    ngx_rtmp_publish = ngx_rtmp_record_publish;

    next_close_stream = ngx_rtmp_close_stream;
    ngx_rtmp_close_stream = ngx_rtmp_record_close_stream;

    next_stream_begin = ngx_rtmp_stream_begin;
    ngx_rtmp_stream_begin = ngx_rtmp_record_stream_begin;

    next_stream_eof = ngx_rtmp_stream_eof;
    ngx_rtmp_stream_eof = ngx_rtmp_record_stream_eof;

第二步是将录制模块加入到next_publish、next_close_stream函数链中。

ngx_rtmp_record_publish

由于ngx_rtmp_record_publish在next_publish函数链中,所以当有直播推流时会触发这个函数执行。

    if (s->auto_pushed) {
        goto next;
    }

    racf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_record_module);

    if (racf == NULL || racf->rec.nelts == 0) {
        goto next;
    }

1、判断是否是本机自动转推的流,如果是,则直接跳过;
2、判断是否存在录制模块,并且已经配置了record{}块;

    if (ngx_rtmp_record_init(s) != NGX_OK) {
        return NGX_ERROR;
    }

3、初始化此RTMP会话的录制。

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_record_module);

    ngx_memcpy(ctx->name, v->name, sizeof(ctx->name));
    ngx_memcpy(ctx->args, v->args, sizeof(ctx->args));

    /* terminate name on /../ */
    for (p = ctx->name; *p; ++p) {
        if (ngx_path_separator(p[0]) &&
            p[1] == '.' && p[2] == '.' &&
            ngx_path_separator(p[3]))
        {
            *p = 0;
            break;
        }
    }

4、获取当前的录制路径。

ngx_rtmp_record_start(s);

5、开始录制推流的RTMP会话。

ngx_rtmp_record_init
    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_record_module);

    if (ctx) {
        return NGX_OK;
    }

1、判断RTMP会话是否已经设置了录制模块的ctx,如果已经设置了,就不需要进行以下的设置步骤;否则需要创建并设置RTMP会话的录制模块ctx。

    racf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_record_module);

    if (racf == NULL || racf->rec.nelts == 0) {
        return NGX_OK;
    }

2、判断是否存在录制模块,并且已经配置了record{}块;

    ctx = ngx_pcalloc(s->connection->pool, sizeof(ngx_rtmp_record_ctx_t));

    if (ctx == NULL) {
        return NGX_ERROR;
    }

    ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_record_module);

3、申请创建RTMP会话的录制模块的ctx,并将其设置给该RTMP会话。

    if (ngx_array_init(&ctx->rec, s->connection->pool, racf->rec.nelts,
                       sizeof(ngx_rtmp_record_rec_ctx_t))
        != NGX_OK)
    {
        return NGX_ERROR;
    }

    rracf = racf->rec.elts;

    rctx = ngx_array_push_n(&ctx->rec, racf->rec.nelts);

    if (rctx == NULL) {
        return NGX_ERROR;
    }

    for (n = 0; n < racf->rec.nelts; ++n, ++rracf, ++rctx) {
        ngx_memzero(rctx, sizeof(*rctx));

        rctx->conf = *rracf;
        rctx->file.fd = NGX_INVALID_FILE;
    }

4、racf->rec.elts是配置文件中所有的record{}配置项的数量,对于每一个record{}配置,RTMP会话都需要设置rctx,以保存不同的录制规则。

ngx_rtmp_record_start

ngx_rtmp_record_start是开始录制的接口。

    racf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_record_module);
    if (racf == NULL || racf->rec.nelts == 0) {
        return;
    }

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_record_module);
    if (ctx == NULL) {
        return;
    }

    ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                   "record: start");

1、判断当前RTMP会话是否存在了record模块,并且配置了record{}块。
2、判断当前RTMP会话是否设置了record模块的ctx。

rctx = ctx->rec.elts;
    for (n = 0; n < ctx->rec.nelts; ++n, ++rctx) {
        if (rctx->conf->flags & (NGX_RTMP_RECORD_OFF|NGX_RTMP_RECORD_MANUAL)) {
            continue;
        }
        ngx_rtmp_record_node_open(s, rctx);
    }

3、针对每一个配置的record{}块,判断其record指令参数,如果record指令是关闭的,或者是使用录制控制接口进行控制的,那么久直接跳过,否则就会进行ngx_rtmp_record_node_open打开录制文件。

ngx_rtmp_record_node_open

ngx_rtmp_record_node_open的作用是打开文件供录制数据的写入,并且根据需求是否在文件起始位置写入FLV头信息。

    if (rctx->file.fd != NGX_INVALID_FILE) {
        return NGX_AGAIN;
    }

1、如果文件ID已经被打开,则跳过。

    rctx->conf = rracf;
    rctx->last = *ngx_cached_time;
    rctx->timestamp = ngx_cached_time->sec;

    ngx_rtmp_record_make_path(s, rctx, &path);

2、根据时间戳,设置录制的路径。

    mode = rracf->append ? NGX_FILE_RDWR : NGX_FILE_WRONLY;
    create_mode = rracf->append ? NGX_FILE_CREATE_OR_OPEN : NGX_FILE_TRUNCATE;

    ngx_memzero(&rctx->file, sizeof(rctx->file));
    rctx->file.offset = 0;
    rctx->file.log = s->connection->log;
    rctx->file.fd = ngx_open_file(path.data, mode, create_mode,
                                  NGX_FILE_DEFAULT_ACCESS);
    ngx_str_set(&rctx->file.name, "recorded");

3、判断录制的模式是否为追加,然后配置打开文件的参数。

    if (rctx->file.fd == NGX_INVALID_FILE) {
        err = ngx_errno;

        if (err != NGX_ENOENT) {
            ngx_log_error(NGX_LOG_CRIT, s->connection->log, err,
                          "record: %V failed to open file '%V'",
                          &rracf->id, &path);
        }

        ngx_rtmp_record_notify_error(s, rctx);

        return NGX_OK;
    }

4、如果打开文件失败,上报错误并返回。

#if !(NGX_WIN32)
    if (rracf->lock_file) {
        err = ngx_lock_fd(rctx->file.fd);
        if (err) {
            ngx_log_error(NGX_LOG_CRIT, s->connection->log, err,
                          "record: %V lock failed", &rracf->id);
        }
    }
#endif

5、对于非windows系统,如果配置record_lock打开时,当前录制的文件将通过fcntl调用锁定。

    if (rracf->notify) {
        ngx_rtmp_send_status(s, "NetStream.Record.Start", "status",
                             rracf->id.data ? (char *) rracf->id.data : "");
    }

6、如果配置record_notify,在开始录制打开文件时会向发布者发送NetStream.Record.Start信令。

    if (rracf->append) {

        file_size = 0;
        timestamp = 0;

#if (NGX_WIN32)
        {
            LONG  lo, hi;

            lo = 0;
            hi = 0;
            lo = SetFilePointer(rctx->file.fd, lo, &hi, FILE_END);
            file_size = (lo == INVALID_SET_FILE_POINTER ?
                         (off_t) -1 : (off_t) hi << 32 | (off_t) lo);
        }
#else
        file_size = lseek(rctx->file.fd, 0, SEEK_END);
#endif
        if (file_size == (off_t) -1) {
            ngx_log_error(NGX_LOG_CRIT, s->connection->log, ngx_errno,
                          "record: %V seek failed", &rracf->id);
            goto done;
        }

        if (file_size < 4) {
            goto done;
        }

        if (ngx_read_file(&rctx->file, buf, 4, file_size - 4) != 4) {
            ngx_log_error(NGX_LOG_CRIT, s->connection->log, ngx_errno,
                          "record: %V tag size read failed", &rracf->id);
            goto done;
        }

        p = (u_char *) &tag_size;
        p[0] = buf[3];
        p[1] = buf[2];
        p[2] = buf[1];
        p[3] = buf[0];

        if (tag_size == 0 || tag_size + 4 > file_size) {
            file_size = 0;
            goto done;
        }

        if (ngx_read_file(&rctx->file, buf, 8, file_size - tag_size - 4) != 8)
        {
            ngx_log_error(NGX_LOG_CRIT, s->connection->log, ngx_errno,
                          "record: %V tag read failed", &rracf->id);
            goto done;
        }

        p = (u_char *) &mlen;
        p[0] = buf[3];
        p[1] = buf[2];
        p[2] = buf[1];
        p[3] = 0;

        if (tag_size != mlen + 11) {
            ngx_log_error(NGX_LOG_CRIT, s->connection->log, ngx_errno,
                          "record: %V tag size mismatch: "
                          "tag_size=%uD, mlen=%uD", &rracf->id, tag_size, mlen);
            goto done;
        }

        p = (u_char *) &timestamp;
        p[3] = buf[7];
        p[0] = buf[6];
        p[1] = buf[5];
        p[2] = buf[4];

done:
        rctx->file.offset = file_size;
        rctx->time_shift = timestamp;

        ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "record: append offset=%O, time=%uD, tag_size=%uD",
                       file_size, timestamp, tag_size);
    }

7、如果配置了record_append录制追加模式。通过seek到文件末尾,然后读取flv最后一个音视频帧的timestamp信息,将该timestamp作为起始时间戳。

ngx_rtmp_record_av

对于接收到的每一个音视频帧,都会调用ngx_rtmp_record_av进入录制逻辑。

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_record_module);

    if (ctx == NULL) {
        return NGX_OK;
    }

    rctx = ctx->rec.elts;

    for (n = 0; n < ctx->rec.nelts; ++n, ++rctx) {
        ngx_rtmp_record_node_av(s, rctx, h, in);
    }

1、判断当前会话是否设置了录制模块的ctx,如果没有配置,直接返回。对于设置完成的会话,会根据配置文件中record{}项的数量,分别调用ngx_rtmp_record_node_av。

ngx_rtmp_record_node_av
    rracf = rctx->conf;

    if (rracf->flags & NGX_RTMP_RECORD_OFF) {
        ngx_rtmp_record_node_close(s, rctx);
        return NGX_OK;
    }

1、如果配置中是关闭了录制,就调用ngx_rtmp_record_node_close,将当前会话的录制ctx关闭。

    keyframe = (h->type == NGX_RTMP_MSG_VIDEO)
             ? (ngx_rtmp_get_video_frame_type(in) == NGX_RTMP_VIDEO_KEY_FRAME)
             : 0;

2、获取当前帧是否是视频的关键帧。

    brkframe = (h->type == NGX_RTMP_MSG_VIDEO)
             ? keyframe
             : (rracf->flags & NGX_RTMP_RECORD_VIDEO) == 0;

3、当前是视频帧时,brkframe = keyframe的值;对于非视频帧,当前record配置需要录制视频数据时为0 ,否则为1。

if (brkframe && (rracf->flags & NGX_RTMP_RECORD_MANUAL) == 0) {

        if (rracf->interval != (ngx_msec_t) NGX_CONF_UNSET) {

            next = rctx->last;
            next.msec += rracf->interval;
            next.sec  += (next.msec / 1000);
            next.msec %= 1000;

            if (ngx_cached_time->sec  > next.sec ||
               (ngx_cached_time->sec == next.sec &&
                ngx_cached_time->msec > next.msec))
            {
                ngx_rtmp_record_node_close(s, rctx);
                ngx_rtmp_record_node_open(s, rctx);
            }

        } else if (!rctx->failed) {
            ngx_rtmp_record_node_open(s, rctx);
        }
    }

4、打开录制文件

    codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module);
    if(codec_ctx) {
        
    }

5、获取RTMP会话的编码信息。

/* AAC header */
        if (!rctx->aac_header_sent && codec_ctx->aac_header &&
           (rracf->flags & NGX_RTMP_RECORD_AUDIO))
        {
            ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                           "record: %V writing AAC header", &rracf->id);

            ch.type = NGX_RTMP_MSG_AUDIO;
            ch.mlen = ngx_rtmp_record_get_chain_mlen(codec_ctx->aac_header);

            if (ngx_rtmp_record_write_frame(s, rctx, &ch,
                                            codec_ctx->aac_header, 0)
                != NGX_OK)
            {
                return NGX_OK;
            }

            rctx->aac_header_sent = 1;
        }

6、如果需要录制音频数据,判断音频AAC头是否已经写入录制文件,如果没有写入,就调用ngx_rtmp_record_write_frame将音频AAC的编码信息帧写入到录制文件中。

/* AVC header */
        if (!rctx->avc_header_sent && codec_ctx->avc_header &&
           (rracf->flags & (NGX_RTMP_RECORD_VIDEO|
                            NGX_RTMP_RECORD_KEYFRAMES)))
        {
            ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                           "record: %V writing AVC header", &rracf->id);

            ch.type = NGX_RTMP_MSG_VIDEO;
            ch.mlen = ngx_rtmp_record_get_chain_mlen(codec_ctx->avc_header);

            if (ngx_rtmp_record_write_frame(s, rctx, &ch,
                                            codec_ctx->avc_header, 0)
                != NGX_OK)
            {
                return NGX_OK;
            }

            rctx->avc_header_sent = 1;
        }

7、同理,如果需要录制视频数据,判断视频AVC头是否已经写入录制文件,如果没有写入,就调用ngx_rtmp_record_write_frame将视频AVC的编码信息帧写入到录制文件中。

if (h->type == NGX_RTMP_MSG_VIDEO) {
        if (codec_ctx && codec_ctx->video_codec_id == NGX_RTMP_VIDEO_H264 &&
            !rctx->avc_header_sent)
        {
            ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                           "record: %V skipping until H264 header", &rracf->id);
            return NGX_OK;
        }

        if (ngx_rtmp_get_video_frame_type(in) == NGX_RTMP_VIDEO_KEY_FRAME &&
            ((codec_ctx && codec_ctx->video_codec_id != NGX_RTMP_VIDEO_H264) ||
             !ngx_rtmp_is_codec_header(in)))
        {
            rctx->video_key_sent = 1;
        }

        if (!rctx->video_key_sent) {
            ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                           "record: %V skipping until keyframe", &rracf->id);
            return NGX_OK;
        }

    } else {
        if (codec_ctx && codec_ctx->audio_codec_id == NGX_RTMP_AUDIO_AAC &&
            !rctx->aac_header_sent)
        {
            ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                           "record: %V skipping until AAC header", &rracf->id);
            return NGX_OK;
        }
    }

8、判断是否需要跳过当前接收到的帧数据。对于视频帧,如果还没有接收到视频编码头信息或者还没有接收到关键帧,就会跳过当前的视频帧数据;对应音频帧,如果还没有收到音频的AAC编码信息,也一样会跳过当前的音频帧。

ngx_rtmp_record_write_frame(s, rctx, h, in, 1);

9、最后,将需要录制的音视频帧数据调用ngx_rtmp_record_write_frame写入录制文件中。

ngx_rtmp_record_write_frame
 /* write tag header */
    ph = hdr;

    *ph++ = (u_char)h->type;

    p = (u_char*)&h->mlen;
    *ph++ = p[2];
    *ph++ = p[1];
    *ph++ = p[0];

    p = (u_char*)&timestamp;
    *ph++ = p[2];
    *ph++ = p[1];
    *ph++ = p[0];
    *ph++ = p[3];

    *ph++ = 0;
    *ph++ = 0;
    *ph++ = 0;

    tag_size = (ph - hdr) + h->mlen;

    if (ngx_write_file(&rctx->file, hdr, ph - hdr, rctx->file.offset)
        == NGX_ERROR)
    {
        ngx_rtmp_record_notify_error(s, rctx);

        ngx_close_file(rctx->file.fd);

        return NGX_ERROR;
    }

1、计算FLV tag的头信息,包括长度,时间戳;并计算当前tag的大小。
然后将tag头信息写入文件中。

/* write tag body
     * FIXME: NGINX
     * ngx_write_chain seems to fit best
     * but it suffers from uncontrollable
     * allocations.
     * we're left with plain writing */
    for(; in; in = in->next) {
        if (in->buf->pos == in->buf->last) {
            continue;
        }

        if (ngx_write_file(&rctx->file, in->buf->pos, in->buf->last
                           - in->buf->pos, rctx->file.offset)
            == NGX_ERROR)
        {
            return NGX_ERROR;
        }
    }

2、在tag头写入录制文件后,将数据写入。音视频数据都保存在ngx_chain_t in中。

    /* write tag size */
    ph = hdr;
    p = (u_char*)&tag_size;

    *ph++ = p[3];
    *ph++ = p[2];
    *ph++ = p[1];
    *ph++ = p[0];

    if (ngx_write_file(&rctx->file, hdr, ph - hdr,
                       rctx->file.offset)
        == NGX_ERROR)
    {
        return NGX_ERROR;
    }

3、最后将tag大小写入文件中。

小结

本章介绍了Nginx RTMP的录制模块,我们可以清楚的知道RTMP直播流是如何录制成文件的全过程。首先我们了解了FLV的封装格式,因为RTMP的录制文件是FLV格式的。其次,我们知道应该如何设置配置文件,用来生成录制文件;最后,我们知道了录制中对每一个音视频帧数据的处理过程。

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2021-08-07 12:29:14  更:2021-08-07 12:30:54 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 19:52:58-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码