• FFmpeg流媒体处理-收流与推流


    转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10623968.html


    1. 简介

    流媒体是使用了流式传输的多媒体应用技术。如下是维基百科关于流媒体概念的定义:

    流媒体 (streaming media) 是指将一连串的媒体数据压缩后,经过网络分段发送数据,在网络上即时传输影音以供观赏的一种技术与过程,此技术使得数据包得以像流水一样发送;如果不使用此技术,就必须在使用前下载整个媒体文件。

    关于流媒体的基础概念,观止云的 “流媒体|从入门到出家” 系列文章极具参考价值,请参考本文第 5 节参考资料部分。

    1.1 FFmpeg 影音处理的层次

    FFmpeg 中对影音数据的处理,可以划分为协议层、容器层、编码层与原始数据层四个层次:

    协议层:提供网络协议收发功能,可以接收或推送含封装格式的媒体流。协议层由 libavformat 库及第三方库(如 librtmp)提供支持。

    容器层:处理各种封装格式。容器层由 libavformat 库提供支持。

    编码层:处理音视频编码及解码。编码层由各种丰富的编解码器(libavcodec 库及第三方编解码库(如 libx264))提供支持。

    原始数据层:处理未编码的原始音视频帧。原始数据层由各种丰富的音视频滤镜(libavfilter 库)提供支持。

    本文提及的收流与推流的功能,属于协议层的处理。

    FFmpeg 中 libavformat 库提供了丰富的协议处理及封装格式处理功能,在打开输入/输出时,FFmpeg 会根据 输入 URL / 输出 URL 探测输入/输出格式,选择合适的协议和封装格式。例如,如果输出 URL 是 "rtmp://192.168.0.104/live",那么 FFmpeg 打开输出时,会确定使用 rtmp 协议,封装格式为 flv。

    FFmpeg 中打开输入/输出的内部处理细节用户不必关注,因此本文流处理的例程和前面转封装的例程非常相似,不同之处主要在于输入/输出 URL 形式不同,若 URL 携带 "rtmp://"、"rpt://"、"udp://"等前缀,则表示涉及流处理;否则,处理的是本地文件。

    1.2 流媒体系统中的角色

    流媒体系统是一个比较复杂的系统,简单来说涉及三个角色:流媒体服务器、推流客户端和收流客户端。推流客户端是内容生产者,收流客户端是内容消费者。示意图如下:
    流媒体系统示意简图

    1.3 收流与推流

    如果输入是网络流,输出是本地文件,则实现的是收流功能,将网络流存储为本地文件,如下:
    收流

    如果输入是本地文件,输出是网络流,则实现的是推流功能,将本地文件推送到网络,如下:
    推流

    如果输入是网络流,输出也是网络流,则实现的是转流功能,将一个流媒体服务器上的流推送到另一个流媒体服务器,如下:
    转流

    2. 源码

    源码和转封装例程大部分相同,可以认为是转封装例程的增强版:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    #include <stdbool.h>
    #include <libavutil/timestamp.h>
    #include <libavformat/avformat.h>
    
    // ffmpeg -re -i tnhaoxc.flv -c copy -f flv rtmp://192.168.0.104/live
    // ffmpeg -i rtmp://192.168.0.104/live -c copy tnlinyrx.flv
    // ./streamer tnhaoxc.flv rtmp://192.168.0.104/live
    // ./streamer rtmp://192.168.0.104/live tnhaoxc.flv
    int main(int argc, char **argv)
    {
        AVOutputFormat *ofmt = NULL;
        AVFormatContext *ifmt_ctx = NULL, *ofmt_ctx = NULL;
        AVPacket pkt;
        const char *in_filename, *out_filename;
        int ret, i;
        int stream_index = 0;
        int *stream_mapping = NULL;
        int stream_mapping_size = 0;
    
        if (argc < 3) {
            printf("usage: %s input output
    "
                   "API example program to remux a media file with libavformat and libavcodec.
    "
                   "The output format is guessed according to the file extension.
    "
                   "
    ", argv[0]);
            return 1;
        }
    
        in_filename  = argv[1];
        out_filename = argv[2];
    
        // 1. 打开输入
        // 1.1 读取文件头,获取封装格式相关信息
        if ((ret = avformat_open_input(&ifmt_ctx, in_filename, 0, 0)) < 0) {
            printf("Could not open input file '%s'", in_filename);
            goto end;
        }
        
        // 1.2 解码一段数据,获取流相关信息
        if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0) {
            printf("Failed to retrieve input stream information");
            goto end;
        }
    
        av_dump_format(ifmt_ctx, 0, in_filename, 0);
    
        // 2. 打开输出
        // 2.1 分配输出ctx
        bool push_stream = false;
        char *ofmt_name = NULL;
        if (strstr(out_filename, "rtmp://") != NULL) {
            push_stream = true;
            ofmt_name = "flv";
        }
        else if (strstr(out_filename, "udp://") != NULL) {
            push_stream = true;
            ofmt_name = "mpegts";
        }
        else {
            push_stream = false;
            ofmt_name = NULL;
        }
        avformat_alloc_output_context2(&ofmt_ctx, NULL, ofmt_name, out_filename);
        if (!ofmt_ctx) {
            printf("Could not create output context
    ");
            ret = AVERROR_UNKNOWN;
            goto end;
        }
    
        stream_mapping_size = ifmt_ctx->nb_streams;
        stream_mapping = av_mallocz_array(stream_mapping_size, sizeof(*stream_mapping));
        if (!stream_mapping) {
            ret = AVERROR(ENOMEM);
            goto end;
        }
    
        ofmt = ofmt_ctx->oformat;
    
        AVRational frame_rate;
        double duration;
    
        for (i = 0; i < ifmt_ctx->nb_streams; i++) {
            AVStream *out_stream;
            AVStream *in_stream = ifmt_ctx->streams[i];
            AVCodecParameters *in_codecpar = in_stream->codecpar;
    
            if (in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO &&
                in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO &&
                in_codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
                stream_mapping[i] = -1;
                continue;
            }
    
            if (push_stream && (in_codecpar->codec_type == AVMEDIA_TYPE_VIDEO)) {
                frame_rate = av_guess_frame_rate(ifmt_ctx, in_stream, NULL);
                duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
            }
    
            stream_mapping[i] = stream_index++;
    
            // 2.2 将一个新流(out_stream)添加到输出文件(ofmt_ctx)
            out_stream = avformat_new_stream(ofmt_ctx, NULL);
            if (!out_stream) {
                printf("Failed allocating output stream
    ");
                ret = AVERROR_UNKNOWN;
                goto end;
            }
    
            // 2.3 将当前输入流中的参数拷贝到输出流中
            ret = avcodec_parameters_copy(out_stream->codecpar, in_codecpar);
            if (ret < 0) {
                printf("Failed to copy codec parameters
    ");
                goto end;
            }
            out_stream->codecpar->codec_tag = 0;
        }
        av_dump_format(ofmt_ctx, 0, out_filename, 1);
    
        if (!(ofmt->flags & AVFMT_NOFILE)) {    // TODO: 研究AVFMT_NOFILE标志
            // 2.4 创建并初始化一个AVIOContext,用以访问URL(out_filename)指定的资源
            ret = avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE);
            if (ret < 0) {
                printf("Could not open output file '%s'", out_filename);
                goto end;
            }
        }
    
        // 3. 数据处理
        // 3.1 写输出文件头
        ret = avformat_write_header(ofmt_ctx, NULL);
        if (ret < 0) {
            printf("Error occurred when opening output file
    ");
            goto end;
        }
    
        while (1) {
            AVStream *in_stream, *out_stream;
    
            // 3.2 从输出流读取一个packet
            ret = av_read_frame(ifmt_ctx, &pkt);
            if (ret < 0) {
                break;
            }
    
            in_stream  = ifmt_ctx->streams[pkt.stream_index];
            if (pkt.stream_index >= stream_mapping_size ||
                stream_mapping[pkt.stream_index] < 0) {
                av_packet_unref(&pkt);
                continue;
            }
    
            int codec_type = in_stream->codecpar->codec_type;
            if (push_stream && (codec_type == AVMEDIA_TYPE_VIDEO)) {
                av_usleep((int64_t)(duration*AV_TIME_BASE));
            }
    
            pkt.stream_index = stream_mapping[pkt.stream_index];
            out_stream = ofmt_ctx->streams[pkt.stream_index];
    
            /* copy packet */
            // 3.3 更新packet中的pts和dts
            // 关于AVStream.time_base(容器中的time_base)的说明:
            // 输入:输入流中含有time_base,在avformat_find_stream_info()中可取到每个流中的time_base
            // 输出:avformat_write_header()会根据输出的封装格式确定每个流的time_base并写入文件中
            // AVPacket.pts和AVPacket.dts的单位是AVStream.time_base,不同的封装格式AVStream.time_base不同
            // 所以输出文件中,每个packet需要根据输出封装格式重新计算pts和dts
            av_packet_rescale_ts(&pkt, in_stream->time_base, out_stream->time_base);
            pkt.pos = -1;
    
            // 3.4 将packet写入输出
            ret = av_interleaved_write_frame(ofmt_ctx, &pkt);
            if (ret < 0) {
                printf("Error muxing packet
    ");
                break;
            }
            av_packet_unref(&pkt);
        }
    
        // 3.5 写输出文件尾
        av_write_trailer(ofmt_ctx);
    
    end:
        avformat_close_input(&ifmt_ctx);
    
        /* close output */
        if (ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE)) {
            avio_closep(&ofmt_ctx->pb);
        }
        avformat_free_context(ofmt_ctx);
    
        av_freep(&stream_mapping);
    
        if (ret < 0 && ret != AVERROR_EOF) {
            printf("Error occurred: %s
    ", av_err2str(ret));
            return 1;
        }
    
        return 0;
    }
    

    2.1 收流

    收流的代码与打开普通文件的代码没有区别,打开输入时,FFmpeg 能识别流协议及封装格式,根据相应的协议层代码来接收流,收到流数据去掉协议层后得到的数据和普通文件内容是一样的,后续的处理流程也就一样了。

    2.2 推流

    推流有两个需要注意的地方。

    一是需要根据输出流协议显式指定输出 URL 的封装格式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
        bool push_stream = false;
        char *ofmt_name = NULL;
        if (strstr(out_filename, "rtmp://") != NULL) {
            push_stream = true;
            ofmt_name = "flv";
        }
        else if (strstr(out_filename, "udp://") != NULL) {
            push_stream = true;
            ofmt_name = "mpegts";
        }
        else {
            push_stream = false;
            ofmt_name = NULL;
        }
        avformat_alloc_output_context2(&ofmt_ctx, NULL, ofmt_name, out_filename);
    

    这里只写了两种。rtmp 推流必须推送 flv 封装格式,udp 推流必须推送 mpegts 封装格式,其他情况就当作是输出普通文件。这里使用 push_stream 变量来标志是否使用推流功能,这个标志后面会用到。

    二是要注意推流的速度,不能一股脑将收到的数据全推出去,这样流媒体服务器承受不住。可以按视频播放速度(帧率)来推流。因此每推送一个视频帧,要延时一个视频帧的时长。音频流的数据量很小,可以不必关心此问题。

    在打开输入 URL 时,获取视频帧的持续时长:

    1
    2
    3
    4
    5
    6
        AVRational frame_rate;
        double duration;
        if (push_stream && (in_codecpar->codec_type == AVMEDIA_TYPE_VIDEO)) {
            frame_rate = av_guess_frame_rate(ifmt_ctx, in_stream, NULL);
            duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
        }
    

    在 av_read_frame() 之后,av_interleaved_write_frame() 之前增加延时,延时时长就是一个视频帧的持续时长:

    1
    2
    3
    4
        int codec_type = in_stream->codecpar->codec_type;
        if (push_stream && (codec_type == AVMEDIA_TYPE_VIDEO)) {
            av_usleep((int64_t)(duration*AV_TIME_BASE));
        }
    

    3. 验证

    3.1 编译第三方库 librtmp

    FFmpeg 默认并不支持 rtmp 协议。需要先编译安装第三方库 librtmp,然后开启 --enable-librtmp 选项重新编译安装 FFmpeg。具体方法参考:"FFmpeg 开发环境构建"

    3.2 搭建流媒体服务器

    测试收流与推流功能需要搭建流媒体服务器。我们选用 nginx-rtmp 作为流媒体服务器用于测试。nginx-rtmp 服务器运行于虚拟机上,推流客户端与收流客户端和 nginx-rtmp 服务器处于同一局域网即可。我的虚拟机是 OPENSUSE LEAP 42.3,IP 是 192.168.0.104(就是 nginx-rtmp 服务器的地址)。

    为避免搭建服务器的繁琐过程,我们直接使用 docker 拉取一个 nginx-rtmp 镜像。步骤如下:

    [1] 安装与配置docker服务

    安装 docker:

    sudo zypper install docker
    

    将当前用户添加到 docker 组(若 docker 组不存在则先创建),从而可以免 sudo 使用 docker 命令:

    sudo gpasswd -a ${USER} docker
    

    [2] 配置镜像加速

    docker 镜像源位于美国,摘取镜像非常缓慢。可配置国内镜像源,加快镜像拉取速度。

    修改 /etc/docker/daemon.json 文件并添加上 registry-mirrors 键值:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
        "registry-mirrors":
        [
            "https://registry.docker-cn.com",
            "https://docker.mirrors.ustc.edu.cn",
            "https://hub-mirror.c.163.com",
            "https://mirror.ccs.tencentyun.com"
        ]
    }
    

    上述配置文件添加了四个国内镜像源:docker 中国、清华、163 和腾讯。

    修改配置文件后重启 docker 服务:

    systemctl restart docker
    

    [3] 拉取 nginx-rtmp 镜像

    docker pull tiangolo/nginx-rtmp
    

    [4] 打开容器

    docker run -d -p 1935:1935 --name nginx-rtmp tiangolo/nginx-rtmp
    

    [5] 防火墙添加例外端口

    如果无法推流,应在防火墙中将 1935 端口添加例外

    openSUSE 系统:修改 /etc/sysconfig/SuSEfirewall2 文件,在 FW_SERVICES_EXT_TCP 项中添加 1935 端口,如下:

    FW_SERVICES_EXT_TCP="ssh 1935"
    

    然后重启防火墙:

    systemctl restart SuSEfirewall2
    

    CentOS 8 系统:运行如下命令将 1935 端口添加到防火墙例外端口中:

    firewall-cmd --permanent --zone=public --add-port=1935/tcp
    

    [6] 验证服务器

    测试文件下载(右键另存为):tnhaoxc.flv

    ffmpeg 推流测试:

    ffmpeg -re -i tnhaoxc.flv -c copy -f flv rtmp://192.168.0.104/live
    

    "-re":按视频帧率的速度读取输入
    "-c copy":输出流使用和输入流相同的编解码器
    "-f flv":指定输出流封装格式为flv

    ffplay 收流播放测试:

    ffplay rtmp://192.168.0.104/live
    

    ffplay 播放正常,说明 nginx-rtmp 流媒体服务器搭建成功,可正常使用。

    3.3 编译

    在 shell 中运行如下命令下载例程源码:

    svn checkout https://github.com/leichn/exercises/trunk/source/ffmpeg/ffmpeg_stream
    

    在源码目录执行 ./compile.sh 命令,生成 streamer 可执行文件。

    3.4 验证

    测试文件下载(右键另存为):shifu.mkv,将测试文件保存在和源码同一目录。

    推流测试:

    ./streamer shifu.mkv rtmp://192.168.0.104/live
    

    使用 vlc 播放器打开网络串流,输入流地址 "rtmp://192.168.0.104/live",播放正常。上述测试命令等价于:

    ffmpeg -re -i shifu.mkv -c copy -f flv rtmp://192.168.0.104/live
    

    师父

    收流测试:先按照上一步命令启动推流,然后运行如下命令收流

    ./streamer rtmp://192.168.0.104/live shifu.ts
    

    以上测试命令等价于:

    ffmpeg -i rtmp://192.168.0.104/live -c copy shifu.ts
    

    接收结束后检查一下生成的本地文件 shifu.ts 能否正常播放。

    4. 遗留问题

    推流的问题:不管是用 ffmpeg 命令,还是用本测试程序,推流结束时会打印如下信息

    1
    2
    3
    4
    [flv @ 0x22ab9c0] Timestamps are unset in a packet for stream 0. This is deprecated and will stop working in the future. Fix your code to set the timestamps properly
    Larger timestamp than 24-bit: 0xffffffc2
    [flv @ 0x22ab9c0] Failed to update header with correct duration.
    [flv @ 0x22ab9c0] Failed to update header with correct filesize.
    

    收流的问题:推流结束后,收流超时未收以数据,会打印如下信息后程序退出运行

    RTMP_ReadPacket, failed to read RTMP packet header
    

    5. 参考资料

    [1] 雷霄骅, RTMP流媒体技术零基础学习方法
    [2] 观止云, 【流媒体|从入门到出家】:流媒体原理(上)
    [3] 观止云, 【流媒体|从入门到出家】:流媒体原理(下)
    [4] 观止云, 【流媒体|从入门到出家】:流媒体系统(上)
    [5] 观止云, 【流媒体|从入门到出家】:流媒体系统(下)
    [6] 观止云, 总结:从一个直播APP看流媒体系统的应用

    6. 修改记录

    2019-03-29 V1.0 初稿

  • 相关阅读:
    原生态ajax
    用js提交表单,没有submit按钮如何验证,使用button提交方法
    易买网吐血文档(图片拖不上来,要文档留下联系)
    时序图Sequence DiaGram
    starUML用例图
    泛型自动扩容的原理
    深入C#数据类型
    了解.NET框架
    自定义jstl标签实现页面级的权限控制
    SharePoint 2013 REST 服务使用简介
  • 原文地址:https://www.cnblogs.com/bigben0123/p/14485202.html
Copyright © 2020-2023  润新知