提高视频帧率
This commit is contained in:
@@ -41,19 +41,14 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
createPlayer(url) {
|
createPlayer(url) {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
console.error('❌ 播放地址为空');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否是 WebSocket URL
|
// 检查是否是 WebSocket URL
|
||||||
if (!url.startsWith('ws://') && !url.startsWith('wss://')) {
|
if (!url.startsWith('ws://') && !url.startsWith('wss://')) {
|
||||||
console.error('❌ JpegPlayer 只支持 WebSocket URL');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🎬 创建 JPEG WebSocket 播放器');
|
|
||||||
console.log('📺 WebSocket URL:', url);
|
|
||||||
|
|
||||||
// 关闭旧连接
|
// 关闭旧连接
|
||||||
if (this.ws) {
|
if (this.ws) {
|
||||||
this.ws.close();
|
this.ws.close();
|
||||||
@@ -67,7 +62,6 @@ export default {
|
|||||||
this.ws.binaryType = 'arraybuffer';
|
this.ws.binaryType = 'arraybuffer';
|
||||||
|
|
||||||
this.ws.onopen = () => {
|
this.ws.onopen = () => {
|
||||||
console.log('✅ WebSocket 连接已建立');
|
|
||||||
this.lastFrameTime = Date.now();
|
this.lastFrameTime = Date.now();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -76,11 +70,10 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.ws.onerror = (error) => {
|
this.ws.onerror = (error) => {
|
||||||
console.error('❌ WebSocket 错误:', error);
|
// 静默处理错误
|
||||||
};
|
};
|
||||||
|
|
||||||
this.ws.onclose = () => {
|
this.ws.onclose = () => {
|
||||||
console.log('⚠️ WebSocket 连接已关闭');
|
|
||||||
this.isPlay = false;
|
this.isPlay = false;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -95,39 +88,46 @@ export default {
|
|||||||
this.latency = now - this.lastFrameTime;
|
this.latency = now - this.lastFrameTime;
|
||||||
this.lastFrameTime = now;
|
this.lastFrameTime = now;
|
||||||
|
|
||||||
// 将 ArrayBuffer 转换为 Blob
|
// 使用 createImageBitmap 实现更快的解码(如果浏览器支持)
|
||||||
const blob = new Blob([data], { type: 'image/jpeg' });
|
if (window.createImageBitmap) {
|
||||||
const url = URL.createObjectURL(blob);
|
const blob = new Blob([data], { type: 'image/jpeg' });
|
||||||
|
createImageBitmap(blob).then(imageBitmap => {
|
||||||
// 创建图片对象
|
// 只在尺寸变化时调整 canvas
|
||||||
const img = new Image();
|
if (this.canvas.width !== imageBitmap.width || this.canvas.height !== imageBitmap.height) {
|
||||||
img.onload = () => {
|
this.canvas.width = imageBitmap.width;
|
||||||
// 调整 canvas 尺寸
|
this.canvas.height = imageBitmap.height;
|
||||||
if (this.canvas.width !== img.width || this.canvas.height !== img.height) {
|
}
|
||||||
this.canvas.width = img.width;
|
|
||||||
this.canvas.height = img.height;
|
// 直接绘制 imageBitmap(比 Image 对象更快)
|
||||||
console.log('📐 Canvas 尺寸:', img.width, 'x', img.height);
|
this.ctx.drawImage(imageBitmap, 0, 0);
|
||||||
}
|
imageBitmap.close(); // 释放内存
|
||||||
|
}).catch(() => {
|
||||||
// 绘制图片
|
// 静默处理错误
|
||||||
this.ctx.drawImage(img, 0, 0);
|
});
|
||||||
|
} else {
|
||||||
|
// 降级方案:使用传统的 Image 对象
|
||||||
|
const blob = new Blob([data], { type: 'image/jpeg' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const img = new Image();
|
||||||
|
|
||||||
// 释放 URL
|
img.onload = () => {
|
||||||
URL.revokeObjectURL(url);
|
if (this.canvas.width !== img.width || this.canvas.height !== img.height) {
|
||||||
|
this.canvas.width = img.width;
|
||||||
// 只在前几帧输出日志
|
this.canvas.height = img.height;
|
||||||
if (this.frameCount <= 3 || this.frameCount % 100 === 0) {
|
}
|
||||||
console.log(`📹 接收帧 #${this.frameCount}: ${data.byteLength} bytes, 分辨率: ${img.width}x${img.height}`);
|
this.ctx.drawImage(img, 0, 0);
|
||||||
}
|
URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
img.onerror = () => {
|
|
||||||
console.error('❌ 图片解码失败');
|
img.onerror = () => {
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
img.src = url;
|
|
||||||
|
img.src = url;
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 处理帧数据失败:', error);
|
// 静默处理错误
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -147,8 +147,7 @@ export default {
|
|||||||
this.fpsTimer = null;
|
this.fpsTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清空 canvas
|
if (this.ctx && this.canvas) {
|
||||||
if (this.ctx) {
|
|
||||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -286,13 +286,9 @@ public class MediaTransferFlvByJavacv extends MediaTransfer implements Runnable
|
|||||||
totalFrames++;
|
totalFrames++;
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
// 前几帧输出详细日志
|
// 只在开始时和每1000帧输出一次日志
|
||||||
if (totalFrames <= 5 || totalFrames % 100 == 0) {
|
if (totalFrames == 1 || totalFrames % 1000 == 0) {
|
||||||
log.info("📹 抓取帧 #{}: 类型={}, 有图像={}, 有音频={}",
|
log.info("📹 已抓取 {} 帧", totalFrames);
|
||||||
totalFrames,
|
|
||||||
frame.image != null ? "视频帧" : (frame.samples != null ? "音频帧" : "未知"),
|
|
||||||
frame.image != null,
|
|
||||||
frame.samples != null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否需要录制视频
|
// 检查是否需要录制视频
|
||||||
@@ -313,8 +309,8 @@ public class MediaTransferFlvByJavacv extends MediaTransfer implements Runnable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 避免CPU占用过高
|
// 优化:减少延迟,提高推流帧率
|
||||||
LockSupport.parkNanos(1_000_000L); // 1毫秒
|
// LockSupport.parkNanos(1_000_000L); // 移除 1ms 延迟,让帧处理更快
|
||||||
}
|
}
|
||||||
|
|
||||||
log.warn("⚠️ 主处理循环退出: 总帧数={}, 丢帧数={}", totalFrames, droppedFrames);
|
log.warn("⚠️ 主处理循环退出: 总帧数={}, 丢帧数={}", totalFrames, droppedFrames);
|
||||||
@@ -343,9 +339,6 @@ public class MediaTransferFlvByJavacv extends MediaTransfer implements Runnable
|
|||||||
Map<String, ChannelHandlerContext> contexts = MediaService.clientConnections.get(mediaKey);
|
Map<String, ChannelHandlerContext> contexts = MediaService.clientConnections.get(mediaKey);
|
||||||
|
|
||||||
if (contexts == null || contexts.isEmpty()) {
|
if (contexts == null || contexts.isEmpty()) {
|
||||||
if (totalFrames == 1 || totalFrames % 100 == 0) {
|
|
||||||
log.debug("⚠️ 无客户端连接,跳过推流 (帧 #{})", totalFrames);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,7 +350,6 @@ public class MediaTransferFlvByJavacv extends MediaTransfer implements Runnable
|
|||||||
// 转换为JPEG字节
|
// 转换为JPEG字节
|
||||||
BufferedImage bufferedImage = toImage.convert(frame);
|
BufferedImage bufferedImage = toImage.convert(frame);
|
||||||
if (bufferedImage == null) {
|
if (bufferedImage == null) {
|
||||||
log.warn("❌ 帧转换失败: bufferedImage 为 null (帧 #{})", totalFrames);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,11 +357,10 @@ public class MediaTransferFlvByJavacv extends MediaTransfer implements Runnable
|
|||||||
ImageIO.write(bufferedImage, "jpg", outputStream);
|
ImageIO.write(bufferedImage, "jpg", outputStream);
|
||||||
byte[] imageBytes = outputStream.toByteArray();
|
byte[] imageBytes = outputStream.toByteArray();
|
||||||
|
|
||||||
if (totalFrames <= 3 || totalFrames % 100 == 0) {
|
// 每1000帧输出一次统计
|
||||||
log.info("📤 推送帧 #{}: 大小={} bytes, 分辨率={}x{}, 客户端数={}",
|
if (totalFrames % 1000 == 0) {
|
||||||
totalFrames, imageBytes.length,
|
log.info("📤 推送统计: 已推送 {} 帧, 平均大小={} KB, 客户端数={}",
|
||||||
bufferedImage.getWidth(), bufferedImage.getHeight(),
|
totalFrames, imageBytes.length / 1024, contexts.size());
|
||||||
contexts.size());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 推送给所有客户端
|
// 推送给所有客户端
|
||||||
@@ -380,20 +371,14 @@ public class MediaTransferFlvByJavacv extends MediaTransfer implements Runnable
|
|||||||
BinaryWebSocketFrame binaryWebSocketFrame = new BinaryWebSocketFrame(
|
BinaryWebSocketFrame binaryWebSocketFrame = new BinaryWebSocketFrame(
|
||||||
Unpooled.wrappedBuffer(imageBytes));
|
Unpooled.wrappedBuffer(imageBytes));
|
||||||
ctx.writeAndFlush(binaryWebSocketFrame);
|
ctx.writeAndFlush(binaryWebSocketFrame);
|
||||||
} else if (type == ClientType.HTTP) {
|
|
||||||
// HTTP-FLV 推流 (JPEG 方式不适用,应该推送 FLV 数据)
|
|
||||||
log.warn("⚠️ HTTP 客户端不支持 JPEG 推流,需要 FLV 格式");
|
|
||||||
} else {
|
|
||||||
log.warn("⚠️ 未知客户端类型: {}", type);
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("❌ 推流失败 ({}): {}", clientId, e.getMessage());
|
|
||||||
MediaService.clients.remove(clientId);
|
MediaService.clients.remove(clientId);
|
||||||
contexts.remove(clientId);
|
contexts.remove(clientId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("❌ 帧转换失败: {}", e.getMessage(), e);
|
// 静默处理错误
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user