《ZLMediaKit 全流程实战:从部署到 API 调用与前后端集成》
目录
编辑
一、引言
1. 官方文档地址
2. 介绍
3. 使用场景示例
二、环境准备与部署
1. 系统要求
2. Docker 安装 ZLMediaKit
3. 本地访问测试
(1) 查看管理界面
(2) 访问流信息界面
三、ZLMediaKit 配置说明
1. 配置文件目录结构
四、API 基础使用详解
1. 获取媒体列表
(1) /index/api/getApiList
2. 获取流列表
(1) index/api/getMediaList
3. 动态添加 rtsp/rtmp/hls/http-ts/http-flv 拉流代理
(1) /index/api/addStreamProxy
4. 停止流、关闭流接口
(1) /index/api/delStreamProxy(流注册成功后,也可以使用close_streams接口替代)
五、前后端对接实战
☆ 前端gitee完整项目地址:An/ZLK流媒体前端代码
1. JavaScript(vue)前端:视频播放页
(1)获取所有流列表页面
(2)拉流协议视频播放页面
(3)拉流列表页面
(4)服务器配置页面
2. Python(aiohttp)接入 ZLMediaKit 接口
☆ 后端gitee完整项目地址:An/ZLK流媒体后端代码
(1)主路由以及视图搭建
(2)主业务ZLK接口实现
(3)公共参数及方法配置搭建
(4)配置文件信息
六、总结
1. 使用体验
2. 性能表现
一、引言
1. 官方文档地址
https://docs.zlmediakit.com/zh/
2. 介绍
ZLMediaKit 是一个高性能流媒体服务器框架。它基于 C++ 实现,采用多线程异步事件驱动架构,具有轻量、模块化、稳定性强等特点,支持多种主流流媒体协议,包括:
-
RTMP(Real-Time Messaging Protocol)
-
RTSP(Real-Time Streaming Protocol)
-
HLS(HTTP Live Streaming)
-
WebRTC(实时通信协议)
-
HTTP-FLV、MP4录制、GB28181 国标协议 等
其最大的优势在于对协议支持全面,部署简单,性能优越,可作为企业级视频平台的流媒体内核使用。
3. 使用场景示例
ZLMediaKit 被广泛应用于各类音视频传输和处理场景中,典型场景包括:
-
监控视频接入与转发
支持接入 IPC 摄像头(如 RTSP),并转发为 WebRTC、RTMP、HLS 等格式,供前端实时查看。 -
视频点播系统
支持录制与回放,结合 HTTP-FLV、HLS、MP4 录制功能,构建轻量级点播平台。 -
直播分发平台
可对主播推流的 RTMP 进行多协议转发,支持百万级并发观看,适用于低延迟直播场景。 -
边缘计算与 IoT 视频平台
在边缘设备中嵌入轻量 ZLMediaKit,用于本地视频采集、编码、转发或 AI 分析。
二、环境准备与部署
1. 系统要求
-
系统环境(如 Ubuntu 20.04 Centos 主流linux服务器 + Docker)
-
端口要求:1935(RTMP)、554(RTSP)、80/443(HTTP/HTTPS)、8000+(UDP/HLS/WebRTC)
2. Docker 安装 ZLMediaKit
docker run --restart=always -id -p 1935:1935 -p 8085:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp zlmediakit/zlmediakit:master
3. 本地访问测试
(1) 查看管理界面
http://localhost:8085
(2) 访问流信息界面
http://localhost:8085/webassist/?secret=获取方式如下图
三、ZLMediaKit 配置说明
1. 配置文件目录结构
-
配置文件位置( /opt/media/conf/config.ini)
#!!!!此配置文件为范例配置文件,意在告诉读者,各个配置项的具体含义和作用,
#!!!!该配置文件在执行cmake时,会拷贝至release/${操作系统类型}/${编译类型}(例如release/linux/Debug) 文件夹。
#!!!!该文件夹(release/${操作系统类型}/${编译类型})同时也是可执行程序生成目标路径,在执行MediaServer进程时,它会默认加载同目录下的config.ini文件作为配置文件,
#!!!!你如果修改此范例配置文件(conf/config.ini),并不会被MediaServer进程加载,因为MediaServer进程默认加载的是release/${操作系统类型}/${编译类型}/config.ini。
#!!!!当然,你每次执行cmake,该文件确实会被拷贝至release/${操作系统类型}/${编译类型}/config.ini,
#!!!!但是一般建议你直接修改release/${操作系统类型}/${编译类型}/config.ini文件,修改此文件一般不起作用,除非你运行MediaServer时使用-c参数指定到此文件。[api]
#是否调试http api,启用调试后,会打印每次http请求的内容和回复
apiDebug=1
#一些比较敏感的http api在访问时需要提供secret,否则无权限调用
#如果是通过127.0.0.1访问,那么可以不提供secret
secret=035c73f7-bb6b-4889-a715-d9eb2d1925cc
#截图保存路径根目录,截图通过http api(/index/api/getSnap)生成和获取
snapRoot=./www/snap/
#默认截图图片,在启动FFmpeg截图后但是截图还未生成时,可以返回默认的预设图片
defaultSnap=./www/logo.png[ffmpeg]
#FFmpeg可执行程序路径,支持相对路径/绝对路径
bin=/usr/bin/ffmpeg
#FFmpeg拉流再推流的命令模板,通过该模板可以设置再编码的一些参数
cmd=%s -re -i %s -c:a aac -strict -2 -ar 44100 -ab 48k -c:v libx264 -f flv %s
#FFmpeg生成截图的命令,可以通过修改该配置改变截图分辨率或质量
snap=%s -i %s -y -f mjpeg -frames:v 1 %s
#FFmpeg日志的路径,如果置空则不生成FFmpeg日志
#可以为相对(相对于本可执行程序目录)或绝对路径
log=./ffmpeg/ffmpeg.log
# 自动重启的时间(秒), 默认为0, 也就是不自动重启. 主要是为了避免长时间ffmpeg拉流导致的不同步现象
restart_sec=0#转协议相关开关;如果addStreamProxy api和on_publish hook回复未指定转协议参数,则采用这些配置项
[protocol]
#转协议时,是否开启帧级时间戳覆盖
# 0:采用源视频流绝对时间戳,不做任何改变
# 1:采用zlmediakit接收数据时的系统时间戳(有平滑处理)
# 2:采用源视频流时间戳相对时间戳(增长量),有做时间戳跳跃和回退矫正
modify_stamp=2
#转协议是否开启音频
enable_audio=1
#添加acc静音音频,在关闭音频时,此开关无效
add_mute_audio=1
#无人观看时,是否直接关闭(而不是通过on_none_reader hook返回close)
#此配置置1时,此流如果无人观看,将不触发on_none_reader hook回调,
#而是将直接关闭流
auto_close=0#推流断开后可以在超时时间内重新连接上继续推流,这样播放器会接着播放。
#置0关闭此特性(推流断开会导致立即断开播放器)
#此参数不应大于播放器超时时间;单位毫秒
continue_push_ms=15000#是否开启转换为hls(mpegts)
enable_hls=1
#是否开启转换为hls(fmp4)
enable_hls_fmp4=0
#是否开启MP4录制
enable_mp4=0
#是否开启转换为rtsp/webrtc
enable_rtsp=1
#是否开启转换为rtmp/flv
enable_rtmp=1
#是否开启转换为http-ts/ws-ts
enable_ts=1
#是否开启转换为http-fmp4/ws-fmp4
enable_fmp4=1#是否将mp4录制当做观看者
mp4_as_player=0
#mp4切片大小,单位秒
mp4_max_second=3600
#mp4录制保存路径
mp4_save_path=./www#hls录制保存路径
hls_save_path=./www###### 以下是按需转协议的开关,在测试ZLMediaKit的接收推流性能时,请把下面开关置1
###### 如果某种协议你用不到,你可以把以下开关置1以便节省资源(但是还是可以播放,只是第一个播放者体验稍微差点),
###### 如果某种协议你想获取最好的用户体验,请置0(第一个播放者可以秒开,且不花屏)
#hls协议是否按需生成,如果hls.segNum配置为0(意味着hls录制),那么hls将一直生成(不管此开关)
hls_demand=0
#rtsp[s]协议是否按需生成
rtsp_demand=0
#rtmp[s]、http[s]-flv、ws[s]-flv协议是否按需生成
rtmp_demand=0
#http[s]-ts协议是否按需生成
ts_demand=0
#http[s]-fmp4、ws[s]-fmp4协议是否按需生成
fmp4_demand=0[general]
#是否启用虚拟主机
enableVhost=0
#播放器或推流器在断开后会触发hook.on_flow_report事件(使用多少流量事件),
#flowThreshold参数控制触发hook.on_flow_report事件阈值,使用流量超过该阈值后才触发,单位KB
flowThreshold=1024
#播放最多等待时间,单位毫秒
#播放在播放某个流时,如果该流不存在,
#ZLMediaKit会最多让播放器等待maxStreamWaitMS毫秒
#如果在这个时间内,该流注册成功,那么会立即返回播放器播放成功
#否则返回播放器未找到该流,该机制的目的是可以先播放再推流
maxStreamWaitMS=15000
#某个流无人观看时,触发hook.on_stream_none_reader事件的最大等待时间,单位毫秒
#在配合hook.on_stream_none_reader事件时,可以做到无人观看自动停止拉流或停止接收推流
streamNoneReaderDelayMS=20000
#拉流代理时如果断流再重连成功是否删除前一次的媒体流数据,如果删除将重新开始,
#如果不删除将会接着上一次的数据继续写(录制hls/mp4时会继续在前一个文件后面写)
resetWhenRePlay=1
#合并写缓存大小(单位毫秒),合并写指服务器缓存一定的数据后才会一次性写入socket,这样能提高性能,但是会提高延时
#开启后会同时关闭TCP_NODELAY并开启MSG_MORE
mergeWriteMS=0
#服务器唯一id,用于触发hook时区别是哪台服务器
mediaServerId=your_server_id#最多等待未初始化的Track时间,单位毫秒,超时之后会忽略未初始化的Track
wait_track_ready_ms=10000
#如果流只有单Track,最多等待若干毫秒,超时后未收到其他Track的数据,则认为是单Track
#如果协议元数据有声明特定track数,那么无此等待时间
wait_add_track_ms=3000
#如果track未就绪,我们先缓存帧数据,但是有最大个数限制,防止内存溢出
unready_frame_cache=100[hls]
#hls写文件的buf大小,调整参数可以提高文件io性能
fileBufSize=65536
#hls最大切片时间
segDur=2
#m3u8索引中,hls保留切片个数(实际保留切片个数大2~3个)
#如果设置为0,则不删除切片,而是保存为点播
segNum=3
#HLS切片从m3u8文件中移除后,继续保留在磁盘上的个数
segRetain=5
#是否广播 hls切片(ts/fmp4)完成通知(on_record_ts)
broadcastRecordTs=0
#直播hls文件删除延时,单位秒,issue: #913
deleteDelaySec=10
#是否保留hls文件,此功能部分等效于segNum=0的情况
#不同的是这个保留不会在m3u8文件中体现
#0为不保留,不起作用
#1为保留,则不删除hls文件,如果开启此功能,注意磁盘大小,或者定期手动清理hls文件
segKeep=0[hook]
#在推流时,如果url参数匹对admin_params,那么可以不经过hook鉴权直接推流成功,播放时亦然
#该配置项的目的是为了开发者自己调试测试,该参数暴露后会有泄露隐私的安全隐患
admin_params=secret=035c73f7-bb6b-4889-a715-d9eb2d1925cc
#是否启用hook事件,启用后,推拉流都将进行鉴权
enable=0
#播放器或推流器使用流量事件,置空则关闭
on_flow_report=https://127.0.0.1/index/hook/on_flow_report
#访问http文件鉴权事件,置空则关闭鉴权
on_http_access=https://127.0.0.1/index/hook/on_http_access
#播放鉴权事件,置空则关闭鉴权
on_play=https://127.0.0.1/index/hook/on_play
#推流鉴权事件,置空则关闭鉴权
on_publish=https://127.0.0.1/index/hook/on_publish
#录制mp4切片完成事件
on_record_mp4=https://127.0.0.1/index/hook/on_record_mp4
# 录制 hls ts(或fmp4) 切片完成事件
on_record_ts=https://127.0.0.1/index/hook/on_record_ts
#rtsp播放鉴权事件,此事件中比对rtsp的用户名密码
on_rtsp_auth=https://127.0.0.1/index/hook/on_rtsp_auth
#rtsp播放是否开启专属鉴权事件,置空则关闭rtsp鉴权。rtsp播放鉴权还支持url方式鉴权
#建议开发者统一采用url参数方式鉴权,rtsp用户名密码鉴权一般在设备上用的比较多
#开启rtsp专属鉴权后,将不再触发on_play鉴权事件
on_rtsp_realm=https://127.0.0.1/index/hook/on_rtsp_realm
#远程telnet调试鉴权事件
on_shell_login=https://127.0.0.1/index/hook/on_shell_login
#直播流注册或注销事件
on_stream_changed=https://127.0.0.1/index/hook/on_stream_changed
#无人观看流事件,通过该事件,可以选择是否关闭无人观看的流。配合general.streamNoneReaderDelayMS选项一起使用
on_stream_none_reader=https://127.0.0.1/index/hook/on_stream_none_reader
#播放时,未找到流事件,通过配合hook.on_stream_none_reader事件可以完成按需拉流
on_stream_not_found=https://127.0.0.1/index/hook/on_stream_not_found
#服务器启动报告,可以用于服务器的崩溃重启事件监听
on_server_started=https://127.0.0.1/index/hook/on_server_started
#服务器退出报告,当服务器正常退出时触发
on_server_exited=https://127.0.0.1/index/hook/on_server_exited
#server保活上报
on_server_keepalive=https://127.0.0.1/index/hook/on_server_keepalive
#发送rtp(startSendRtp)被动关闭时回调
on_send_rtp_stopped=https://127.0.0.1/index/hook/on_send_rtp_stopped
#rtp server 超时未收到数据
on_rtp_server_timeout=https://127.0.0.1/index/hook/on_rtp_server_timeout#hook api最大等待回复时间,单位秒
timeoutSec=10
#keepalive hook触发间隔,单位秒,float类型
alive_interval=10.0
#hook通知失败重试次数,正整数。为0不重试,1时重试一次,以此类推
retry=1
#hook通知失败重试延时,单位秒,float型
retry_delay=3.0[cluster]
#设置源站拉流url模板, 格式跟printf类似,第一个%s指定app,第二个%s指定stream_id,
#开启集群模式后,on_stream_not_found和on_stream_none_reader hook将无效.
#溯源模式支持以下类型:
#rtmp方式: rtmp://127.0.0.1:1935/%s/%s
#rtsp方式: rtsp://127.0.0.1:554/%s/%s
#hls方式: http://127.0.0.1:80/%s/%s/hls.m3u8
#http-ts方式: http://127.0.0.1:80/%s/%s.live.ts
#支持多个源站,不同源站通过分号(;)分隔
origin_url=
#溯源总超时时长,单位秒,float型;假如源站有3个,那么单次溯源超时时间为timeout_sec除以3
#单次溯源超时时间不要超过general.maxStreamWaitMS配置
timeout_sec=15
#溯源失败尝试次数,-1时永久尝试
retry_count=3[http]
#http服务器字符编码,windows上默认gb2312
charSet=utf-8
#http链接超时时间
keepAliveSecond=30
#http请求体最大字节数,如果post的body太大,则不适合缓存body在内存
maxReqSize=40960
#404网页内容,用户可以自定义404网页
#notFound=<html><head><title>404 Not Found</title></head><body bgcolor="white"><center><h1>您访问的资源不存在!</h1></center><hr><center>ZLMediaKit-4.0</center></body></html>
#http服务器监听端口
port=80
#http文件服务器根目录
#可以为相对(相对于本可执行程序目录)或绝对路径
rootPath=./www
#http文件服务器读文件缓存大小,单位BYTE,调整该参数可以优化文件io性能
sendBufSize=65536
#https服务器监听端口
sslport=443
#是否显示文件夹菜单,开启后可以浏览文件夹
dirMenu=1
#虚拟目录, 虚拟目录名和文件路径使用","隔开,多个配置路径间用";"隔开
#例如赋值为 app_a,/path/to/a;app_b,/path/to/b 那么
#访问 http://127.0.0.1/app_a/file_a 对应的文件路径为 /path/to/a/file_a
#访问 http://127.0.0.1/app_b/file_b 对应的文件路径为 /path/to/b/file_b
#访问其他http路径,对应的文件路径还是在rootPath内
virtualPath=
#禁止后缀的文件使用mmap缓存,使用“,”隔开
#例如赋值为 .mp4,.flv
#那么访问后缀为.mp4与.flv 的文件不缓存
forbidCacheSuffix=
#可以把http代理前真实客户端ip放在http头中:https://github.com/ZLMediaKit/ZLMediaKit/issues/1388
#切勿暴露此key,否则可能导致伪造客户端ip
forwarded_ip_header=
#默认允许所有跨域请求
allow_cross_domains=1[multicast]
#rtp组播截止组播ip地址
addrMax=239.255.255.255
#rtp组播起始组播ip地址
addrMin=239.0.0.0
#组播udp ttl
udpTTL=64[record]
#mp4录制或mp4点播的应用名,通过限制应用名,可以防止随意点播
#点播的文件必须放置在此文件夹下
appName=record
#mp4录制写文件缓存,单位BYTE,调整参数可以提高文件io性能
fileBufSize=65536
#mp4点播每次流化数据量,单位毫秒,
#减少该值可以让点播数据发送量更平滑,增大该值则更节省cpu资源
sampleMS=500
#mp4录制完成后是否进行二次关键帧索引写入头部
fastStart=0
#MP4点播(rtsp/rtmp/http-flv/ws-flv)是否循环播放文件
fileRepeat=0[rtmp]
#rtmp必须在此时间内完成握手,否则服务器会断开链接,单位秒
handshakeSecond=15
#rtmp超时时间,如果该时间内未收到客户端的数据,
#或者tcp发送缓存超过这个时间,则会断开连接,单位秒
keepAliveSecond=15
#在接收rtmp推流时,是否重新生成时间戳(很多推流器的时间戳着实很烂)
modifyStamp=0
#rtmp服务器监听端口
port=1935
#rtmps服务器监听地址
sslport=0[rtp]
#音频mtu大小,该参数限制rtp最大字节数,推荐不要超过1400
#加大该值会明显增加直播延时
audioMtuSize=600
#视频mtu大小,该参数限制rtp最大字节数,推荐不要超过1400
videoMtuSize=1400
#rtp包最大长度限制,单位KB,主要用于识别TCP上下文破坏时,获取到错误的rtp
rtpMaxSize=10
# rtp 打包时,低延迟开关,默认关闭(为0),h264存在一帧多个slice(NAL)的情况,在这种情况下,如果开启可能会导致画面花屏
lowLatency=0
# H264 rtp打包模式是否采用stap-a模式(为了在老版本浏览器上兼容webrtc)还是采用Single NAL unit packet per H.264 模式
# 有些老的rtsp设备不支持stap-a rtp,设置此配置为0可提高兼容性
h264_stap_a=1[rtp_proxy]
#导出调试数据(包括rtp/ps/h264)至该目录,置空则关闭数据导出
dumpDir=
#udp和tcp代理服务器,支持rtp(必须是ts或ps类型)代理
port=10000
#rtp超时时间,单位秒
timeoutSec=15
#随机端口范围,最少确保36个端口
#该范围同时限制rtsp服务器udp端口范围
port_range=30000-35000
#rtp h264 负载的pt
h264_pt=98
#rtp h265 负载的pt
h265_pt=99
#rtp ps 负载的pt
ps_pt=96
#rtp opus 负载的pt
opus_pt=100
#RtpSender相关功能是否提前开启gop缓存优化级联秒开体验,默认开启
#如果不调用startSendRtp相关接口,可以置0节省内存
gop_cache=1[rtc]
#rtc播放推流、播放超时时间
timeoutSec=15
#本机对rtc客户端的可见ip,作为服务器时一般为公网ip,可有多个,用','分开,当置空时,会自动获取网卡ip
#同时支持环境变量,以$开头,如"$EXTERN_IP"; 请参考:https://github.com/ZLMediaKit/ZLMediaKit/pull/1786
externIP=
#rtc udp服务器监听端口号,所有rtc客户端将通过该端口传输stun/dtls/srtp/srtcp数据,
#该端口是多线程的,同时支持客户端网络切换导致的连接迁移
#需要注意的是,如果服务器在nat内,需要做端口映射时,必须确保外网映射端口跟该端口一致
port=8000
#rtc tcp服务器监听端口号,在udp 不通的情况下,会使用tcp传输数据
#该端口是多线程的,同时支持客户端网络切换导致的连接迁移
#需要注意的是,如果服务器在nat内,需要做端口映射时,必须确保外网映射端口跟该端口一致
tcpPort = 8000
#设置remb比特率,非0时关闭twcc并开启remb。该设置在rtc推流时有效,可以控制推流画质
#目前已经实现twcc自动调整码率,关闭remb根据真实网络状况调整码率
rembBitRate=0
#rtc支持的音频codec类型,在前面的优先级更高
#以下范例为所有支持的音频codec
preferredCodecA=PCMU,PCMA,opus,mpeg4-generic
#rtc支持的视频codec类型,在前面的优先级更高
#以下范例为所有支持的视频codec
preferredCodecV=H264,H265,AV1,VP9,VP8[srt]
#srt播放推流、播放超时时间,单位秒
timeoutSec=5
#srt udp服务器监听端口号,所有srt客户端将通过该端口传输srt数据,
#该端口是多线程的,同时支持客户端网络切换导致的连接迁移
port=9000
#srt 协议中延迟缓存的估算参数,在握手阶段估算rtt ,然后latencyMul*rtt 为最大缓存时长,此参数越大,表示等待重传的时长就越大
latencyMul=4
#包缓存的大小
pktBufSize=8192[rtsp]
#rtsp专有鉴权方式是采用base64还是md5方式
authBasic=0
#rtsp拉流、推流代理是否是直接代理模式
#直接代理后支持任意编码格式,但是会导致GOP缓存无法定位到I帧,可能会导致开播花屏
#并且如果是tcp方式拉流,如果rtp大于mtu会导致无法使用udp方式代理
#假定您的拉流源地址不是264或265或AAC,那么你可以使用直接代理的方式来支持rtsp代理
#如果你是rtsp推拉流,但是webrtc播放,也建议关闭直接代理模式,
#因为直接代理时,rtp中可能没有sps pps,会导致webrtc无法播放; 另外webrtc也不支持Single NAL Unit Packets类型rtp
#默认开启rtsp直接代理,rtmp由于没有这些问题,是强制开启直接代理的
directProxy=1
#rtsp必须在此时间内完成握手,否则服务器会断开链接,单位秒
handshakeSecond=15
#rtsp超时时间,如果该时间内未收到客户端的数据,
#或者tcp发送缓存超过这个时间,则会断开连接,单位秒
keepAliveSecond=15
#rtsp服务器监听地址
port=554
#rtsps服务器监听地址
sslport=0
#rtsp 转发是否使用低延迟模式,当开启时,不会缓存rtp包,来提高并发,可以降低一帧的延迟
lowLatency=0
#强制协商rtp传输方式 (0:TCP,1:UDP,2:MULTICAST,-1:不限制)
#当客户端发起RTSP SETUP的时候如果传输类型和此配置不一致则返回461 Unsupported transport
#迫使客户端重新SETUP并切换到对应协议。目前支持FFMPEG和VLC
rtpTransportType=-1
[shell]
#调试telnet服务器接受最大bufffer大小
maxReqSize=1024
#调试telnet服务器监听端口
port=0
四、API 基础使用详解
1. 获取媒体列表
(1)/index/api/getApiList
-
功能:获取 API 列表
-
方式:GET
-
范例:http://127.0.0.1:8085/index/api/getApiList
-
参数:
参数 是否必选 释意 secret Y api 操作密钥(配置文件配置)
2. 获取流列表
(1)
index/api/getMediaList
-
功能:获取流列表,可选筛选参数
-
方式:GET
-
范例:http://127.0.0.1/index/api/getMediaListhttp://127.0.0.1/index/api/getMediaListhttp://127.0.0.1/index/api/getMediaList
-
参数:
参数 是否必选 释意 secret Y api 操作密钥(配置文件配置) schema N 筛选协议,例如 rtsp 或 rtmp vhost N 筛选虚拟主机,例如 __defaultVhost__
app N 筛选应用名,例如 live stream N 筛选流 id,例如 test
3. 动态添加 rtsp/rtmp/hls/http-ts/http-flv 拉流代理
(1)
/index/api/addStreamProxy
-
功能:动态添加 rtsp/rtmp/hls/http-ts/http-flv 拉流代理(只支持 H264/H265/aac/G711/opus 负载)
-
方式:GET
-
范例:http://127.0.0.1/index/api/addStreamProxy?vhost=__defaultVhost__&app=proxy&stream=0&url=rtmp://live.hkstv.hk.lxdns.com/live/hks2
-
参数:
参数 参数类型 释意 是否必选 secret string
api 操作密钥(配置文件配置) Y vhost string
添加的流的虚拟主机,例如 __defaultVhost__
Y app string
添加的流的应用名,例如 live Y stream string
添加的流的 id 名,例如 test Y url string
拉流地址,例如 rtmp://live.hkstv.hk.lxdns.com/live/hks2 Y retry_count int
拉流重试次数,默认为-1 无限重试 N rtp_type int
rtsp 拉流时,拉流方式,0:tcp,1:udp,2:组播 N timeout_sec int
拉流超时时间,单位秒,float 类型 N enable_hls
bool
是否转换成 hls-mpegts 协议 N enable_hls_fmp4
bool
是否转换成 hls-fmp4 协议 N enable_mp4
bool
是否允许 mp4 录制 N enable_rtsp
bool
是否转 rtsp 协议 N enable_rtmp
bool
是否转 rtmp/flv 协议 N enable_ts
bool
是否转 http-ts/ws-ts 协议 N enable_fmp4
bool
是否转 http-fmp4/ws-fmp4 协议 N hls_demand
bool
该协议是否有人观看才生成 N rtsp_demand
bool
该协议是否有人观看才生成 N rtmp_demand
bool
该协议是否有人观看才生成 N ts_demand
bool
该协议是否有人观看才生成 N fmp4_demand
bool
该协议是否有人观看才生成 N enable_audio
bool
转协议时是否开启音频 N add_mute_audio
bool
转协议时,无音频是否添加静音 aac 音频 N mp4_save_path
string
mp4 录制文件保存根目录,置空使用默认 N mp4_max_second
int
mp4 录制切片大小,单位秒 N mp4_as_player
bool
MP4 录制是否当作观看者参与播放人数计数 N hls_save_path
string
hls 文件保存保存根目录,置空使用默认 N modify_stamp
int
该流是否开启时间戳覆盖(0:绝对时间戳/1:系统时间戳/2:相对时间戳) N auto_close
bool
无人观看是否自动关闭流(不触发无人观看 hook) N
4. 停止流、关闭流接口
(1) /index/api/delStreamProxy(流注册成功后,也可以使用close_streams接口替代)
-
功能:关闭拉流代理
-
范例:http://127.0.0.1/index/api/delStreamProxy?key=__defaultVhost__/proxy/0
-
参数:
参数 是否必选 释意 secret Y api 操作密钥(配置文件配置) key Y addStreamProxy 接口返回的 key
五、前后端对接实战
☆ 前端gitee完整项目地址:An/ZLK流媒体前端代码
1. JavaScript(vue)前端:视频播放页
(1)获取所有流列表页面
<template><section><transition enter-active-class="animate__animated animate__fadeIn" mode="out-in"><section>
<ZyTableQueryForm :ruleForm="query.params" :rules="queryRule" @query="goPage(1)" @reset="goReset"><el-form-item prop="app"><el-input v-model="query.params.app" clearable @change="() => goPage(1)" placeholder="App 名称查询" /></el-form-item><el-form-item prop="schema" style="width: 220px"><el-selectv-model="query.params.schema"clearableplaceholder="请选择协议"@change="() => goPage(1)"style="width: 100%"><el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.label" /></el-select></el-form-item>
</ZyTableQueryForm><el-table:data="tableData"borderstripestyle="width: 100%":loading="loading.list"element-loading-text="加载中..."@sort-change="handleSortChange"@selection-change="handleSelectionChange"row-key="app":expand-row-keys="expandedRowKeys"><el-table-column type="selection" align="center" /><!-- 序号列 --><el-table-column type="index" label="序号" width="60" align="center"/><el-table-column prop="app" label="流id" align="center" width="120" show-overflow-tooltip /><el-table-column prop="schema" label="协议" align="center" width="100" show-overflow-tooltip /><el-table-column prop="readerCount" label="在线人数" align="center" width="100" show-overflow-tooltip /><el-table-column prop="bytesSpeed" label="速度 (B/s)" align="center" width="120" show-overflow-tooltip /><el-table-column prop="originUrl" label="源地址" align="left" width="300" show-overflow-tooltip /><el-table-column prop="stream" label="流名称" align="left" width="250" show-overflow-tooltip /><el-table-column label="网络信息" align="left" width="300"><template #default="{ row }"><div>local_ip:{{ row.originSock.local_ip }}</div><div>local_port{{ row.originSock.local_port }}</div><div>peer_ip:{{ row.originSock.peer_ip }}</div><div>peer_port:{{ row.originSock.peer_port }}</div></template></el-table-column><!-- <el-table-column label="媒体轨道" prop="tracks" align="left" width="150" /> --></el-table><ZyElPagination:current-page="query.pagination.current":page-size="query.pagination.pageSize":total="tempData.total"@size-change="sizeChange"@current-change="currentChange"/></section></transition></section>
</template><script>
import api from '@/api/axios.js'export default {data() {return {options: [{ value: '1', label: 'hls' },{ value: '2', label: 'ts' },{ value: '3', label: 'rtmp' },{ value: '4', label: 'fmp4' },{ value: '5', label: 'rtsp' }],query: {params: {app: '',schema: '' // 👈 新增字段用于筛选协议},pagination: {current: 1,pageSize: 10}},queryRule: {},loading: {list: false,text: '加载中...'},tempData: {total: 0,showEdit: false,showView: false,dialogTitle: '编辑',updateData: {}},tableData: [],expandedRowKeys: []}},methods: {async getData() {this.loading.list = truetry {const res = await api.post('/getMediaList')const data = res.data || []const { current, pageSize } = this.query.paginationconst filtered = data.filter(item => {const matchApp = !this.query.params.app || (item.app && item.app.includes(this.query.params.app))const matchSchema = !this.query.params.schema || (item.schema && item.schema === this.query.params.schema)return matchApp && matchSchema})this.tempData.total = filtered.lengththis.tableData = filtered.slice((current - 1) * pageSize, current * pageSize)this.expandedRowKeys = []} catch (err) {console.error('获取数据失败', err)} finally {this.loading.list = false}},sizeChange(size) {this.query.pagination.pageSize = sizethis.goPage(1)},currentChange(page) {this.goPage(page)},goPage(page) {this.query.pagination.current = pagethis.getData()},goReset() {this.query.params.app = ''this.query.params.schema = ''this.goPage(1)},handleSortChange(e) {console.log('排序:', e)},handleSelectionChange(selection) {console.log('选中:', selection)},formatTime(timestamp) {if (!timestamp) return '-'const date = new Date(timestamp)const pad = n => String(n).padStart(2, '0')return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +`${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`}},mounted() {this.getData()}
}
</script><style scoped lang="scss">
.hidden-header {display: none;
}
</style>
(2)拉流协议视频播放页面
<template><section><transition enter-active-class="animate__animated animate__fadeIn" mode="out-in"><section><!-- 查询表单 -->
<ZyTableQueryFormv-model:ruleForm="query.params":rules="queryRule"@query="goPage(1)"@reset="goReset"
><el-form-item style="margin-right: 200px;"><el-button type="primary" style="width: 68px; height: 24px;" @click="openAddDialog">新增</el-button></el-form-item>
</ZyTableQueryForm><!-- 新增弹窗 --><el-dialog title="新增 RTSP 流" v-model="adddialogVisible" width="500px"><el-form :model="addform" ref="addFormRef" label-width="100px" :rules="addRules"><el-form-item label="RTSP 流地址" prop="rtspUrl"><el-input v-model="addform.rtspUrl" placeholder="请输入 RTSP 流地址" /></el-form-item></el-form><template #footer><el-button @click="adddialogVisible = false">取消</el-button><el-button type="primary" @click="submitAddForm">提交</el-button></template></el-dialog><!-- 数据表格 --><el-table:data="tableData"borderstripestyle="width: 100%"@sort-change="handleSortChange"@selection-change="handleSelectionChange"row-key="app":expand-row-keys="expandedRowKeys"><el-table-column type="selection" align="center" /><!-- 序号列 --><el-table-column type="index" label="序号" width="60" align="center"/><el-table-column prop="url" label="拉流地址" align="center" header-align="center" width="100" show-overflow-tooltip /><el-table-column prop="key" label="键" align="center" width="100" show-overflow-tooltip /><el-table-column prop="api" label="转流播放协议地址" align="center" header-align="center" width="1000"><template #default="{ row }"><el-table:data="[row.api || {}]"bordersize="small"style="width: 100%; font-size: 12px;"header-row-class-name="hidden-header"><el-table-column label="flv" prop="flv" align="center" header-align="center" /><el-table-column label="mp4" prop="mp4" align="center" header-align="center" /><el-table-column label="rtmp" prop="rtmp" align="center" header-align="center" /><el-table-column label="rtsp" prop="rtsp" align="center" header-align="center" /><el-table-column label="ts" prop="ts" align="center" header-align="center" /><!-- <el-table-column label="WebRtc" prop="WebRtc" align="center" header-align="center" /> --></el-table></template></el-table-column><!-- 源信息嵌套表格 --><el-table-column prop="src" label="源信息" align="center" header-align="center" width="300"><template #default="{ row }"><el-table:data="[row.src || {}]"bordersize="small"style="width: 100%; font-size: 12px;"header-row-class-name="hidden-header"><el-table-column label="应用名称" prop="app" align="center" header-align="center" /><el-table-column label="流名称" prop="stream" align="center" header-align="center" /><el-table-column label="虚拟主机名" prop="vhost" align="center" header-align="center" /></el-table></template></el-table-column><el-table-column prop="status" label="流状态" width="150px" show-overflow-tooltip header-align="center" align="center"><template #default="{ row }"><el-button:type="row.status === 0 ? 'success' : 'danger'"size="small"roundplain>{{ row.status === 0 ? '正常' : '异常' }}</el-button></template></el-table-column><el-table-column label="操作" align="center" header-align="center" width="150"><template #default="{ row }"><el-button type="primary" @click="handlePlay(row.api.mp4)">播放</el-button><el-button type="danger" @click="openDelDialog(row.key)">删除</el-button></template></el-table-column></el-table><ZyElPaginationv-model:currentPage="query.pagination.current"v-model:pageSize="query.pagination.pageSize":total="tempData.total"/><!-- 删除确认弹窗 --><el-dialog v-model="dialogVisible" title="确认删除" width="400px"><el-form :model="form" ref="delFormRef"><el-form-item label="Key"><el-input v-model="form.key" disabled /></el-form-item></el-form><template #footer><el-button @click="dialogVisible = false">取消</el-button><el-button type="danger" @click="submitDelForm">确认删除</el-button></template></el-dialog><!-- 视频播放弹窗 --><el-dialogv-model="playDialogVisible"width="60%":before-close="handleClosePlayDialog"centertitle="视频预览"class="custom-video-dialog"><div class="video-wrapper"><videoref="videoRef":src="currentPlayUrl"controlsautoplayclass="custom-video-player"></video></div><template #footer><el-button type="primary" @click="handleClosePlayDialog">关闭</el-button></template></el-dialog></section></transition></section>
</template><script>import { reactive, ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import api from '@/api/axios.js'
import { API_BASE_URL, API_ZLK_HTTP, API_ZLK_RTMP, API_ZLK_RTSP } from '@/config.js'export default {name: 'YourComponentName',data() {return {// ...已有数据playDialogVisible: false,playUrl: '', // 播放地址query: reactive({params: { app: '' },pagination: { current: 1, pageSize: 10 }}),queryRule: reactive({}),tempData: reactive({ total: 0 }),dialogVisible: false,form: reactive({ key: '' }),adddialogVisible: false,addform: reactive({ rtspUrl: '' }),expandedRowKeys: ref([]),tableData: ref([]),}},methods: {handlePlay(url) {this.currentPlayUrl = urlthis.playDialogVisible = true},handleClosePlayDialog() {this.playDialogVisible = falsethis.currentPlayUrl = ''const video = this.$refs.videoRefif (video && !video.paused) {video.pause()}},openDelDialog(key) {this.form.key = keythis.dialogVisible = true},async submitDelForm() {const formData = new URLSearchParams()formData.append('key', this.form.key)try {await api.post('/delStreamProxy', formData)ElMessage.success('删除成功')this.dialogVisible = falsethis.getData()} catch (err) {ElMessage.error('删除失败')console.error(err)}},openAddDialog() {this.addform.rtspUrl = ''this.adddialogVisible = true},submitAddForm() {this.$refs.addFormRef.validate(async (valid) => {if (!valid) returntry {await api.post('/addStreamProxy', new URLSearchParams({ url: this.addform.rtspUrl })).then(res=>{console.log(res)if(res.code!=400){ElMessage.success('新增成功!')}else{ElMessage.error('新增失败!')}})this.adddialogVisible = falsethis.getData()} catch (err) {ElMessage.error('新增失败')console.error(err)}})},async getData() {try {const res = await api.post('/listStreamProxy')const data = res.data || []const enhancedData = data.map(item => {const app = item.src.app || ''const stream = item.src.stream || ''return {...item,api: {'mp4': `${API_ZLK_HTTP}${app}/${stream}.live.mp4`,'rtmp': `${API_ZLK_RTMP}${app}/${stream}`,'rtsp': `${API_ZLK_RTSP}${app}/${stream}`,'ts':`${API_ZLK_HTTP}${app}/${stream}.live.ts`,'flv':`${API_ZLK_HTTP}${app}/${stream}.live.flv`,// 'WebRtc':`${API_ZLK_HTTP}index/api/webrtc?app=${app}&stream=${stream}&type=play`,}}})const { current, pageSize } = this.query.paginationconst filtered = enhancedData .filter(item => !this.query.params.app || item.app?.includes(this.query.params.app))this.tempData.total = filtered.lengththis.tableData = filtered.slice((current - 1) * pageSize, current * pageSize)this.expandedRowKeys = []} catch (err) {console.error('获取数据失败', err)// 请求失败时清空数据this.tableData = []this.tempData.total = 0this.expandedRowKeys = []} finally {}
},goPage(page) {this.query.pagination.current = pagethis.getData()},goReset() {this.query.params.app = ''this.query.pagination.current = 1this.getData()},handleSortChange(e) {console.log('排序:', e)},handleSelectionChange(selection) {console.log('选中:', selection)}},mounted() {this.getData()}
}
</script>
<style scoped>
.custom-video-dialog .el-dialog__header {text-align: center;font-size: 18px;font-weight: bold;background-color: #1f1f1f;color: #fff;border-bottom: 1px solid #333;border-top-left-radius: 10px;border-top-right-radius: 10px;
}.custom-video-dialog .el-dialog {background-color: #2b2b2b;color: #ffffff;border-radius: 12px;
}.video-wrapper {display: flex;justify-content: center;align-items: center;background-color: #000;padding: 12px;border-radius: 10px;box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
}.custom-video-player {width: 100%;max-height: 70vh;border-radius: 8px;background-color: #000;
}
</style>
(3)拉流列表页面
<template><section><transition enter-active-class="animate__animated animate__fadeIn" mode="out-in"><section><!-- 查询表单 -->
<ZyTableQueryFormv-model:ruleForm="query.params":rules="queryRule"@query="goPage(1)"@reset="goReset"
><el-form-item style="margin-right: 200px;"><el-button type="primary" style="width: 68px; height: 24px;" @click="openAddDialog">新增</el-button></el-form-item>
</ZyTableQueryForm><!-- 新增弹窗 --><el-dialog title="新增 RTSP 流" v-model="adddialogVisible" width="500px"><el-form :model="addform" ref="addFormRef" label-width="100px" :rules="addRules"><el-form-item label="RTSP 流地址" prop="rtspUrl"><el-input v-model="addform.rtspUrl" placeholder="请输入 RTSP 流地址" /></el-form-item></el-form><template #footer><el-button @click="adddialogVisible = false">取消</el-button><el-button type="primary" @click="submitAddForm">提交</el-button></template></el-dialog><!-- 数据表格 --><el-table:data="tableData"borderstripestyle="width: 100%"@sort-change="handleSortChange"@selection-change="handleSelectionChange"row-key="app":expand-row-keys="expandedRowKeys"><el-table-column type="selection" align="center" /><!-- 序号列 --><el-table-column type="index" label="序号" width="60" align="center"/><el-table-column prop="url" label="拉流地址" align="center" header-align="center" width="250" show-overflow-tooltip /><el-table-column prop="key" label="键" align="center" width="100" show-overflow-tooltip /><el-table-column prop="bytesSpeed" label="传输速度" align="center" width="120" show-overflow-tooltip /><el-table-column prop="liveSecs" label="流运行时间" align="center" width="100" show-overflow-tooltip /><el-table-column prop="rePullCount" label="拉流失败后重新拉流的次数" align="center" width="120" show-overflow-tooltip /><!-- 源信息嵌套表格 --><el-table-column prop="src" label="源信息" align="center" header-align="center" width="300"><template #default="{ row }"><el-table:data="[row.src || {}]"bordersize="small"style="width: 100%; font-size: 12px;"header-row-class-name="hidden-header"><el-table-column label="应用名称" prop="app" align="center" header-align="center" /><el-table-column label="流名称" prop="stream" align="center" header-align="center" /><el-table-column label="虚拟主机名" prop="vhost" align="center" header-align="center" /></el-table></template></el-table-column><el-table-column prop="status" label="流状态" width="300" show-overflow-tooltip header-align="center" align="center"><template #default="{ row }"><el-button:type="row.status === 0 ? 'success' : 'danger'"size="small"roundplain>{{ row.status === 0 ? '正常' : '异常' }}</el-button></template></el-table-column><el-table-column prop="totalBytes" label="累计已传输的总字节数" align="center" header-align="center" width="250" show-overflow-tooltip /><el-table-column prop="totalReaderCount" label="观看者数量" align="center" header-align="center" width="300" show-overflow-tooltip /><el-table-column label="操作" align="center" header-align="center" width="150"><template #default="{ row }"><el-button type="danger" @click="openDelDialog(row.key)">删除</el-button></template></el-table-column></el-table><ZyElPaginationv-model:currentPage="query.pagination.current"v-model:pageSize="query.pagination.pageSize":total="tempData.total"/><!-- 删除确认弹窗 --><el-dialog v-model="dialogVisible" title="确认删除" width="400px"><el-form :model="form" ref="delFormRef"><el-form-item label="Key"><el-input v-model="form.key" disabled /></el-form-item></el-form><template #footer><el-button @click="dialogVisible = false">取消</el-button><el-button type="danger" @click="submitDelForm">确认删除</el-button></template></el-dialog></section></transition></section>
</template><script>
import { reactive, ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import api from '@/api/axios.js'export default {name: 'YourComponentName',data() {return {query: reactive({params: { app: '' },pagination: { current: 1, pageSize: 10 }}),queryRule: reactive({}),tempData: reactive({ total: 0 }),dialogVisible: false,form: reactive({ key: '' }),adddialogVisible: false,addform: reactive({ rtspUrl: '' }),expandedRowKeys: ref([]),tableData: ref([]),}},methods: {openDelDialog(key) {this.form.key = keythis.dialogVisible = true},async submitDelForm() {const formData = new URLSearchParams()formData.append('key', this.form.key)try {await api.post('/delStreamProxy', formData)ElMessage.success('删除成功')this.dialogVisible = falsethis.getData()} catch (err) {ElMessage.error('删除失败')console.error(err)}},openAddDialog() {this.addform.rtspUrl = ''this.adddialogVisible = true},submitAddForm() {this.$refs.addFormRef.validate(async (valid) => {if (!valid) returntry {await api.post('/addStreamProxy', new URLSearchParams({ url: this.addform.rtspUrl })).then(res=>{console.log(res)if(res.code!=400){ElMessage.success('新增成功!')}else{ElMessage.error('新增失败!')}})this.adddialogVisible = falsethis.getData()} catch (err) {ElMessage.error('新增失败')console.error(err)}})},async getData() {try {const res = await api.post('/listStreamProxy')const data = res.data || []const { current, pageSize } = this.query.paginationconst filtered = data.filter(item => !this.query.params.app || item.app?.includes(this.query.params.app))this.tempData.total = filtered.lengththis.tableData = filtered.slice((current - 1) * pageSize, current * pageSize)this.expandedRowKeys = []} catch (err) {console.error('获取数据失败', err)// 请求失败时清空数据this.tableData = []this.tempData.total = 0this.expandedRowKeys = []} finally {}
},goPage(page) {this.query.pagination.current = pagethis.getData()},goReset() {this.query.params.app = ''this.query.pagination.current = 1this.getData()},handleSortChange(e) {console.log('排序:', e)},handleSelectionChange(selection) {console.log('选中:', selection)}},mounted() {this.getData()}
}
</script>
(4)服务器配置页面
<template><section><transition enter-active-class="animate__animated animate__fadeIn" mode="out-in"><section><!-- 查询表单 --><ZyTableQueryFormv-model:ruleForm="query.params":rules="queryRule"@query="goPage(1)"@reset="goReset"><!-- <el-form-item prop="app"><el-input v-model="query.params.app" clearable placeholder="App 名称查询" /></el-form-item> --></ZyTableQueryForm><!-- 数据表格 --><el-table:data="tableData"borderstripestyle="width: 100%"@sort-change="handleSortChange"@selection-change="handleSelectionChange"row-key="app":expand-row-keys="expandedRowKeys"><el-table-column type="selection" align="center" /><!-- 序号列 --><el-table-column type="index" label="序号" width="60" align="center"/><el-table-column prop="http.port" label="HTTP 监听端口" align="center" width="120" show-overflow-tooltip /><el-table-column prop="http.allow_ip_range" label="允许访问的 IP 范围" align="center" width="100" show-overflow-tooltip /><el-table-column prop="rtmp.port" label="RTMP 服务端口(默认 1935)" align="center" width="120" show-overflow-tooltip /><el-table-column prop="rtsp.port" label="RTSP 服务端口(默认 554)" align="center" width="120" show-overflow-tooltip /><el-table-column prop="rtc.port" label="WebRTC UDP 端口" align="center" width="120" show-overflow-tooltip /><el-table-column prop="rtc.tcpPort" label="WebRTC TCP 端口" align="center" width="120" show-overflow-tooltip /><el-table-column prop="general.listen_ip" label="服务监听的 IP" align="center" header-align="center" width="250" show-overflow-tooltip /><el-table-column prop="api.secret" label="流媒体唯一键" align="center" header-align="center" width="250" show-overflow-tooltip /></el-table><ZyElPaginationv-model:currentPage="query.pagination.current"v-model:pageSize="query.pagination.pageSize":total="tempData.total"/></section></transition></section>
</template><script>
import { reactive, ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import api from '@/api/axios.js'export default {name: 'YourComponentName',data() {return {query: reactive({params: { app: '' },pagination: { current: 1, pageSize: 10 }}),queryRule: reactive({}),tempData: reactive({ total: 0 }),dialogVisible: false,form: reactive({ key: '' }),adddialogVisible: false,addform: reactive({ rtspUrl: '' }),expandedRowKeys: ref([]),tableData: ref([]),}},methods: {async getData() {try {const res = await api.post('/getServerConfig')const data = res.data || []const { current, pageSize } = this.query.paginationconst filtered = data.filter(item => !this.query.params.app || item.app?.includes(this.query.params.app))this.tempData.total = filtered.lengththis.tableData = filtered.slice((current - 1) * pageSize, current * pageSize)this.expandedRowKeys = []} catch (err) {console.error('获取数据失败', err)// 请求失败时清空数据this.tableData = []this.tempData.total = 0this.expandedRowKeys = []} finally {}
},goPage(page) {this.query.pagination.current = pagethis.getData()},goReset() {this.query.params.app = ''this.query.pagination.current = 1this.getData()},},mounted() {this.getData()}
}
</script>
☆ 后端gitee完整项目地址:An/ZLK流媒体后端代码
2. Python(aiohttp)接入 ZLMediaKit 接口
(1)主路由以及视图搭建
from config import *
from Zlk_transition import *logging.basicConfig(level=logging.INFO)if platform.system() == "Windows":asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())MAX_CONCURRENT_PROCESSES = 20
semaphore = asyncio.Semaphore(MAX_CONCURRENT_PROCESSES)# 封装/转码任务
async def perform_transcoding(ip, camera_id, rtmp_server, stream_type):async with semaphore:if stream_type == 1:# 仅封装:复制视频/音频,不转码# 推送到 RTSP 服务端(只封装)# 1. srs# ffmpeg_command = [# "ffmpeg",# "-re",# "-i", ip,# "-c", "copy",# "-rtsp_transport", "tcp",# "-f", "rtsp",# # "rtsp://192.168.14.93:8554/stream"# f'{rtmp_server}/openUr/{camera_id}'# ]# 2. zlm (顺序不可颠倒)ffmpeg_command = ["ffmpeg","-rtsp_transport", "tcp","-re","-i", ip,"-c", "copy","-f", "rtsp","-rtsp_transport", "tcp",f'{rtmp_server}/openUr/{camera_id}']else:# 转码处理ffmpeg_command = ['ffmpeg','-rtsp_transport', 'tcp','-i', ip,'-r', '10','-s', '640x360','-b:v', '400k','-c:v', 'libx264','-c:a', 'aac','-f', 'flv',f'{rtmp_server}/openUr/{camera_id}']try:process = await asyncio.create_subprocess_exec(*ffmpeg_command,stdout=asyncio.subprocess.DEVNULL,stderr=asyncio.subprocess.DEVNULL)return processexcept Exception as e:logging.error(f'启动 ffmpeg 失败: {e}')return None# 定时关闭
async def stop_transcoding_after_delay(process, stream_id, delay):await asyncio.sleep(delay)try:process.kill()await process.wait()logging.info(f'流 {stream_id} 超时 {delay}s,已终止')except Exception as e:logging.error(f'终止流 {stream_id} 失败: {e}')# 播放接口
async def play_camera(request):data = await request.post()ip = data.get('rtsp')expire = int(data.get('expire', 3600))stream_type = int(data.get('stream_type', 2))SE_type = 1flv_url = ''if not ip:return web.json_response({'message': '缺少 rtsp 参数', 'code': 400})random_id = generate_mixed_id()if stream_type == 1:# 返回 RTSP播放地址SE_type = SE_RTSPflv_url = f'{SE_type}/openUr/{random_id}'if stream_type == 2:# 返回 HTTP-FLV 播放地址SE_type = SE_RTMPflv_url = f'{SE_VIDEO}/openUr/{random_id}.flv'# 启动转码/封装任务task = await perform_transcoding(ip, random_id, SE_type, stream_type)if not task:return web.json_response({'message': '无法播放视频流', 'code': 400})asyncio.create_task(stop_transcoding_after_delay(task, random_id, expire))return web.json_response({'message': '封装/转码启动成功','data': flv_url,# 'expire_seconds': expire})# aiohttp + CORS 初始化
app = web.Application()
cors = aiohttp_cors.setup(app, defaults={"*": aiohttp_cors.ResourceOptions(allow_credentials=True,expose_headers="*",allow_headers="*",)
})app.router.add_route('POST', '/play_camera', play_camera)
app.router.add_route('POST', '/addStreamProxy', addStreamProxy) # 开启拉流接口
app.router.add_route('POST', '/delStreamProxy', delStreamProxy) # 关闭拉流接口
app.router.add_route('POST', '/getMediaList', getMediaList) # 获取所有流列表
app.router.add_route('POST', '/getMediaPlayerList', getMediaPlayerList) # 获取某个流观看者列表
app.router.add_route('POST', '/listStreamProxy', listStreamProxy) # 拉流列表
app.router.add_route('POST', '/getServerConfig', getServerConfig) # 获取服务器配置for route in list(app.router.routes()):cors.add(route)if __name__ == '__main__':web.run_app(app, host='0.0.0.0', port=7000, access_log=logging.getLogger())
(2)主业务ZLK接口实现
import json
import requests
from config import *# Zlk 流媒体 相关转换信息接口# 动态添加 rtsp/rtmp/hls/http-ts/http-flv 拉流代理(只支持 H264/H265/aac/G711/opus 负载)
async def addStreamProxy(request):# 数据项data = await request.post()# 流地址url = data.get('url')# addStreamProxy 接口请求地址api = "http://{}:{}/index/api/addStreamProxy".format(ZLK_HOST,ZLK_PORT)# 秘钥 ZLK_SECRET (必填)alk_secret = ZLK_SECRET# 虚拟主机host 192.168.14.93 (必填)vhost = ZLK_HOST# app 流的应用名 例如live (必填)app = "openUrl"# stream 流的id 例如test (必填)stream = generate_mixed_id()# url rtsp流地址 rtsp://admin:sxygsj123@192.168.7.50:554/Streaming/Unicast/Channels/101 (必填)url = urlenable_rtsp = True# retry_count 拉流重试次数,默认为-1 无限重试 int# timeout_sec 拉流超时时间,单位秒,float 类型 int# enable_hls 是否转换成 hls-mpegts 协议 bool(true false)enable_hls = True# enable_hls_fmp4 是否转换成 hls-fmp4 协议 bool(true false)enable_hls_fmp4 = True# enable_rtmp 是否转 rtmp/flv 协议 bool(true false)enable_rtmp = True# enable_ts 是否转 http-ts/ws-ts 协议 bool(true false)enable_ts = True# enable_fmp4 是否转 http-fmp4/ws-fmp4 协议 bool(true false)enable_fmp4 = True# add_mute_audio 转协议时,无音频是否添加静音 aac 音频 bool(true false)# enable_audio 转协议时是否开启音频 bool(true false)params = {'secret':alk_secret,'vhost':vhost,'app':app,'stream':stream,'url':url,'enable_hls':enable_hls,'enable_hls_fmp4':enable_hls_fmp4,'enable_rtmp':enable_rtmp,'enable_ts':enable_ts,'enable_fmp4':enable_fmp4,'enable_rtsp':enable_rtsp,}zlk_info = requests.get(api,params=params)zlk_info = json.loads(zlk_info.text)if zlk_info.get('code') == 0 and zlk_info.get('data').get('key'):return web.json_response({'rtsp': "rtsp://{}:8554/{}/{}".format(ZLK_HOST, app, stream),'mp4': "http://{}:8080/{}/{}.live.mp4".format(ZLK_HOST, app, stream),'ts': "http://{}:8080/{}/{}.live.ts".format(ZLK_HOST, app, stream),'flv': "http://{}:8080/{}/{}.live.flv".format(ZLK_HOST, app, stream),'rtmp': "rtmp://{}:1935/{}/{}".format(ZLK_HOST, app, stream),})return web.json_response({'code': 400,'msg':'添加流失败!'})# 关闭拉流代理
async def delStreamProxy(request):# 数据项data = await request.post()# addStreamProxy 接口返回的 keykey = data.get('key')print(key,'111')# addStreamProxy 接口请求地址api = "http://{}:{}/index/api/delStreamProxy".format(ZLK_HOST,ZLK_PORT)# 秘钥 ZLK_SECRET (必填)alk_secret = ZLK_SECRETparams = {'secret':alk_secret,'key':key,}zlk_info = requests.get(api,params=params)zlk_info = json.loads(zlk_info.text)print(zlk_info)if zlk_info.get('code') == 0 and zlk_info.get('data').get('flag'):return web.json_response({'code':200,'msg':'关闭流成功!'})return web.json_response({'code': 400,'msg':'关闭流失败!'})# 获取流列表,可选筛选参数
async def getMediaList(request):# 数据项data = await request.post()# addStreamProxy 接口返回的 keykey = data.get('key')# 筛选协议,例如 rtsp 或 rtmpschema = data.get('schema')# addStreamProxy 接口请求地址api = "http://{}:{}/index/api/getMediaList".format(ZLK_HOST,ZLK_PORT)# 秘钥 ZLK_SECRET (必填)alk_secret = ZLK_SECRETparams = {'secret':alk_secret,'schema':schema,}zlk_info = requests.get(api,params=params)zlk_info = json.loads(zlk_info.text)print(zlk_info)if zlk_info.get('code') == 0 and zlk_info.get('data'):return web.json_response({'code':200,'data':zlk_info.get('data'),})return web.json_response({'code': 400,'msg':'获取失败!'})# 获取某个流观看者列表
async def getMediaPlayerList(request):# 数据项data = await request.post()print(data)# 协议,例如 rtsp 或 rtmpschema = data.get('schema')# 虚拟主机,例如__defaultVhost__vhost = data.get('vhost')# 应用名,例如 liveapp = data.get('app')# 流 id,例如 obsstream = data.get('stream')api = "http://{}:{}/index/api/getMediaPlayerList".format(ZLK_HOST,ZLK_PORT)# 秘钥 ZLK_SECRET (必填)alk_secret = ZLK_SECRETparams = {'secret':alk_secret,'schema':schema,'vhost':vhost,'app':app,'stream':stream,}zlk_info = requests.get(api,params=params)zlk_info = json.loads(zlk_info.text)print(zlk_info)if zlk_info.get('code') == 0:return web.json_response({'code':200,'data':zlk_info.get('data'),})return web.json_response({'code': 400,'msg':'获取失败!'})# 获取拉流列表
async def listStreamProxy(request):# 数据项data = await request.post()api = "http://{}:{}/index/api/listStreamProxy".format(ZLK_HOST,ZLK_PORT)# 秘钥 ZLK_SECRET (必填)alk_secret = ZLK_SECRETparams = {'secret':alk_secret,}zlk_info = requests.get(api,params=params)print(zlk_info)zlk_info = json.loads(zlk_info.text)if zlk_info.get('code') == 0:return web.json_response({'code':200,'data':zlk_info.get('data'),})return web.json_response({'code': 400,'msg':'获取失败!'})# 服务器配置
async def getServerConfig(request):# 数据项data = await request.post()api = "http://{}:{}/index/api/getServerConfig".format(ZLK_HOST,ZLK_PORT)# 秘钥 ZLK_SECRET (必填)alk_secret = ZLK_SECRETparams = {'secret':alk_secret,}zlk_info = requests.get(api,params=params)zlk_info = json.loads(zlk_info.text)if zlk_info.get('code') == 0:return web.json_response({'code':200,'data':zlk_info.get('data'),})return web.json_response({'code': 400,'msg':'获取失败!'})
(3)公共参数及方法配置搭建
import os
import configparser
import random
import string
from aiohttp import web
import asyncio
import aiohttp_cors
import logging
import random
import string
import platformdef load_config(config_path):config = configparser.ConfigParser()config.read(config_path,encoding='utf-8')return configconfig_path = "config.ini"
config = load_config(config_path)# 从配置文件中读取服务器配置
SE_RTMP = config['setting']['SE_RTMP']
SE_RTSP = config['setting']['SE_RTSP']
SE_VIDEO = config['setting']['SE_VIDEO']
ZLK_SECRET = config['setting']['ZLK_SECRET']
ZLK_HOST = config['setting']['ZLK_HOST']
ZLK_PORT = config['setting']['ZLK_PORT']# 随机字符数字
def generate_mixed_id(length=40):if length < 3:raise ValueError("长度至少为3,才能保证包含大小写字母和数字")upper = random.choice(string.ascii_uppercase)lower = random.choice(string.ascii_lowercase)digit = random.choice(string.digits)others = random.choices(string.ascii_letters + string.digits, k=length - 3)combined = [upper, lower, digit] + othersrandom.shuffle(combined)return ''.join(combined)
(4)配置文件信息
[setting]
# RTMP 服务器地址
SE_RTMP = rtmp:/localhost:1935
# 流媒体播放地址、
SE_VIDEO = http://localhost:8085
# RTSP 服务器地址
SE_RTSP = rtsp://localhost:8554
# ZLK 秘钥
ZLK_SECRET = XNgynIrL9HCvXQJepStSYgtMmwqHEvxY
# ZLK 地址
ZLK_HOST = localhost
# ZLK 端口
ZLK_PORT = 8085
六、总结
本文详细介绍了 ZLMediaKit 的全流程实战操作,涵盖了从环境部署、服务配置、API 调用到前后端集成的完整过程。通过系统化的步骤演示,帮助读者快速掌握 ZLMediaKit 的应用与扩展能力。
1. 使用体验
在实际操作中,ZLMediaKit 展现了高效的流媒体处理能力和灵活的接口设计。部署过程较为简便,支持多种协议的流处理,使得流媒体接入更加多样化。前后端集成部分,利用其开放的 API 和稳定的事件回调机制,实现了流的实时管理与播放控制,极大提升了系统的可用性和扩展性。
2. 性能表现
-
ZLMediaKit 在多路流同时处理时表现稳定,转码及封装延迟低,满足实时视频传输需求。
-
资源消耗优化合理,尤其在开启硬件加速和合理配置缓存后,系统运行更加高效。
-
异步架构和多线程设计保障了高并发场景下的流畅体验。