ZLMediaKit流媒体服务器WebRTC页面显示:使用docker部署
先使用本程序
j-media-server: 基于ZLM4J搭建的SpringBoot流媒体服务器
看效果:
接上回文章:ZLMediaKit流媒体服务器WebRTC页面显示:不用docker ,直接一个工程部署搞定_zlmediakit 搭建流媒体服务器-CSDN博客
现在使用docker
打好包的目录地址是:/usr/local/zlmwebrtc/
开始拉取代码后会有Dockerfile
# 使用基于 Debian 的 OpenJDK 镜像
FROM openjdk:11-jdk-slim-bullseye# 使用阿里云 Debian 镜像源
RUN sed -i 's/http:\/\/deb.debian.org\/debian\//https:\/\/mirrors.aliyun.com\/debian\//g' /etc/apt/sources.list && \sed -i 's/http:\/\/security.debian.org\//https:\/\/mirrors.aliyun.com\/debian-security\//g' /etc/apt/sources.list# 设置工作目录
WORKDIR /app# 复制 JAR 文件
COPY j-media-server.jar app.jarCOPY natives /usr/local/lib# 设置 LD_LIBRARY_PATH 环境变量指向 native 目录
ENV LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH# 启动命令(让 JavaCPP 自动处理 native)
ENTRYPOINT ["java", \"-Xms128m", \"-Xmx1024m", \"-jar", \"app.jar"]
上传到服务器后执行:
docker build -t j-media-server:1.0.3 .
再启动:
* # 运行容器并挂载录像保存目录docker run -id --restart=always --network host -p 1935:1935 -p 8180:80 -p 8899:8899 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp -v /media:/media --name mediakit j-media-server:1.0.3
用apifox导入地址:
测试拉流:
NVR中配置的摄像头地址:rtsp://admin:123456@192.168.0.190:554/Streaming/Channels/201
参数:
{
"app": "live",
"stream": "camera_002",
"url": "rtsp://admin:123456@192.168.0.190:554/Streaming/Channels/201",
"vhost": "192.168.0.239",
"rtpType": 0,
"retryCount": 3,
"timeoutSec": 10,
"enableHls": 1,
"enableRtsp": 1,
"enableRtmp": 0,
"enableTs": 0,
"enableAudio": 1,
"enableFmp4": 0,
"enableMp4": 0,
"mp4MaxSecond": 3600,
"rtspSpeed": 1.0
}
返回:
{
"code": 200,
"msg": "操作成功",
"data": {
"rtsp": "rtsp://192.168.0.239:8554/live/camera_002",
"mp4": "http://192.168.0.239:8180/live/camera_002.live.mp4",
"ts": "http://192.168.0.239:8180/live/camera_002.live.ts",
"flv": "http://192.168.0.239:8180/live/camera_002.live.flv",
"rtmp": "rtmp://192.168.0.239:1935/live/camera_002",
"key": "hIU9uOCd1x"
}
}
key是用来关流的: hIU9uOCd1x
rtsp是用来播放和截图的。
Stream:camera_002 要记住,可以用来做很多事。
转webrtc:用填写:流标识 (stream): camera_002
最后献上前端代码:
<!DOCTYPE html>
<html lang="zh">
<head><meta charset="UTF-8"><title>WebRTC 调试页面</title><style>body { font-family: Arial; padding: 20px; }label, input, textarea, button { display: block; width: 100%; margin-top: 10px; }video {width: 100%;max-width: 800px;margin-top: 20px;background-color: black;}</style>
</head>
<body>
<h2>WebRTC 拉流测试</h2><!-- 共用输入框 -->
<div><label for="app">应用名称 (app)</label><input type="text" id="app" value="live">
</div><div><label for="stream">流标识 (stream)</label><input type="text" id="stream" value="camera_001">
</div><div><label for="type">SDP 类型 (type): play / push / echo</label><input type="text" id="type" value="play">
</div><div><label for="pcSdp">PC SDP 内容 (pcSdp)</label><textarea id="pcSdp"></textarea>
</div><!-- 左右布局容器 -->
<div style="display: flex; gap: 20px; margin-top: 30px;"><!-- 实时播放区域 --><div style="flex: 2;"><h3>实时播放</h3><div><button onclick="generateOffer()">生成 Offer 并播放</button><button onclick="stopPlayback()">关闭播放</button><button onclick="testBackend()">测试后端接口</button></div><video id="videoElement" autoplay playsinline controls style="width: 100%; max-width: 600px; background-color: black;"></video></div><!-- 回放区域 --><div style="flex: 2;"><h3>录像回放</h3><!-- 日期选择 --><div><label for="date">选择日期</label><input type="date" id="date" value="2025-07-23"></div><!-- 开始时间 --><div><label for="startTime">开始时间</label><input type="datetime-local" id="startTime"></div><!-- 结束时间 --><div><label for="endTime">结束时间</label><input type="datetime-local" id="endTime"></div><!-- 按钮 --><button onclick="playSelectedVideo()">播放选中时间段</button><video id="recordVideo" class="video-js vjs-default-skin" controls style="width: 100%; max-width: 600px; hight: 100%;"><source id="recordSource" src="" type="video/mp4">您的浏览器不支持 video 标签。</video><!-- 时间轴容器 --><div id="timeline-container" style="margin-top: 20px; width: 100%; height: 30px; background-color: #f0f0f0; position: relative; overflow-x: auto;"></div></div>
</div><!--<video controls autoplay>-->
<!-- <source src="flv://192.168.0.198:8180/live/camera_001.live.flv" type="video/x-flv">-->
<!-- 您的浏览器不支持 video 标签。-->
<!--</video>--><script>window.onload = function () {const today = new Date();const dateInput = document.getElementById('date');const startDateInput = document.getElementById('startTime');const endDateInput = document.getElementById('endTime');// 设置默认日期(格式:YYYY-MM-DD)const year = today.getFullYear();const month = String(today.getMonth() + 1).padStart(2, '0');const day = String(today.getDate()).padStart(2, '0');dateInput.value = `${year}-${month}-${day}`;// 默认开始时间为当天 00:00startDateInput.value = `${year}-${month}-${day}T00:00`;// 默认结束时间为当天 23:59endDateInput.value = `${year}-${month}-${day}T23:59`;};// const serverUrl = "http://192.168.0.239:8180"; // 替换为你的本地服务地址const baseserverUrl = "http://192.168.0.198:8280"; // 替换为你的本地服务地址// const serverUrl = "http://192.168.0.239:8899"; // 替换为你的本地服务地址const serverUrl = "https://media.shenglong.com"; // 替换为你的本地服务地址let pc = null;var myHeaders = new Headers();myHeaders.append("secret", "035c73f7-bb6b-4889-a715-d9eb2d1925cc");myHeaders.append("User-Agent", "Apifox/1.0.0 (https://apifox.com)");myHeaders.append("Accept", "*/*");myHeaders.append("Host", serverUrl);myHeaders.append("Connection", "keep-alive");myHeaders.append("Origin", serverUrl); // 添加 originmyHeaders.append("Referer", serverUrl); // 添加 referervar requestOptions = {method: 'GET',headers: myHeaders,mode: 'cors', // 显式声明使用 CORScredentials: 'same-origin', // 如果需要携带 Cookie 可以设为 includeredirect: 'follow'};async function generateOffer() {console.log("【DEBUG】generateOffer 开始");const app = document.getElementById('app').value.trim();const stream = document.getElementById('stream').value.trim();const type = document.getElementById('type').value.trim();const pcSdpInput = document.getElementById('pcSdp');const videoElement = document.getElementById('videoElement');pc = new RTCPeerConnection();// 添加调试日志pc.onicecandidate = async (event) => {console.log("【ICE Candidate】新候选地址:", event.candidate);if (event.candidate) {// 构造请求头,避免使用多个 Content-Typeconst headers = new Headers();headers.append("secret", "035c73f7-bb6b-4889-a715-d9eb2d1925cc");headers.append("Origin", serverUrl);headers.append("Referer", serverUrl);headers.append("Content-Type", "application/json"); // 使用 text/plain 避免 MIME 错误// 将 candidate 发送到信令服务器fetch(`${serverUrl}/index/api/webrtc_ice`, {method: 'POST',headers: headers,body: JSON.stringify(event.candidate)});}};pc.ontrack = (event) => {console.log("【Track】收到远程流");if (!videoElement.srcObject) {videoElement.srcObject = event.streams[0];}};pc.onconnectionstatechange = () => {console.log("【连接状态变化】", pc.connectionState);};// 添加音视频收发器pc.addTransceiver("video", {direction: 'recvonly'});pc.addTransceiver("audio", {direction: 'recvonly'});try {// 创建 Offerconst offer = await pc.createOffer();console.log("✅ 成功创建 Offer SDP");// 设置本地描述await pc.setLocalDescription(offer);console.log("📌 已设置 localDescription");const response = await fetch(`${serverUrl}/index/api/webrtc?app=${encodeURIComponent(app)}&stream=${encodeURIComponent(stream)}&type=play`, {method: 'POST',headers: {'Content-Type': 'text/plain;charset=UTF-8','secret': '035c73f7-bb6b-4889-a715-d9eb2d1925cc','Origin': serverUrl,'Referer': serverUrl},body: pc.localDescription.sdp});const data = await response.json(); // 解析为 JSONconsole.log("📨 接口响应数据:", data);if (data.code === 0 && data.sdp) {const answerDesc = new RTCSessionDescription({type: 'answer',sdp: data.sdp // 使用返回的 SDP 构造 Answer});await pc.setRemoteDescription(answerDesc);console.log("✅ 成功设置 remoteDescription");} else {alert("❌ 获取 Answer SDP 失败:" + (data.message || data.data));}} catch (error) {console.error("⚠️ 发生错误:", error);alert("发生错误:" + error.message);}}// 新增函数:关闭播放function stopPlayback() {console.log("【关闭播放】开始");if (pc) {pc.close();pc = null;}const videoElement = document.getElementById('videoElement');if (videoElement.srcObject) {videoElement.srcObject = null;}alert("✅ 播放已关闭");}async function testBackend() {myHeaders.append("Content-Type", "application/json");const url = `${serverUrl}/index/api/getMediaList?app=live&schema=rtsp&stream=camera_001&secret=035c73f7-bb6b-4889-a715-d9eb2d1925cc`;try {const res = await fetch(url, requestOptions);const data = await res.json();console.log("🔗 后端接口返回:", data);alert("✅ 后端接口可访问");} catch (e) {console.error("❌ 连接失败:", e);alert("❌ 后端接口不可访问,请检查服务是否运行");}}let videoUrls = []; // 从接口获取的视频 URL 列表async function playRecordedVideo() {const stream = document.getElementById('stream').value;const date = document.getElementById('date').value;const response = await fetch(`${baseserverUrl}/supply/media/api/video/fragments?cameraId=${stream}&date=${date}`);videoUrls = await response.json();if (videoUrls.length === 0) {alert("未找到对应日期的录像");return;}// 默认播放第一个// const recordVideo = document.getElementById('recordVideo');// const recordSource = document.getElementById('recordSource');// recordSource.src = videoUrls[0];// recordVideo.load();// recordVideo.play();}function playSelectedVideo() {playRecordedVideo()const selectedDate = document.getElementById('date').value;const startTime = document.getElementById('startTime').value;const endTime = document.getElementById('endTime').value;if (!startTime || !endTime) {alert("请选择开始和结束时间");return;}const start = new Date(startTime).getTime() / 1000;const end = new Date(endTime).getTime() / 1000;const matchedVideos = videoUrls.filter(url => {const fileName = url.split('/').pop();const parts = fileName.split('-');// 提取日期和时间部分const fileDate = `${parts[0]}-${parts[1]}-${parts[2]}`;const fileTimeStr = `${parts[3]}:${parts[4]}:${parts[5]}`;const fileDateTimeStr = `${fileDate}T${parts[3]}:${parts[4]}:${parts[5]}`;const fileTime = new Date(fileDateTimeStr).getTime() / 1000;return fileTime >= start && fileTime <= end;});if (matchedVideos.length === 0) {alert("未找到匹配的录像");return;}// 生成时间轴generateTimeline(matchedVideos);// 默认播放第一个playVideo(matchedVideos[0]);}function generateTimeline(videoList) {const container = document.getElementById('timeline-container');container.innerHTML = ""; // 清空旧内容if (videoList.length === 0) return;// 获取容器总宽度const containerWidth = container.clientWidth;// 提取最早和最晚时间,用于比例计算const times = videoList.map(url => {const fileName = url.split('/').pop();const parts = fileName.split('-');const timeStr = `${parts[3]}:${parts[4]}:${parts[5]}`;const dateStr = `${parts[0]}-${parts[1]}-${parts[2]}T${timeStr}`;return {url,startTime: new Date(dateStr).getTime() / 1000,endTime: new Date(dateStr).getTime() / 1000 + 10, // 每个视频 10 秒};});const minTime = Math.min(...times.map(t => t.startTime));const maxTime = Math.max(...times.map(t => t.endTime));const totalDuration = maxTime - minTime;times.forEach(item => {const startOffset = ((item.startTime - minTime) / totalDuration) * 100;const duration = ((item.endTime - item.startTime) / totalDuration) * 100;const segment = document.createElement('div');segment.className = 'timeline-segment';segment.style.left = `${startOffset}%`;segment.style.width = `${duration}%`;segment.style.backgroundColor = getRandomColor(); // 随机颜色区分片段segment.title = `时间:${formatTime(item.startTime)}`;segment.onclick = () => playVideo(item.url);container.appendChild(segment);});}function formatTime(seconds) {const date = new Date(seconds * 1000);return date.toTimeString().split(' ')[0];}function getRandomColor() {const letters = '6789ABCDEF'.split('');let color = '#';for (let i = 0; i < 6; i++) {color += letters[Math.floor(Math.random() * 10)];}return color;}function playVideo(url) {const recordVideo = document.getElementById('recordVideo');const recordSource = document.getElementById('recordSource');recordSource.src = url;recordVideo.load();recordVideo.play();}</script>
<script src="https://vjs.zencdn.net/7.20.1/video.min.js"></script>
<link href="https://vjs.zencdn.net/7.20.1/video-js.css" rel="stylesheet"></body>
</html>
<style>.timeline-segment {position: absolute;height: 100%;background-color: #007bff;border: 1px solid #0056b3;border-radius: 4px;cursor: pointer;color: white;font-size: 12px;display: flex;align-items: center;justify-content: center;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;}
</style>