Java mp4parser 实现视频mp4 切割

pom文件

XML 复制代码
<dependency>
	<groupId>org.mp4parser</groupId>
	<artifactId>isoparser</artifactId>
	<version>1.9.36</version>
</dependency>
<dependency>
	<groupId>org.mp4parser</groupId>
	<artifactId>muxer</artifactId>
	<version>1.9.36</version>
</dependency>

实现代码

java 复制代码
package com.test.vodio;

import org.mp4parser.BasicContainer;
import org.mp4parser.Container;
import org.mp4parser.IsoFile;
import org.mp4parser.boxes.iso14496.part12.CompositionTimeToSample;
import org.mp4parser.boxes.iso14496.part12.SampleDependencyTypeBox;
import org.mp4parser.boxes.iso14496.part12.SampleDescriptionBox;
import org.mp4parser.boxes.iso14496.part12.SubSampleInformationBox;
import org.mp4parser.boxes.sampleentry.SampleEntry;
import org.mp4parser.boxes.samplegrouping.GroupEntry;
import org.mp4parser.muxer.*;
import org.mp4parser.muxer.builder.DefaultMp4Builder;
import org.mp4parser.muxer.container.mp4.MovieCreator;


import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.*;
import java.util.stream.Collectors;


/**
 * @author JR
 * @version 1.0
 * @description:
 * @date 2025年09月11日 17:01
 */
public class Test {

    // 4.3G的字节数 (1G = 1024^3字节)
    private static final long MAX_FILE_SIZE_BYTES = (long) (4.3 * 1024 * 1024 * 1024);

    /**
     * 按时长切割MP4文件,大于4.3G时自动计算切割次数
     * @param inputFilePath 输入文件路径
     * @param outputDir 输出目录
     * @throws IOException 处理过程中可能抛出的IO异常
     */
    public void splitMP4ByDuration(String inputFilePath, String outputDir) throws IOException {
        File outputDirectory = new File(outputDir);
        if (!outputDirectory.exists()) {
            outputDirectory.mkdirs();
        }

        File inputFile = new File(inputFilePath);
        long fileSize = inputFile.length();

        // 转换文件大小为GB
        double fileSizeGB = (double) fileSize / (1024 * 1024 * 1024);
        System.out.printf("文件大小: %.2f GB%n", fileSizeGB);

        // 如果文件小于等于4.3G,不处理
        if (fileSize <= MAX_FILE_SIZE_BYTES) {
            System.out.println("文件小于等于4.3G,不需要切割");
            return;
        }

        // 计算需要切割的次数(向上取整)
        int splitCount = (int) Math.ceil(fileSizeGB / 4.3);
        System.out.printf("文件大于4.3G,需要切割为 %d 个片段%n", splitCount);

        // 读取原始视频
        Movie movie = MovieCreator.build(inputFilePath);
        List<Track> tracks = movie.getTracks();

        // 获取视频总时长(微秒)- 修正:使用1.9.41版本支持的方式
        long totalDurationUs = getTotalDurationUs(tracks);
        // 计算每个片段的时长(微秒)
        long segmentDurationUs = totalDurationUs / splitCount;

        // 切割并保存每个片段
        for (int i = 0; i < splitCount; i++) {
            long startTimeUs = i * segmentDurationUs;
            // 最后一个片段包含剩余的所有时长
            long endTimeUs = (i == splitCount - 1) ? totalDurationUs : (i + 1) * segmentDurationUs;

            List<Track> segmentTracks = new ArrayList<>();
            for (Track track : tracks) {
                Track segmentTrack = new CroppedTrack(track, startTimeUs, endTimeUs);
                segmentTracks.add(segmentTrack);
            }

            Movie segmentMovie = new Movie();
            for (Track track : segmentTracks) {
                segmentMovie.addTrack(track);
            }

            DefaultMp4Builder builder = new DefaultMp4Builder();
            BasicContainer build = (BasicContainer) builder.build(segmentMovie);

            String outputFilePath = outputDir + File.separator +
                    getFileNameWithoutExtension(inputFile) + "_part" + (i + 1) + ".mp4";

            try (FileOutputStream fos = new FileOutputStream(outputFilePath);
                 FileChannel fc = fos.getChannel()) {
                build.writeContainer(fc);
            }

            System.out.printf("已生成片段 %d: %s%n", i + 1, outputFilePath);
        }
    }

    /**
     * 修正:计算视频总时长(微秒),使用1.9.41版本的TrackMetaData
     */
    private long getTotalDurationUs(List<Track> tracks) {
        long maxDuration = 0;
        for (Track track : tracks) {
            // 1.9.41版本通过TrackMetaData获取时间信息
            TrackMetaData trackMetaData = track.getTrackMetaData();
            if (trackMetaData != null) {
                // 时长 = 轨道持续时间 / 时间尺度 * 1000000(转换为微秒)
                long duration = (long)(track.getDuration() * 1000000.0 / trackMetaData.getTimescale());
                if (duration > maxDuration) {
                    maxDuration = duration;
                }
            }
        }
        return maxDuration;
    }

    private String getFileNameWithoutExtension(File file) {
        String fileName = file.getName();
        int lastDotIndex = fileName.lastIndexOf('.');
        if (lastDotIndex > 0) {
            return fileName.substring(0, lastDotIndex);
        }
        return fileName;
    }

    /**
     * 完整实现Track接口的裁剪轨道类
     */
    private static class CroppedTrack implements Track {
        private final Track source;
        private final long startTimeUs;
        private final long endTimeUs;
        private List<Sample> samples;
        private TrackMetaData trackMetaData;
        private List<Long> sampleDurationsList;
        private long[] sampleDurations;
        private List<CompositionTimeToSample.Entry> compositionTimeEntries;
        private long[] syncSamples;
        private List<SampleDependencyTypeBox.Entry> sampleDependencies;
        private SubSampleInformationBox subSampleInformationBox;
        private String name;
        private List<Edit> edits;
        private Map<GroupEntry, long[]> sampleGroups;

        public CroppedTrack(Track source, long startTimeUs, long endTimeUs) {
            this.source = source;
            this.startTimeUs = startTimeUs;
            this.endTimeUs = endTimeUs;
            this.trackMetaData = source.getTrackMetaData();
            this.sampleDurationsList = new ArrayList<>();
            calculateTrackData();
        }

        /**
         * 计算裁剪后轨道的所有核心数据
         */
        private void calculateTrackData() {
            samples = new ArrayList<>();
            sampleDurationsList.clear();

            long currentTimeUs = 0;
            long timeScale = trackMetaData.getTimescale();
            List<Sample> sourceSamples = source.getSamples();
            List<Long> sourceSampleDurationsList = Arrays.stream(source.getSampleDurations())
                    .boxed()
                    .collect(Collectors.toList());

            // 1. 筛选样本并记录时长
            for (int i = 0; i < sourceSamples.size(); i++) {
                Sample sample = sourceSamples.get(i);
                long sampleDuration = sourceSampleDurationsList.get(i);
                long sampleDurationUs = (long)(sampleDuration * 1000000.0 / timeScale);

                if (currentTimeUs + sampleDurationUs > startTimeUs && currentTimeUs < endTimeUs) {
                    samples.add(sample);
                    sampleDurationsList.add(sampleDuration);
                }

                currentTimeUs += sampleDurationUs;
                if (currentTimeUs >= endTimeUs) break;
            }

            // 2. 转换样本时长为数组
            sampleDurations = sampleDurationsList.stream().mapToLong(Long::longValue).toArray();

            // 3. 处理合成时间条目
            compositionTimeEntries = source.getCompositionTimeEntries();


            // 4. 筛选同步样本(关键帧等)
            syncSamples = filterSyncSamples(source.getSyncSamples());

            sampleDependencies = source.getSampleDependencies();

            // 6. 子样本信息
            subSampleInformationBox = source.getSubsampleInformationBox();

            // 7. 轨道名称
            name = source.getName();

            // 8. 编辑信息
            edits = source.getEdits();

            // 9. 样本组信息
            sampleGroups = source.getSampleGroups();
        }



        /**
         * 筛选出在裁剪样本范围内的同步样本索引
         */
        private long[] filterSyncSamples(long[] sourceSyncSamples) {
            if (sourceSyncSamples == null || sourceSyncSamples.length == 0) {
                return new long[0];
            }

            List<Long> filtered = new ArrayList<>();
            int sourceSampleCount = source.getSamples().size();
            int targetSampleCount = samples.size();

            // 只保留在裁剪后样本范围内的同步点
            for (long syncSample : sourceSyncSamples) {
                if (syncSample > 0 && syncSample <= sourceSampleCount &&
                        syncSample <= targetSampleCount) {
                    filtered.add(syncSample);
                }
            }

            return filtered.stream().mapToLong(Long::longValue).toArray();
        }

        // === 实现Track接口的所有方法 ===

        @Override
        public List<SampleEntry> getSampleEntries() {
            return source.getSampleEntries();
        }

        @Override
        public long[] getSampleDurations() {
            return sampleDurations;
        }

        @Override
        public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() {
            return compositionTimeEntries;
        }

        @Override
        public long[] getSyncSamples() {
            return syncSamples;
        }

        @Override
        public List<SampleDependencyTypeBox.Entry> getSampleDependencies() {
            return sampleDependencies;
        }

        @Override
        public SubSampleInformationBox getSubsampleInformationBox() {
            return subSampleInformationBox;
        }

        @Override
        public String getName() {
            return name;
        }

        @Override
        public List<Edit> getEdits() {
            return edits;
        }

        @Override
        public Map<GroupEntry, long[]> getSampleGroups() {
            return sampleGroups;
        }

        @Override
        public List<Sample> getSamples() {
            return samples;
        }

        @Override
        public TrackMetaData getTrackMetaData() {
            return trackMetaData;
        }

        @Override
        public String getHandler() {
            return source.getHandler();
        }

        @Override
        public long getDuration() {
            return (long)((endTimeUs - startTimeUs) * trackMetaData.getTimescale() / 1000000.0);
        }

        @Override
        public void close() throws IOException {
            source.close();
        }
    }


    public static void main(String[] args) {
        Test splitter = new Test();
        try {
            splitter.splitMP4ByDuration("C:\\Users\\Administrator\\Desktop\\魔童闹海.mp4", "output_segments");
            System.out.println("切割完成");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
相关推荐
冬天vs不冷2 小时前
Java基础(十一):关键字final详解
java·开发语言
上官浩仁2 小时前
springboot maven 多环境配置入门与实战
java·spring boot·maven
元直数字电路验证2 小时前
新建Jakarta EE项目,Maven Archetype 选项无法加载出内容该怎么办?
java·maven
我叫汪枫2 小时前
Spring Boot图片验证码功能实现详解 - 从零开始到完美运行
java·前端·javascript·css·算法·html
EvanSun__2 小时前
Flask 框架引入
后端·python·flask
小王不爱笑1322 小时前
Java基础知识(十四)
java·windows·python
却道天凉_好个秋2 小时前
音视频学习(六十三):AVCC和HVCC
音视频·h264·h265·avcc·hvcc
烟锁池塘柳03 小时前
【已解决,亲测有效】解决使用Python Matplotlib库绘制图表中出现中文乱码(中文显示为框)的问题的方法
开发语言·python·matplotlib
周小码3 小时前
llama-stack实战:Python构建Llama应用的可组合开发框架(8k星)
开发语言·python·llama