java获取近期视频流关键帧与截图

1、背景

最近在做视频转发的开发时,遇到一个问题,前端订阅播放h264视频流时,有时会出现一段时间黑屏,经过测试发现是没有收到关键帧,只有第一帧是关键帧才能保证后续播放正常。所以后端需要实现一个功能,就是前端在进入播放页面时,后端把最近的一个关键帧发过去。

2、思路(环形缓存区)

后端接收到的视频流是一个个的字节数组,所以在接收时没法直接判断一帧的开始和结束,需要将最近的一段视频流截取出来,然后利用ffmpeg工具进行整体的解析和关键帧提取。

查看ffmpeg工具的代码,可以发现ffmpeg工具入参是inputstream,该工具会不断调用inputstream的read方法进行字节的读取。所以就想通过一个环形缓存区不断的记录最新的一段视频流数据。该环形缓存区再实现inputstream的接口,重写read方法,read读取的开始位置即是环形缓存区头,到环形缓存区的尾时自动结束。

3、依赖包

XML 复制代码
<!--   javacv    -->
<dependency>
	<groupId>org.bytedeco</groupId>
	<artifactId>javacpp</artifactId>
	<version>1.4.3</version>
</dependency>
<dependency>
	<groupId>org.bytedeco</groupId>
	<artifactId>javacv</artifactId>
	<version>1.4.3</version>
</dependency>
<dependency>
	<groupId>org.bytedeco.javacpp-presets</groupId>
	<artifactId>ffmpeg-platform</artifactId>
	<version>4.0.2-1.4.3</version>
</dependency>

4、环形缓存区定义

java 复制代码
public class CycleBufferInputStream extends InputStream {

    /************************************************  环形缓冲区  ***********************************************/
    private ByteBuffer buffer = null;

    private int readPos = 0; //将要读取的位置
    private int writePos = 0; //将要写入的位置
    private boolean isCycle = false; //判断是否已经形成一个环

    public CycleBufferInputStream(int capacity) {
        this.buffer = ByteBuffer.allocateDirect(capacity);
    }

    /**
     * 将字节数组以覆盖的方式放入环形缓冲区
     */
    public void put(byte[] bytes) {
        int used = buffer.capacity() - buffer.position();
        if (used < bytes.length) {
            buffer.put(bytes, 0, used);
            buffer.clear();
            buffer.put(bytes, used, bytes.length - used);
            isCycle = true;
        } else if (used == bytes.length) {
            buffer.put(bytes, 0, used);
            buffer.clear();
            isCycle = true;
        } else {
            buffer.put(bytes, 0, bytes.length);
        }
        writePos = buffer.position();
    }

    /**
     * 定位读取的初始位置(执行inputstream 读取前,必须要先调用该方法)
     */
    public void readPrepare() {
        if (buffer.capacity() == writePos || !isCycle) {
            readPos = 0;
        } else {
            readPos = buffer.position() + 1;
        }
    }

    /*************************************************** 输入流传输  ***************************************************/

    /**
     * Reads the next byte of data from the input stream. The value byte is returned as an int in the range 0 to 255. If no byte is available because the end of the stream has been reached, the value -1 is returned. This method blocks until input data is available, the end of the stream is detected, or an exception is thrown.
     * A subclass must provide an implementation of this method.
     * Returns: the next byte of data, or -1 if the end of the stream is reached.
     * Throws: IOException -- if an I/O error occurs.
     */
    @Override
    public int read() throws IOException {
        if (readPos == buffer.capacity()) readPos = 0;
        if (readPos == writePos) return -1;
        int value = buffer.get(readPos++);
        if (value < 0) value = value + 256;
        return value;
    }

}

5、从环形缓存区提取关键帧

java 复制代码
    /**
     * 从环形缓存环获取最近一帧关键帧字节数组
     * 这里返回的堆外内存,所以注意要及时进行内存释放
     * @param inputStream
     */
    public static ByteBuffer dealVideo(InputStream inputStream)  {
        try {
            int j = 0;
            FFmpegFrameGrabber ff = new FFmpegFrameGrabber(inputStream);
            ff.start();
            Frame frame = null;
            Frame last = null;
            while ((frame = ff.grabKeyFrame()) != null && frame.image != null) {
                last = frame.clone();
                System.out.println("获取一帧" + j++);
            }
            ff.stop();

            if (last != null) {
                System.out.println("获取最近的一个关键帧");
                ByteBuffer byteBuffer = (ByteBuffer)last.image[0];
                return byteBuffer;
            }
        } catch (Exception e) {
            log.error("提取最近一个关键帧异常\n",e);
        }
        return null;
    }

6、使用

java 复制代码
    public static void main(String[] args) throws Exception {
        CycleBufferInputStream stream = new CycleBufferInputStream(1024 * 1024 * 10);

        FileInputStream fis = new FileInputStream("D:\\tmp-data\\1694511149969.h264");
        byte[] bytes = new byte[fis.available()];
        fis.read(bytes);
        stream.put(bytes);

        stream.readPrepare();
        dealVideo(stream);
    }

7、扩展:获取近期视频的截图

其实就是从近期的关键帧中提取出图片,关键代码如下:

java 复制代码
    public static void dealImage(InputStream inputStream) {
        try {
            Java2DFrameConverter converter = new Java2DFrameConverter();

            FFmpegFrameGrabber ff = new FFmpegFrameGrabber(inputStream);
            ff.start();
            int j = 0;
            Frame frame = null;
            Frame last = null;
            while ((frame = ff.grabImage()) != null) {
                last = frame.clone();
                System.out.println("获取一张图片" + j++);
            }
            ff.stop();

            if (last != null && last.image != null) {
                System.out.println("存储最后一张图片 ");
                BufferedImage fecthedImage = converter.getBufferedImage(last);
                File screenshotFile = new File("D:\\tmp-data\\", System.currentTimeMillis() + ".jpg");
                ImageIO.write(fecthedImage, "jpg", screenshotFile);
            }

        } catch (Exception e) {
            log.error("提取最近一个图片异常\n",e);
        }
    }

8、整体代码

java 复制代码
package com.qq.utils;

import lombok.extern.slf4j.Slf4j;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.ByteBuffer;

@Slf4j
public class CycleBufferInputStream extends InputStream {

    /************************************************  环形缓冲区  ***********************************************/
    private ByteBuffer buffer = null;

    private int readPos = 0; //将要读取的位置
    private int writePos = 0; //将要写入的位置
    private boolean isCycle = false; //判断是否已经形成一个环

    public CycleBufferInputStream(int capacity) {
        this.buffer = ByteBuffer.allocateDirect(capacity);
    }

    /**
     * 将字节数组以覆盖的方式放入环形缓冲区
     */
    public void put(byte[] bytes) {
        int used = buffer.capacity() - buffer.position();
        if (used < bytes.length) {
            buffer.put(bytes, 0, used);
            buffer.clear();
            buffer.put(bytes, used, bytes.length - used);
            isCycle = true;
        } else if (used == bytes.length) {
            buffer.put(bytes, 0, used);
            buffer.clear();
            isCycle = true;
        } else {
            buffer.put(bytes, 0, bytes.length);
        }
        writePos = buffer.position();
    }

    /**
     * 定位读取的初始位置(执行inputstream方法前,必须要先调用该方法)
     */
    public void readPrepare() {
        if (buffer.capacity() == writePos || !isCycle) {
            readPos = 0;
        } else {
            readPos = buffer.position() + 1;
        }
    }

    /*************************************************** 输入流传输  ***************************************************/
    @Override
    public int read() throws IOException {
        if (readPos == buffer.capacity()) readPos = 0;
        if (readPos == writePos) return -1;
        int value = buffer.get(readPos++);
        if (value < 0) value = value + 256;
        return value;
    }

    /**
     * 从环形缓存环获取最近一帧关键帧字节数组
     * 这里返回的堆外内存,所以注意要及时进行内存释放
     * @param inputStream
     */
    public static ByteBuffer dealVideo(InputStream inputStream)  {
        try {
            int j = 0;
            FFmpegFrameGrabber ff = new FFmpegFrameGrabber(inputStream);
            ff.start();
            Frame frame = null;
            Frame last = null;
            while ((frame = ff.grabKeyFrame()) != null && frame.image != null) {
                last = frame.clone();
                System.out.println("获取一帧" + j++);
            }
            ff.stop();

            if (last != null) {
                System.out.println("获取最近的一个关键帧");
                ByteBuffer byteBuffer = (ByteBuffer)last.image[0];
                return byteBuffer;
            }
        } catch (Exception e) {
            log.error("提取最近一个关键帧异常\n",e);
        }
        return null;
    }

    public static void dealImage(InputStream inputStream) {
        try {
            Java2DFrameConverter converter = new Java2DFrameConverter();

            FFmpegFrameGrabber ff = new FFmpegFrameGrabber(inputStream);
            ff.start();
            int j = 0;
            Frame frame = null;
            Frame last = null;
            while ((frame = ff.grabImage()) != null) {
                last = frame.clone();
                System.out.println("获取一张图片" + j++);
            }
            ff.stop();

            if (last != null && last.image != null) {
                System.out.println("存储最后一张图片 ");
                BufferedImage fecthedImage = converter.getBufferedImage(last);
                File screenshotFile = new File("D:\\tmp-data\\", System.currentTimeMillis() + ".jpg");
                ImageIO.write(fecthedImage, "jpg", screenshotFile);
            }

        } catch (Exception e) {
            log.error("提取最近一个图片异常\n",e);
        }
    }

}
相关推荐
zjw_rp9 分钟前
Spring-AOP
java·后端·spring·spring-aop
Oneforlove_twoforjob22 分钟前
【Java基础面试题033】Java泛型的作用是什么?
java·开发语言
TodoCoder30 分钟前
【编程思想】CopyOnWrite是如何解决高并发场景中的读写瓶颈?
java·后端·面试
向宇it39 分钟前
【从零开始入门unity游戏开发之——C#篇24】C#面向对象继承——万物之父(object)、装箱和拆箱、sealed 密封类
java·开发语言·unity·c#·游戏引擎
小蜗牛慢慢爬行41 分钟前
Hibernate、JPA、Spring DATA JPA、Hibernate 代理和架构
java·架构·hibernate
星河梦瑾2 小时前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
黄名富2 小时前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
love静思冥想2 小时前
JMeter 使用详解
java·jmeter
言、雲2 小时前
从tryLock()源码来出发,解析Redisson的重试机制和看门狗机制
java·开发语言·数据库
TT哇2 小时前
【数据结构练习题】链表与LinkedList
java·数据结构·链表