From 1deafab5b321eef178e5814d632a9a570630654d Mon Sep 17 00:00:00 2001 From: 86156 <823267011@qq.com> Date: Thu, 2 Oct 2025 15:06:35 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=B7=A5=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../video/device/component/cusPlayer.vue | 29 +++++-- .../src/views/video/device/component/flv.vue | 6 +- .../thread/MediaTransferFlvByFFmpeg.java | 27 ++----- .../thread/MediaTransferFlvByJavacv.java | 75 +++++++++++++++++-- 4 files changed, 99 insertions(+), 38 deletions(-) diff --git a/rtsp-vue/src/views/video/device/component/cusPlayer.vue b/rtsp-vue/src/views/video/device/component/cusPlayer.vue index d19c257..5ca23d9 100644 --- a/rtsp-vue/src/views/video/device/component/cusPlayer.vue +++ b/rtsp-vue/src/views/video/device/component/cusPlayer.vue @@ -120,16 +120,31 @@ export default { // 如果找到了 flv 实例,绑定事件 if (flvInstance && typeof flvInstance.on === 'function') { console.log('✅ flv.js 实例已找到,开始绑定事件'); + console.log('🔍 flv.js 配置:', flvInstance._config); // 监听 flv.js 的统计信息 + let statsCount = 0; flvInstance.on('statistics_info', (stats) => { - console.log('📈 flv.js 统计:', { - speed: stats.speed, - videoBufferSize: stats.videoBufferSize, - audioBufferSize: stats.audioBufferSize, - decodedFrames: stats.decodedFrames, - droppedFrames: stats.droppedFrames - }); + statsCount++; + // 只输出前几次和有变化的统计 + if (statsCount <= 3 || stats.decodedFrames > 0 || statsCount % 20 === 0) { + console.log('📈 flv.js 统计 #' + statsCount + ':', { + speed: stats.speed.toFixed(2) + ' KB/s', + videoBufferSize: stats.videoBufferSize, + audioBufferSize: stats.audioBufferSize, + decodedFrames: stats.decodedFrames, + droppedFrames: stats.droppedFrames + }); + } + + // 如果收到了很多数据但没有解码任何帧,说明格式有问题 + if (statsCount > 10 && stats.decodedFrames === 0) { + console.error('❌ 已接收数据但无法解码!可能的原因:'); + console.error(' 1. FLV 流缺少元数据(onMetaData)'); + console.error(' 2. 视频编码格式不支持(需要 H.264)'); + console.error(' 3. FLV 封装格式错误'); + console.error(' 建议:检查后端 FFmpeg 日志是否有错误'); + } }); // 监听 flv.js 的错误 diff --git a/rtsp-vue/src/views/video/device/component/flv.vue b/rtsp-vue/src/views/video/device/component/flv.vue index 552719d..b04cced 100644 --- a/rtsp-vue/src/views/video/device/component/flv.vue +++ b/rtsp-vue/src/views/video/device/component/flv.vue @@ -70,10 +70,10 @@ const getDetails = async (deviceId) => { tableData.value = res.data; // 直接访问后端视频流服务器(10083端口),绕过Nginx反代,避免缓冲问题 // 注意:RTSP URL需要原样传递,不要编码,&&&作为特殊分隔符 - // 使用 ffmpeg=true,系统 FFmpeg 包含完整的编码器支持(包括 libx264) + // 🔧 测试:使用 JavaCV 方案(不使用 FFmpeg) const videoServerUrl = 'http://49.232.154.205:10083'; - playUrl.value = `${videoServerUrl}/live?url=${tableData.value.url}&&&ffmpeg=true`; - console.log('📺 播放地址(直连后端):', playUrl.value); + playUrl.value = `${videoServerUrl}/live?url=${tableData.value.url}`; // 不加 &&&ffmpeg=true + console.log('📺 播放地址(JavaCV 方案):', playUrl.value); handlePlay(); } diff --git a/ruoyi-video/src/main/java/com/ruoyi/video/thread/MediaTransferFlvByFFmpeg.java b/ruoyi-video/src/main/java/com/ruoyi/video/thread/MediaTransferFlvByFFmpeg.java index 9ec5181..c42d6c0 100644 --- a/ruoyi-video/src/main/java/com/ruoyi/video/thread/MediaTransferFlvByFFmpeg.java +++ b/ruoyi-video/src/main/java/com/ruoyi/video/thread/MediaTransferFlvByFFmpeg.java @@ -178,30 +178,18 @@ public class MediaTransferFlvByFFmpeg extends MediaTransfer { this.addArgument("-loglevel").addArgument("info") .addArgument("-fflags").addArgument("+genpts") // 生成PTS时间戳(在输入之前) .addArgument("-rtsp_transport").addArgument("tcp") - .addArgument("-i").addArgument(cameraDto.getUrl()) - .addArgument("-max_delay").addArgument("1"); + .addArgument("-i").addArgument(cameraDto.getUrl()); if (useSystemFFmpeg) { - // 系统 FFmpeg:使用 libx264 重新编码 - log.info("🎬 使用 libx264 编码器(重新编码)"); + // 系统 FFmpeg:使用 libx264 重新编码(简化版,避免过多参数导致问题) + log.info("🎬 使用 libx264 编码器(简化配置)"); this.addArgument("-c:v").addArgument("libx264") .addArgument("-preset").addArgument("ultrafast") .addArgument("-tune").addArgument("zerolatency") .addArgument("-profile:v").addArgument("baseline") - .addArgument("-level").addArgument("3.0") - .addArgument("-pix_fmt").addArgument("yuv420p") // 像素格式 - .addArgument("-g").addArgument("50") // GOP大小(关键帧间隔) - .addArgument("-keyint_min").addArgument("50") // 最小关键帧间隔 - .addArgument("-sc_threshold").addArgument("0") // 禁用场景切换检测 - .addArgument("-r").addArgument("25") // 帧率 - .addArgument("-b:v").addArgument("1000k") - .addArgument("-maxrate").addArgument("1000k") - .addArgument("-bufsize").addArgument("2000k") + .addArgument("-pix_fmt").addArgument("yuv420p") .addArgument("-c:a").addArgument("aac") - .addArgument("-strict").addArgument("experimental") - .addArgument("-b:a").addArgument("64k") - .addArgument("-ar").addArgument("44100") - .addArgument("-ac").addArgument("1"); // 单声道 + .addArgument("-ar").addArgument("44100"); } else { // JavaCV FFmpeg:使用 copy(不重新编码,只重新封装) log.info("📦 使用 copy 编码器(不重新编码,只重新封装)"); @@ -209,9 +197,8 @@ public class MediaTransferFlvByFFmpeg extends MediaTransfer { .addArgument("-c:a").addArgument("copy"); } - // 🔑 关键:添加 FLV 格式参数,确保 flv.js 能正确解析 - this.addArgument("-f").addArgument("flv") - .addArgument("-flvflags").addArgument("add_keyframe_index"); // 添加关键帧索引 + // 🔑 FLV 格式输出(最简化配置) + this.addArgument("-f").addArgument("flv"); } // private void buildCommand() { diff --git a/ruoyi-video/src/main/java/com/ruoyi/video/thread/MediaTransferFlvByJavacv.java b/ruoyi-video/src/main/java/com/ruoyi/video/thread/MediaTransferFlvByJavacv.java index 8160119..18f84b8 100644 --- a/ruoyi-video/src/main/java/com/ruoyi/video/thread/MediaTransferFlvByJavacv.java +++ b/ruoyi-video/src/main/java/com/ruoyi/video/thread/MediaTransferFlvByJavacv.java @@ -275,11 +275,24 @@ public class MediaTransferFlvByJavacv extends MediaTransfer implements Runnable long tsPerFrame = 1000 / (grabber.getFrameRate() > 0 ? (long) grabber.getFrameRate() : 25); totalFrames = droppedFrames = 0; long t0 = System.currentTimeMillis(); + + log.info("🎬 开始主处理循环"); + log.info("📊 视频信息: 宽度={}, 高度={}, 帧率={}", + grabber.getImageWidth(), grabber.getImageHeight(), grabber.getFrameRate()); while (running && (frame = grabber.grab()) != null) { totalFrames++; long now = System.currentTimeMillis(); + // 前几帧输出详细日志 + if (totalFrames <= 5 || totalFrames % 100 == 0) { + log.info("📹 抓取帧 #{}: 类型={}, 有图像={}, 有音频={}", + totalFrames, + frame.image != null ? "视频帧" : (frame.samples != null ? "音频帧" : "未知"), + frame.image != null, + frame.samples != null); + } + // 检查是否需要录制视频 if (sessionRecorder != null) { try { @@ -301,6 +314,8 @@ public class MediaTransferFlvByJavacv extends MediaTransfer implements Runnable // 避免CPU占用过高 LockSupport.parkNanos(1_000_000L); // 1毫秒 } + + log.warn("⚠️ 主处理循环退出: 总帧数={}, 丢帧数={}", totalFrames, droppedFrames); } /** 处理视频帧 */ @@ -324,17 +339,37 @@ public class MediaTransferFlvByJavacv extends MediaTransfer implements Runnable private void pushFrameToClients(Frame frame) { String mediaKey = cameraDto.getMediaKey(); Map contexts = MediaService.clientConnections.get(mediaKey); - if (contexts == null || contexts.isEmpty()) return; + + if (contexts == null || contexts.isEmpty()) { + if (totalFrames == 1 || totalFrames % 100 == 0) { + log.debug("⚠️ 无客户端连接,跳过推流 (帧 #{})", totalFrames); + } + return; + } + + if (totalFrames == 1) { + log.info("📤 开始推流到客户端,客户端数量: {}", contexts.size()); + } try { // 转换为JPEG字节 BufferedImage bufferedImage = toImage.convert(frame); - if (bufferedImage == null) return; + if (bufferedImage == null) { + log.warn("❌ 帧转换失败: bufferedImage 为 null (帧 #{})", totalFrames); + return; + } ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ImageIO.write(bufferedImage, "jpg", outputStream); byte[] imageBytes = outputStream.toByteArray(); + if (totalFrames <= 3 || totalFrames % 100 == 0) { + log.info("📤 推送帧 #{}: 大小={} bytes, 分辨率={}x{}, 客户端数={}", + totalFrames, imageBytes.length, + bufferedImage.getWidth(), bufferedImage.getHeight(), + contexts.size()); + } + // 推送给所有客户端 contexts.forEach((clientId, ctx) -> { try { @@ -343,22 +378,26 @@ public class MediaTransferFlvByJavacv extends MediaTransfer implements Runnable BinaryWebSocketFrame binaryWebSocketFrame = new BinaryWebSocketFrame( Unpooled.wrappedBuffer(imageBytes)); ctx.writeAndFlush(binaryWebSocketFrame); + } else if (type == ClientType.HTTP) { + // HTTP-FLV 推流 (JPEG 方式不适用,应该推送 FLV 数据) + log.warn("⚠️ HTTP 客户端不支持 JPEG 推流,需要 FLV 格式"); } else { - // 其他客户端类型的处理... + log.warn("⚠️ 未知客户端类型: {}", type); } } catch (Exception e) { - log.warn("推流失败 ({}): {}", clientId, e.getMessage()); + log.error("❌ 推流失败 ({}): {}", clientId, e.getMessage()); MediaService.clients.remove(clientId); contexts.remove(clientId); } }); } catch (Exception e) { - log.warn("帧转换失败: {}", e.getMessage()); + log.error("❌ 帧转换失败: {}", e.getMessage(), e); } } /** ================ 拉流配置 ================ */ protected boolean createGrabber() { + log.info("🎥 创建拉流器: {}", cameraDto.getUrl()); grabber = new FFmpegFrameGrabber(cameraDto.getUrl()); String fiveSecUs = "5000000"; @@ -375,13 +414,16 @@ public class MediaTransferFlvByJavacv extends MediaTransfer implements Runnable grabber.setOption("loglevel", "error"); if (cameraDto.getUrl().toLowerCase().startsWith("rtsp://")) { + log.info("📡 检测到 RTSP 流,使用 TCP 传输"); grabber.setOption("rtsp_transport", "tcp"); grabber.setOption("allowed_media_types", "video"); grabber.setOption("max_delay", "500000"); grabber.setOption("user_agent", "Lavf/60"); } else if (cameraDto.getUrl().toLowerCase().startsWith("rtmp://")) { + log.info("📡 检测到 RTMP 流"); grabber.setOption("rtmp_buffer", "1000"); } else if ("desktop".equalsIgnoreCase(cameraDto.getUrl())) { + log.info("🖥️ 检测到桌面捕获"); grabber.setFormat("gdigrab"); grabber.setOption("draw_mouse", "1"); grabber.setNumBuffers(0); @@ -391,12 +433,17 @@ public class MediaTransferFlvByJavacv extends MediaTransfer implements Runnable } try { + log.info("⏳ 正在连接视频源..."); grabber.start(); - log.info("启动拉流器成功: {}", cameraDto.getUrl()); + log.info("✅ 拉流器启动成功"); + log.info("📊 流信息: 编码器={}, 格式={}, 比特率={}", + grabber.getVideoCodec(), grabber.getFormat(), grabber.getVideoBitrate()); return (grabberStatus = true); } catch (FrameGrabber.Exception e) { MediaService.cameras.remove(cameraDto.getMediaKey()); - log.error("启动拉流器失败: {} ({})", cameraDto.getUrl(), e.getMessage()); + log.error("❌ 拉流器启动失败: {}", cameraDto.getUrl()); + log.error(" 错误信息: {}", e.getMessage()); + log.error(" 可能原因: 1) RTSP地址错误 2) 网络不通 3) 认证失败 4) 编码格式不支持"); return false; } } @@ -426,6 +473,17 @@ public class MediaTransferFlvByJavacv extends MediaTransfer implements Runnable // 实现客户端添加逻辑 String clientId = ctx.channel().id().asLongText(); + log.info("🔗 新客户端连接: ID={}, 类型={}, 运行状态={}", + clientId, clientType, running); + + // ⚠️ 重要提示:JavaCV 方案推送 JPEG 帧,只适用于 WebSocket + if (clientType == ClientType.HTTP) { + log.warn("⚠️ JavaCV 方案不支持 HTTP-FLV 客户端!"); + log.warn(" 当前推流方式: JPEG 图片帧 (适用于 WebSocket)"); + log.warn(" HTTP-FLV 需要: 连续的 FLV 格式数据流"); + log.warn(" 解决方案: 使用 FFmpeg 方案 (添加 &&&ffmpeg=true 参数)"); + } + // 添加到全局客户端映射 MediaService.clients.put(clientId, clientType); @@ -438,7 +496,8 @@ public class MediaTransferFlvByJavacv extends MediaTransfer implements Runnable } contexts.put(clientId, ctx); - log.info("客户端已添加: {}, 类型: {}", clientId, clientType); + log.info("✅ 客户端已添加: {}, 类型: {}, 当前客户端总数: {}", + clientId, clientType, contexts.size()); } /** 导出最近一次"叠好框的帧"(深拷贝),用于截图/存证。调用方负责释放 Mat */