# ffmpeg **Repository Path**: ywyh/ffmpeg ## Basic Information - **Project Name**: ffmpeg - **Description**: 视频,图片,语音操作 - **Primary Language**: Java - **License**: GPL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 2 - **Created**: 2020-08-14 - **Last Updated**: 2024-07-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 此文章基于[dadiyang/jave](https://github.com/dadiyang/jave) ## 借鉴jave拷贝可执行文件代码修改后[gitee传送门](https://gitee.com/ywyh/ffmpeg) > 项目修改后,为执行命令的方式,封装了几个常用工具类,暂不支持进度 ### ywyh/ffmpeg项目使用示例 #### amr转mp3 ```java public static void main(String[] args) throws IOException, InterruptedException { AudioUtil.amrToMp3(new File("a.amr"), new File("./a.mp3")); } ``` #### 通过m3u8链接下载mp4视频 > 通过m3u8连接下载视频,并合并为mp4文件 ```java public static void main(String[] args) throws IOException, InterruptedException { VideoUtil.downloadByM3u8ToMp4("m3u8 url", new File("./test.mp4")); } ``` #### mp4转m3u8 > 将mp4装为m3u8,使用nginx,hls可以实现进度条快进 ```java public static void main(String[] args) throws IOException, InterruptedException { VideoUtil.mp4ToM3u8(new File("./t.mp4"), new File("m3u8/t")); } ``` #### 截图(每秒截几张图) > 每秒截0.1张图,就是10秒截一张图 ```java public static void main(String[] args) throws IOException, InterruptedException { VideoUtil.thumbnailByfps(new File("t.mp4"), new File("img/t"), 0.1F); } ``` #### 截图(按时间截图) > 在视频第10秒的时候截一张图 ```java public static void main(String[] args) throws IOException, InterruptedException { VideoUtil.thumbnailByTime(new File("t.mp4"), new File("img/t"), 10); } ``` ## jave源码分析 > **注意:jave中,使用的linux ffmpeg可执行二进制文件有问题,目前使用官网最新版替换,在centos7.6上运行正常** > 在工作中,基于微信开发,遇到一个问题,就是微信公众号发送的语音是amr格式,音频有效期内可以使用serverId进行播放(发送的语音都是临时文件,有效期为3天),但是小程序无法使用serverId进行播放,包括js也无法简单的播放amr格式的音频文件,故需求转换为mp3格式的音频文件,以便统一各个平台的语音播放。 >基于jave mp4转m3u8,使用jave的时候,发现里面并没有次方法,故而仔细阅读了一下jave的源码,在源码的基础上做了如下修改 >* 开放FFMPEGExecutor类和FFMPEGLocator类的createExecutor方发 >* 新增FormatEnum枚举类,定义各个格式类型转换需要设定的参数,在Encoder类的encode方法执行ffmpeg.execute()前,执行FormatEnum.get(formatAttribute).handle(ffmpeg, target), > * 添加对应的执行命令 VideoUtils 新增m3u8方法 > 后续详细介绍这几个方法的作用以及用法 ### 源码分析: #### 本人修改后的源码地址 [github传送门](https://github.com/zywayh/jave) > AudioUtils -> amrToMp3方法为原自带方法,通过此入口,找到了根执行文件 * AudioUtils -> amrToMp3 ```java public static void amrToMp3(File source, File target) { convert(source, target, "mp3"); }``` * AudioUtils -> convert > 在此方法上,设置了基础的格式信息等 ```java public static void convert(File source, File target, String format) { AudioAttributes audio = new AudioAttributes(); Encoder encoder = new IgnoreErrorEncoder(); audio.setCodec(LIBMP_3_LAME); EncodingAttributes attrs = new EncodingAttributes(); attrs.setFormat(format); attrs.setAudioAttributes(audio); try { encoder.encode(source, target, attrs); } catch (Exception e) { throw new IllegalStateException("convert amr to " + format + " error: ", e); } }``` * Encoder -> encode > 此方法根据EncodingAttributes下的AudioAttributes和VideoAttributes设置的参数,封装ffmpeg的执行命令 > 此方法过长,不贴源码了,请大家自行去看 * FFMPEGExecutor -> execute > 大家可以看到,最终框架依然使用的是`Runtime runtime = Runtime.getRuntime();`执行。 ```java public void execute() throws IOException { int argsSize = args.size(); String[] cmd = new String[argsSize + 1]; cmd[0] = ffmpegExecutablePath; for (int i = 0; i < argsSize; i++) { cmd[i + 1] = (String) args.get(i); } Runtime runtime = Runtime.getRuntime(); log.info("exec cmd: {}", Arrays.toString(cmd)); ffmpeg = runtime.exec(cmd); ffmpegKiller = new ProcessKiller(ffmpeg); runtime.addShutdownHook(ffmpegKiller); inputStream = ffmpeg.getInputStream(); outputStream = ffmpeg.getOutputStream(); errorStream = ffmpeg.getErrorStream(); } ``` 看到这里大家应该已经有了一个清晰的思路。 **在使用中,本来不想改源码,基于框架内置的ffmpeg命令进行转换,但是发现,怎么都无法获取地址** 原因如下: ```java public abstract class FFMPEGLocator { protected abstract String getFFMPEGExecutablePath(); FFMPEGExecutor createExecutor() { return new FFMPEGExecutor(getFFMPEGExecutablePath()); } } ``` 看这段源码可以看到,内置的ffmpeg命令执行路径,被隐藏了,只能同包内引用。 后准备使用内置的创建命令,查看源码后,发现FFMPEGLocator类是public,故准备执行 `new FFMPEGLocator();`来创建对象获取ffmpeg命令执行路径,看源码后,发现次方法暴露的依然是protected,故最终对源码进行了修改,打包并发布到maven私服上。 **本次修改,新增了FormatEnum枚举类,在Encoder -> encode方法执行是,执行`FormatEnum.get(formatAttribute).handle(ffmpeg, target);`,** > def 默认执行类,在类型无法匹配或不存在时,执行此方法(兼容原来的代码,不需要在方法修改后去修改之前的项目代码) > ssegment 当使用此编码格式的时候,添加执行的命令集(此处因为没有做更多的测试,仅仅满足了我自己的项目要求,如有更多需求,可自行修改或添加更多格式) ```java public enum FormatEnum { def(){ @Override public void handle(FFMPEGExecutor ffmpeg, File target) { ffmpeg.addArgument("-y"); ffmpeg.addArgument(target.getAbsolutePath()); } }, ssegment(){ @Override public void handle(FFMPEGExecutor ffmpeg, File target) { ffmpeg.addArgument("-map"); ffmpeg.addArgument("0"); ffmpeg.addArgument("-segment_format"); ffmpeg.addArgument("mpegts"); ffmpeg.addArgument("-segment_list"); if(!target.getParentFile().exists())target.getParentFile().mkdirs(); ffmpeg.addArgument(target.getAbsolutePath());//拼接存放目录 ffmpeg.addArgument("-segment_time"); ffmpeg.addArgument("10"); ffmpeg.addArgument(target.getParent() + File.separator + "%03d.ts");//拼接存放目录 } }; FormatEnum() {} public abstract void handle(FFMPEGExecutor ffmpeg, File target); public static FormatEnum get(String name){ if(name == null || name.isEmpty()) return def; try{ return FormatEnum.valueOf(name); }catch (IllegalArgumentException e){ return def; } } public static void main(String[] args) { System.out.println(FormatEnum.get("ssegment")); } } ```