基于Java实现CCTV视频流TCP采集并转推至Spring Boot端点的技术求助
问题描述
需要用Java实现一套方案:通过TCP Socket连接CCTV服务器,获取直播视频流后输出至Spring Boot端点(支持RTSP等协议),以便在Web播放器中展示,当前遇到技术瓶颈。
现有代码
try (Socket socket = new Socket(hostname, port)) { DataOutputStream outToServer = new DataOutputStream(socket.getOutputStream()); BufferedReader inFromServer = new BufferedReader(new InputStreamReader(socket.getInputStream())); // Login command outToServer.writeBytes(login); // Start LIVE video outToServer.writeBytes(live); ByteArrayOutputStream outStreamObj; ByteArrayOutputStream buffer = new ByteArrayOutputStream(); byte[] byteChunk = new byte[1024]; InputStream input = socket.getInputStream(); int c = input.read(byteChunk); while (c != -1) { buffer.write(byteChunk, 0, c); c = input.read(byteChunk); // If placed in its own loop, it will read forever (or until the stream stops) so it will never exit BufferedImage image = ImageIO.read(new ByteArrayInputStream(buffer.toByteArray())); // create the object of ByteArrayOutputStream class outStreamObj = new ByteArrayOutputStream(); // write the image into the object of ByteArrayOutputStream class ImageIO.write(image, "jpg", outStreamObj); // create the byte array from image byte[] byteArray = outStreamObj.toByteArray(); // create the object of ByteArrayInputStream class // and initialized it with the byte array. ByteArrayInputStream inStreambj = new ByteArrayInputStream(byteArray); // read image from byte array BufferedImage newImage = ImageIO.read(inStreambj); // write output image ImageIO.write(newImage, "jpg", new File("outputImage.jpg")); System.out.println("Image generated from the byte array."); } } catch (UnknownHostException ex) { ... } catch (IOException ex) { ... }
当前问题
- 执行到
BufferedImage image = ImageIO.read(new ByteArrayInputStream(buffer.toByteArray()));时,image为null - 不确定当前实现思路是否正确
- 仅临时保存图像到磁盘测试,实际不需要持久化
预期完整流程
- 用户访问Angular网页
- Web播放器加载指向Spring Boot后端的直播流URL
- Spring Boot后端通过TCP连接CCTV服务器,并发送
live命令 - CCTV服务器在同一Socket会话中推送直播流字节
- Spring Boot应用将这些字节转发至浏览器
解决方案建议
1. 纠正核心实现思路
你的代码错误地将连续的视频流字节直接当作单张JPG图片解析,但CCTV通过TCP推送的直播流通常是封装后的视频帧(比如H.264/H.265裸流,或带有头部标识的帧序列),并非完整的JPG文件。ImageIO.read返回null的原因就是输入的字节数组不是完整可识别的图片格式。
正确思路:不需要将字节转成BufferedImage,而是直接处理原始视频流字节,通过Spring Boot转发给前端,同时需要做协议转换(因为Web播放器通常不支持直接播放原始TCP流,需要转成HTTP-FLV、HLS或WebSocket流)。
2. 优化TCP流读取逻辑
现有代码的buffer会一直累积字节,导致内存溢出,且每次循环都尝试解析整个buffer是错误的。应该按视频帧的边界分割字节:
- 先查阅CCTV服务器的协议文档,获取帧的头部标识(比如特定的字节序列)
- 读取字节时,先识别帧头,再读取完整的一帧数据,避免累积无效字节
- 示例调整后的读取逻辑:
try (Socket socket = new Socket(hostname, port)) { DataOutputStream outToServer = new DataOutputStream(socket.getOutputStream()); InputStream input = socket.getInputStream(); // 发送登录和直播命令 outToServer.writeBytes(login); outToServer.writeBytes(live); outToServer.flush(); byte[] frameHeader = new byte[4]; // 假设帧头是4字节,需根据实际协议调整 byte[] frameBuffer = new byte[1024 * 1024]; // 1MB帧缓冲区,按需调整大小 while (true) { // 读取帧头 int headerRead = input.read(frameHeader); if (headerRead != frameHeader.length) break; // 根据帧头解析帧长度(需根据CCTV协议实现) int frameLength = parseFrameLength(frameHeader); // 读取完整帧数据 int totalRead = 0; while (totalRead < frameLength) { int read = input.read(frameBuffer, totalRead, frameLength - totalRead); if (read == -1) break; totalRead += read; } // 处理完整帧:转发给前端(后续步骤实现) forwardFrameToClient(frameBuffer, 0, frameLength); } } catch (IOException e) { e.printStackTrace(); } // 需根据CCTV协议实现帧长度解析方法 private int parseFrameLength(byte[] header) { // 示例:假设前4字节是大端序的帧长度 return ByteBuffer.wrap(header).getInt(); }
3. Spring Boot端点适配
Web播放器无法直接播放原始TCP流,需要将视频流封装成Web兼容的格式,推荐两种方案:
方案A:HTTP-FLV流端点
Spring Boot可以提供一个StreamingResponseBody的接口,将视频帧封装成FLV格式输出:
@GetMapping("/live/flv") public ResponseEntity<StreamingResponseBody> liveFlv() { StreamingResponseBody responseBody = outputStream -> { // 这里复用上面的TCP连接和帧读取逻辑 try (Socket socket = new Socket(hostname, port)) { DataOutputStream outToServer = new DataOutputStream(socket.getOutputStream()); InputStream input = socket.getInputStream(); outToServer.writeBytes(login); outToServer.writeBytes(live); outToServer.flush(); // 先写入FLV文件头(固定格式) writeFlvHeader(outputStream); byte[] frameHeader = new byte[4]; byte[] frameBuffer = new byte[1024 * 1024]; while (!Thread.currentThread().isInterrupted()) { int headerRead = input.read(frameHeader); if (headerRead != frameHeader.length) break; int frameLength = parseFrameLength(frameHeader); int totalRead = 0; while (totalRead < frameLength) { int read = input.read(frameBuffer, totalRead, frameLength - totalRead); if (read == -1) break; totalRead += read; } // 将视频帧封装成FLV Tag并写入输出流 writeFlvTag(outputStream, frameBuffer, 0, frameLength); outputStream.flush(); } } catch (IOException e) { e.printStackTrace(); } }; return ResponseEntity.ok() .contentType(MediaType.parseMediaType("video/x-flv")) .body(responseBody); } // 实现FLV头和Tag的写入方法(需参考FLV格式规范) private void writeFlvHeader(OutputStream out) throws IOException { // 写入FLV签名、版本、标志位、数据偏移等固定内容 out.write(new byte[]{0x46, 0x4C, 0x56, 0x01, 0x05, 0x00, 0x00, 0x00, 0x09}); } private void writeFlvTag(OutputStream out, byte[] frameData, int offset, int length) throws IOException { // 根据帧类型(视频/音频)封装成FLV Tag,需结合CCTV流的编码格式处理 // 示例:假设是H.264视频帧 byte[] tagHeader = new byte[11]; tagHeader[0] = 0x09; // 视频Tag类型 // 填充数据长度、时间戳等字段,具体参考FLV规范 out.write(tagHeader); out.write(frameData, offset, length); // 写入Tag大小的4字节校验 out.write(ByteBuffer.allocate(4).putInt(11 + length).array()); }
方案B:WebSocket实时推送
如果需要低延迟,可以用WebSocket推送帧数据,前端通过Canvas渲染:
@ServerEndpoint("/live/ws") public class LiveWebSocket { @OnOpen public void onOpen(Session session) { // 启动TCP连接线程,推送帧数据到WebSocket new Thread(() -> { try (Socket socket = new Socket(hostname, port)) { DataOutputStream outToServer = new DataOutputStream(socket.getOutputStream()); InputStream input = socket.getInputStream(); outToServer.writeBytes(login); outToServer.writeBytes(live); outToServer.flush(); byte[] frameHeader = new byte[4]; byte[] frameBuffer = new byte[1024 * 1024]; while (session.isOpen()) { int headerRead = input.read(frameHeader); if (headerRead != frameHeader.length) break; int frameLength = parseFrameLength(frameHeader); int totalRead = 0; while (totalRead < frameLength) { int read = input.read(frameBuffer, totalRead, frameLength - totalRead); if (read == -1) break; totalRead += read; } // 将帧数据转成Base64发送给前端(或直接发送二进制) String base64Frame = Base64.getEncoder().encodeToString(Arrays.copyOfRange(frameBuffer, 0, frameLength)); session.getBasicRemote().sendText(base64Frame); } } catch (IOException e) { e.printStackTrace(); } }).start(); } }
4. Web播放器兼容
- 若用HTTP-FLV流:可以用
flv.js播放器,在Angular中引入后直接播放:
import flvjs from 'flv.js'; // 初始化播放器 const videoElement = document.getElementById('live-video'); if (flvjs.isSupported()) { const flvPlayer = flvjs.createPlayer({ type: 'flv', url: 'http://your-spring-boot-host/live/flv' }); flvPlayer.attachMediaElement(videoElement); flvPlayer.load(); flvPlayer.play(); }
- 若用WebSocket:前端接收Base64后转成Blob,用Canvas渲染每一帧,或转成MediaSource流播放。
关键注意事项
- 必须获取CCTV服务器的TCP协议文档:了解帧的结构、头部标识、编码格式(H.264/H.265等),否则无法正确解析流
- 避免内存泄漏:TCP连接和流资源要确保在请求结束或异常时关闭
- 性能优化:帧读取和转发要避免阻塞主线程,建议用异步线程处理
- 异常处理:添加重连逻辑,避免CCTV连接断开后服务不可用
内容的提问来源于stack exchange,提问作者Comforse
相关产品推荐
相关产品推荐

