Java,Android实现MP4裁剪功能

版权声明:本文为博主原创文章,未经博主允许不得转载。 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();
    }
}

代码下载


Java,Android实现MP4裁剪功能

猜你喜欢

转载自blog.csdn.net/BlogRecord/article/details/54772623
今日推荐