First POST build by Jekyll.github:https://github.com/nishibaiyang
Android视频拼接
直接进入正题
###拼接方式
视频的拼接一般就是两种方式:一,通过ffmpeg。二,通过视频操作工具mp4parser。
###FFmpeg
在android上能否直接操作ffmpeg呢?那就要通过一些开源库了。这些开源库直接封装的ffmpeg的操作,只需要通过调用命令即可执行。在Java中通过JNI使用ffmpeg具体的实现感兴趣可以深入了解。
关于这个开源库可以上github上查查类似FFmpegAndroid。https://androidlearnersite.wordpress.com/2017/03/17/ffmpeg-video-editor/
那剩下的其实就是ffmpeg的操作指令了。这些简单介绍一些我用到的。
####应用场景1:格式转换
我想把用iPhone拍的.MOV文件转成.avi文件。最简单了,可以执行下面的命令:
ffmpeg -i D:\Media\IMG_0873.MOV D:\Media\output.avi
意思是,把D:\Media目录下的源文件IMG_0873.MOV(视频:h.264,音频:aac)转换成output.avi(编码格式自动选择为:视频mpeg4,音频mp3),目标文件仍然保存到D:\Media目录下。问题来了:我想自己指定编码格式,怎么办呢?一种方法是,通过目标文件的扩展名(.flv、.mpg、.mp4、.wmv等)来控制,比如:
ffmpeg -i D:\Media\IMG_0873.MOV D:\Media\output2.flv
另一种方法是通过-c:v参数来控制,比如我想输出的视频格式是H.265(警告:编码时间会比较长哦)。命令行如下:
ffmpeg -i D:\Media\IMG_0873.MOV -c:v libx265 D:\Media\output265.avi
注:可以先用ffmpeg -encoders命令查看一下所有可选的编码格式。
不再深究了,我们继续。我发现源文件的图像帧尺寸是1920x 1080,我不需要这么大——能有720 x 480就够了。于是,就要用上-s参数了。为了保证图像缩放后的质量,最好加上码流参数-b:v。如下:
ffmpeg -i D:\Media\IMG_0873.MOV -s 720x480 -b:v 1500k D:\Media\output2.avi
还可以更简单一点,使用-target参数匹配行业标准,参数值可以是vcd、svcd、dvd、dv、dv50等,可能还需要加上电视制式作为前缀(pal-、ntsc-或film-)。如下:
ffmpeg -i D:\Media\IMG_0873.MOV -target pal-dvd D:\Media\output2dvd.avi
又来一个问题:我发现用手机拍的视频中,有些是颠倒的,我想让它顺时针旋转90度。这时候,可以使用-vf参数加入一个过滤器,如下:
ffmpeg -i D:\Media\IMG_0873.MOV -vf “rotate=90*PI/180” D:\Media\output3.avi
注:如果想逆时针旋转90度,90前面加个负号就可以了。
如果我只需要从源视频里截取一小段,怎么办呢?比如从第2秒的地方开始,往后截取10秒钟。命令行可以这样:
ffmpeg -ss 2 -t 10 -i D:\Media\IMG_0873.MOV D:\Media\output4.avi
注:这种情况下,-ss和-t参数必须放在-i前面,表示是限定后面跟着的输入文件的。
####应用场景2:视频合成
我发现,用手机拍的视频有时候背景噪音比较大。怎么把噪音去掉,换成一段美妙的音乐呢?使用FFmpeg也能轻易做到。
第一步:把源文件里的音频去掉,生成一个临时文件tmp.mov
ffmpeg -i D:\Media\IMG_0873.MOV -vcodec copy -an D:\Media\tmp.mov
注:-vcodeccopy的意思是对源视频不解码,直接拷贝到目标文件;-an的意思是将源文件里的音频丢弃。
第二步:把这个无声的视频文件(tmp.mov)与一个音乐文件(music.mp3)合成,最终生成output.mov
ffmpeg -i D:\Media\tmp.mov -ss 30 -t 52 -i D:\Media\music.mp3 -vcodec copy D:\Media\output5.avi
为了保证良好的合成效果,音乐时长必须匹配视频时长。这里我们事先知道视频时长为52秒,于是截取music.mp3文件的第30秒往后的52秒与视频合成。另外,为了保证音频时长截取的准确性,我们这里没有使用-acodec copy,而是让音频重新转码。
还有一种情况:我们希望在一段视频上叠加一张图片。可以简单实现如下:
ffmpeg -i D:\Media\IMG_0873.MOV -i D:\Media\logo.png -filter_complex ‘overlay’ D:\Media\output6.avi
####应用场景3:视频播放
格式转换或合成之后,我们需要试着播放一下。播放器的选择很多。这里顺手用ffplay工具也行:
ffplay -i D:\Media\output6.avi
应用场景4:获取视频信息
有时候,我只是想看看这个视频文件的格式信息。可以用ffprobe工具:
ffprobe -i D:\Media\IMG_0873.MOV
####其他应用
FFmpeg的功能非常强大。关键是要理解各种参数的意义,并且巧妙搭配。必要的话,就把在线文档完整读一遍吧:http://www.ffmpeg.org/ffmpeg.html
###MP4Parse 这个其实比ffmpeg更好用,毕竟操作简单,速度还快嘛。但是看名字就知道是针对mp4格式的,所以还是局限性呀。
####简单介绍下封装的基本方法
/**
* 对Mp4文件集合进行追加合并(按照顺序一个一个拼接起来)
*
* @param mp4PathList [输入]Mp4文件路径的集合(支持m4a)(不支持wav)
* @param outPutPath [输出]结果文件全部名称包含后缀(比如.mp4)
* @throws IOException 格式不支持等情况抛出异常
*/
public static void appendMp4List(List mp4PathList, String outPutPath){
try {
List mp4MovieList = new ArrayList<>();// Movie对象集合[输入]
for (String mp4Path : mp4PathList) {// 将每个文件路径都构建成一个Movie对象
mp4MovieList.add(MovieCreator.build(mp4Path));
}
List audioTracks = new LinkedList<>();// 音频通道集合
List videoTracks = new LinkedList<>();// 视频通道集合
for (Movie mp4Movie : mp4MovieList) {// 对Movie对象集合进行循环
for (Track inMovieTrack : mp4Movie.getTracks()) {
if ("soun".equals(inMovieTrack.getHandler())) {// 从Movie对象中取出音频通道
audioTracks.add(inMovieTrack);
}
if ("vide".equals(inMovieTrack.getHandler())) {// 从Movie对象中取出视频通道
videoTracks.add(inMovieTrack);
}
}
}
Movie resultMovie = new Movie();// 结果Movie对象[输出]
if (!audioTracks.isEmpty()) {// 将所有音频通道追加合并
resultMovie.addTrack(new AppendTrack(audioTracks.toArray(new Track[audioTracks.size()])));
}
if (!videoTracks.isEmpty()) {// 将所有视频通道追加合并
resultMovie.addTrack(new AppendTrack(videoTracks.toArray(new Track[videoTracks.size()])));
}
Container outContainer = new DefaultMp4Builder().build(resultMovie);// 将结果Movie对象封装进容器
FileChannel fileChannel = new RandomAccessFile(String.format(outPutPath), "rw").getChannel();
outContainer.writeContainer(fileChannel);// 将容器内容写入磁盘
fileChannel.close();
}catch(Exception e){
e.printStackTrace();
}
}
/**
* 对AAC文件集合进行追加合并(按照顺序一个一个拼接起来)
*
* @param aacPathList [输入]AAC文件路径的集合(不支持wav)
* @param outPutPath [输出]结果文件全部名称包含后缀(比如.aac)
* @throws IOException 格式不支持等情况抛出异常
*/
public static void appendAacList(List aacPathList, String outPutPath){
try{
List audioTracks = new LinkedList<>();// 音频通道集合
for (int i = 0; i < aacPathList.size(); i++) {// 将每个文件路径都构建成一个AACTrackImpl对象
audioTracks.add(new AACTrackImpl(new FileDataSourceImpl(aacPathList.get(i))));
}
Movie resultMovie = new Movie();// 结果Movie对象[输出]
if (!audioTracks.isEmpty()) {// 将所有音频通道追加合并
resultMovie.addTrack(new AppendTrack(audioTracks.toArray(new Track[audioTracks.size()])));
}
Container outContainer = new DefaultMp4Builder().build(resultMovie);// 将结果Movie对象封装进容器
FileChannel fileChannel = new RandomAccessFile(String.format(outPutPath), "rw").getChannel();
outContainer.writeContainer(fileChannel);// 将容器内容写入磁盘
fileChannel.close();
}catch (Exception e){
e.printStackTrace();
}
}
private static List moviesList = new ArrayList<>();
private static List videoTracks = new ArrayList<>();
private static List audioTracks = new ArrayList<>();
//将两个mp4视频进行拼接
public static void appendMp4(List mMp4List,String outputpath){
try {
for (int i=0;i<mMp4List.size();i++) {
Movie movie=MovieCreator.build(mMp4List.get(i));
moviesList.add(movie);
}
} catch (IOException e) {
e.printStackTrace();
}
for (Movie m : moviesList) {
for (Track t : m.getTracks()) {
if (t.getHandler().equals("soun")) {
audioTracks.add(t);
}
if (t.getHandler().equals("vide")) {
videoTracks.add(t);
}
}
}
Movie result = new Movie();
try {
if (audioTracks.size() > 0) {
result.addTrack(new AppendTrack(audioTracks.toArray(new Track[audioTracks.size()])));
}
if (videoTracks.size() > 0) {
result.addTrack(new AppendTrack(videoTracks.toArray(new Track[videoTracks.size()])));
}
} catch (IOException e) {
e.printStackTrace();
}
Container out = new DefaultMp4Builder().build(result);
try {
FileChannel fc = new FileOutputStream(new File(outputpath)).getChannel();
out.writeContainer(fc);
fc.close();
Log.d("yzj","success");
} catch (Exception e) {
e.printStackTrace();
}
moviesList.clear();
}
/**
* 将 AAC 和 MP4 进行混合[替换了视频的音轨]
*
* @param aacPath .aac
* @param mp4Path .mp4
* @param outPath .mp4
*/
public static boolean muxAacMp4(String aacPath, String mp4Path, String outPath) {
boolean flag=false;
try {
AACTrackImpl aacTrack = new AACTrackImpl(new FileDataSourceImpl(aacPath));
Movie videoMovie = MovieCreator.build(mp4Path);
Track videoTracks = null;// 获取视频的单纯视频部分
for (Track videoMovieTrack : videoMovie.getTracks()) {
if ("vide".equals(videoMovieTrack.getHandler())) {
videoTracks = videoMovieTrack;
}
}
Movie resultMovie = new Movie();
resultMovie.addTrack(videoTracks);// 视频部分
resultMovie.addTrack(aacTrack);// 音频部分
Container out = new DefaultMp4Builder().build(resultMovie);
FileOutputStream fos = new FileOutputStream(new File(outPath));
out.writeContainer(fos.getChannel());
fos.close();
flag=true;
Log.e("update_tag","merge finish");
} catch (Exception e) {
e.printStackTrace();
flag=false;
}
return flag;
}
/**
* 将 M4A 和 MP4 进行混合[替换了视频的音轨]
*
* @param m4aPath .m4a[同样可以使用.mp4]
* @param mp4Path .mp4
* @param outPath .mp4
*/
public static void muxM4AMp4(String m4aPath, String mp4Path, String outPath) throws IOException {
Movie audioMovie = MovieCreator.build(m4aPath);
Track audioTracks = null;// 获取视频的单纯音频部分
for (Track audioMovieTrack : audioMovie.getTracks()) {
if ("soun".equals(audioMovieTrack.getHandler())) {
audioTracks = audioMovieTrack;
}
}
Movie videoMovie = MovieCreator.build(mp4Path);
Track videoTracks = null;// 获取视频的单纯视频部分
for (Track videoMovieTrack : videoMovie.getTracks()) {
if ("vide".equals(videoMovieTrack.getHandler())) {
videoTracks = videoMovieTrack;
}
}
Movie resultMovie = new Movie();
resultMovie.addTrack(videoTracks);// 视频部分
resultMovie.addTrack(audioTracks);// 音频部分
Container out = new DefaultMp4Builder().build(resultMovie);
FileOutputStream fos = new FileOutputStream(new File(outPath));
out.writeContainer(fos.getChannel());
fos.close();
}
/**
* 分离mp4视频的音频部分,只保留视频部分
*
* @param mp4Path .mp4
* @param outPath .mp4
*/
public static void splitMp4(String mp4Path, String outPath){
try{
Movie videoMovie = MovieCreator.build(mp4Path);
Track videoTracks = null;// 获取视频的单纯视频部分
for (Track videoMovieTrack : videoMovie.getTracks()) {
if ("vide".equals(videoMovieTrack.getHandler())) {
videoTracks = videoMovieTrack;
}
}
Movie resultMovie = new Movie();
resultMovie.addTrack(videoTracks);// 视频部分
Container out = new DefaultMp4Builder().build(resultMovie);
FileOutputStream fos = new FileOutputStream(new File(outPath));
out.writeContainer(fos.getChannel());
fos.close();
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 分离mp4的视频部分,只保留音频部分
*
* @param mp4Path .mp4
* @param outPath .aac
*/
public static void splitAac(String mp4Path, String outPath){
try{
Movie videoMovie = MovieCreator.build(mp4Path);
Track videoTracks = null;// 获取音频的单纯视频部分
for (Track videoMovieTrack : videoMovie.getTracks()) {
if ("soun".equals(videoMovieTrack.getHandler())) {
videoTracks = videoMovieTrack;
}
}
Movie resultMovie = new Movie();
resultMovie.addTrack(videoTracks);// 音频部分
Container out = new DefaultMp4Builder().build(resultMovie);
FileOutputStream fos = new FileOutputStream(new File(outPath));
out.writeContainer(fos.getChannel());
fos.close();
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 分离mp4视频的音频部分,只保留视频部分
*
* @param mp4Path .mp4
* @param mp4OutPath mp4视频输出路径
* @param aacOutPath aac视频输出路径
*/
public static void splitVideo(String mp4Path, String mp4OutPath,String aacOutPath){
try{
Movie videoMovie = MovieCreator.build(mp4Path);
Track videTracks = null;// 获取视频的单纯视频部分
Track sounTracks = null;// 获取视频的单纯音频部分
for (Track videoMovieTrack : videoMovie.getTracks()) {
if ("vide".equals(videoMovieTrack.getHandler())) {
videTracks = videoMovieTrack;
}
if ("soun".equals(videoMovieTrack.getHandler())) {
sounTracks = videoMovieTrack;
}
}
Movie videMovie = new Movie();
videMovie.addTrack(videTracks);// 视频部分
Movie sounMovie = new Movie();
sounMovie.addTrack(sounTracks);// 音频部分
// 视频部分
Container videout = new DefaultMp4Builder().build(videMovie);
FileOutputStream videfos = new FileOutputStream(new File(mp4OutPath));
videout.writeContainer(videfos.getChannel());
videfos.close();
// 音频部分
Container sounout = new DefaultMp4Builder().build(sounMovie);
FileOutputStream sounfos = new FileOutputStream(new File(aacOutPath));
sounout.writeContainer(sounfos.getChannel());
sounfos.close();
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 对 Mp4 添加字幕
*
* @param mp4Path .mp4 添加字幕之前
* @param outPath .mp4 添加字幕之后
*/
public static void addSubtitles(String mp4Path, String outPath) throws IOException {
Movie videoMovie = MovieCreator.build(mp4Path);
TextTrackImpl subTitleEng = new TextTrackImpl();// 实例化文本通道对象
subTitleEng.getTrackMetaData().setLanguage("eng");// 设置元数据(语言)
subTitleEng.getSubs().add(new TextTrackImpl.Line(0, 1000, "Five"));// 参数时间毫秒值
subTitleEng.getSubs().add(new TextTrackImpl.Line(1000, 2000, "Four"));
subTitleEng.getSubs().add(new TextTrackImpl.Line(2000, 3000, "Three"));
subTitleEng.getSubs().add(new TextTrackImpl.Line(3000, 4000, "Two"));
subTitleEng.getSubs().add(new TextTrackImpl.Line(4000, 5000, "one"));
subTitleEng.getSubs().add(new TextTrackImpl.Line(5001, 5002, " "));// 省略去测试
videoMovie.addTrack(subTitleEng);// 将字幕通道添加进视频Movie对象中
Container out = new DefaultMp4Builder().build(videoMovie);
FileOutputStream fos = new FileOutputStream(new File(outPath));
out.writeContainer(fos.getChannel());
fos.close();
}
/**
* 将 MP4 切割
*
* @param mp4Path .mp4
* @param fromSample 起始位置 不是传入的秒数
* @param toSample 结束位置 不是传入的秒数
* @param outPath .mp4
*/
public static void cropMp4(String mp4Path, long fromSample, long toSample, String outPath){
try{
Movie mp4Movie = MovieCreator.build(mp4Path);
Track videoTracks = null;// 获取视频的单纯视频部分
for (Track videoMovieTrack : mp4Movie.getTracks()) {
if ("vide".equals(videoMovieTrack.getHandler())) {
videoTracks = videoMovieTrack;
}
}
Track audioTracks = null;// 获取视频的单纯音频部分
for (Track audioMovieTrack : mp4Movie.getTracks()) {
if ("soun".equals(audioMovieTrack.getHandler())) {
audioTracks = audioMovieTrack;
}
}
Movie resultMovie = new Movie();
resultMovie.addTrack(new AppendTrack(new CroppedTrack(videoTracks, fromSample, toSample)));// 视频部分
resultMovie.addTrack(new AppendTrack(new CroppedTrack(audioTracks, fromSample, toSample)));// 音频部分
Container out = new DefaultMp4Builder().build(resultMovie);
FileOutputStream fos = new FileOutputStream(new File(outPath));
out.writeContainer(fos.getChannel());
fos.close();
}catch(Exception e){
e.printStackTrace();
}
}
</code></pre>
基本使用就到此为止,后面如果用到在继续补充。
[jekyll]: http://jekyllrb.com
[jekyll-gh]: https://github.com/jekyll/jekyll
[jekyll-help]: https://github.com/jekyll/jekyll-help