
架构图核心说明
组件职责:
推流端(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
推流地址:(或 NGINX 的
rtmp://localhost:1935/live/test)HLS 播放地址:
rtmp://localhost:1936/live/test
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 启动失败:检查端口占用(),或修改配置端口。FFmpeg 编码器缺失:Ubuntu 18.04 默认 FFmpeg4 可能缺少 HEVC 编码器,需安装
netstat -tulpn | grep 1935:
libx265-dev
sudo apt install -y libx265-dev
推流延迟:SRS 配置中可降低延迟(需关闭 GOP 缓存)。
min_latency on
