当前位置: 首页 > news >正文

《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

  • 参数:

    参数是否必选释意
    secretYapi 操作密钥(配置文件配置)

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

  • 参数:

    参数是否必选释意
    secretYapi 操作密钥(配置文件配置)
    schemaN筛选协议,例如 rtsp 或 rtmp
    vhostN筛选虚拟主机,例如__defaultVhost__
    appN筛选应用名,例如 live
    streamN筛选流 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

  • 参数:

    参数参数类型释意是否必选
    secretstringapi 操作密钥(配置文件配置)Y
    vhoststring添加的流的虚拟主机,例如__defaultVhost__Y
    appstring添加的流的应用名,例如 liveY
    streamstring添加的流的 id 名,例如 testY
    urlstring拉流地址,例如 rtmp://live.hkstv.hk.lxdns.com/live/hks2Y
    retry_countint拉流重试次数,默认为-1 无限重试N
    rtp_typeintrtsp 拉流时,拉流方式,0:tcp,1:udp,2:组播N
    timeout_secint拉流超时时间,单位秒,float 类型N
    enable_hlsbool是否转换成 hls-mpegts 协议N
    enable_hls_fmp4bool是否转换成 hls-fmp4 协议N
    enable_mp4bool是否允许 mp4 录制N
    enable_rtspbool是否转 rtsp 协议N
    enable_rtmpbool是否转 rtmp/flv 协议N
    enable_tsbool是否转 http-ts/ws-ts 协议N
    enable_fmp4bool是否转 http-fmp4/ws-fmp4 协议N
    hls_demandbool该协议是否有人观看才生成N
    rtsp_demandbool该协议是否有人观看才生成N
    rtmp_demandbool该协议是否有人观看才生成N
    ts_demandbool该协议是否有人观看才生成N
    fmp4_demandbool该协议是否有人观看才生成N
    enable_audiobool转协议时是否开启音频N
    add_mute_audiobool转协议时,无音频是否添加静音 aac 音频N
    mp4_save_pathstringmp4 录制文件保存根目录,置空使用默认N
    mp4_max_secondintmp4 录制切片大小,单位秒N
    mp4_as_playerboolMP4 录制是否当作观看者参与播放人数计数N
    hls_save_pathstringhls 文件保存保存根目录,置空使用默认N
    modify_stampint该流是否开启时间戳覆盖(0:绝对时间戳/1:系统时间戳/2:相对时间戳)N
    auto_closebool无人观看是否自动关闭流(不触发无人观看 hook)N

4. 停止流、关闭流接口

(1) /index/api/delStreamProxy(流注册成功后,也可以使用close_streams接口替代)
  • 功能:关闭拉流代理

  • 范例:http://127.0.0.1/index/api/delStreamProxy?key=__defaultVhost__/proxy/0

  • 参数:

    参数是否必选释意
    secretYapi 操作密钥(配置文件配置)
    keyYaddStreamProxy 接口返回的 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 在多路流同时处理时表现稳定,转码及封装延迟低,满足实时视频传输需求。

  • 资源消耗优化合理,尤其在开启硬件加速和合理配置缓存后,系统运行更加高效。

  • 异步架构和多线程设计保障了高并发场景下的流畅体验。

相关文章:

  • 【生物信息学】摇摆配对(Wobble Hypothesis)
  • RAG系统向量数据库选型与Prompt Engineering鲁棒性测试实践
  • 人工智能产业融合新时代:路径、挑战与战略思维
  • 从设备监控到人员调度,可视化赋能车间全场景
  • KubeSphere 容器平台高可用:环境搭建与可视化操作指南
  • go全局配置redis,全局只需要连接一次,然后全局可以引用使用
  • 光伏功率预测 | BP神经网络多变量单步光伏功率预测(Matlab完整源码和数据)
  • filebeat原理架构
  • 可视化在车间质量管控中的创新应用,提升品质
  • QT中实现tcp连接
  • 计算机网络笔记(三十四)——5.6TCP可靠传输的实现
  • node+express+jwt+sequelize+mysql+本地服务器部署前端+云服务器公网部署:入门教程
  • 2N65-ASEMI电源领域核心组件2N65
  • 目标跟踪_学习
  • 【大模型】解耦大语言模型中的记忆与推理能力
  • 6.10 - 常用 SQL 语句以及知识点
  • ArcGIS Pro 3.4 二次开发 - 流图层
  • linux等保思路与例题
  • 什么是软件开发的边际成本?有什么作用?
  • vue 监听页面滚动
  • 做亚马逊需要的图片外链网站/百度互联网营销顾问
  • 织梦怎么做中英文网站/排行榜哪个网站最好
  • 做网站业务员怎么查找客户/百度app怎么找人工客服
  • 聊城做网站建设的公司/百度一直不收录网站
  • 网站开发(源代码)/抖音企业推广
  • 莒县做网站和微信/广州关于进一步优化疫情防控措施