ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SRS DVR 实战指南:配置 RTMP 流录制为 FLV/MP4,详解 session/segment 策略、自定义路径与 on_dvr 回调

2026/9/9 23:29:25 拓冰建站 浏览量
SRS DVR 实战指南:配置 RTMP 流录制为 FLV/MP4,详解 session/segment 策略、自定义路径与 on_dvr 回调 SRS DVR 实战指南配置 RTMP 流录制为 FLV/MP4详解 session/segment 策略、自定义路径与 on_dvr 回调【免费下载链接】srsSRS is a simple, high-performance, AI-driven real-time media server supporting RTMP, WebRTC, HLS, HTTP-FLV, HTTP-TS, SRT, MPEG-DASH, and GB28181, with codec support for H.264, H.265, AV1, VP9, AAC, Opus, and G.711.项目地址: https://gitcode.com/GitHub_Trending/sr/srsSRS 的 DVRDigital Video Recorder功能可将 FFmpeg/OBS 推入的 RTMP 流实时录制为 FLV 或 MP4 文件是直播录制、内容存档场景的基础能力。本文基于 SRS 仓库中的 DVR 官方文档与trunk/src/app/srs_app_dvr.*、trunk/src/app/srs_app_config.cpp等源码完整讲解dvr配置块的全部参数、两种切片策略session/segment的行为差异、dvr_path自定义路径规则、dvr_apply流过滤器、time_jitter时间抖动算法以及通过http_hooks.on_dvr在切片落盘后触发外部处理的回调机制帮助你在 SRS 上搭建可靠、可控的录制链路。DVR 定位与整体工作流SRS 支持将 RTMP 流通过 DVR 写成 FLV/MP4 文件。当 FFmpeg/OBS 向 SRS 推流时SRS 会把流写入 FLV/MP4 文件整体链路为------------ ------- --------------- FFmpeg/OBS ---RTMP---- SRS ---DVR---- FLV/MP4 File ------------ ------- ---------------DVR 功能本身可以做得非常复杂SRS 只实现了基础 DVR 能力。如果你需要更高级的录制特性建议改用 Oryx参见 Oryx DVR例如Oryx 支持 S3 云存储可将最终 MP4 文件移动到 S3 云存储Oryx 支持 glob 过滤器只录制指定流而非所有流Oryx 支持把多次推流会话合并到一个 MP4 文件中。从源码结构看DVR 在 SRS 中是一套独立的「计划Plan 分段器Segmenter」框架SrsDvr负责挂载在每路推流上SrsDvrPlan决定何时开/关文件何时 reapSrsDvrSegmenter负责把音频/视频包编码写盘类定义见 srs_app_dvr.hpp。构建与默认配置DVR 在 SRS3 中始终启用always enabled无需额外编译选项。各参数的默认值可以在配置解析源码中直接确认srs_app_config.cpp参数含义默认值源码依据enabled是否启用 DVRoff见 get_dvr_enableddvr_applyDVR 过滤器all未配置时对所有流生效dvr_plan切片策略session见 get_dvr_plandvr_path输出路径目录或含变量的文件名./objs/nginx/html/[app]/[stream].[timestamp].flv见 get_dvr_pathdvr_durationsegment 策略下单段时长秒30见 get_dvr_durationdvr_wait_keyframe是否等关键帧再切段on见 get_dvr_wait_keyframetime_jitter时间抖动修正算法full见 get_dvr_time_jitter另外注意DVR 的「难点」在于 FLV 文件名——SRS 使用app/stream随机名的命名方式用户可通过 http-callback 在 DVR 产出 FLV 文件时重命名详见后文 HTTP Callback 一节。完整配置示例DVR 的完整配置如下继承自官方文档并逐参数注释可直接复制到 vhost 中修改使用vhost yourvhost { # DVR RTMP stream to file, # start to record to file when encoder publish, # reap flv/mp4 according by specified dvr_plan. dvr { # whether enabled dvr features # default: off enabled on; # the filter for dvr to apply to. # all, dvr all streams of all apps. # app/stream, apply to specified stream of app. # for example, to dvr the following two streams: # live/stream1 live/stream2 # default: all dvr_apply all; # the dvr plan. can be: # session reap flv/mp4 when session end(unpublish). # segment reap flv/mp4 when flv duration exceed the specified dvr_duration. # remark The plan append is removed in SRS3, for its no use. # default: session dvr_plan session; # the dvr output path, *.flv or *.mp4. # we supports some variables to generate the filename. # [vhost], the vhost of stream. # [app], the app of stream. # [stream], the stream name of stream. # [2006], replace this const to current year. # [01], replace this const to current month. # [02], replace this const to current date. # [15], replace this const to current hour. # [04], replace this const to current minute. # [05], replace this const to current second. # [999], replace this const to current millisecond. # [timestamp], replace this const to current UNIX timestamp in ms. # remark we use golang time format 2006-01-02 15:04:05.999 as [2006]-[01]-[02]_[15].[04].[05]_[999] # for example, for url rtmp://ossrs.net/live/livestream and time 2015-01-03 10:57:30.776 # 1. No variables, the rule of SRS1.0(auto add [stream].[timestamp].flv as filename): # dvr_path ./objs/nginx/html; # # dvr_path ./objs/nginx/html/live/livestream.1420254068776.flv; # 2. Use stream and date as dir name, time as filename: # dvr_path /data/[vhost]/[app]/[stream]/[2006]/[01]/[02]/[15].[04].[05].[999].flv; # # dvr_path /data/ossrs.net/live/livestream/2015/01/03/10.57.30.776.flv; # 3. Use stream and year/month as dir name, date and time as filename: # dvr_path /data/[vhost]/[app]/[stream]/[2006]/[01]/[02]-[15].[04].[05].[999].flv; # # dvr_path /data/ossrs.net/live/livestream/2015/01/03-10.57.30.776.flv; # 4. Use vhost/app and year/month as dir name, stream/date/time as filename: # dvr_path /data/[vhost]/[app]/[2006]/[01]/[stream]-[02]-[15].[04].[05].[999].flv; # # dvr_path /data/ossrs.net/live/2015/01/livestream-03-10.57.30.776.flv; # 5. DVR to mp4: # dvr_path ./objs/nginx/html/[app]/[stream].[timestamp].mp4; # # dvr_path ./objs/nginx/html/live/livestream.1420254068776.mp4; # segment,session apply it. # default: ./objs/nginx/html/[app]/[stream].[timestamp].flv dvr_path ./objs/nginx/html/[app]/[stream].[timestamp].flv; # the duration for dvr file, reap if exceed, in seconds. # segment apply it. # session,append ignore. # default: 30 dvr_duration 30; # whether wait keyframe to reap segment, # if off, reap segment when duration exceed the dvr_duration, # if on, reap segment when duration exceed and got keyframe. # segment apply it. # session,append ignore. # default: on dvr_wait_keyframe on; # about the stream monotonically increasing: # 1. video timestamp is monotonically increasing, # 2. audio timestamp is monotonically increasing, # 3. video and audio timestamp is interleaved monotonically increasing. # its specified by RTMP specification, see 3. Byte Order, Alignment, and Time Format # however, some encoder cannot provides this feature, please set this to off to ignore time jitter. # the time jitter algorithm: # 1. full, to ensure stream start at zero, and ensure stream monotonically increasing. # 2. zero, only ensure stream start at zero, ignore timestamp jitter. # 3. off, disable the time jitter algorithm, like atc. # apply for all dvr plan. # default: full time_jitter full; # on_dvr, never config in here, should config in http_hooks. # for the dvr http callback, see http_hooks.on_dvr of vhost hooks.callback.srs.com } }仓库中提供了两个可直接参考的现成配置conf/dvr.session.confdvr_plan session推流结束即关闭文件conf/dvr.segment.confdvr_plan segmentdvr_duration 30dvr_wait_keyframe on按 30 秒切片。注意on_dvr回调不能写在dvr块里必须配置在http_hooks中后文有完整示例。切片策略session 与 segmentdvr_plan决定「何时关闭reap当前 FLV/MP4 文件」源码中通过SrsDvrPlan::create_plan工厂函数创建对应策略非法取值会直接报illegal plan错误srs_app_dvr.cppsrs_error_t SrsDvrPlan::create_plan(ISrsAppConfig *config, string vhost, ISrsDvrPlan **pplan) { std::string plan config-get_dvr_plan(vhost); if (srs_config_dvr_is_plan_segment(plan)) { *pplan new SrsDvrSegmentPlan(); } else if (srs_config_dvr_is_plan_session(plan)) { *pplan new SrsDvrSessionPlan(); } else { return srs_error_new(ERROR_DVR_ILLEGAL_PLAN, illegal plan%s, vhost%s, plan.c_str(), vhost.c_str()); } return srs_success; }session 策略开始推流时打开 FLV/MP4 文件停止推流unpublish时关闭文件即「一次推流 一个完整文件」。源码中SrsDvrSessionPlan::on_publish在首次推流时调用segment_-open()on_unpublish时调用segment_-close()并忽略关闭错误、随后触发on_dvr异步回调srs_app_dvr.cpp。适合「整场直播录一个文件」的存档场景。segment 策略按dvr_duration和dvr_wait_keyframe定期切开文件每个片段生成独立 FLV/MP4。核心逻辑在SrsDvrSegmentPlan::update_duration每收到一个音视频包都会执行// trunk/src/app/srs_app_dvr.cpp (update_duration) // ignore if duration ok. SrsFragment *fragment segment_-current(); if (cduration_ 0 || fragment-duration() cduration_) { return err; } // when wait keyframe, ignore if no frame arrived. if (wait_keyframe_) { if (!msg-is_video()) { return err; } // 仅接受 H264/HEVC 的关键帧且不是序列头才切段 bool is_key_frame codec_ok SrsFlvVideo::keyframe(payload, size) !SrsFlvVideo::sh(payload, size); if (!is_key_frame) { return err; } } // reap segment segment_-close(); // 关闭当前文件内部触发 rename on_dvr 回调 segment_-open(); // 打开新的片段文件 hub_-on_dvr_request_sh(); // 向源重新请求序列头SPS/PPS、AAC config即片段时长达到dvr_duration后才会切段dvr_wait_keyframe on时切段点必须落在 H264/HEVC 关键帧上非序列头帧保证每个片段都能从关键帧起播dvr_wait_keyframe off时时长一到立即切段不等待关键帧切段后通过on_dvr_request_sh()重新拉取序列头写入新片段保证新文件也包含编码参数。此外segment 策略在写视频时还有一道关键帧门禁新片段在遇到第一个关键帧之前的视频包会被丢弃除非关闭等待关键帧。见SrsDvrFlvSegmenter::encode_videosrs_app_dvr.cppbool sh (format-video_-avc_packet_type_ SrsVideoAvcFrameTraitSequenceHeader); bool keyframe (!sh format-video_-frame_type_ SrsVideoAvcFrameTypeKeyFrame); if (keyframe) { has_keyframe_ true; } // accept the sequence header here. // when got no keyframe, ignore when should wait keyframe. if (!has_keyframe_ !sh wait_keyframe_) { return err; // 首个关键帧前的 P 帧直接丢弃 }自定义路径 dvr_pathdvr_path同时指定目录和文件名设计目标是用日期、时间和流信息作为目录名避免单个目录文件过多用日期、时间和流信息作为文件名便于检索提供日期/时间/流信息变量用中括号标识保持 SRS1.0 规则只写目录无文件名时自动使用[stream].[timestamp].flv作为文件名兼容 SRS1.0 行为。日期时间变量遵循 Go 的时间格式字符串设计用一个真实年份 2006 而不是 YYYY2006-01-02 15:04:05.999支持的变量年[2006]替换为当前年月[01]替换为当前月日[02]替换为当前日时[15]替换为当前时分[04]替换为当前分秒[05]替换为当前秒毫秒[999]替换为当前毫秒时间戳[timestamp]替换为当前 UNIX 时间戳毫秒流信息[vhost]、[app]、[stream]。以 URLrtmp://ossrs.net/live/livestream、时间2015-01-03 10:57:30.776为例各规则的展开结果无变量SRS1.0 规则自动补[stream].[timestamp].flv作为文件名dvr_path ./objs/nginx/html;./objs/nginx/html/live/livestream.1420254068776.flv流和日期作为目录名时间作为文件名dvr_path /data/[vhost]/[app]/[stream]/[2006]/[01]/[02]/[15].[04].[05].[999].flv;/data/ossrs.net/live/livestream/2015/01/03/10.57.30.776.flv流和年/月作为目录名日期和时间作为文件名dvr_path /data/[vhost]/[app]/[stream]/[2006]/[01]/[02]-[15].[04].[05].[999].flv;/data/ossrs.net/live/livestream/2015/01/03-10.57.30.776.flvvhost/app 和年/月作为目录名stream/日期/时间作为文件名dvr_path /data/[vhost]/[app]/[2006]/[01]/[stream]-[02]-[15].[04].[05].[999].flv;/data/ossrs.net/live/2015/01/livestream-03-10.57.30.776.flvapp 作为目录名stream 和时间戳作为文件名SRS1.0 规则dvr_path /data/[app]/[stream].[timestamp].flv;/data/live/livestream.1420254068776.flv路径展开的实现见SrsDvrSegmenter::generate_pathsrs_app_dvr.cpp若配置的路径不以.flv/.mp4结尾则自动追加/[stream].[timestamp].flv随后依次执行srs_path_build_stream替换[vhost]/[app]/[stream]和srs_path_build_timestamp替换日期时间变量与[timestamp]。输出格式的 FLV/MP4 选择也由dvr_path的文件扩展名决定SrsDvr::initialize中路径以.mp4结尾创建SrsDvrMp4Segmenter否则创建SrsDvrFlvSegmentersrs_app_dvr.cpp。dvr_apply录制过滤器dvr_apply是一个过滤器用于启用或禁用指定流的 DVR作用类似 nginx 的访问控制模块但更强用户还可以用 HTTP RAW API 动态控制何时录制指定流参见 RAW DVR API。以下示例只录制live/stream1和live/stream2vhost xxx { dvr { dvr_apply live/stream1 live/stream2; } }源码实现是srs_config_apply_filtersrs_app_config.cpp未配置或为空时默认返回true全量录制参数为单个all时返回true否则将当前请求拼成app/stream精确匹配参数列表命中才录制。需要说明的是dvr_apply不匹配时 DVR 框架仍然会初始化创建 plan 和 segmenter 对象只是不会真正产生文件。time_jitter时间抖动修正RTMP 规范要求流的时间戳单调递增视频时间戳单调递增、音频时间戳单调递增、且音视频交织后单调递增。但部分编码器无法满足该特性此时可将time_jitter设为off忽略时间抖动。三种算法对应源码中的SrsRtmpJitterAlgorithm每个音视频包写入前都会经SrsRtmpJitter::correct修正取值行为full默认确保流从 0 开始并确保单调递增zero只确保流从 0 开始忽略时间戳抖动off完全禁用时间抖动算法类似 atc 行为该配置对所有dvr plan 生效在SrsDvrSegmenter::initialize中读取 srs_app_dvr.cpp。HTTP Callback切片落盘后的外部处理DVR 的难点之一是 FLV 文件名不可控SRS 推荐通过on_dvr回调在文件产出reap时通知外部系统处理如重命名、搬运。启用方式为在http_hooks中配置on_dvrvhost your_vhost { dvr { enabled on; dvr_path ./objs/nginx/html/[app]/[stream]/[2006]/[01]/[02]/[15].[04].[05].[999].flv; dvr_plan segment; dvr_duration 30; dvr_wait_keyframe on; } http_hooks { enabled on; on_dvr http://127.0.0.1:8085/api/v1/dvrs; } }开启后每次片段文件关闭并落盘时SRS 会向on_dvrURL 发起 POST 请求。api-server 侧的日志形如[2015-01-03 15:25:48][trace] post to dvrs, req{action:on_dvr,client_id:108,ip:127.0.0.1,vhost:__defaultVhost__,app:live,stream:livestream,cwd:/home/winlin/git/srs/trunk,file:./objs/nginx/html/live/livestream/2015/1/3/15.25.18.442.flv} [2015-01-03 15:25:48][trace] srs on_dvr: client id108, ip127.0.0.1, vhost__defaultVhost__, applive, streamlivestream, cwd/home/winlin/git/srs/trunk, file./objs/nginx/html/live/livestream/2015/1/3/15.25.18.442.flv 127.0.0.1 - - [03/Jan/2015:15:25:48] POST /api/v1/dvrs HTTP/1.1 200 1 SRS(Simple RTMP Server)2.0.88更多回调细节见 HttpCallback。源码侧的回调链路可以验证这一行为SrsDvrSegmenter::close()在文件关闭、临时文件重命名后调用plan_-on_reap_segment()SrsDvrPlan::on_reap_segment将当前片段完整路径包装成SrsDvrAsyncCallOnDvr任务提交给异步调用线程_srs_dvr_async执行call()读取 vhost 的http_hooks.on_dvr指令逐个 URL 调用hooks_-on_dvr()srs_app_dvr.cpp 与 L534-L580。回调是异步执行的不会阻塞推流写入线程。写盘实现细节源码级结合 srs_app_dvr.cpp有几个值得了解的工程细节临时文件 原子重命名SrsDvrSegmenter::open()打开的是fragment_-tmppath()临时路径close()时先关编码器再关文件最后fragment_-rename()把临时文件重命名为最终文件名保证落盘文件要么不存在、要么是完整文件拒绝覆盖同名文件open()会先检查目标路径是否已存在存在则报ERROR_DVR_CANNOT_APPEND错误DVR 不支持向已有文件追加因此自定义路径中保留时间变量可以避免冲突64KB 写缓存SRS_FWRITE_CACHE_SIZE设为 65536 字节通过fs_-set_iobuf_size()设置 libc 文件写缓冲区减少系统调用次数FLV 的 duration/filesize 回填SrsDvrFlvSegmenter写入 onMetaData 时记录duration_offset_与filesize_offset_两个文件偏移close_encoder()时通过refresh_metadata()回seek 到这两处把真实的文件大小和时长毫秒换算为秒写回使 FLV 文件的 metadata 与实际内容一致srs_app_dvr.cppMP4 封装SrsDvrMp4Segmenter使用SrsMp4Encoder逐样本写入write_sample关闭时flush()补齐 moov 索引MP4 不回填 metadatarefresh_metadata为空实现。配置热更新Reload与动态控制dvr 配置的变更与 reload修改 dvr 配置并 reload 后SRS 会重启 DVR——即关闭当前 DVR 文件然后应用新配置。源码依据是SrsDvr、SrsDvrPlan、SrsDvrSegmenter均实现了ISrsReloadHandler接口并在assemble()中向配置对象subscribe配置重载会触发 plan 的重新初始化srs_app_dvr.hpp。环境变量覆盖从 srs_app_config.cpp 的解析函数可以看到enabled、dvr_path、dvr_plan、dvr_duration、dvr_wait_keyframe、time_jitter等参数均支持通过环境变量覆盖如srs.vhost.dvr.enabled、srs.vhost.dvr.dvr_path等便于容器化部署时不修改配置文件即可调整录制行为。运行时控制除了dvr_apply静态过滤SRS 还提供 RAW API 动态控制指定流何时开始/停止 DVR可查阅 RAW API 文档。常见问题非法 plan 值dvr_plan只接受session/segment其他值会在创建计划时报illegal plan错误srs_app_dvr.cpp。文档中的appendplan 已在 SRS3 中移除。片段切不开/首帧缺失segment 策略下若长时间等不到关键帧且dvr_wait_keyframe on切段会延迟到下一个关键帧若编码器 GOP 过长可调大dvr_duration或确认源流 GOP 设置。录制文件不完整DVR 写入使用临时文件 rename正常关闭后文件才以正式名称出现若 SRS 进程异常退出可能留下未重命名的临时文件排查磁盘时可留意。需要合并多次推流/上云SRS 的 DVR 只负责基础录制多次会话合并、S3 存储等高级能力请使用 Oryx参见 Oryx 入门文档。相关文档DVR 官方文档本仓库HTTP APIRAW DVR 控制HttpCallbackon_dvr 回调Oryx DVR高级录制源码srs_app_dvr.cpp、srs_app_dvr.hpp、配置解析配置示例dvr.session.conf、dvr.segment.conf【免费下载链接】srsSRS is a simple, high-performance, AI-driven real-time media server supporting RTMP, WebRTC, HLS, HTTP-FLV, HTTP-TS, SRT, MPEG-DASH, and GB28181, with codec support for H.264, H.265, AV1, VP9, AAC, Opus, and G.711.项目地址: https://gitcode.com/GitHub_Trending/sr/srs创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考