最近在對之前寫的一個 Spring Boot 的視訊網站專案做功能完善,需要利用 FFmpeg 實現讀取視訊資訊和自動截圖的功能,查閱資料後發現網上這部分的內容非常少,於是就有了這篇文章。
視訊網站專案地址 GitHub:https://github.com/PuZhiweizuishuai/PornTube
碼雲: https://gitee.com/puzhiweizuishuai/VideoWeb
本文將介紹如何利用Javacv實現在視訊網站中常見的讀取視訊資訊和自動獲取封面圖的功能。
javacv可以幫助我們在java中很方便的使用 OpenCV 以及 FFmpeg 相關的功能介面
專案地址:https://github.com/bytedeco/javacv
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>${javacv.version}</version>
</dependency>
package com.buguagaoshu.porntube.vo;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Getter;
import lombok.Setter;
/**
* @author Pu Zhiwei {@literal [email protected]}
* create 2022-06-06 19:15
*/
@Getter
@Setter
public class VideoInfo {
/**
* 總幀數
**/
private int lengthInFrames;
/**
* 影格率
**/
private double frameRate;
/**
* 時長
**/
private double duration;
/**
* 視訊編碼
*/
private String videoCode;
/**
* 音訊編碼
*/
private String audioCode;
private int width;
private int height;
private int audioChannel;
private String md5;
/**
* 音訊取樣率
*/
private Integer sampleRate;
public String toJson() {
try {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.writeValueAsString(this);
} catch (Exception e) {
return "";
}
}
}
public static VideoInfo getVideoInfo(File file) {
VideoInfo videoInfo = new VideoInfo();
FFmpegFrameGrabber grabber = null;
try {
grabber = new FFmpegFrameGrabber(file);
// 啟動 FFmpeg
grabber.start();
// 讀取視訊幀數
videoInfo.setLengthInFrames(grabber.getLengthInVideoFrames());
// 讀取視訊影格率
videoInfo.setFrameRate(grabber.getVideoFrameRate());
// 讀取視訊秒數
videoInfo.setDuration(grabber.getLengthInTime() / 1000000.00);
// 讀取視訊寬度
videoInfo.setWidth(grabber.getImageWidth());
// 讀取視訊高度
videoInfo.setHeight(grabber.getImageHeight());
videoInfo.setAudioChannel(grabber.getAudioChannels());
videoInfo.setVideoCode(grabber.getVideoCodecName());
videoInfo.setAudioCode(grabber.getAudioCodecName());
// String md5 = MD5Util.getMD5ByInputStream(new FileInputStream(file));
videoInfo.setSampleRate(grabber.getSampleRate());
return videoInfo;
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
try {
if (grabber != null) {
// 此處程式碼非常重要,如果沒有,可能造成 FFmpeg 無法關閉
grabber.stop();
grabber.release();
}
} catch (FFmpegFrameGrabber.Exception e) {
log.error("getVideoInfo grabber.release failed 獲取檔案資訊失敗:{}", e.getMessage());
}
}
}
讀取資訊沒有什麼難度,但是在對視訊截圖的過程中,出現了一些問題,在我查詢截圖實現的程式碼時,大多數的程式碼都是這麼寫的
/**
* 獲取視訊縮圖
* @param filePath:視訊路徑
* @param mod:視訊長度/mod獲取第幾幀
* @throws Exception
*/
public static String randomGrabberFFmpegImage(String filePath, int mod) {
String targetFilePath = "";
try{
FFmpegFrameGrabber ff = FFmpegFrameGrabber.createDefault(filePath);
ff.start();
//圖片位置是否正確
String rotate = ff.getVideoMetadata(ROTATE);
//獲取幀數
int ffLength = ff.getLengthInFrames();
Frame f;
int i = 0;
//設定擷取幀數
int index = ffLength / mod;
while (i < ffLength) {
f = ff.grabImage();
if(i == index){
if (null != rotate && rotate.length() > 1) {
OpenCVFrameConverter.ToIplImage converter = new OpenCVFrameConverter.ToIplImage();
IplImage src = converter.convert(f);
f = converter.convert(rotate(src, Integer.parseInt(rotate)));
}
targetFilePath = getImagePath(filePath, i);
doExecuteFrame(f, targetFilePath);
break;
}
i++;
}
ff.stop();
}catch (Exception e){
log.error("獲取視訊縮圖異常:" + e.getMessage());
}
return targetFilePath;
}
這樣寫本身沒有什麼問題,但是在獲取需要擷取幀數的部分,使用的是通過迴圈來一幀一幀的判斷,這樣在視訊較短的時候沒有什麼問題,但是如果視訊較長,就會出現嚴重的效能問題。
while (i < ffLength) {
f = ff.grabImage();
if(i == index){
......
break;
}
i++;
}
FFmpeg 的命令列引數有一個 -ss
的引數,使用 -ss
可以快速的幫助我們跳到視訊的指定位置,完成操作,不用一幀一幀的判斷。
所以現在的問題就是如何在 javacv 中實現 -ss
引數
我在 javacv 的 GitHub Issues 中發現了這個操作,即使用 setTimestamp()
方法,使用 setTimestamp()
方法可以使 FFmpeg 跳轉到指定時間,完成截圖,於是,最後的截圖程式碼就變成了這樣
/**
* 隨機獲取視訊截圖
* @param videFile 視訊檔
* @param count 輸出截圖數量
* @return 截圖列表
* */
public static List<FileTableEntity> randomGrabberFFmpegImage(File videFile, int count, long userId) {
FFmpegFrameGrabber grabber = null;
String path = FileTypeEnum.filePath();
try {
List<FileTableEntity> images = new ArrayList<>(count);
grabber = new FFmpegFrameGrabber(videFile);
grabber.start();
// 獲取視訊總幀數
// int lengthInVideoFrames = grabber.getLengthInVideoFrames();
// 獲取視訊時長, / 1000000 將單位轉換為秒
long delayedTime = grabber.getLengthInTime() / 1000000;
Random random = new Random();
for (int i = 0; i < count; i++) {
// 跳轉到響應時間
grabber.setTimestamp((random.nextInt((int)delayedTime - 1) + 1) * 1000000L);
Frame f = grabber.grabImage();
Java2DFrameConverter converter = new Java2DFrameConverter();
BufferedImage bi = converter.getBufferedImage(f);
String imageName = FileTypeEnum.newFilename(SUFFIX);
File out = Paths.get(path, imageName).toFile();
ImageIO.write(bi, "jpg", out);
FileTableEntity fileTable = FileUtils.createFileTableEntity(imageName, SUFFIX, path, f.image.length, "系統生成截圖", userId, FileTypeEnum.VIDEO_PHOTO.getCode());
images.add(fileTable);
}
return images;
} catch (Exception e) {
return null;
} finally {
try {
if (grabber != null) {
grabber.stop();
grabber.release();
}
} catch (FFmpegFrameGrabber.Exception e) {
log.error("getVideoInfo grabber.release failed 獲取檔案資訊失敗:{}", e.getMessage());
}
}
}
這樣我們就能快速的實現截圖了。
轉載請註明來源