用netty和opencv从摄像头抓取视频在前端播放
0. 描述
这个demo实现了利用opencv从摄像头抓取视频,并使用netty框架通过websocket将视频传到前端,前端不断将图像写入canvas实现播放,但只有图像没有声音。
1. 基本思路
摄像头:笔记本的摄像头
抽帧:opencv
队列:线程安全的java.util.concurrent.ConcurrentLinkedQueue
消息分发:自己实现。用来支持多个连接,主要逻辑就是将生产者生产的数据拷贝多份分发给各个websocket连接。
websocket服务:netty
帧数据的传输路径如下图所示:

2. 使用opencv抽帧
利用opencv的videoCapture
从摄像头抽帧,并将帧数据存入队列。
import org.opencv.core.Mat;
import org.opencv.videoio.VideoCapture;
import java.util.concurrent.ConcurrentLinkedQueue;
public class FrameProducer extends Thread {
private static final int MAX_QUEUE_SIZE = 30;
private final VideoCapture videoCapture = new VideoCapture(0);
private final ConcurrentLinkedQueue<Mat> queue = new ConcurrentLinkedQueue<>();
public void close() {
this.videoCapture.release();
}
public ConcurrentLinkedQueue<Mat> queue() {
return queue;
}
@Override
public void run() {
videoCapture.open(0);
while (videoCapture.isOpened()) {
if (queue.size() < MAX_QUEUE_SIZE) {
Mat mat = new Mat();
videoCapture.read(mat);
queue.offer(mat);
}
}
}
}
2. 消息分发
import org.opencv.core.Mat;
public class FrameDistributor extends Thread {
private static final int MAX_QUEUE_SIZE = 30;
FrameProducer producer;
ConcurrentLinkedQueue<Mat> queue;
Map<String, Queue<Mat>> matQueueMap = new ConcurrentHashMap<>();
public boolean running = true;
public FrameDistributor(FrameProducer producer) {
this.producer = producer;
this.queue = producer.queue();
}
synchronized public void register(String key, Queue<Mat> matQueue) {
matQueueMap.put(key, matQueue);
System.out.println("alive matQueue num: " + matQueueMap.entrySet().size());
}
synchronized public void unregister(String key) {
matQueueMap.remove(key);
System.out.println("alive matQueue num: " + matQueueMap.entrySet().size());
}
@Override
public void run() {
while (running) {
Mat mat = queue.poll();
if (mat != null) {
matQueueMap.forEach((key, value) -> {
if (value.size() < MAX_QUEUE_SIZE) {
// 取到的mat复制一份
value.offer(mat.clone());
} else {
mat.release();
}
});
}
}
}
}
3. 使用netty实现websocket服务器
Websocket服务器:
import org.opencv.core.Mat;
public class WebsocketServer {
private int port = 9999;
private String address = "127.0.0.1";
ConcurrentLinkedQueue<Mat> queue;
public void run(ConcurrentLinkedQueue<Mat> queue) {
this.queue = queue;
ServerBootstrap bootstrap = new ServerBootstrap();
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
try {
bootstrap.group(boss, worker);
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.handler(new LoggingHandler(LogLevel.INFO));
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new HttpServerCodec())
.addLast(new ChunkedWriteHandler())
.addLast(new HttpObjectAggregator(65536))
.addLast(new WebSocketServerProtocolHandler("/", null, true))//websocket协议处理器
.addLast(new WebSocketServerHandler(queue));//自定义websocket处理器,在此实现消息推送
}
});
bootstrap.bind(address, port).sync().channel().closeFuture().sync();
} catch (Exception ex) {
ex.printStackTrace();
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
WebSocket消息处理器:
import org.opencv.core.Mat;
public class WebSocketServerHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
private final ConcurrentLinkedQueue<Mat> queue = new ConcurrentLinkedQueue<>();
private final FrameDistributor distributor;
private ScheduledFuture<?> future;
public WebSocketServerHandler(FrameDistributor distributor) {
super();
this.distributor = distributor;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
String id = ctx.channel().id().asLongText();
System.out.println("channelActive: " + id);
distributor.register(id, queue);
// 周期任务
ctx.executor().scheduleAtFixedRate(() -> {
Mat mat = queue.poll();
if (mat != null) {
// 这里用到了一个工具函数,将opencv的Mat对象转为BufferedImage
BufferedImage image = Utils.matToBufferedImage(mat);
ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(1024 * 1024);
ByteBufOutputStream stream = new ByteBufOutputStream(buffer);
ImageIO.write(image, "JPEG", stream);
ctx.channel().writeAndFlush(new BinaryWebSocketFrame(buffer));
// 由于消息分发的时候我没有将数据拷贝,而是直接将mat对象的引用分发出去,因此这里的mat用完还不能释放,我索性将
// 其交给JVM的GC来释放了。如果这里直接释放,那么第一个连接将数据用完之后直接释放,其他连接拿到的同样的数据就
// 不可用了。
// mat.release();
}
// 第三个参数period不能填0,否则直接出错,而且还没报错信息。
// period尽量调整到合适的大小。(也许可以动态调整适应来环境?
// 我的电脑摄像头帧率30fps,最慢每33ms就得处理完一帧,不然就会丢帧。这里平均每12ms就能处理完一帧送出去。
// 因此每20ms调用一次差不多刚好。
}, 100, 20, TimeUnit.MILLISECONDS);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
String id = ctx.channel().id().asLongText();
if (future != null) {
future.cancel(true);
}
distributor.unregister(id);
System.out.println("channelInactive: " + id);
}
}
工具类
public class Utils {
// Mat转BufferedImage
public static BufferedImage matToBufferedImage(Mat mat) {
int dataSize = mat.cols() * mat.rows() * (int) mat.elemSize();
byte[] data = new byte[dataSize];
mat.get(0, 0, data);
int type = mat.channels() == 1 ? BufferedImage.TYPE_BYTE_GRAY : BufferedImage.TYPE_3BYTE_BGR;
if (type == BufferedImage.TYPE_3BYTE_BGR) {
for (int i = 0; i < dataSize; i += 3) {
byte blue = data[i];
data[i] = data[i + 2];
data[i + 2] = blue;
}
}
BufferedImage image = new BufferedImage(mat.cols(), mat.rows(), type);
image.getRaster().setDataElements(0, 0, mat.cols(), mat.rows(), data);
return image;
}
}
4. 启动类
import org.opencv.core.Core;
import org.opencv.core.Mat;
public class Application {
static {
// 这里加载opencv的动态库
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}
public static void main(String[] args) {
FrameProducer producer = new FrameProducer();
producer.setDaemon(true);
producer.start();
FrameDistributor distributor = new FrameDistributor(producer);
distributor.setDaemon(true);
distributor.start();
new WebsocketServer().run(distributor);
}
}
5. 前端页面
页面需要代理一下。可以用python的http server做代理,切到需要代理的目录,执行下面的脚本即可。
python -m http.server 8080 --bind 127.0.0.1
ws-index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ws</title>
</head>
<body>
<div style="text-align: center; margin-top: 100px;">
<button onclick='stop()'>stop</button>
<h4>服务端消息</h4>
<div id="msg">...</div>
<div>
<canvas id="canvas" width="640" height="480"></canvas>
</div>
</div>
<script>
const ws = new WebSocket("ws://127.0.0.1:9999/");
const canvas = document.getElementById("canvas");
const context = canvas.getContext("2d");
const msg = document.getElementById("msg");
let count = 0;
ws.onopen = function (evt) {
console.log("Connection open ...");
};
const image = new Image();
ws.onmessage = function (evt) {
if (evt?.data != undefined && evt.data instanceof Blob) {
msg.innerHTML = 'Got a Frame '+ (count++);
let frame = evt.data;
frame.type = 'image/jpeg';
blobToDataURI(frame, (url) => {
image.src = url;
image.onload = function (e) {
context.drawImage(image, 0, 0, 640, 480);
}
});
}
};
ws.onclose = function (evt) {
console.log("Connection closed.");
};
function stop() {
ws.close();
}
function blobToDataURI(blob, callback) {
let reader = new FileReader();
reader.onload = function (e) {
callback(e.target.result);
}
reader.readAsDataURL(blob);
}
</script>
</body>
</html>
6. 总结
我发现Java确实不太适合做图像这方面的处理。因为图像数据本身就大,处理过程中难免产生中间数据,而且中间数据都跟原数据一个量级,累积起来太吃内存了。如果使用C++的话,可以在对象使用过后就及时手动释放,Java里面得等GC去释放。
我遇到的几个疑点和坑:
-
为什么要加个消息分发?
我一开始是没有加消息分发的,直到我打开了三个页面,发现事情不太对劲。打开的连接越多,视频就越卡。
原来,抽帧线程拿到的帧数据都存放在那一个队列里面,websocket的连接是被并发处理的,多个连接同时从一个队列里面抢数据,结果就是你一个我一个,连续的帧序列被瓜分,每个连接拿到的都是原序列的子序列,播放出来当然就卡成PPT了。
-
mat什么时候释放?
其实mat可以不释放,虽然它是个native对象,但由于其实现了JNI,它也在GC的管理范围内。
-
为什么消息分发的时候非得复制一份?复用同一份对象可以吗?
消息分发的时候,可以不用复制(事实上连接多的时候,这么做会更好),直接将对象的引用分发给各个channel,由于消息分发之后没有再对这个mat对象进行修改,因此也不存在线程安全问题,这个对象用完之后会被GC回收掉。
我之前在WebSocketServerHandler中的周期任务里面,将用完的mat释放掉了,后面matToBufferedImage这个方法疯狂报错。冷静下来思考之后发现,这是个线程安全问题,同一份mat被多个channel引用,先来的线程用完mat跑完任务之后就把mat释放了,后到的线程用到mat的时候发现数据没了 (😂),然后给我抛异常:
java.lang.IllegalArgumentException: Width (0) and height (0) must be > 0 at java.awt.image.SampleModel.<init>(SampleModel.java:126) at java.awt.image.ComponentSampleModel.<init>(ComponentSampleModel.java:146) at java.awt.image.PixelInterleavedSampleModel.<init>(PixelInterleavedSampleModel.java:87) at java.awt.image.Raster.createInterleavedRaster(Raster.java:641) at java.awt.image.Raster.createInterleavedRaster(Raster.java:278) at java.awt.image.BufferedImage.<init>(BufferedImage.java:376) at cn.kui.app.opencv.Utils.matToBufferedImage(Utils.java:27) at cn.kui.app.websocket.WebSocketServerHandler.lambda$channelActive$0(WebSocketServerHandler.java:66)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)