当前位置: 首页 > 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.dtcms.com/a/294244.html

相关文章:

  • Android多开实现方案深度分析
  • Android13重置锁屏(2)
  • 论文略读:Knowledge is a Region in Weight Space for Finetuned Language Models
  • springboot集成LangChain4j
  • 世博会无法在Android上启动项目:无法连接到TCP端口5554:连接被拒绝
  • 2025暑期—05神经网络-BP网络
  • PyCharm配置python软件安装步骤(附安装包)PyCharm 2025 超详细下载安装教程
  • 【CNN】LeNet网络架构
  • 盟接之桥说制造:浅谈“客供共生关系”:构建能力闭环,实现价值共赢
  • 论文笔记:On the Biology of a Large Language Model
  • Java 高频算法
  • Python通关秘籍(七)数据结构——集合
  • mysql什么时候用char,varchar,text,longtext
  • Git 完全手册:从入门到团队协作实战(4)
  • 经典神经网络之LetNet
  • 【前沿技术动态】【AI总结】RustFS:从 0 到 1 打造下一代分布式对象存储
  • Java 时间处理 API 全解析:从 JDK7 到 JDK8 的演进
  • 有序数组中出现次数超过25%的元素
  • 数字人形象视频:开启虚拟世界的全新篇章
  • Linux 723 磁盘配额 限制用户写入 quota;snap快照原理
  • IRF 真机实验
  • [AI8051U入门第八步]硬件IIC驱动AHT10温湿度传感器
  • 密码学中的概率论与统计学:从频率分析到现代密码攻击
  • 【Kubernetes】集群启动nginx,观察端口映射,work节点使用kubectl配置
  • scikit-learn 包
  • 【后端】 FastAPI
  • AI替代人工:浪潮中的沉浮与觉醒
  • LNMP-zblog分布式部署
  • 前端/后端,前台/中台/后台概念区别
  • kafka 消费者组