aoi学院

Aisaka's Blog, School of Aoi, Aisaka University

Android-音视频-视频开发-(五)视频处理

简介

一般来说视频处理有两种方式,一种是软编解码(软解,FFmpeg),一种是硬编解码(硬解,MediaCodec)。

两者的优缺点如下:

FFmpegMediaCodec
优点(1)封装了很多的格式,使用起来比较灵活、简单、兼容性好,功能强大;(2)命令行的方式很方便,比如视频裁剪的步骤:ffmpeg -ss 10 -t 20 -i INPUT -acodec copy -vcodec copy OUTPUT,相对来说写个函数去实现就太麻烦。功耗低,速度快
缺点软编解码功耗大扩展性不强,不同芯片厂商提供的支持方案不同,导致程序移植性差

软编解码

FFmpeg库的引入

引入库的方式有两种:aar或者源码依赖

aar依赖

1
2
3
dependencies {
compile 'com.writingminds:FFmpegAndroid:0.3.2'
}

源码依赖

1
2
// 用git将ffmpeg-android-java clone到本地,然后把项目中FFmpegAndroid库的源码加入到项目与app同级的目录中。
// https://github.com/WritingMinds/ffmpeg-android-java

库的初始化

初始化的目的是根据Android手机的cpu架构,load对应架构的ffmpeg库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ZApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
initFFmpegBinary(this);
}

private void initFFmpegBinary(Context context) {
try {
FFmpeg.getInstance(context).loadBinary(new LoadBinaryResponseHandler() {
@Override
public void onFailure() {
}
});
} catch (FFmpegNotSupportedException e) {
e.printStackTrace();
}
}
}

执行ffmpeg的commend命令

这个库是对ffmpeg的在Linux系统中命令行的一个封装,在FFmpegInterface.java类中找到了如下的API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Executes a command
* @param environvenmentVars Environment variables
* @param cmd command to execute
* @param ffmpegExecuteResponseHandler {@link FFmpegExecuteResponseHandler}
* @throws FFmpegCommandAlreadyRunningException
*/
public void execute(Map<String, String> environvenmentVars, String[] cmd, FFmpegExecuteResponseHandler ffmpegExecuteResponseHandler) throws FFmpegCommandAlreadyRunningException;

/**
* Executes a command
* @param cmd command to execute
* @param ffmpegExecuteResponseHandler {@link FFmpegExecuteResponseHandler}
* @throws FFmpegCommandAlreadyRunningException
*/
public void execute(String[] cmd, FFmpegExecuteResponseHandler ffmpegExecuteResponseHandler) throws FFmpegCommandAlreadyRunningException;

视频裁剪

视频裁剪:ffmpeg -ss START -t DURATION -i INPUT -vcodec copy -acodec copy OUTPUT

视频合并

方法一:FFmpeg concat 协议

1
2
3
4
5
6
7
8
9
对于 MPEG 格式的视频,可以直接连接:
ffmpeg -i "concat:input1.mpg|input2.mpg|input3.mpg" -c copy output.mpg

对于非 MPEG 格式容器,但是是 MPEG 编码器(H.264、DivX、XviD、MPEG4、MPEG2、AAC、MP2、MP3 等),可以包装进 TS 格式的容器再合并。在新浪视频,有很多视频使用 H.264 编码器,可以采用这个方法
ffmpeg -i input1.flv -c copy -bsf:v h264_mp4toannexb -f mpegts input1.ts
ffmpeg -i input2.flv -c copy -bsf:v h264_mp4toannexb -f mpegts input2.ts
ffmpeg -i input3.flv -c copy -bsf:v h264_mp4toannexb -f mpegts input3.ts
ffmpeg -i "concat:input1.ts|input2.ts|input3.ts" -c copy -bsf:a aac_adtstoasc -movflags +faststart output.mp4
保存 QuickTime/MP4 格式容器的时候,建议加上 -movflags +faststart。这样分享文件给别人的时候可以边下边看。

方法二:FFmpeg concat 分离器

1
2
3
4
5
6
7
8
先创建一个文本文件filelist.txt:
file 'input1.mkv'
file 'input2.mkv'
file 'input3.mkv'

然后:
ffmpeg -f concat -i filelist.txt -c copy output.mkv
注意:使用 FFmpeg concat 分离器时,如果文件名有奇怪的字符,要在 filelist.txt 中转义。

方法三:Mencoder 连接文件并重建索引

1
2
对于没有使用 MPEG 编码器的视频(如 FLV1 编码器),可以尝试这种方法。
mencoder -forceidx -of lavf -oac copy -ovc copy -o output.flv input1.flv input2.flv input3.flv

方法四:使用 FFmpeg concat 过滤器重新编码(有损)

1
2
3
4
这个方法可以合并不同编码器的视频片段,也可以作为其他方法失效的后备措施。
ffmpeg -i input1.mp4 -i input2.webm -i input3.avi -filter_complex '[0:0] [0:1] [1:0] [1:1] [2:0] [2:1] concat=n=3:v=1:a=1 [v] [a]' -map '[v]' -map '[a]' <编码器选项> output.mkv
如你所见,上面的命令合并了三种不同格式的文件,FFmpeg concat 过滤器会重新编码它们。注意这是有损压缩。
[0:0] [0:1] [1:0] [1:1] [2:0] [2:1] 分别表示第一个输入文件的视频、音频、第二个输入文件的视频、音频、第三个输入文件的视频、音频。concat=n=3:v=1:a=1 表示有三个输入文件,输出一条视频流和一条音频流。[v] [a] 就是得到的视频流和音频流的名字,注意在 bash 等 shell 中需要用引号,防止通配符扩展。

其它常用命令

FFmpeg常用基本命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
1.分离视频音频流
ffmpeg -i input_file -vcodec copy -an output_file_video  //分离视频流
ffmpeg -i input_file -acodec copy -vn output_file_audio  //分离音频流

2.视频解复用
ffmpeg –i test.mp4 –vcodec copy –an –f m4v test.264
ffmpeg –i test.avi –vcodec copy –an –f m4v test.264

3.视频转码
ffmpeg –i test.mp4 –vcodec h264 –s 352*278 –an –f m4v test.264 //转码为码流原始文件
ffmpeg –i test.mp4 –vcodec h264 –bf 0 –g 25 –s 352*278 –an –f m4v test.264 //转码为码流原始文件
ffmpeg –i test.avi -vcodec mpeg4 –vtag xvid –qsame test_xvid.avi //转码为封装文件
//-bf B帧数目控制,-g 关键帧间隔控制,-s 分辨率控制

4.视频封装
ffmpeg –i video_file –i audio_file –vcodec copy –acodec copy output_file

5.视频剪切
ffmpeg –i test.avi –r 1 –f image2 image-%3d.jpeg //提取图片
ffmpeg -ss 0:1:30 -t 0:0:20 -i input.avi -vcodec copy -acodec copy output.avi //剪切视频
//-r 提取图像的频率,-ss 开始时间,-t 持续时间

6.视频录制
ffmpeg –i rtsp://192.168.3.205:5555/test –vcodec copy out.avi

7.YUV序列播放
ffplay -f rawvideo -video_size 1920x1080 input.yuv

8.YUV序列转AVI
ffmpeg –s w*h –pix_fmt yuv420p –i input.yuv –vcodec mpeg4 output.avi


【常用参数说明】
主要参数:
-i 设定输入流
-f 设定输出格式
-ss 开始时间
视频参数:
-b 设定视频流量,默认为200Kbit/s
-r 设定帧速率,默认为25
-s 设定画面的宽与高
-aspect 设定画面的比例
-vn 不处理视频
-vcodec 设定视频编解码器,未设定时则使用与输入流相同的编解码器
音频参数:
-ar 设定采样率
-ac 设定声音的Channel数
-acodec 设定声音编解码器,未设定时则使用与输入流相同的编解码器
-an 不处理音频

硬编解码

exoplayer

如果播放器部分是用exoplayer,也是硬解,这样就可以减少很多体积,且内置了一些功能可以直接调用。

使用exoplayer的原因:

  • 谷歌官方出品的开源库,易于自定义和扩展,exoplayer专门为此做了设计,准许很多组件可以被自定义的实现类替换
  • java编写,相比于native code,开发更容易,更清楚的获得一些异常源和进行部分代码调试
  • 较少的设备兼容问题
功能exoplayer是否已有api支持拓展使用的基类和接口
视频裁剪已有支持ClippingMediaSource
素材拼接已有支持ConcatenatingMediaSource
视频变速已有支持SimpleExoPlayer.setPlaybackParameters

视频裁剪:

视频裁剪播放使用ClippingMediaSource设置裁剪素材,按api文档传入起始时间和结束时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Creates a new clipping source that wraps the specified source and provides samples between the
* specified start and end position.
*
* @param mediaSource The single-period source to wrap.
* @param startPositionUs The start position within {@code mediaSource}'s window at which to start
* providing samples, in microseconds.
* @param endPositionUs The end position within {@code mediaSource}'s window at which to stop
* providing samples, in microseconds. Specify {@link C#TIME_END_OF_SOURCE} to provide samples
* from the specified start point up to the end of the source. Specifying a position that
* exceeds the {@code mediaSource}'s duration will also result in the end of the source not
* being clipped.
*/
public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs) {
this(
mediaSource,
startPositionUs,
endPositionUs,
/* enableInitialDiscontinuity= */ true,
/* allowDynamicClippingUpdates= */ false,
/* relativeToDefaultPosition= */ false);
}

素材拼接:

多个视频拼接播放,使用ConcatenatingMediaSource可以用来无缝地合并播放多个素材。

1
2
3
4
5
6
7
/**
* @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same {@link
* MediaSource} instance to be present more than once in the array.
*/
public ConcatenatingMediaSource(MediaSource... mediaSources) {
this(/* isAtomic= */ false, mediaSources);
}

视频变速:

变速使用setPlaybackParameters设置速度参数

1
2
3
4
SimpleExoPlayer simpleExoPlayer = player.getExoPlayer();
if (simpleExoPlayer != null) {
simpleExoPlayer.setPlaybackParameters(new PlaybackParameters(speed));
}

MediaCodec+MediaExtractor+MediaMuxer

音视频编辑中,对多段媒体素材进行截取和拼接是非常常见的操作,截取和拼接实际上是对媒体文件数据重新进行组合的过程。

要实现这些功能,就需要对媒体文件进行编解码操作,即先解码要处理的媒体文件数据,然后再按照某种规则对这些数据进行编码,以生成我们所需的目标。

注意:当要向文件中同时写入视频和音频数据时,必需先writeSampleData所有视频数据,再写音频数据,或者反之,即二者必需连续调用writeSampleData,不能交叉调用,否则写出的文件会有问题。

具体操作过程:

先初始化MediaExtractor并分离音视频轨道、MediaMuxer初始化,再把分离后的数据送到MediaCodec中进行解码处理操作元数据,最后用MediaMuxer对文件进行封装。

视频裁剪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static boolean genVideoUsingMuxer(Context context, String srcPath, String dstPath, int startMs, int endMs, boolean useAudio, boolean useVideo) throws IOException {
boolean success = true;
// Set up MediaExtractor to read from the source.
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(srcPath);
int trackCount = extractor.getTrackCount();
// Set up MediaMuxer for the destination.
MediaMuxer muxer;
muxer = new MediaMuxer(dstPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
// Set up the tracks and retrieve the max buffer size for selected tracks.
HashMap<Integer, Integer> indexMap = new HashMap<>(trackCount);
int bufferSize = -1;
for (int i = 0; i < trackCount; i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
boolean selectCurrentTrack = false;
if (mime.startsWith("audio/") && useAudio) {
selectCurrentTrack = true;
} else if (mime.startsWith("video/") && useVideo) {
selectCurrentTrack = true;
}
if (selectCurrentTrack) {
extractor.selectTrack(i);
int dstIndex = muxer.addTrack(format);
indexMap.put(i, dstIndex);
if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) {
int newSize = format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);
bufferSize = newSize > bufferSize ? newSize : bufferSize;
}
}
}
if (bufferSize < 0) {
bufferSize = 1080*1920*30;
}

// Set up the orientation and starting time for extractor.
MediaMetadataRetriever retrieverSrc = new MediaMetadataRetriever();
retrieverSrc.setDataSource(srcPath);
String degreesString = retrieverSrc.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
if (degreesString != null) {
int degrees = Integer.parseInt(degreesString);
if (degrees >= 0) {
muxer.setOrientationHint(degrees);
}
}
if (startMs > 0) {
extractor.seekTo(startMs * 1000, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
}
// Copy the samples from MediaExtractor to MediaMuxer. We will loop
// for copying each sample and stop when we get to the end of the source
// file or exceed the end time of the trimming.
int offset = 0;
int trackIndex = -1;
ByteBuffer dstBuf = ByteBuffer.allocate(bufferSize);
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
try {
muxer.start();
while (true) {
bufferInfo.offset = offset;
bufferInfo.size = extractor.readSampleData(dstBuf, offset);
if (bufferInfo.size < 0) {
Log.d(TAG, "Saw input EOS.");
bufferInfo.size = 0;
break;
} else {
bufferInfo.presentationTimeUs = extractor.getSampleTime();
if (endMs > 0 && bufferInfo.presentationTimeUs > (endMs * 1000)) {
Log.d(TAG, "The current sample is over the trim end time.");
break;
} else {
bufferInfo.flags = extractor.getSampleFlags();
trackIndex = extractor.getSampleTrackIndex();
muxer.writeSampleData(indexMap.get(trackIndex), dstBuf, bufferInfo);
extractor.advance();
}
}
}
muxer.stop();
} catch (Exception e) {
// Swallow the exception due to malformed source.
Log.w(TAG, "The source video file is malformed");
success = false;
} finally {
muxer.release();
}
return success;
}

其他处理:OpenGL+MediaCodec

众多视频编辑sdk封装ffmpeg对视频进行转码、裁剪、合并、压缩。在音视频领域,一般大型成熟的商用sdk是跨平台的,各终端sdk公用一套由c++开发的底层引擎,针对各端硬件的不同做不同处理,例如分别对pc、android、ios(已有AVFoundation框架)提供硬编硬解的功能。(七牛云短视频SDK 12w/年、腾讯云短视频SDK 50w/年、阿里云短视频SDK 15w/年、VE视频编辑SDK 60w/年)

硬解用到的技术:

先用MediaExtractor读出视频数据,再用MediaCodec进行解码,将解码的画面通过OpenGL渲染到SurfaceTexture上,由SurfaceTexture生成Surface把画面重新编码进MediaCodec,最后通过MediaMuxer合成视频,视频长度由MediaCodec控制,而画面大小由opengl来裁剪。

OpenGL处理:

OpenGL视频处理流程即使用OpenGL对原始视频帧进行二次处理。创建OpenGL渲染环境,通过SurfaceTexture的updateTexImage接口,可将视频流中最新的帧数据更新到对应的GL纹理,再操作GL纹理可对数据进行处理。

(OpenGL还不熟悉,待补充)


参考与鸣谢

https://www.jianshu.com/p/41957301f4a3
https://www.jianshu.com/p/2cf527f2129f
https://www.cnblogs.com/duanxiaojun/articles/6904878.html
https://www.cnblogs.com/dwdxdy/p/3240167.html
https://blog.csdn.net/u010302327/article/details/81363402
https://www.jianshu.com/p/a56505bfc15a
https://www.jianshu.com/p/eb8615fe9f7a
https://blog.csdn.net/yangxi_pekin/article/details/48374827