使用ffmpeg进行视频转码(java)

309 阅读2分钟

1、首先在本地安装ffmpeg

下载地址:ffmpeg.org/download.ht…

0aad0001010b45bb7adc94053ee634e.png

image.png

2、配置环境变量

image.png

3、验证是否安装成功

image.png

4、在项目中引入依赖

<!--ffmpeg的依赖包-->

<dependency>
    <groupId>org.bytedeco</groupId>
    <artifactId>javacv</artifactId>
    <version>1.5.1</version>
    <exclusions>
        <exclusion>
            <groupId>org.bytedeco</groupId>
            <artifactId>*</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<dependency>
    <groupId>org.bytedeco</groupId>
    <artifactId>ffmpeg-platform</artifactId>
    <version>4.1.3-1.5.1</version>
</dependency>

5、工具类

package com.sdecloud.common.utils;

import com.google.gson.Gson;
import org.apache.commons.codec.binary.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import javax.crypto.KeyGenerator;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

/**
 * @author jhx
 */
public class FFmpegUtils {

    private static final Logger LOGGER = LoggerFactory.getLogger(FFmpegUtils.class);

    /**
     * 跨平台换行符
     */
    private static final String LINE_SEPARATOR = System.getProperty("line.separator");

    private static ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4,
            60, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>(),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy());

    /**
     * 生成随机16个字节的AESKEY
     *
     * @return byte
     */
    private static byte[] genAesKey() {
        try {
            KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
            keyGenerator.init(128);
            return keyGenerator.generateKey().getEncoded();
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    /**
     * 在指定的目录下生成key_info, key文件,返回key_info文件
     *
     * @param folder folder
     */
    private static Path genKeyInfo(String folder) throws IOException {
        // AES 密钥
        byte[] aesKey = genAesKey();
        // AES 向量
        String iv = Hex.encodeHexString(genAesKey());

        // key 文件写入
        Path keyFile = Paths.get(folder, "key");
        Files.write(keyFile, aesKey, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);

        // key_info 文件写入
        StringBuilder stringBuilder = new StringBuilder();
        // m3u8加载key文件网络路径
        stringBuilder.append("key").append(LINE_SEPARATOR);
        // FFmeg加载key_info文件路径
        stringBuilder.append(keyFile).append(LINE_SEPARATOR);
        // ASE 向量
        stringBuilder.append(iv);
        Path keyInfo = Paths.get(folder, "key_info");

        Files.write(keyInfo, stringBuilder.toString().getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);

        return keyInfo;
    }

    /**
     * 指定的目录下生成 master index.m3u8 文件
     *
     * @param file      master m3u8文件地址
     * @param indexPath 访问子index.m3u8的路径
     * @param bandWidth 流码率
     */
    private static void genIndex(String file, String indexPath, String bandWidth) throws IOException {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("#EXTM3U").append(LINE_SEPARATOR);
        // 码率
        stringBuilder.append("#EXT-X-STREAM-INF:BANDWIDTH=" + bandWidth).append(LINE_SEPARATOR);
        stringBuilder.append(indexPath);
        Files.write(Paths.get(file), stringBuilder.toString().getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
    }

    /**
     * 转码视频为m3u8
     *
     * @param source     源视频
     * @param destFolder 目标文件夹
     * @param config     配置信息
     */
    public static void transcodeToM3u8(String fileName, String source, String destFolder, TranscodeConfig config) throws IOException, InterruptedException {
        // 判断源视频是否存在
        if (!Files.exists(Paths.get(source))) {
            throw new IllegalArgumentException("文件不存在:" + source);
        }
        // 创建工作目录
        Path workDir = Paths.get(destFolder);
        Files.createDirectories(workDir);
        // 在工作目录生成KeyInfo文件
        Path keyInfo = genKeyInfo(workDir.toString());
        // 构建命令
        List<String> commands = new ArrayList<>();
        commands.add("ffmpeg");
        commands.add("-i");
        // 源文件
        commands.add(source);
        commands.add("-c:v");
        // 视频编码为H264
        commands.add("libx264");
        commands.add("-c:a");
        // 音频直接copy
        commands.add("copy");
        commands.add("-hls_key_info_file");
        // 指定密钥文件路径
        commands.add(keyInfo.toString());
        commands.add("-hls_time");
        // ts切片大小
        commands.add(config.getTsSeconds());
        commands.add("-hls_playlist_type");
        // 点播模式
        commands.add("vod");
        commands.add("-hls_segment_filename");
        // ts切片文件名称
        commands.add("%06d.ts");

        if (StringUtils.hasText(config.getCutStart())) {
            commands.add("-ss");
            // 开始时间
            commands.add(config.getCutStart());
        }
        if (StringUtils.hasText(config.getCutEnd())) {
            commands.add("-to");
            // 结束时间
            commands.add(config.getCutEnd());
        }
        // 生成m3u8文件
        commands.add(fileName + "ts.m3u8");

        // 构建进程
        Process process = new ProcessBuilder()
                .command(commands)
                .directory(workDir.toFile())
                .start();

        // 读取进程标准输出
        executor.execute(()-> {
            try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                String line = null;
                while ((line = bufferedReader.readLine()) != null) {
                    LOGGER.info(line);
                }
            } catch (IOException e) {
                System.out.println(e.getMessage());
            }
        });
        // 读取进程异常输出
        executor.execute(()-> {
            try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
                String line = null;
                while ((line = bufferedReader.readLine()) != null) {
                    LOGGER.info(line);
                }
            } catch (IOException e) {
                LOGGER.error(e.getMessage());
            }
        });

        // 阻塞直到任务结束
        if (process.waitFor() != 0) {
            throw new RuntimeException("视频切片异常");
        }
            // 切出封面,我这里不需要封面,所以把这部分屏蔽了,有需要的可以放开
//        if (!screenShots(source, String.join(File.separator, destFolder, "poster.jpg"), config.getPoster())) {
//            throw new RuntimeException("封面截取异常");
//        }
        // 获取视频信息
        final MediaInfo[] mediaInfo = {getMediaInfo(source)};
        if (mediaInfo[0] == null) {
            throw new RuntimeException("获取媒体信息异常");
        }
        // 生成index.m3u8文件
        genIndex(String.join(File.separator, destFolder, fileName + ".m3u8"), fileName + "ts.m3u8", mediaInfo[0].getFormat().getBitRate());
        // 删除keyInfo文件
        Files.delete(keyInfo);
    }

    /**
     * 获取视频文件的媒体信息
     *
     * @param source source
     * @return MediaInfo
     */
    public static MediaInfo getMediaInfo(String source) throws IOException, InterruptedException {
        List<String> commands = new ArrayList<>();
        commands.add("ffprobe");
        commands.add("-i");
        commands.add(source);
        commands.add("-show_format");
        commands.add("-show_streams");
        commands.add("-print_format");
        commands.add("json");

        Process process = new ProcessBuilder(commands)
                .start();

        MediaInfo mediaInfo = null;

        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
            mediaInfo = new Gson().fromJson(bufferedReader, MediaInfo.class);
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (process.waitFor() != 0) {
            return null;
        }

        if (process.waitFor() != 0) {
            return null;
        }
        return mediaInfo;
    }

    /**
     * 截取视频的指定时间帧,生成图片文件
     *
     * @param source 源文件
     * @param file   图片文件
     * @param time   截图时间 HH:mm:ss.[SSS]
     */
    public static boolean screenShots(String source, String file, String time) throws IOException, InterruptedException {

        List<String> commands = new ArrayList<>();
        commands.add("ffmpeg");
        commands.add("-i");
        commands.add(source);
        commands.add("-ss");
        commands.add(time);
        commands.add("-y");
        commands.add("-q:v");
        commands.add("1");
        commands.add("-frames:v");
        commands.add("1");
        commands.add("-f");
        commands.add("image2");
        commands.add(file);
        Process process = new ProcessBuilder(commands)
                .start();

        // 读取进程标准输出
        executor.execute(()-> {
            try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                String line = null;
                while ((line = bufferedReader.readLine()) != null) {
                    LOGGER.info(line);
                }
            } catch (IOException e) {
                LOGGER.error(e.getMessage());
            }
        });
        // 读取进程异常输出
        executor.execute(()-> {
            try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
                String line = null;
                while ((line = bufferedReader.readLine()) != null) {
                    LOGGER.info(line);
                }
            } catch (IOException e) {
                LOGGER.error(e.getMessage());
            }
        });

        return process.waitFor() == 0;
    }
}

MediaInfo.java

package com.sdecloud.common.utils;

import com.google.gson.annotations.SerializedName;
import lombok.Data;

import java.util.List;

/**
 * @author jhx
 */
@Data
public class MediaInfo {

   @Data
   public static class Format {

      @SerializedName("bit_rate")
      private String bitRate;

   }

   @Data
   public static class Stream {

      @SerializedName("index")
      private int index;

      @SerializedName("codec_name")
      private String codecName;

      @SerializedName("codec_long_name")
      private String codecLongName;

      @SerializedName("profile")
      private String profile;
   }
   
   @SerializedName("streams")
   private List<Stream> streams;

   @SerializedName("format")
   private Format format;
}

TranscodeConfig.java

package com.sdecloud.common.utils;

import lombok.Data;

/**
 * @author jhx
 */
@Data
public class TranscodeConfig {

    /**
     * 截取封面的时间 HH:mm:ss.[SSS]
     */
    private String poster = "00:00:00.001";
    /**
     * ts分片大小,单位是秒
     */
    private String tsSeconds = "15";
    /**
     * 视频裁剪,开始时间 HH:mm:ss.[SSS]
     */
    private String cutStart;
    /**
     * 视频裁剪,结束时间 HH:mm:ss.[SSS]
     */
    private String cutEnd;
}

6、使用

/**
 * 上传视频
 * @param fileName 文件名称
 * @param bucketName bucketName
 * @param file 视频文件
 * @return R 返回的结果
 */
public R uploadVideo(String fileName, String bucketName, MultipartFile file) {
    //返回数据
    Map<String, Object> result = new HashMap<>();
    
    Path tempDir = Paths.get(Global.getTempPath());
    String videoFolder = Global.getSysUploadPath() + bucketName;
    TranscodeConfig transcodeConfig = new TranscodeConfig();
    // io到临时文件
    Path tempFile = tempDir.resolve(fileName);

    try {
        file.transferTo(tempFile);
        // 删除后缀
        fileName = fileName.substring(0, fileName.lastIndexOf("."));
        // 尝试创建视频目录
        Path targetFolder = Files.createDirectories(Paths.get(videoFolder, fileName));
        try {
            Files.createDirectories(targetFolder);
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
        // 执行转码操作
        System.out.println("开始转码");
        try {
            FFmpegUtils.transcodeToM3u8(fileName, tempFile.toString(), targetFolder.toString(), transcodeConfig);
        } catch (Exception e) {
            return R.failed("转码异常:" + e.getMessage());
        }
        // 路径根据自己项目中的文件访问路径填写
        String url = String.format("/hswjordermanage/orderlecture/%s/%s/%s", bucketName, fileName, fileName + ".m3u8");
        result.put("bucketName", bucketName);
        result.put("fileName", fileName + ".m3u8");
        result.put("url", url);
        
        // 存储视频文件部分
        File fileBean = new File();
        fileBean.setFileName(fileName + ".m3u8");
        fileBean.setOriginal(file.getOriginalFilename());
        fileBean.setFileSize(this.byte2FitMemorySize(file.getSize()));
        fileBean.setType("m3u8");
        fileBean.setBucketName(bucketName);
        fileBean.setUrl(url);
        fileBean.setSysType(Global.getConfig(Constants.FILE_TYPE_KEY));
        fileJbxxMapper.addFile(fileBean);

        result.put("id", fileBean.getId());
    } catch (IOException e) {
        return R.failed(e.getMessage());
    } finally {
        try {
            // 始终删除临时文件
            Files.delete(tempFile);
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }
    return R.ok(result);
}