第一、FFmpeg 简介和基础知识
1.1 FFmpeg 简介
FFmpeg的名称来自MPEG视频编码标准,前面的“FF”代表“Fast Forward”,FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。可以轻易地实现多种视频格式之间的相互转换。包括如下几个部分:
1.2 FFmpeg 基础知识
容器(Container):一种文件格式,比如flv,mkv等。
流(Stream):一种视频数据信息的传输方式,5种流:音频,视频,字幕,附件,数据。
帧(Frame):帧代表一幅静止的图像,分为I帧,P帧,B帧。
编解码器(Codec):是对视频进行压缩或者解压缩,CODEC = COde(编码) +DECode(解码)。
复用/解复用(mux/demux):把不同的流按照某种容器的规则放入容器,这种行为叫做复用(mux)。
把不同的流从某种容器中解析出来,这种行为叫做解复用(demux)。
帧率:帧率也叫帧频率,帧率是视频文件中每一秒的帧数,肉眼想看到连续移动图像至少需要15帧。
码率:比特率(也叫码率,数据率)是一个确定整体视频/音频质量的参数,秒为单位处理的字节数,码率和视频质量成正比,在视频文件中中比特率用bps来表达。
第二、FFmpeg 命令行工具使用指南
2.1 什么是FFmpeg
FFmpeg (命令行工具) 是一个快速的音视频转换工具。
2.2 FFmpeg 使用方法
FFmpeg 语法格式:
ffmpeg {1} {2} -i {3} {4} {5}
上面命令中,五个部分的参数依次如下。
示例:将 mp4 文件转成 webm 文件,这两个都是容器格式。输入的 mp4 文件的音频编码格式是 aac,视频编码格式是 H.264;输出的 webm 文件的视频编码格式是 VP9,音频格式是 Vorbis
ffmpeg \
-y \ # 全局参数
-c:a libfdk_aac -c:v libx264 \ # 输入文件参数
-i input.mp4 \ # 输入文件
-c:v libvpx-vp9 -c:a libvorbis \ # 输出文件参数
output.webm # 输出文件
2.3 FFmpeg 常用命令行参数
-c:指定编码器
-c copy:直接复制,不经过重新编码(这样比较快)
-c:v:指定视频编码器
-c:a:指定音频编码器
-i:指定输入文件
-an:去除音频流
-vn: 去除视频流
-preset:指定输出的视频质量,会影响文件的生成速度,有以下几个可用的值 ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow。
-y:不经过确认,输出时直接覆盖同名文件。
2.4 FFmpeg 常见用法
2.4.1 查看文件信息:
查看视频文件的元信息,比如编码格式和比特率,可以只使用-i参数。
ffmpeg -i input.mp4
上面命令会输出很多冗余信息,加上-hide_banner参数,可以只显示元信息。
ffmpeg -i input.mp4 -hide_banner
2.4.2 转换编码格式
转换编码格式(transcoding)指的是, 将视频文件从一种编码转成另一种编码。比如转成 H.264 编码,一般使用编码器libx264,所以只需指定输出文件的视频编码器即可。
ffmpeg -i [input.file] -c:v libx264 output.mp4
转成 H.265 编码的写法。
ffmpeg -i [input.file] -c:v libx265 output.mp4
2.4.3 转换容器格式
转换容器格式(transmuxing)指的是,将视频文件从一种容器转到另一种容器。下面是 mp4 转 webm 的写法。
ffmpeg -i input.mp4 -c copy output.webm
注意:上面例子中,只是转一下容器,内部的编码格式不变,所以使用-c copy指定直接拷贝,不经过转码,这样比较快。
2.4.4 调整码率
调整码率(transrating)指的是,改变编码的比特率,一般用来将视频文件的体积变小。下面的例子指定码率最小为964K,最大为3856K,缓冲区大小为 2000K。
ffmpeg \
-i input.mp4 \
-minrate 964K -maxrate 3856K -bufsize 2000K \
output.mp4
2.4.5 改变分辨率(transsizing)
下面是改变视频分辨率(transsizing)的例子,从 1080p 转为 480p 。
ffmpeg \
-i input.mp4 \
-vf scale=480:-1 \
output.mp4
2.4.6 提取音频
有时,需要从视频里面提取音频(demuxing),可以像下面这样写。
ffmpeg \
-i input.mp4 \
-vn -c:a copy \
output.aac
上面例子中,-vn
表示去掉视频,-c:a copy
表示不改变音频编码,直接拷贝。
2.4.7 添加音轨
添加音轨(muxing)指的是,将外部音频加入视频,比如添加背景音乐或旁白。
ffmpeg \
-i input.aac -i input.mp4 \
output.mp4
上面例子中,有音频和视频两个输入文件,FFmpeg 会将它们合成为一个文件。
2.4.8 截图
下面的例子是从指定时间开始,连续对1秒钟的视频进行截图。
ffmpeg \
-y \
-i input.mp4 \
-ss 00:01:24 -t 00:00:01 \
output_%3d.jpg
如果只需要截一张图,可以指定只截取一帧。
ffmpeg \
-ss 01:23:45 \
-i input \
-vframes 1 -q:v 2 \
output.jpg
上面例子中,-vframes 1
指定只截取一帧,-q:v 2
表示输出的图片质量,一般是1到5之间(1 为质量最高)。
2.4.9 裁剪
裁剪(cutting)指的是,截取原始视频里面的一个片段,输出为一个新视频。可以指定开始时间(start)和持续时间(duration),也可以指定结束时间(end)。
$ ffmpeg -ss [start] -i [input] -t [duration] -c copy [output]
$ ffmpeg -ss [start] -i [input] -to [end] -c copy [output]
下面是实际的例子。
ffmpeg -ss 00:01:50 -i [input] -t 10.5 -c copy [output]
ffmpeg -ss 2.5 -i [input] -to 10 -c copy [output]
上面例子中,-c copy
表示不改变音频和视频的编码格式,直接拷贝,这样会快很多。
2.5.0 为音频添加封面
有些视频网站只允许上传视频文件。如果要上传音频文件,必须为音频添加封面,将其转为视频,然后上传。
下面命令可以将音频文件,转为带封面的视频文件。
ffmpeg \
-loop 1 \
-i cover.jpg -i input.mp3 \
-c:v libx264 -c:a aac -b:a 192k -shortest \
output.mp4
上面命令中,有两个输入文件,一个是封面图片cover.jpg
,另一个是音频文件input.mp3
。-loop 1
参数表示图片无限循环,-shortest
参数表示音频文件结束,输出视频就结束。
第三、FFplay 命令行工具使用指南
3.1 什么是FFplay
FFplay 是一个使用了 ffmpeg 和 sdl 库的简单的可移植的媒体播放器。
3.2 FFplay 使用方法
FFplay 语法格式:
ffplay [选项] ['输入文件']
FFplay使用示例:
1、播放 test.mkv ,播放完成后自动退出
ffplay -autoexit test.mkv
2、以 320 × 240 的大学播放 test.mkv
ffplay -x 320 -y 240 test.mkv
3.3 FFplay 选项类别
3.3.1 FFplay 通用选项
'-L' 显示 license
'-h, -?, -help, --help [arg]' 打印帮助信息;可以指定一个参数 arg ,如果不指定,只打印基本选项
可选的 arg 选项:
'long' 除基本选项外,还将打印高级选项
'full' 打印一个完整的选项列表,包含 encoders, decoders, demuxers, muxers, filters 等的共享以及私有选项
'decoder=decoder_name' 打印名称为 "decoder_name" 的解码器的详细信息
'encoder=encoder_name' 打印名称为 "encoder_name" 的编码器的详细信息
'demuxer=demuxer_name' 打印名称为 "demuxer_name" 的 demuxer 的详细信息
'muxer=muxer_name' 打印名称为 "muxer_name" 的 muxer 的详细信息
'filter=filter_name' 打印名称为 "filter_name" 的过滤器的详细信息
'-version' 显示版本信息
'-formats' 显示有效的格式
'-codecs' 显示 libavcodec 已知的所有编解码器
'-decoders' 显示有效的解码器
'-encoders' 显示有效的编码器
'-bsfs' 显示有效的比特流过滤器
'-protocols' 显示有效的协议
'-filters' 显示 libavfilter 有效的过滤器
'-pix_fmts' 显示有效的像素格式
'-sample_fmts' 显示有效的采样格式
'-layouts' 显示通道名称以及标准通道布局
'-colors' 显示认可的颜色名称
'-hide_banner' 禁止打印欢迎语;也就是禁止默认会显示的版权信息、编译选项以及库版本信息等
3.3.2 FFplay 主要选项
'-x width' 强制以 "width" 宽度显示
'-y height' 强制以 "height" 高度显示
'-an' 禁止音频
'-vn' 禁止视频
'-ss pos' 跳转到指定的位置(秒)
'-t duration' 播放 "duration" 秒音/视频
'-bytes' 按字节跳转
'-nodisp' 禁止图像显示(只输出音频)
'-f fmt' 强制使用 "fmt" 格式
'-window_title title' 设置窗口标题(默认为输入文件名)
'-loop number' 循环播放 "number" 次(0将一直循环)
'-showmode mode' 设置显示模式
可选的 mode :
'0, video' 显示视频
'1, waves' 显示音频波形
'2, rdft' 显示音频频带
默认值为 'video',你可以在播放进行时,按 "w" 键在这几种模式间切换
'-i input_file' 指定输入文件
3.3.3 FFplay 高级选项
'-sync type' 设置主时钟为音频、视频、或者外部。默认为音频。主时钟用来进行音视频同步
'-threads count' 设置线程个数
'-autoexit' 播放完成后自动退出
'-exitonkeydown' 任意键按下时退出
'-exitonmousedown' 任意鼠标按键按下时退出
'-acodec codec_name' 强制指定音频解码器为 "codec_name"
'-vcodec codec_name' 强制指定视频解码器为 "codec_name"
'-scodec codec_name' 强制指定字幕解码器为 "codec_name"
3.3.4 FFplay 快捷键
'q, ESC' 退出
'f' 全屏
'p, SPC' 暂停
'w' 切换显示模式(视频/音频波形/音频频带)
's' 步进到下一帧
'left/right' 快退/快进 10 秒
'down/up' 快退/快进 1 分钟
'page down/page up' 跳转到前一章/下一章(如果没有章节,快退/快进 10 分钟)
'mouse click' 跳转到鼠标点击的位置(根据鼠标在显示窗口点击的位置计算百分比)
参考资料
第四、FFprobe 命令行工具使用指南
4.1 什么是FFprobe
ffprobe 是一个多媒体流分析工具。它从多媒体流中收集信息,并且以人类和机器可读的形式打印出来。它可以用来检测多媒体流的容器类型,以及每一个多媒体流的格式和类型。它可以作为一个独立的应用来使用,也可以结合文本过滤器执行更复杂的处理。
4.2 FFprobe 使用方法
FFprobe 语法格式:
ffprobe [选项] ['输入文件']
FFprobe 使用示例:
最简单的使用方式
ffprobe test.mp4
不显示欢迎信息
ffprobe -hide_banner test.mp4
以 JSON 格式显示每个流的信息
ffprobe -hide_banner -print_format json -show_streams test.mp4
显示容器格式相关信息
ffprobe -hide_banner -show_format test.mp4
4.3 FFprobe选项
‘-f format’ 强制使用的格式
‘-unit’ 显示值的单位
‘-prefix’ 显示的值使用标准国际单位制词头
‘-byte_binary_prefix’ 对字节值强制使用二进制前缀
‘-sexagesimal’ 时间值使用六十进位的格式 HH:MM:SS.MICROSECONDS
‘-pretty’ 美化显示值的格式。它相当于 "-unit -prefix -byte_binary_prefix -sexagesimal"
‘-of, -print_format writer_name[=writer_options]’
设置输出打印格式。writer_name 指定打印程序 (writer) 的名称,writer_options
指定传递给 writer 的选项。例如:将输出打印为 JSON 格式:-print_format json
‘-select_streams stream_specifier’
只选择 stream_specifier 指定的流。该选项只影响那些与流相关的选项
(例如:show_streams, show_packets, 等)。
举例:只显示音频流,使用命令:
ffprobe -show_streams -select_streams a INPUT
‘-show_data’ 显示有效载荷数据,以十六进制和ASCII转储。与 ‘-show_packets’ 结合使用,它将
dump 包数据;与 ‘-show_streams’ 结合使用,它将 dump codec 附加数据。
‘-show_error’ 显示探测输入文件时的错误信息
‘-show_format’ 显示输入多媒体流的容器格式信息
‘-show_packets’ 显示输入多媒体流中每一个包的信息
‘-show_frames’ 显示输入多媒体流中的每一帧以及字幕的信息
‘-show_streams’ 显示输入多媒体流中每一个流的信息
‘-show_programs’ 显示输入多媒体流中程序以及它们的流的信息
‘-show_chapters’ 显示格式中存储的章节信息
‘-count_frames’ 计算每一个流中的帧数,在相应的段中进行显示
‘-count_packets’ 计算每一个流中的包数,在相应的段中进行显示
‘-show_program_version’ 显示程序版本及配置相关信息
‘-show_library_versions’ 显示库版本相关信息
‘-show_versions’ 显示程序和库版本相关信息。相当于同时设置‘-show_program_version’ 和
‘-show_library_versions’
‘-i input_file’ 指定输入文件
第五、FFmpeg 之Java功能代码封装
package com.zzg.ffmpeg;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import com.zzg.ffmpeg.config.FfmpegConfig;
import com.zzg.ffmpeg.config.params.FfmpegParam;
import com.zzg.ffmpeg.config.params.FfmpegParams;
import com.zzg.ffmpeg.exception.FfmpegTransformException;
import com.zzg.ffmpeg.media.Media;
import com.zzg.ffmpeg.media.MediaType;
/**
* ffmpeg 核心功能代码
*
* @author zzg
*
*/
public class Ffmpeg {
private final FfmpegConfig ffmpegConfig;
private final List medias;
private final FfmpegParams params;
private List successValues = new ArrayList(Arrays.asList(0));
/**
* Timeout to wait while generating a PDF, in seconds
*/
private int timeout = 10;
public int getTimeout() {
return timeout;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
public Ffmpeg() {
this(new FfmpegConfig());
}
public Ffmpeg(FfmpegConfig ffmpegConfig) {
super();
this.ffmpegConfig = ffmpegConfig;
this.medias = new ArrayList();
this.params = new FfmpegParams();
}
/**
* 输入文件
*
* @param source
*/
public void addMediaInput(String source) {
this.medias.add(new Media(source, MediaType.input));
}
/**
* 输出文件
*
* @param source
*/
public void addMediaOutput(String source) {
this.medias.add(new Media(source, MediaType.output));
}
/**
* ffmpeg 请求参数
*
* @param param
* @param params
*/
public void addParam(FfmpegParam param, FfmpegParam... params) {
this.params.add(param, params);
}
public byte[] getFFMPEG() throws IOException, InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
// cmd 执行命令
String command = getCommand();
System.out.println("输出指令:" + command);
Process process = Runtime.getRuntime().exec(getCommandAsArray());
Future inputStreamToByteArray = executor.submit(streamToByteArrayTask(process.getInputStream()));
Future outputStreamToByteArray = executor.submit(streamToByteArrayTask(process.getErrorStream()));
process.waitFor();
if (!successValues.contains(process.exitValue())) {
byte[] errorStream = getFuture(outputStreamToByteArray);
throw new FfmpegTransformException(command, process.exitValue(), errorStream,
getFuture(inputStreamToByteArray));
}
return getFuture(inputStreamToByteArray);
} finally {
executor.shutdownNow();
}
}
private Callable streamToByteArrayTask(final InputStream input) {
return new Callable() {
public byte[] call() throws Exception {
return IOUtils.toByteArray(input);
}
};
}
private byte[] getFuture(Future future) {
try {
return future.get(this.timeout, TimeUnit.SECONDS);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Gets the final ffmpeg command as string
*
* @return the generated command from params
* @throws IOException
*/
public String getCommand() throws IOException {
return StringUtils.join(getCommandAsArray(), " ");
}
protected String[] getCommandAsArray() throws IOException {
List commandLine = new ArrayList();
// 指令部分ffmpeg
commandLine.add(ffmpegConfig.getFfmpegCommand());
commandLine.add("-i");
// 输入文件
for (Media media : medias) {
if (media.getType().equals(MediaType.input)) {
commandLine.add(media.getSource());
}
}
// 参数部分
commandLine.addAll(params.getParamsAsStringList());
// 输出文件
for (Media media : medias) {
if (media.getType().equals(MediaType.output)) {
commandLine.add(media.getSource());
}
}
return commandLine.toArray(new String[commandLine.size()]);
}
}
package com.zzg.ffmpeg.config;
import java.io.IOException;
import java.nio.charset.Charset;
import org.apache.commons.io.IOUtils;
import com.zzg.ffmpeg.exception.FfmpegConfigurationException;
/**
* ffmpeg 配置对象
*
* @author zzg
*
*/
public class FfmpegConfig {
private String ffmpegCommand = "ffmpeg";
public String getFfmpegCommand() {
return ffmpegCommand;
}
public void setFfmpegCommand(String ffmpegCommand) {
this.ffmpegCommand = ffmpegCommand;
}
public FfmpegConfig() {
setFfmpegCommand(findExecutable());
}
public FfmpegConfig(String ffmpegCommand) {
super();
this.ffmpegCommand = ffmpegCommand;
}
public String findExecutable() {
try {
String osname = System.getProperty("os.name").toLowerCase();
String cmd = osname.contains("windows") ? "where.exe ffmpeg" : "which ffmpeg";
Process p = Runtime.getRuntime().exec(cmd);
p.waitFor();
String text = IOUtils.toString(p.getInputStream(), Charset.defaultCharset()).trim();
if (text.isEmpty()){
throw new FfmpegConfigurationException("ffmpeg command was not found in your classpath. " +
"Verify its installation or initialize wrapper configurations with correct path/to/ffmpeg");
}
setFfmpegCommand(text);
} catch (IOException e) {
// log日志记录错误信息, 暂时打印堆栈信息
e.printStackTrace();
} catch (InterruptedException e) {
// log日志记录错误信息, 暂时打印堆栈信息
e.printStackTrace();
}
return getFfmpegCommand();
}
}
package com.zzg.ffmpeg.config.params;
import java.util.ArrayList;
import java.util.List;
/**
* ffmpeg 请求参数实体对象封住
*
* @author zzg
*
*/
public class FfmpegParam {
private String key;
// 某些指令接受多个参数值
private List values = new ArrayList();
public FfmpegParam(String key, String... valueArray) {
this.key = key;
for (String value : valueArray) {
values.add(value);
}
}
// 某些指令无参数值
public FfmpegParam(String key) {
this(key, new String[0]);
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public List getValues() {
return values;
}
public void setValues(List values) {
this.values = values;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder().append(FfmpegSymbol.separator).append(FfmpegSymbol.param).append(key);
for (String value : values) {
sb.append(FfmpegSymbol.separator).append(value);
}
return sb.toString();
}
}
package com.zzg.ffmpeg.config.params;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
* 批量处理FfmpegParam 请求参数对象
* @author zzg
*
*/
public class FfmpegParams {
private Collection params;
public FfmpegParams() {
this.params = new ArrayList();
}
public void add(FfmpegParam param, FfmpegParam... params) {
this.params.add(param);
this.params.addAll( Arrays.asList( params ) );
}
public List getParamsAsStringList() {
List commandLine = new ArrayList();
for (FfmpegParam p : params) {
commandLine.add(p.getKey());
for (String value : p.getValues()) {
if (value != null) {
commandLine.add(value);
}
}
}
return commandLine;
}
}
package com.zzg.ffmpeg.config.params;
/**
* ffmpeg 符号
* @author zzg
*
*/
public enum FfmpegSymbol {
separator(" "), param("");
private final String symbol;
FfmpegSymbol(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
}
package com.zzg.ffmpeg.exception;
@SuppressWarnings("serial")
public class FfmpegConfigurationException extends RuntimeException {
public FfmpegConfigurationException(String message) {
super(message);
// TODO Auto-generated constructor stub
}
}
package com.zzg.ffmpeg.exception;
/**
* ffmpeg 转换异常
*
* @author zzg
*
*/
@SuppressWarnings("serial")
public class FfmpegTransformException extends RuntimeException {
private String command;
private int exitStatus;
private byte[] out;
private byte[] err;
public FfmpegTransformException(String command, int exitStatus, byte[] err, byte[] out) {
this.command = command;
this.exitStatus = exitStatus;
this.err = err;
this.out = out;
}
public String getCommand() {
return command;
}
public int getExitStatus() {
return exitStatus;
}
public byte[] getOut() {
return out;
}
public byte[] getErr() {
return err;
}
@Override
public String getMessage() {
return "Process (" + this.command + ") exited with status code " + this.exitStatus + ":\n" + new String(err);
}
}
package com.zzg.ffmpeg.exception;
/**
* ffmpeg 转换异常
*
* @author zzg
*
*/
@SuppressWarnings("serial")
public class FfmpegTransformException extends RuntimeException {
private String command;
private int exitStatus;
private byte[] out;
private byte[] err;
public FfmpegTransformException(String command, int exitStatus, byte[] err, byte[] out) {
this.command = command;
this.exitStatus = exitStatus;
this.err = err;
this.out = out;
}
public String getCommand() {
return command;
}
public int getExitStatus() {
return exitStatus;
}
public byte[] getOut() {
return out;
}
public byte[] getErr() {
return err;
}
@Override
public String getMessage() {
return "Process (" + this.command + ") exited with status code " + this.exitStatus + ":\n" + new String(err);
}
}
package com.zzg.ffmpeg.media;
/**
* 媒体文件:输入输出类型
* @author zzg
*
*/
public enum MediaType {
input,
output
}
package com.zzg.test;
import java.io.IOException;
import com.zzg.ffmpeg.Ffmpeg;
import com.zzg.ffmpeg.config.params.FfmpegParam;
public class FfmpegTest {
public static void main(String[] args) throws IOException, InterruptedException {
// TODO Auto-generated method stub
Ffmpeg ffmpeg = new Ffmpeg();
ffmpeg.addMediaInput("C:\\ffmpg\\ffmpeg\\bin\\output.mp4");
ffmpeg.addMediaOutput("C:\\ffmpg\\ffmpeg\\bin\\test.mp4");
ffmpeg.addParam(new FfmpegParam("-vcodec", new String[]{"copy"}), new FfmpegParam[]{new FfmpegParam("-an", new String[]{})});
byte[] bytes = ffmpeg.getFFMPEG();
System.out.println(new String(bytes));
}
}
pom.xml 文件相关依赖:
2.6
3.9
commons-io
commons-io
${commons-io.version}
org.apache.commons
commons-lang3
${commons-lang3.version}