Ubuntu 18.04下SRS+NGINX流媒体服务器搭建及FFmpeg H265推拉流实现指南

内容分享2周前发布
1 0 0

Ubuntu 18.04下SRS+NGINX流媒体服务器搭建及FFmpeg H265推拉流实现指南

架构图核心说明

组件职责

推流端(FFmpeg):负责将本地视频编码为 H265 格式,通过 RTMP 协议推送到服务器NGINX:双角色(RTMP 流转发 + HTTP 静态文件分发),解决 SRS 的 HLS 访问和负载均衡需求SRS:核心流媒体服务器,接收 RTMP 流并转码为 HLS 切片,支持低延迟 RTMP 拉流拉流端(FFmpeg):拉取 SRS 的 RTMP 流解码为 YUV 原始数据,或播放器直接访问 NGINX 的 HLS 流播放

关键链路

推流链路(延迟低):视频文件 → FFmpeg 编码 → RTMP → NGINX → SRS拉流链路(双选项):
低延迟:SRS → RTMP → FFmpeg 解码 → YUV 文件跨平台播放:SRS → HLS 切片 → NGINX → HTTP → 播放器(支持浏览器 / 移动端)

协议与端口

RTMP:1935(SRS)、1936(NGINX 转发)HTTP(HLS):8080(NGINX)编码格式:H265/HEVC(推流)、YUV420P(解码输出)

一、SRS+NGINX 流媒体服务器搭建步骤(Ubuntu 18.04)

1. 系统依赖安装


sudo apt update && sudo apt upgrade -y
sudo apt install -y git gcc g++ make cmake libssl-dev libpcre3-dev zlib1g-dev wget
2. SRS 服务器搭建

SRS(Simple Real-Time Server)是轻量级流媒体服务器,支持 RTMP/HLS/WebRTC 等协议。

(1)克隆 SRS 源码


git clone https://gitee.com/ossrs/srs.git
cd srs/trunk
(2)编译 SRS


# 配置编译选项(启用SSL/HLS/FFmpeg)
./configure --prefix=/usr/local/srs --with-ssl --with-hls --with-dvr
make -j$(nproc)  # 多核编译
sudo make install
(3)配置 SRS

编辑主配置文件:


sudo vi /usr/local/srs/conf/srs.conf

修改核心配置(开启 RTMP/HLS,设置路径):



listen              1935;        # RTMP默认端口
max_connections     1000;
daemon              on;          # 后台运行
srs_log_tank        file;        # 日志输出到文件
srs_log_file        /usr/local/srs/logs/srs.log;
 
vhost __defaultVhost__ {
    # HLS配置
    hls {
        enabled         on;
        hls_path        /usr/local/srs/objs/nginx/html;  # HLS文件存储路径
        hls_fragment    10;       # 切片时长(秒)
        hls_window      60;       # 播放列表长度(秒)
    }
    # RTMP配置
    rtmp {
        gop_cache       on;       # 缓存关键帧减少延迟
        queue_length    10;
        min_latency     off;
    }
}
(4)启动 SRS


sudo /usr/local/srs/objs/srs start   # 启动
sudo /usr/local/srs/objs/srs status  # 检查状态
3. NGINX 搭建(带 RTMP 模块)

NGINX 用于反向代理和 HLS 静态文件分发,需编译 RTMP 模块。

(1)下载 NGINX 和 RTMP 模块


cd ~
git clone https://github.com/arut/nginx-rtmp-module.git
wget http://nginx.org/download/nginx-1.20.1.tar.gz  # 稳定版本
tar -zxvf nginx-1.20.1.tar.gz
cd nginx-1.20.1
(2)编译安装 NGINX


# 配置编译选项(添加RTMP模块+SSL)
./configure --prefix=/usr/local/nginx 
            --add-module=../nginx-rtmp-module 
            --with-http_ssl_module 
            --with-pcre 
            --with-zlib
 
make -j$(nproc)
sudo make install
(3)配置 NGINX

编辑 NGINX 配置文件:


sudo vi /usr/local/nginx/conf/nginx.conf

添加 RTMP 模块配置(推流转发到 SRS):



# RTMP模块配置(http块外)
rtmp {
    server {
        listen 1936;  # 避免与SRS的1935端口冲突
        chunk_size 4096;
 
        # RTMP推流应用
        application live {
            live on;       # 开启直播
            record off;    # 关闭录制
            push rtmp://localhost:1935/live;  # 转发到SRS
        }
 
        # HLS应用
        application hls {
            live on;
            hls on;
            hls_path /usr/local/nginx/html/hls;  # HLS切片存储路径
            hls_fragment 10s;
            hls_playlist_length 60s;
        }
    }
}
 
# HTTP服务配置(用于HLS播放)
http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;
 
    server {
        listen       8080;  # HTTP播放端口
        server_name  localhost;
 
        # HLS播放路径
        location /hls {
            types {
                application/vnd.apple.mpegurl m3u8;  # M3U8文件类型
                video/mp2t ts;                      # TS切片类型
            }
            root   /usr/local/nginx/html;
            add_header Cache-Control no-cache;  # 禁用缓存
        }
 
        # 静态页面
        location / {
            root   /usr/local/nginx/html;
            index  index.html index.htm;
        }
    }
}
(4)创建 HLS 目录


sudo mkdir -p /usr/local/nginx/html/hls
sudo chmod 777 /usr/local/nginx/html/hls  # 赋予写入权限
(5)启动 NGINX


sudo /usr/local/nginx/sbin/nginx       # 启动
sudo /usr/local/nginx/sbin/nginx -t    # 检查配置
sudo /usr/local/nginx/sbin/nginx -s reload  # 重载配置
4. 测试 SRS+NGINX

推流地址:
rtmp://localhost:1935/live/test
(或 NGINX 的
rtmp://localhost:1936/live/test
)HLS 播放地址:
http://localhost:8080/hls/test.m3u8

二、FFmpeg H265 推流编码(C 代码实现)

基于 FFmpeg4 API,实现本地视频文件转 H265 编码并推流到 SRS。

1. 编译依赖

确保安装 FFmpeg 开发库:


sudo apt install -y libavformat-dev libavcodec-dev libavutil-dev libswscale-dev
2. 推流代码(h265_push.c)


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
 
// 错误处理函数
#define handle_error(ret) 
    if (ret < 0) { 
        char errbuf[1024]; 
        av_strerror(ret, errbuf, sizeof(errbuf)); 
        fprintf(stderr, "Error: %s
", errbuf); 
        exit(EXIT_FAILURE); 
    }
 
int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <input_file> <rtmp_url>
", argv[0]);
        return 1;
    }
    const char *input_file = argv[1];
    const char *rtmp_url = argv[2];
 
    // 初始化网络(FFmpeg4需要)
    avformat_network_init();
 
    // 1. 打开输入文件
    AVFormatContext *ifmt_ctx = NULL;
    int ret = avformat_open_input(&ifmt_ctx, input_file, NULL, NULL);
    handle_error(ret);
 
    // 2. 获取流信息
    ret = avformat_find_stream_info(ifmt_ctx, NULL);
    handle_error(ret);
    av_dump_format(ifmt_ctx, 0, input_file, 0);
 
    // 3. 查找视频流索引
    int video_stream_idx = -1;
    AVCodecParameters *in_codecpar = NULL;
    for (int i = 0; i < ifmt_ctx->nb_streams; i++) {
        if (ifmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            video_stream_idx = i;
            in_codecpar = ifmt_ctx->streams[i]->codecpar;
            break;
        }
    }
    if (video_stream_idx == -1) {
        fprintf(stderr, "No video stream found
");
        return 1;
    }
 
    // 4. 打开输入解码器
    const AVCodec *in_codec = avcodec_find_decoder(in_codecpar->codec_id);
    if (!in_codec) {
        fprintf(stderr, "Input codec not found
");
        return 1;
    }
    AVCodecContext *in_codec_ctx = avcodec_alloc_context3(in_codec);
    ret = avcodec_parameters_to_context(in_codec_ctx, in_codecpar);
    handle_error(ret);
    ret = avcodec_open2(in_codec_ctx, in_codec, NULL);
    handle_error(ret);
 
    // 5. 创建输出上下文(RTMP)
    AVFormatContext *ofmt_ctx = NULL;
    ret = avformat_alloc_output_context2(&ofmt_ctx, NULL, "flv", rtmp_url);
    handle_error(ret);
 
    // 6. 创建输出视频流
    AVStream *out_stream = avformat_new_stream(ofmt_ctx, NULL);
    if (!out_stream) {
        fprintf(stderr, "Failed to create output stream
");
        return 1;
    }
 
    // 7. 配置H265编码器参数
    const AVCodec *out_codec = avcodec_find_encoder_by_name("hevc");  // H265=HEVC
    if (!out_codec) {
        fprintf(stderr, "HEVC encoder not found
");
        return 1;
    }
    AVCodecContext *out_codec_ctx = avcodec_alloc_context3(out_codec);
    out_codec_ctx->codec_id = out_codec->id;
    out_codec_ctx->codec_type = AVMEDIA_TYPE_VIDEO;
    out_codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;  // H265标准像素格式
    out_codec_ctx->width = in_codec_ctx->width;
    out_codec_ctx->height = in_codec_ctx->height;
    out_codec_ctx->bit_rate = 2000000;  // 码率2Mbps
    out_codec_ctx->gop_size = 25;       // 关键帧间隔
    out_codec_ctx->time_base = (AVRational){1, 25};  // 帧率25fps
    out_codec_ctx->framerate = (AVRational){25, 1};
 
    // 编码器参数复制到输出流
    ret = avcodec_parameters_from_context(out_stream->codecpar, out_codec_ctx);
    handle_error(ret);
    out_stream->time_base = out_codec_ctx->time_base;
 
    // 8. 打开H265编码器
    ret = avcodec_open2(out_codec_ctx, out_codec, NULL);
    handle_error(ret);
 
    // 9. 初始化像素格式转换器(输入->YUV420P)
    struct SwsContext *sws_ctx = sws_getContext(
        in_codec_ctx->width, in_codec_ctx->height, in_codec_ctx->pix_fmt,
        out_codec_ctx->width, out_codec_ctx->height, out_codec_ctx->pix_fmt,
        SWS_BICUBIC, NULL, NULL, NULL
    );
    if (!sws_ctx) {
        fprintf(stderr, "Failed to create sws context
");
        return 1;
    }
 
    // 10. 打开输出IO(RTMP)
    if (!(ofmt_ctx->oformat->flags & AVFMT_NOFILE)) {
        ret = avio_open(&ofmt_ctx->pb, rtmp_url, AVIO_FLAG_WRITE);
        handle_error(ret);
    }
 
    // 11. 写入文件头
    ret = avformat_write_header(ofmt_ctx, NULL);
    handle_error(ret);
 
    // 12. 帧处理循环
    AVPacket *pkt = av_packet_alloc();
    AVFrame *in_frame = av_frame_alloc();
    AVFrame *out_frame = av_frame_alloc();
    out_frame->format = out_codec_ctx->pix_fmt;
    out_frame->width = out_codec_ctx->width;
    out_frame->height = out_codec_ctx->height;
    ret = av_frame_get_buffer(out_frame, 0);
    handle_error(ret);
 
    int frame_index = 0;
    while (av_read_frame(ifmt_ctx, pkt) >= 0) {
        if (pkt->stream_index == video_stream_idx) {
            // 解码输入帧
            ret = avcodec_send_packet(in_codec_ctx, pkt);
            handle_error(ret);
            while (ret >= 0) {
                ret = avcodec_receive_frame(in_codec_ctx, in_frame);
                if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
                handle_error(ret);
 
                // 转换像素格式
                sws_scale(sws_ctx,
                          (const uint8_t *const *)in_frame->data, in_frame->linesize,
                          0, in_codec_ctx->height,
                          out_frame->data, out_frame->linesize);
 
                // 设置编码帧参数
                out_frame->pts = frame_index++;
                ret = avcodec_send_frame(out_codec_ctx, out_frame);
                handle_error(ret);
 
                // 编码得到数据包
                while (ret >= 0) {
                    ret = avcodec_receive_packet(out_codec_ctx, pkt);
                    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
                    handle_error(ret);
 
                    // 调整时间戳
                    av_packet_rescale_ts(pkt, out_codec_ctx->time_base, out_stream->time_base);
                    pkt->stream_index = out_stream->index;
 
                    // 写入输出流
                    ret = av_interleaved_write_frame(ofmt_ctx, pkt);
                    handle_error(ret);
                    av_packet_unref(pkt);
                }
            }
        }
        av_packet_unref(pkt);
    }
 
    // 13. 刷新编码器
    ret = avcodec_send_frame(out_codec_ctx, NULL);
    handle_error(ret);
    while (ret >= 0) {
        ret = avcodec_receive_packet(out_codec_ctx, pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
        handle_error(ret);
        av_packet_rescale_ts(pkt, out_codec_ctx->time_base, out_stream->time_base);
        pkt->stream_index = out_stream->index;
        ret = av_interleaved_write_frame(ofmt_ctx, pkt);
        handle_error(ret);
        av_packet_unref(pkt);
    }
 
    // 14. 写入文件尾
    av_write_trailer(ofmt_ctx);
 
    // 15. 资源释放
    av_frame_free(&in_frame);
    av_frame_free(&out_frame);
    av_packet_free(&pkt);
    avcodec_close(in_codec_ctx);
    avcodec_close(out_codec_ctx);
    avcodec_free_context(&in_codec_ctx);
    avcodec_free_context(&out_codec_ctx);
    avformat_close_input(&ifmt_ctx);
    if (!(ofmt_ctx->oformat->flags & AVFMT_NOFILE)) {
        avio_closep(&ofmt_ctx->pb);
    }
    avformat_free_context(ofmt_ctx);
    sws_freeContext(sws_ctx);
    avformat_network_deinit();
 
    return 0;
}
3. 编译推流代码

gcc h265_push.c -o h265_push -lavformat -lavcodec -lavutil -lswscale
4. 运行推流

./h265_push input.mp4 rtmp://localhost:1935/live/test

三、FFmpeg H265 拉流解码(C 代码实现)

实现从 SRS 拉取 H265 流并解码为 YUV 文件。

1. 解码代码(h265_pull.c)


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
 
#define handle_error(ret) 
    if (ret < 0) { 
        char errbuf[1024]; 
        av_strerror(ret, errbuf, sizeof(errbuf)); 
        fprintf(stderr, "Error: %s
", errbuf); 
        exit(EXIT_FAILURE); 
    }
 
int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <rtmp_url> <output_yuv>
", argv[0]);
        return 1;
    }
    const char *rtmp_url = argv[1];
    const char *output_yuv = argv[2];
 
    // 初始化网络
    avformat_network_init();
 
    // 1. 打开RTMP输入流
    AVFormatContext *fmt_ctx = NULL;
    int ret = avformat_open_input(&fmt_ctx, rtmp_url, NULL, NULL);
    handle_error(ret);
 
    // 2. 获取流信息
    ret = avformat_find_stream_info(fmt_ctx, NULL);
    handle_error(ret);
    av_dump_format(fmt_ctx, 0, rtmp_url, 0);
 
    // 3. 查找视频流索引
    int video_stream_idx = -1;
    AVCodecParameters *codecpar = NULL;
    for (int i = 0; i < fmt_ctx->nb_streams; i++) {
        if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            video_stream_idx = i;
            codecpar = fmt_ctx->streams[i]->codecpar;
            break;
        }
    }
    if (video_stream_idx == -1) {
        fprintf(stderr, "No video stream found
");
        return 1;
    }
 
    // 4. 打开H265解码器
    const AVCodec *codec = avcodec_find_decoder(codecpar->codec_id);
    if (!codec) {
        fprintf(stderr, "Decoder not found (must be HEVC/H265)
");
        return 1;
    }
    AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
    ret = avcodec_parameters_to_context(codec_ctx, codecpar);
    handle_error(ret);
    ret = avcodec_open2(codec_ctx, codec, NULL);
    handle_error(ret);
 
    // 5. 打开YUV输出文件
    FILE *yuv_file = fopen(output_yuv, "wb");
    if (!yuv_file) {
        fprintf(stderr, "Failed to open YUV file
");
        return 1;
    }
 
    // 6. 帧处理循环
    AVPacket *pkt = av_packet_alloc();
    AVFrame *frame = av_frame_alloc();
    while (av_read_frame(fmt_ctx, pkt) >= 0) {
        if (pkt->stream_index == video_stream_idx) {
            // 发送数据包到解码器
            ret = avcodec_send_packet(codec_ctx, pkt);
            handle_error(ret);
 
            // 接收解码帧
            while (ret >= 0) {
                ret = avcodec_receive_frame(codec_ctx, frame);
                if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
                handle_error(ret);
 
                // 写入YUV数据(YUV420P: Y+U+V)
                int y_size = frame->width * frame->height;
                fwrite(frame->data[0], 1, y_size, yuv_file);       // Y分量
                fwrite(frame->data[1], 1, y_size/4, yuv_file);     // U分量
                fwrite(frame->data[2], 1, y_size/4, yuv_file);     // V分量
 
                printf("Decoded frame: %d
", codec_ctx->frame_number);
            }
        }
        av_packet_unref(pkt);
    }
 
    // 7. 刷新解码器
    ret = avcodec_send_packet(codec_ctx, NULL);
    handle_error(ret);
    while (ret >= 0) {
        ret = avcodec_receive_frame(codec_ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
        handle_error(ret);
        int y_size = frame->width * frame->height;
        fwrite(frame->data[0], 1, y_size, yuv_file);
        fwrite(frame->data[1], 1, y_size/4, yuv_file);
        fwrite(frame->data[2], 1, y_size/4, yuv_file);
    }
 
    // 8. 资源释放
    fclose(yuv_file);
    av_frame_free(&frame);
    av_packet_free(&pkt);
    avcodec_close(codec_ctx);
    avcodec_free_context(&codec_ctx);
    avformat_close_input(&fmt_ctx);
    avformat_network_deinit();
 
    printf("Decode finished, YUV file saved as %s
", output_yuv);
    return 0;
}
2. 编译解码代码

gcc h265_pull.c -o h265_pull -lavformat -lavcodec -lavutil
3. 运行拉流解码

./h265_pull rtmp://localhost:1935/live/test output.yuv

注意事项

SRS 启动失败:检查端口占用(
netstat -tulpn | grep 1935
),或修改配置端口。FFmpeg 编码器缺失:Ubuntu 18.04 默认 FFmpeg4 可能缺少 HEVC 编码器,需安装
libx265-dev


sudo apt install -y libx265-dev

推流延迟:SRS 配置中
min_latency on
可降低延迟(需关闭 GOP 缓存)。

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...