版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/BlogRecord/article/details/54772623
概述:
主要用的是mp4Parser(根据关键帧进行裁剪) - 自己进行封装以及内部代码处理等,便于使用
MP4的分割和拼接介绍
mp4Parser - github
实现代码:
package com.clip;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import com.coremedia.iso.boxes.Container;
import com.coremedia.iso.boxes.TimeToSampleBox;
import com.googlecode.mp4parser.authoring.Movie;
import com.googlecode.mp4parser.authoring.Track;
import com.googlecode.mp4parser.authoring.builder.DefaultMp4Builder;
import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator;
import com.googlecode.mp4parser.authoring.tracks.CroppedTrack;
/**
* 裁剪视频工具类
*/
public class TrimVideoUtils{
/** 单例对象 */
private static TrimVideoUtils instance = null;
/** 私有的默认构造函数 */
private TrimVideoUtils() {
}
/** 获取单例实体对象 */
public static TrimVideoUtils getInstance() {
if (instance == null) {
instance = new TrimVideoUtils();
}
return instance;
}
// =============== 回调事件 ===============
/** 裁剪回调接口 */
private TrimFileCallBack trimCallBack;
/**
* 设置裁剪回调
* @param trimCallBack
*/
public void setTrimCallBack(TrimFileCallBack trimCallBack) {
this.trimCallBack = trimCallBack;
}
/** 裁剪文件回调接口 */
public interface TrimFileCallBack {
/**
* 裁剪回调
* @param isNew 是否新剪辑
* @param starts 开始时间(秒)
* @param ends 结束时间(秒)
* @param vTime 视频长度
* @param file 需要裁剪的文件路径
* @param trimFile 裁剪后保存的文件路径
*/
public void trimCallback(boolean isNew, int startS, int endS,
int vTotal, File file, File trimFile);
/**
* 裁剪失败回调
* @param eType 错误类型
*/
public void trimError(int eType);
}
// =============== 对外公开方法 ===============
/** 是否暂停裁剪 */
private boolean isStopTrim = false;
// -- 常量 --
/** 裁剪保存的文件地址 */
public static final String TRIM_SAVE_PATH = "trimSavePath";
/** 裁剪选择 */
public static final int TRIM_SWITCH = -8;
/** 停止裁剪 */
public static final int TRIM_STOP = -9;
/** 文件不存在 */
public static final int FILE_NOT_EXISTS = -10;
/** 裁剪失败 */
public static final int TRIM_FAIL = -11;
/** 裁剪成功 */
public static final int TRIM_SUCCESS = -12;
/** 暂停裁剪 */
public void stopTrim(){
isStopTrim = true;
}
/**
* 裁剪回调
* @param isNew 是否新剪辑(true = 新剪辑,false = 修改原剪辑-覆盖)
* @param starts 开始时间(秒)
* @param ends 结束时间(秒)
* @param file 需要裁剪的文件路径
* @param trimFile 裁剪后保存的文件路径
*/
public void startTrim(boolean isNew, int startS, int endS, File file, File trimFile){
// 默认非暂停裁剪
isStopTrim = false;
// 需要裁剪的视频必须存在
if(file != null && file.exists()){
try {
// 获取文件地址
String path = file.getAbsolutePath();
// 生成Movie对象(解析视频信息)
Movie movie = MovieCreator.build(path);
// 获取视频轨道信息(视频流、声道)
List<Track> tracks = movie.getTracks();
// 设置新的轨道信息
movie.setTracks(new LinkedList<Track>());
// 关键则 - 时间是否准确的
boolean timeCorrected = false;
// 开始裁剪的时间
double startTime = startS;
// 结束裁剪的时间
double endTime = endS;
// 获取轨道条数
// int totalSize = tracks.size();
// 遍历轨道,获取视频轨道的帧数
for (Track track : tracks) {
if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) {
if (timeCorrected) {
throw new RuntimeException( "The startTime has already been corrected by another track with SyncSample. Not Supported.");
}
// true = 获取与裁剪时间最大值关键帧时间
// false = 获取与裁剪时间最接近的时间
// --
// true, false表示短截取, false,true表示长截取
startTime = correctTimeToSyncSample(track, startTime, false);
endTime = correctTimeToSyncSample(track, endTime, true);
timeCorrected = true;
}
}
// 遍历计算裁剪时间(视频、音频) - 重新计算裁剪时间
for (Track track : tracks) {
long currentSample = 0;
double currentTime = 0;
long startSample = -1;
long endSample = -1;
// 获取编码过的数据
List<TimeToSampleBox.Entry> listEntrys = track.getDecodingTimeEntries();
// 遍历,计算关键帧差
for (int i = 0, c = listEntrys.size(); i < c; i++) {
TimeToSampleBox.Entry entry = listEntrys.get(i);
for (int j = 0; j < entry.getCount(); j++) {
if (currentTime <= startTime) {
startSample = currentSample;
}
if (currentTime <= endTime) {
endSample = currentSample;
} else {
break;
}
currentTime += (double) entry.getDelta() / (double) track.getTrackMetaData().getTimescale();
currentSample++;
}
}
movie.addTrack(new CroppedTrack(track, startSample, endSample));
}
if(isStopTrim){
if(this.trimCallBack != null){
this.trimCallBack.trimError(TRIM_STOP);
}
return;
}
// 进行处理裁剪视频
Container container = new DefaultMp4Builder().build(movie);
// 判断文件是否存在,不存在则创建
if (!trimFile.exists()) {
trimFile.createNewFile();
}
// 写入流
FileOutputStream fos = new FileOutputStream(trimFile);
FileChannel fc = fos.getChannel();
container.writeContainer(fc);
fc.close();
fos.close();
// --
if (isStopTrim) {
if (trimFile.exists()) {
trimFile.delete();
}
if(this.trimCallBack != null){
this.trimCallBack.trimError(TRIM_STOP);
}
return;
}
// 裁剪成功
if(this.trimCallBack != null){
// 裁剪视频的总时间
int vTotal = endS - startS;
// 触发回调
this.trimCallBack.trimCallback(isNew, startS, endS, vTotal, file, trimFile);
}
} catch (Exception e) {
e.printStackTrace();
// --
if(this.trimCallBack != null){
this.trimCallBack.trimError(TRIM_FAIL);
}
// --
try {
if (trimFile.exists()) {
trimFile.delete();
}
} catch (Exception e2) {
}
}
} else {
if(this.trimCallBack != null){
this.trimCallBack.trimError(FILE_NOT_EXISTS);
}
}
}
/**
* 计算关键帧时间
* @param path 文件路径
* @param dTime 计算失败默认返回时间
*/
public double reckonFrameTime(File file, double dTime){
// 文件必须存在
if(file != null && file.exists()){
try {
// 获取文件地址
String path = file.getAbsolutePath();
// 生成Movie对象(解析视频信息)
Movie movie = MovieCreator.build(path);
// 获取视频轨道信息(视频流、声道)
List<Track> tracks = movie.getTracks();
// 设置新的轨道信息
movie.setTracks(new LinkedList<Track>());
// 关键则 - 时间是否准确的
boolean timeCorrected = false;
// 获取轨道条数
// int totalSize = tracks.size();
// 遍历轨道,获取视频轨道的帧数
for (Track track : tracks) {
if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) {
if (timeCorrected) {
throw new RuntimeException( "The startTime has already been corrected by another track with SyncSample. Not Supported.");
}
double oFrame = correctTimeToSyncSample(track);
timeCorrected = true;
return oFrame;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
return dTime;
}
// --
/**
* 改正正确的关键帧时间
* @param track
* @return
*/
private double correctTimeToSyncSample(Track track) {
// 时间偏差量计算
double[] timeOfSyncSamples = new double[]{-1, -1, -1, -1, -1};
// timeOfSyncSamples = new double[track.getSyncSamples().length];
// 偏移量数组长度
int tLength = timeOfSyncSamples.length;
// 当前偏差量
long currentSample = 0;
// 当前时间
double currentTime = 0;
// 是否返回
boolean isBreak = false;
// 获取编码过的数据
List<TimeToSampleBox.Entry> listEntrys = track.getDecodingTimeEntries();
// 遍历,计算关键帧差
for (int i = 0, c = listEntrys.size(); i < c; i++) {
if(isBreak){
break;
}
TimeToSampleBox.Entry entry = listEntrys.get(i);
for (int j = 0; j < entry.getCount(); j++) {
// 获取偏移量索引
int tofPos = Arrays.binarySearch(track.getSyncSamples(), currentSample + 1);
if (tofPos >= 0) {
// 防止大于数组长度
if(tofPos >= tLength){
isBreak = true;
break;
}
timeOfSyncSamples[tofPos] = currentTime;
}
currentTime += (double) entry.getDelta() / (double) track.getTrackMetaData().getTimescale();
currentSample++;
}
}
// 获取偏移量
double offset = -1d;
// -- 计算关键帧 --
for(int i = 1, c = timeOfSyncSamples.length; i < c; i++){
double above = timeOfSyncSamples[i-1];
double curr = timeOfSyncSamples[i];
if(curr != -1d && above != -1){
double tOffset = curr - above;
if(offset == -1d){ // 初始化第一个计算出来的偏移量
offset = tOffset;
} else {
if(offset < tOffset){ // 如果最新的偏移量比上次的大,则重新保存
offset = tOffset;
}
// 这段代码,直接返回比上次偏移量大的
//if(tOffset > offset){
// return tOffset;
//} else {
// return offset;
//}
}
}
}
if(offset == -1d){
return timeOfSyncSamples[1];
}
return offset;
}
/**
* 改正正确的关键帧时间(关联)
* @param track
* @param cutHere 裁剪的时间
* @param next 是否获取最大值
* @return
*/
private double correctTimeToSyncSample(Track track, double cutHere, boolean next) {
// 偏移量数组长度
int tLength = track.getSyncSamples().length;
// 时间偏差量计算
double[] timeOfSyncSamples = new double[tLength];
// 当前偏差量
long currentSample = 0;
// 当前时间
double currentTime = 0;
// 获取编码过的数据
List<TimeToSampleBox.Entry> listEntrys = track.getDecodingTimeEntries();
// 遍历,计算关键帧差
for (int i = 0, c = listEntrys.size(); i < c; i++) {
TimeToSampleBox.Entry entry = listEntrys.get(i);
for (int j = 0; j < entry.getCount(); j++) {
// 获取偏移量索引
int tofPos = Arrays.binarySearch(track.getSyncSamples(), currentSample + 1);
if (tofPos >= 0) {
timeOfSyncSamples[tofPos] = currentTime;
}
currentTime += (double) entry.getDelta() / (double) track.getTrackMetaData().getTimescale();
currentSample++;
}
}
// 获取裁剪位置,之间的关键帧数据
double previous = 0;
// 遍历全部关键帧时间
for (double timeOfSyncSample : timeOfSyncSamples) {
if (timeOfSyncSample > cutHere) {
if (next) {
return timeOfSyncSample;
} else {
return previous;
}
}
previous = timeOfSyncSample;
}
return timeOfSyncSamples[timeOfSyncSamples.length - 1];
}
}
使用代码:
package com.clip;
import java.io.File;
import java.io.IOException;
import com.clip.TrimVideoUtils.TrimFileCallBack;
public class ClipMain {
public static void main(String[] args) {
cutVideo();
}
public static void cutVideo() {
TrimVideoUtils trimVideoUtils = TrimVideoUtils.getInstance();
// 设置回调
trimVideoUtils.setTrimCallBack(new TrimFileCallBack() {
@Override // 裁剪失败回调
public void trimError(int eType) {
switch(eType){
case TrimVideoUtils.FILE_NOT_EXISTS: // 文件不存在
System.out.println("视频文件不存在");
break;
case TrimVideoUtils.TRIM_STOP: // 手动停止裁剪
System.out.println("停止裁剪");
break;
case TrimVideoUtils.TRIM_FAIL:
default: // 裁剪失败
System.out.println("裁剪失败");
break;
}
}
@Override // 裁剪成功回调
public void trimCallback(boolean isNew, int startS, int endS, int vTotal, File file, File trimFile) {
/**
* 裁剪回调
* @param isNew 是否新剪辑
* @param starts 开始时间(秒)
* @param ends 结束时间(秒)
* @param vTime 视频长度
* @param file 需要裁剪的文件路径
* @param trimFile 裁剪后保存的文件路径
*/
// ===========
System.out.println("isNew : " + isNew);
System.out.println("startS : " + startS);
System.out.println("endS : " + endS);
System.out.println("vTotal : " + vTotal);
System.out.println("file : " + file.getAbsolutePath());
System.out.println("trimFile : " + trimFile.getAbsolutePath());
}
});
// ========
// 需要裁剪的视频路径
String videoPath = "E:\\clip\\Xperia.mp4";
// 保存的路径
String savePath = "E:\\clip\\save\\Xperia_" + System.currentTimeMillis() + "_cut.mp4";
// ==
final File file = new File(videoPath); // 视频文件地址
final File trimFile = new File(savePath);// 裁剪文件保存地址
final int startS = 1; // 开始时间(秒数 - 非毫秒)
final int endS = 20; // 结束时间(秒数 - 非毫秒)
// 进行裁剪
new Thread(new Runnable() {
@Override
public void run() {
try { // 开始裁剪
TrimVideoUtils.getInstance().startTrim(true, startS, endS, file, trimFile);
} catch (Exception e) {
e.printStackTrace();
// 设置回调为null
TrimVideoUtils.getInstance().setTrimCallBack(null);
}
}
}).start();
// 停止裁剪
// TrimVideoUtils.getInstance().stopTrim();
}
}