修复工作
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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<String, ChannelHandlerContext> 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 */
|
||||
|
||||
Reference in New Issue
Block a user