一、背景
项目里面使用com.github.dadiyang-jave-1.0.5获取视频的时长、高度和宽度,当获取高度宽度时,会出现NPM异常,相关代码如下
Encoder videoEncoder = new Encoder(); File f1 = new File(videoLocalFile); MultimediaInfo videoM = videoEncoder.getInfo(f1); vo.setDuration((int) videoM.getDuration() / 1000); try { // 此处获取高度宽度代码可能会抛出NPE异常(某种mp4文件) VideoSize videoSize = videoM.getVideo().getSize(); vo.setMtalHeight(videoSize.getHeight()); vo.setMtalWidth(videoSize.getWidth()); }catch (Exception e){ log.error("待修复,获取视频长度出现异常", e); }
二、问题排查
获取视频高度宽度原理:执行内置的ffmpeg程序,解析程序输出,得出高度宽度。空指针问题就出在解析程序输出上,对于出现问题的MP4,其在linux下的输出为(windows下不会有这个问题):
ffmpeg version 4.4 Copyright (c) 2000-2021 the FFmpeg developers built with gcc 4.8.5 (GCC) 20150623 (Red Hat 4.8.5-44) configuration: --enable-shared --prefix=/usr/local/ffmpeg libavutil 56. 70.100 / 56. 70.100 libavcodec 58.134.100 / 58.134.100 libavformat 58. 76.100 / 58. 76.100 libavdevice 58. 13.100 / 58. 13.100 libavfilter 7.110.100 / 7.110.100 libswscale 5. 9.100 / 5. 9.100 libswresample 3. 9.100 / 3. 9.100 Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '/root/ffmpegs/bad.mp4': Metadata: major_brand : mp42 minor_version : 0 compatible_brands: mp42isom creation_time : 2021-09-28T09:44:11.000000Z Duration: 00:00:04.95, start: 0.000000, bitrate: 742 kb/s Stream #0:0(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, mono, fltp, 64 kb/s (default) Metadata: creation_time : 2021-09-28T09:44:11.000000Z vendor_id : [0][0][0][0] Stream #0:1(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 544x960, 707 kb/s, 15.30 fps, 15.33 tbr, 90k tbn, 34 tbc (default) Metadata: creation_time : 2021-09-28T09:44:11.000000Z vendor_id : [0][0][0][0] encoder : JVT/AVC Coding
很明显,当ffmpeg解析MP4视频的输出中,音频流的信息只要在视频流前面就会认为误判为音频,导致获取视频信息报错
三、解决方法
方法1(未验证):升级版本到1.0.6,但目前由于maven仓库无法引入该依赖,原因如下
https://github.com/dadiyang/jave/issues/14
方法2(已验证):自定义解析器,在获取视频信息为空时,使用自定义解析器获取视频高度宽度
import it.sauronsoftware.jave.DefaultFFMPEGLocator; import it.sauronsoftware.jave.VideoSize; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.lang.reflect.Field; import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 视频信息解析器(为简单起见,只获取宽高) * 由于{@link it.sauronsoftware.jave.Encoder}判断视频宽度高度有bug,可能将视频判断为音频,导致获取不到视频的宽高,影响视频的裁剪 * @author zhangyj */ @Slf4j @Component public class VideoEncoder { private static final Pattern SIZE_PATTERN = Pattern.compile("(\d+)x(\d+)", Pattern.CASE_INSENSITIVE); private static final Pattern VIDEO_PATTERN = Pattern.compile("^\s*Stream #\S+: ((?:Video)): (.*)\s*$", Pattern.CASE_INSENSITIVE); private String programPath; { try { this.programPath = getFfmepgProgramPath(); } catch (Exception e) { log.error("获取ffmpeg程序路径失败", e); } } public VideoSize getVideoSize(File source) throws Exception { String[] commands = {programPath, "-i", source.getAbsolutePath()}; try (BufferedReader reader = getCommandReader(commands)){ return getVideoSize(reader); } } private String getFfmepgProgramPath() throws Exception { DefaultFFMPEGLocator locator = new DefaultFFMPEGLocator(); // 通过反射获取获取ffmpeg程序绝对路径,该属性未提供get方法 Field field = locator.getClass().getDeclaredField("path"); field.setAccessible(true); return String.valueOf(field.get(locator)); } private BufferedReader getCommandReader(String[] commands) throws IOException { Runtime runtime = Runtime.getRuntime(); Process exec = runtime.exec(commands); //noinspection AlibabaAvoidManuallyCreateThread runtime.addShutdownHook(new Thread(exec::destroy)); return new BufferedReader(new InputStreamReader(exec.getErrorStream())); } private VideoSize getVideoSize(BufferedReader reader) throws Exception{ while (true) { String line = reader.readLine(); if (line == null) { break; } Matcher videoMatcher = VIDEO_PATTERN.matcher(line); if (!videoMatcher.matches()) { continue; } StringTokenizer st = new StringTokenizer(videoMatcher.group(2), ","); for (int i = 0; st.hasMoreTokens(); i++) { String token = st.nextToken().trim(); if(i == 0){ continue; } Matcher sizeMatcher = SIZE_PATTERN.matcher(token); if (!sizeMatcher.find()) { continue; } return new VideoSize(Integer.parseInt(sizeMatcher.group(1)), Integer.parseInt(sizeMatcher.group(2))); } } return null; } }
使用代码修改为:
Encoder vEncoder = new Encoder(); File f1 = new File(videoLocalFile); MultimediaInfo videoM = vEncoder.getInfo(f1); vo.setDuration((int) videoM.getDuration() / 1000); VideoInfo videoInfo = videoM.getVideo(); VideoSize videoSize = videoInfo != null? videoInfo.getSize():videoEncoder.getVideoSize(f1); if(videoSize != null){ vo.setMtalHeight(videoSize.getHeight()); vo.setMtalWidth(videoSize.getWidth()); }