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

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>

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

http://www.lryc.cn/news/597085.html

相关文章:

  • 基于Matlab传统图像处理技术的车辆车型识别与分类方法研究
  • 【第三章自定义检视面板_创建自定义编辑器_如何创建自定义PropertyDrawer(9/9)】
  • 第六章 W55MH32 UDP Multicast示例
  • 在离线 Ubuntu 22.04机器上运行 ddkj_portainer-cn 镜像 其他相关操作也可以复刻 docker
  • CCD工业相机系统设计——基于FPGA设计
  • 【后端】FastAPI的Pydantic 模型
  • 【Linux-云原生-笔记】keepalived相关
  • 蒙牛社交电商的升级路径研究:基于开源链动2+1模式、AI智能名片与S2B2C商城小程序源码的融合创新
  • 轻量化RTSP视频通路实践:采集即服务、播放即模块的工程解读
  • 【Redis】在Ubentu环境下安装Redis
  • RCE随笔-奇技淫巧(2)
  • 【Linux-云原生-笔记】Haproxy相关
  • ros0基础-day18
  • OCP NIC 3.0 Ethernet的multiroot complex和multi host complex的区别
  • Android多开实现方案深度分析
  • 【硬件】Fan in和Fan out
  • RAG深入理解和简易实现
  • 海信IP501H-IP502h_GK6323处理器-原机安卓9专用-优盘卡刷固件包
  • springcloud环境和工程搭建
  • 中国多媒体与网络教学学报编辑部中国多媒体与网络教学学报杂志社2025年第6期目录
  • 论文略读:Mitigating Catastrophic Forgetting in Language Transfer via Model Merging
  • 旋变调零技术介绍与方法
  • CVE-2025-32463漏洞:sudo权限提升漏洞全解析
  • 「源力觉醒 创作者计划」深度讲解大模型之在百花齐放的大模型时代看百度文心大模型4.5的能力与未来
  • JS进阶学习
  • 《计算机网络》实验报告七 HTTP协议分析与测量
  • spring-cloud概述
  • 计算机网络学习----域名解析
  • 开源 Arkts 鸿蒙应用 开发(十)通讯--Http
  • WebGIS 中常用公共插件