> 技术文档 > ZLMediaKit流媒体服务器WebRTC页面显示:使用docker部署_docker安装zlmideakit 启用webrtc映射端口

ZLMediaKit流媒体服务器WebRTC页面显示:使用docker部署_docker安装zlmideakit 启用webrtc映射端口

先使用本程序

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 

最后献上前端代码:

  WebRTC 调试页面  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; } 

WebRTC 拉流测试

实时播放

录像回放

<!----> 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:00 startDateInput.value = `${year}-${month}-${day}T00:00`; // 默认结束时间为当天 23:59 endDateInput.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); // 添加 origin myHeaders.append(\"Referer\", serverUrl); // 添加 referer var requestOptions = { method: \'GET\', headers: myHeaders, mode: \'cors\', // 显式声明使用 CORS credentials: \'same-origin\', // 如果需要携带 Cookie 可以设为 include redirect: \'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-Type const 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 { // 创建 Offer const 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(); // 解析为 JSON console.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 { 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(); } .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; }

请求地址记得换成你的docker地址。记住得配置域名和证书才能播放