javacv编码mp4视频

目前在做的java项目里有一个需求,已经将用户在进行一个业务操作的操作行为记录下来了,形成了这些操作行为的指令文件,然后需要将这些指令文件编码为mp4视频。项目之前用的是xuggle来完成的,不过xuggle项目好像有四五年没有更新了,甚至我将OSX升级至10.11之后,xuggle就没法在我本机编译通过了,报了一大堆的错。上xuggle的github仓库一看,人家也说不维护了,推荐使用https://github.com/artclarke/humble-video了,不过我尝试了下,依然没能把humble-video在我本机编译通过。看来得找其它解决方案了。上网搜索过后,找到两个替代方案jcodec和javacv,对比编码性能后,最终选择了javacv,纯java方案相对于jni方案性能差得不是一星半点啊。不过在使用javacv过程中还是遇到了不少坑,在这里分享一下,也可以帮助一下正在这些坑里的兄弟们。

首先参照javacv的文档,在pom.xml里添加

<dependency>
  <groupId>org.bytedeco</groupId>
  <artifactId>javacv</artifactId>
  <version>1.1</version>
</dependency>

然后快速地写了个JavaCVMp4Encoder

package test;

public class JavaCVMp4Encoder implements Mp4Encoder {
    private String fileName;
    private FFmpegFrameRecorder recorder;
    private static final double FRAME_RATE = 25.0;
    private static final double MOTION_FACTOR = 1;
    private static final Java2DFrameConverter java2dConverter;
    private static final Logger log = LoggerFactory.getLogger(JavaCVMp4Encoder.class);

    public JavaCVMp4Encoder(){
    	this.java2dConverter = new Java2DFrameConverter();
    }

    @Override
    public void make(String fileName) {
        this.fileName = fileName;
    }

    @Override
    public void configVideo(int width, int height) {
        recorder = new FFmpegFrameRecorder(this.fileName, width, height);
        recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);

        recorder.setFrameRate(FRAME_RATE);
        /*
        * videoBitRate这个参数很重要,当然越大,越清晰,但最终的生成的视频也越大。查看一个资料,说均衡考虑建议设为videoWidth*videoHeight*frameRate*0.07*运动因子,运动因子则与视频中画面活动频繁程度有关,如果很频繁就设为4,不频繁则设为1
        */
        recorder.setVideoBitrate((int)((width*height*FRAME_RATE)*MOTION_FACTOR*0.07));

        recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
        recorder.setFormat("mp4");
        try {
            recorder.start();
        } catch (FrameRecorder.Exception e) {
            log.error("JavaCVMp4Encoder configure video error.", e);
        }

    }

    @Override
    public void encodeFrame(BufferedImage image, long timestamp) {
        try {
            recorder.record(java2dConverter.convert(image));
        } catch (FrameRecorder.Exception e) {
            log.error("JavaCVMp4Encoder encode frame error.", e);
        }
    }

    @Override
    public void close() {
        if(recorder != null){
            try {
                recorder.stop();
            } catch (FrameRecorder.Exception e) {
                log.error("JavaCVMp4Encoder stop error.", e);
            }
            try {
                recorder.release();
            } catch (FrameRecorder.Exception e) {
                log.error("JavaCVMp4Encoder release error.", e);
            }
        }
    }
}

顺手写了个测试案例,还好是可以工作的

package test;

public JavaCVMp4EncoderTest {
	public static void main(String[] args){
    	Mp4Encoder encoder = new JavaCVMp4Encoder();
        encoder.make("/tmp/test.mp4");
        encoder.configVideo(1024, 768);
        BufferedImage img = new BufferedImage(1024, 768, BufferedImage.TYPE_3BYTE_BGR);

        Java2DFrameConverter java2dConverter = new Java2DFrameConverter();
        Graphics2D g2 = (Graphics2D)img.getGraphics();
        for (int i = 0; i <= 25 * 20; i++) {
            g2.setColor(Color.white);
            g2.fillRect(0, 0, width, height);
            g2.setPaint(Color.black);
            g2.drawString("frame " + i, 10, 25);
            encoder.encodeFrame(java2dConverter.convert(img), System.currentTimeMillis());
        }

        encoder.close();
    }
}

不过不久就发现在项目中转出的录像播放得太快了,检查代码发现JavaCVMp4EncoderencodeFrame方法的第二个参数timestamp并没有用到,但在项目中进行mp4编码时,实际上是对每一帧指定的时间戳的,于是修改encodeFrame方法

@Override
public void encodeFrame(BufferedImage image, long timestamp) {
  try {
    long t = timestamp * 1000L;
    if (t > recorder.getTimestamp()) {
    	recorder.setTimestamp(t);
    }
    recorder.record(java2dConverter.convert(image));
  } catch (FrameRecorder.Exception e) {
    log.error("JavaCVMp4Encoder encode frame error.", e);
  }
}

终于转出的视频不再飞快播放了。

又过了好几天,在正式环境上运行着,又出问题,进行mp4编码的Java进程crash了。crash日志时仅报了一下跟jni调用相关的错。

Stack: [0x00007f1932fb4000,0x00007f19330b5000],  sp=0x00007f19330b2d88,  free space=1019k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
C  [libswscale.so.3+0x52f41]  sws_getCachedContext+0x1471
[error occurred during error reporting (printing native stack), id 0xb]
Java frames: (J=compiled Java code, j=interpreted, Vv=VM code)
j  org.bytedeco.javacpp.swscale.sws_scale(Lorg/bytedeco/javacpp/swscale$SwsContext;Lorg/bytedeco/javacpp/PointerPointer;Lorg/bytedeco/javacpp/IntPointer;IILorg/bytedeco/javacpp/PointerPointer;Lorg/bytedeco/javacpp/IntPointer;)I+0
j  org.bytedeco.javacv.FFmpegFrameRecorder.recordImage(IIIIII[Ljava/nio/Buffer;)Z+570
j  org.bytedeco.javacv.FFmpegFrameRecorder.record(Lorg/bytedeco/javacv/Frame;I)V+70
j  org.bytedeco.javacv.FFmpegFrameRecorder.record(Lorg/bytedeco/javacv/Frame;)V+3

在网上查阅了很久,终于找到一个线索,说是跟下面的代码相关

if ( (uintptr_t)dst[0]%16 || (uintptr_t)dst[1]%16 || (uintptr_t)dst[2]%16
	|| (uintptr_t)src[0]%16 || (uintptr_t)src[1]%16 || (uintptr_t)src[2]%16
	|| dstStride[0]%16 || dstStride[1]%16 || dstStride[2]%16 || dstStride[3]%16
	|| srcStride[0]%16 || srcStride[1]%16 || srcStride[2]%16 || srcStride[3]%16
	) {
	static int warnedAlready=0;
	int cpu_flags = av_get_cpu_flags();
	if (HAVE_MMXEXT && (cpu_flags & AV_CPU_FLAG_SSE2) && !warnedAlready){
		av_log(c, AV_LOG_WARNING, "Warning: data is not aligned! This can lead to a speedloss\n");
  		warnedAlready=1;
  }
}

意思是视频的宽度必须是16的倍数,否则ffmpeg可能因为无法对齐而crash。这么重要的事情,在ffmpeg文档上竟然从来没提出。但经我实际测试,发现视频的宽度必须是32的倍数,高度必须是2的倍数,于是写了点代码修正了widthheight,然后问题就解决了。

int width = ...;
int height = ...;
if (width % 32 != 0) {
int j = width % 32;
if (j <= 16) {
    width = width - (width % 32);
  } else {
    width = width + (32 - width / 32);
  }
}
if (height % 2 != 0) {
int j = height % 4;
switch (j) {
case1:
      height = height - 1;
break;
case3:
      height = height + 1;
break;
  }
}
Mp4Encoder encoder = new JavaCVMp4Encoder();
encoder.make("/tmp/test.mp4");
encoder.configVideo(width, height);
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏余林丰

访问者模式

这是23种设计模式的最后一个——访问者模式,这个模式确实不怎么好理解,不怎么好用,而且实际中也很少用到这个设计模式。《大话设计模式》中就提到GoF四个人中有一个...

22450
来自专栏烙馅饼喽的技术分享

记一个脚本解释器的开发

最近可以有1个月左右的空闲,可以稍微整理一下这个脚本解释器的开发过程。 一、缘由   2014年左右,我们使用AIR技术,开发了一个3D战争类型的手游。那时候手...

46770
来自专栏Golang语言社区

Go 语言的演化历程

9、hello.c,标准C89 #include <stdio.h> main(void) //译注:与上面hello.c相比,多了个void { pr...

35080
来自专栏熊二哥

.NET工作准备--01前言

01应聘须知(已过时) -1.了解软件开发大环境。 -2.准备简历:不宜超过一页,永远准备中文,模板。 -3.渠道:3大网站,中华英才,前程无忧(51job最...

22680
来自专栏游戏开发那些事

【Unity游戏开发】Lua中的os.date和os.time函数

  最近马三在工作中经常使用到了lua 中的 os.date( ) 和 os.time( )函数,不过使用的时候都是不得其解,一般都是看项目里面怎么用,然后我就...

21640
来自专栏恰童鞋骚年

设计模式的征途—9.组合(Composite)模式

树形结构在软件中随处可见,比如操作系统中的目录结构,公司组织结构等等,如何运用面向对象的方式来处理这种树形结构是组合模式需要解决的问题。组合模式通过一种巧妙的设...

15640
来自专栏AndroidTraveler

责任链模式妙用

除了应用场景比较多的单例模式你能够信手拈来,其他的可能会觉得有点难以掌握。也许压根都没用过。

14130
来自专栏小二的折腾日记

LeetCode-32-Longest-Valid-Parentheses

表示这是一道没有看懂题目的题,看到题目的难度是hard,但是自己的想法很简答,以为直接一个栈就可以了。。 too young啊

12430
来自专栏编程一生

看Lucene源码必须知道的基本概念

13760
来自专栏web前端教室

javascript ES6 初次相见

JS的ES6网上也热炒了好久了, 我一直也没怎么太细看, 今天想起来就写个东西, 也为分享,也为学习。 我喜欢接地气一点,所以网上的那些新名词我就不写了, 就写...

19970

扫码关注云+社区

领取腾讯云代金券