SparseArray 源码分析

SparseArray(稀疏数组),是 Android 内部特有的 api,主要用来替代 HashMap<Integer,E> 这种形式,使用 SparseArray更加节省内存空间的使用,SparseArray 也是以key和value对数据进行保存的.使用的时候只需要指定value的类型即可.并且key不需要封装成对象类型.

gitbook 简易教程

最近开始整理自己这几年的 Android 学习笔记,用到了 gitbook 这个工具,记录一下使用方式。

安装

gitbook 是一个基于 Node.js 的命令行工具,所以要先安装 Node.js。

下载地址:https://nodejs.org/en/download/,安装也很简单。

然后使用 npm 命令安装 gitbook:

1
npm install gitbook-cli -g

查看下版本号:

1
gitbook -V

安装成功会出现版本号:

1
2
CLI version: 2.3.2
GitBook version: 3.2.3

使用

在文件夹下,使用命令

1
gitbook init

生成 README.mdSUMMARY.md 两个文件。

README 是对书籍的简单介绍; SUMMARY 是书籍的目录结构。

image-20201015144811568

使用命令

1
gitbook serve

编译,在浏览器打开 http://localhost:4000 显示效果。

发布

我所有的笔记文件会放在 GitHub 上,这里说一下如何同步 GitHub 仓库到 gitbook,这样每次更新只需要推送到 GitHub 就行了。

gitbook 官网 使用 Github 账号注册登陆,创建一个新的 space:

image-20201014172149874

点击左边的 Intergrations 按钮,选择 GitHub 同步:

image-20201015110009890

然后会展示出你的所有 GitHub 仓库,选择需要同步的仓库。

image-20201015110617954

这里选择从 GitHub 同步到 GitBook。

image-20201015110748123

稍等片刻,就可以了。

image-20201015111100449

预览地址:https://jaqen.gitbook.io/androidguide/。

Mac 版 IDEA 2020.2 最新破解教程

近来换了 MacBook Pro,安装了 IDEA 2020.2 版本,网上大部分激活教程都不适用 2020.2 这个版本了。搜寻了一大圈,总算找到了激活方式。

有效期到 2089 年。

idea-3

激活方式也很简单。

1、随便创建一个项目,把下载好的激活文件(jetbrains-agent-latest.zip),拖拽到 IDEA 项目界面上,然后会出现下面的弹框。

提示 jetbrains-agent 插件已经安装,重启 IDEA 生效。

idea-1

2、重启后配置助手会提示您,需要使用哪种激活方式,这里我们选择默认的 Activation Code,通过注册码来激活,点击为 IDEA 安装:

idea-2

这样就激活成功了,很简单。

需要的朋友后台回复「IDEA」获取激活工具。

Kotlin 基础:内联函数

Kotlin里使用关键字 inline 来表示内联函数,那么到底什么是内联函数,内联函数有什么用呢?

在 Java 中,每个方法被执行的时候都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧入栈、出栈的过程。

Android AspectJ 学习笔记

AOP

AOP : Aspect Oriented Programming 的缩写,意为:面向切面编程

优点:针对同一问题的统一处理;无侵入添加代码

Android 平台,常用的是 hujiang 的一个aspectjx插件,它的工作原理是:通过Gradle Transform,在class文件生成后至dex文件生成前,遍历并匹配所有符合AspectJ文件中声明的切点,然后将事先声明好的代码在切点前后织入。

整个过程发生在编译期,是一种静态织入方式,所以会增加一定的编译时长,但几乎不会影响程序的运行时效率。

AspectJ 能做什么

针对同一问题的统一处理,实际场景比如:

  • 统计埋点
  • 日志打印/打点
  • 数据校验
  • 行为拦截
  • 性能监控
  • 动态权限控制

AspectJ 几个术语

  • JPoint:代码可注入的点,比如一个方法的调用处或者方法内部、“读、写”变量等。
  • Pointcut:一个程序有很多JPoint,Pointcut的目的就是提供一种方法使得开发者能够选择自己感兴趣的JPoint。
  • Advice:指定注入的代码在 Pointcut 何处注入。常见的有 Before、After、Around 等,表示代码执行前、执行后、替换目标代码。
  • Aspect:用它声明一个类,表示一个需要执行的切面。

AspectJ 配置

项目根目录的build.gradle添加

1
2
3
4
5
6
7
buildscript {
...
dependencies {
...
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0'
}
}

app项目的build.gradle新建的module的build.gradle里添加

1
2
3
4
5
6
apply plugin: 'android-aspectjx'

dependencies {
...
api 'org.aspectj:aspectjrt:1.9.5'
}

常用实例:https://github.com/zywudev/AspectJDemo

Aspect 语法

由于语法内容较多,实际使用过程中我们可以参考语法手册





参考文章

Android AspectJ详解

AOP 之 AspectJ 全面剖析 in Android

Android 音视频学习:使用 MediaCodec API 完成音频 AAC 硬编、硬解

这篇文章主要来学习下使用 MediaCodec API 进行音频的编解码。

什么是编码、解码?

音视频领域,我们常说的 编码 就是压缩,解码 就是解压缩。

编码的目的是减小数据的体积,减少存储空间和传输已存储文件所需的带宽。

编码后的数据是不能直接使用的,必须先解码成原来的样子。就像 zip 压缩文件里面有张图片,我们用图片查看器是无法打开的,必须先解压文件,恢复图片原来的数据,这样才能查看。音视频编解码也是同样的道理。

MediaCodec

我们了解一下 Android 官方提供的音频编解码的 API,即 MediaCodec 类,该 API 是在 Andorid 4.1 (API 16) 版本引入的,因此只能工作于 Android 4.1 以上的手机上。

MediaCodec 采用了基于环形缓冲区的「生产者-消费者」模型,异步处理数据。在 input 端,Client 是这个环形缓冲区「生产者」,MediaCodec 是「消费者」。在 output 端,MediaCodec 是这个环形缓冲区「生产者」,而 Client 则变成了「消费者」。

工作流程是这样的:

(1)Client 从 input 缓冲区队列申请 empty buffer [dequeueInputBuffer]

(2)Client 把需要编解码的数据拷贝到 empty buffer,然后放入 input 缓冲区队列 [queueInputBuffer]

(3)MediaCodec 从 input 缓冲区队列取一帧数据进行编解码处理

(4)处理结束后,MediaCodec 将原始数据 buffer 置为 empty 后放回 input 缓冲区队列,将编解码后的数据放入到 output 缓冲区队列

(5)Client 从 output 缓冲区队列申请编解码后的 buffer [dequeueOutputBuffer]

(6)Client 对编解码后的 buffer 进行渲染/播放

(7)渲染/播放完成后,Client 再将该 buffer 放回 output 缓冲区队列 [releaseOutputBuffer]

mediacodec

MediaCodec 使用的基本流程是:

1
2
3
4
5
6
7
8
9
10
11
- createEncoderByType/createDecoderByType
- configure
- start
- while(1) {
- dequeueInputBuffer
- queueInputBuffer
- dequeueOutputBuffer
- releaseOutputBuffer
}
- stop
- release

MediaCodec 的生命周期有三种状态:停止态-Stopped、执行态-Executing、释放态-Released

停止状态(Stopped)包括了三种子状态:未初始化(Uninitialized)、配置(Configured)、错误(Error)。

执行状态(Executing)会经历三种子状态:刷新(Flushed)、运行(Running)、流结束(End-of-Stream)

mediacodec_lifecycle

(1)当创建编解码器的时候处于未初始化状态。首先你需要调用 configure(…) 方法让它处于 Configured 状态,然后调用 start() 方法让其处于 Executing 状态。在 Executing 状态下,你就可以使用上面提到的缓冲区来处理数据。

(2)Executing 的状态下也分为三种子状态:Flushed, Running、End-of-Stream。在 start() 调用后,编解码器处于 Flushed 状态,这个状态下它保存着所有的缓冲区。一旦第一个输入 buffer 出现了,编解码器就会自动运行到 Running 的状态。当带有 end-of-stream 标志的 buffer 进去后,编解码器会进入 End-of-Stream 状态,这种状态下编解码器不在接受输入 buffer,但是仍然在产生输出的 buffer。此时你可以调用 flush() 方法,将编解码器重置于 Flushed 状态。

(3)调用 stop() 将编解码器返回到未初始化状态,然后可以重新配置。 完成使用编解码器后,您必须通过调用 release() 来释放它。

(4)在极少数情况下,编解码器可能会遇到错误并转到错误状态。 这是使用来自排队操作的无效返回值或有时通过异常来传达的。 调用 reset() 使编解码器再次可用。 您可以从任何状态调用它来将编解码器移回未初始化状态。 否则,调用 release() 动到终端释放状态。

AAC 编解码

对音频进行编码的目的用更少的空间来存储和传输,有有损编码和无损编码,其中我们常见的 Mp3 和 ACC 格式就是有损编码。

ACC 音频有 ADIF 和 ADTS 两种格式,第一种适用于磁盘,优点是需要空间小,但是不能边下载边播放;第二种则适用于流的传输,它是一种帧序列,可以逐帧播放。我们这里用 ADTS 这种来进行编码。

ADTS 帧结构:

head :: body

ADTS 帧首部结构:

序号 长度(bits) 说明
1 Syncword 12 all bits must be 1
2 MPEG version 1 0 for MPEG-4, 1 for MPEG-2
3 Layer 2 always 0
4 Protection Absent 1 et to 1 if there is no CRC and 0 if there is CRC
5 Profile 2 the MPEG-4 Audio Object Type minus 1
6 MPEG-4 Sampling Frequency Index 4 MPEG-4 Sampling Frequency Index (15 is forbidden)
7 Private Stream 1 set to 0 when encoding, ignore when decoding
8 MPEG-4 Channel Configuration 3 MPEG-4 Channel Configuration (in the case of 0, the channel configuration is sent via an inband PCE)
9 Originality 1 set to 0 when encoding, ignore when decoding
10 Home 1 set to 0 when encoding, ignore when decoding
11 Copyrighted Stream 1 set to 0 when encoding, ignore when decoding
12 Copyrighted Start 1 set to 0 when encoding, ignore when decoding
13 Frame Length 13 this value must include 7 or 9 bytes of header length: FrameLength = (ProtectionAbsent == 1 ? 7 : 9) + size(AACFrame)
14 Buffer Fullness 11 buffer fullness
15 Number of AAC Frames 2 number of AAC frames (RDBs) in ADTS frame minus 1, for maximum compatibility always use 1 AAC frame per ADTS frame
16 CRC 16 CRC if protection absent is 0

编解码代码:

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
/**
* AAC 编解码
*/
public class AacPcmCoder {
private static final String TAG = "AacPcmCoder";
private final static String AUDIO_MIME = "audio/mp4a-latm";
private final static long AUDIO_BYTES_PER_SAMPLE = 44100 * 1 * 16 / 8;

/**
* PCM 编码为 AAC 格式
*
* @param inPcmFile
* @param outAacFile
* @throws IOException
*/
public static void encodePcmToAac(File inPcmFile, File outAacFile) throws IOException {
FileInputStream fisRawAudio = null;
FileOutputStream fosAccAudio = null;
MediaCodec audioEncoder = createAudioEncoder();

try {
fisRawAudio = new FileInputStream(inPcmFile);
fosAccAudio = new FileOutputStream(outAacFile);
// 开始编码
audioEncoder.start();
ByteBuffer[] audioInputBuffers = audioEncoder.getInputBuffers();
ByteBuffer[] audioOutputBuffers = audioEncoder.getOutputBuffers();
boolean sawInputEOS = false;
boolean sawOutputEOS = false;
long audioTimeUs = 0;
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
boolean readRawAudioEOS = false;
byte[] rawInputBytes = new byte[8192];
int readRawAudioCount;
int rawAudioSize = 0;
long lastAudioPresentationTimeUs = 0;
int inputBufIndex, outputBufIndex;
while (!sawOutputEOS) {
if (!sawInputEOS) {
inputBufIndex = audioEncoder.dequeueInputBuffer(10_000);
if (inputBufIndex >= 0) {
ByteBuffer inputBuffer = audioInputBuffers[inputBufIndex];
inputBuffer.clear();
int bufferSize = inputBuffer.remaining();
if (bufferSize != rawInputBytes.length) {
rawInputBytes = new byte[bufferSize];
}

readRawAudioCount = fisRawAudio.read(rawInputBytes);
if (readRawAudioCount == -1) {
readRawAudioEOS = true;
}

if (readRawAudioEOS) {
audioEncoder.queueInputBuffer(inputBufIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
sawInputEOS = true;
} else {
inputBuffer.put(rawInputBytes, 0, readRawAudioCount);
rawAudioSize += readRawAudioCount;
audioEncoder.queueInputBuffer(inputBufIndex, 0, readRawAudioCount, audioTimeUs, 0);
audioTimeUs = (long) (1_000_000 * ((float) rawAudioSize / AUDIO_BYTES_PER_SAMPLE));
}
}
}

outputBufIndex = audioEncoder.dequeueOutputBuffer(outBufferInfo, 10_000);
if (outputBufIndex >= 0) {
// Simply ignore codec config buffers.
if ((outBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
Log.i(TAG, "audio encoder: codec config buffer");
audioEncoder.releaseOutputBuffer(outputBufIndex, false);
continue;
}
if (outBufferInfo.size != 0) {
ByteBuffer outBuffer = audioOutputBuffers[outputBufIndex];
outBuffer.position(outBufferInfo.offset);
outBuffer.limit(outBufferInfo.offset + outBufferInfo.size);
if (lastAudioPresentationTimeUs <= outBufferInfo.presentationTimeUs) {
lastAudioPresentationTimeUs = outBufferInfo.presentationTimeUs;
int outBufSize = outBufferInfo.size;
int outPacketSize = outBufSize + 7;
outBuffer.position(outBufferInfo.offset);
outBuffer.limit(outBufferInfo.offset + outBufSize);
byte[] outData = new byte[outPacketSize];
addADTStoPacket(outData, outPacketSize);
outBuffer.get(outData, 7, outBufSize);
fosAccAudio.write(outData, 0, outData.length);
//Log.v(TAG, outData.length + " bytes written.");
} else {
Log.e(TAG, "error sample! its presentationTimeUs should not lower than before.");
}
}
audioEncoder.releaseOutputBuffer(outputBufIndex, false);
if ((outBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
sawOutputEOS = true;
}
} else if (outputBufIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
audioOutputBuffers = audioEncoder.getOutputBuffers();
} else if (outputBufIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat audioFormat = audioEncoder.getOutputFormat();
Log.i(TAG, "format change : " + audioFormat);
}
}
} finally {
Log.i(TAG, "encodePcmToAac: finish");
if (fisRawAudio != null) {
fisRawAudio.close();
}
if (fosAccAudio != null) {
fosAccAudio.close();
}
audioEncoder.release();
}
}

/**
* AAC 解码至 PCM 格式
* @param aacFile
* @param pcmFile
* @throws IOException
*/
public static void decodeAacTomPcm(File aacFile, File pcmFile) throws IOException{
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(aacFile.getAbsolutePath());
MediaFormat mediaFormat = null;
for (int i = 0; i < extractor.getTrackCount(); i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("audio/")) {
extractor.selectTrack(i);
mediaFormat = format;
break;
}
}
if (mediaFormat == null) {
Log.e(TAG, "Invalid file with audio track.");
extractor.release();
return;
}

FileOutputStream fosDecoder = new FileOutputStream(pcmFile);
String mediaMime = mediaFormat.getString(MediaFormat.KEY_MIME);
Log.i(TAG, "decodeAacToPcm: mimeType: " + mediaMime);
MediaCodec codec = MediaCodec.createDecoderByType(mediaMime);
codec.configure(mediaFormat, null, null, 0);
codec.start();
ByteBuffer[] codecInputBuffers = codec.getInputBuffers();
ByteBuffer[] codecOutputBuffers = codec.getOutputBuffers();
final long kTimeOutUs = 10_000;
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
boolean sawInputEOS = false;
boolean sawOutputEOS = false;

try {
while (!sawOutputEOS) {
if (!sawInputEOS) {
int inputBufIndex = codec.dequeueInputBuffer(kTimeOutUs);
if (inputBufIndex >= 0) {
ByteBuffer dstBuf = codecInputBuffers[inputBufIndex];
int sampleSize = extractor.readSampleData(dstBuf, 0);
if (sampleSize < 0) {
Log.i(TAG, "saw input EOS.");
sawInputEOS = true;
codec.queueInputBuffer(inputBufIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
} else {
codec.queueInputBuffer(inputBufIndex, 0, sampleSize, extractor.getSampleTime(), 0);
extractor.advance();
}
}
}

int outputBufferIndex = codec.dequeueOutputBuffer(info, kTimeOutUs);
if (outputBufferIndex >= 0) {
// Simply ignore codec config buffers.
if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
Log.i(TAG, "audio encoder: codec config buffer");
codec.releaseOutputBuffer(outputBufferIndex, false);
continue;
}

if (info.size != 0) {
ByteBuffer outBuf = codecOutputBuffers[outputBufferIndex];
outBuf.position(info.offset);
outBuf.limit(info.offset + info.size);
byte[] data = new byte[info.size];
outBuf.get(data);
fosDecoder.write(data);
}

codec.releaseOutputBuffer(outputBufferIndex, false);
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.i(TAG, "saw output EOS.");
sawOutputEOS = true;
}
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
codecOutputBuffers = codec.getOutputBuffers();
Log.i(TAG, "output buffers have changed.");
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat oformat = codec.getOutputFormat();
Log.i(TAG, "output format has changed to " + oformat);
}
}
} finally {
Log.i(TAG, "decodeAacToPcm finish");
codec.stop();
codec.release();
extractor.release();
fosDecoder.close();
}
}

/**
* 创建编码器
* @return
* @throws IOException
*/
private static MediaCodec createAudioEncoder() throws IOException {
MediaCodec codec = MediaCodec.createEncoderByType(AUDIO_MIME);
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, AUDIO_MIME);
format.setInteger(MediaFormat.KEY_BIT_RATE, 64000);
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, 44100);
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
return codec;
}

private static void addADTStoPacket(byte[] packet, int packetLen) {
int profile = 2; //AAC LC
//39=MediaCodecInfo.CodecProfileLevel.AACObjectELD;
int freqIdx = 4; //44.1KHz
int chanCfg = 1; //CPE
// fill in ADTS data
packet[0] = (byte) 0xFF;
packet[1] = (byte) 0xF9;
packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLen >> 11));
packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
packet[6] = (byte) 0xFC;
}
}

具体源码详见 GitHub :AndroidMultiMediaLearning

引用

1、Android 音频开发(5):音频数据的编解码

2、Android 官方文档

3、安卓解码器 MediaCodec 解析

Android 音视频学习:使用 MediaExtractor 和 MediaMuxer 解析和封装 mp4 文件

这篇文章的目的主要是学习 Android 平台的 MediaExtractor 和 MediaMuxer API,知道如何解析和封装 mp4 文件。

一个音视频文件是包含音频和视频,Android 中可以通过 MediaExtractor API 把音频或视频给单独抽取出来,通过 MediaMuxer 合成新的视频。

MediaExtractor

MediaExtractor 的作用就是将音频和视频分离。

主要是以下几个步骤:

1、创建实例

1
MediaExtractor mediaExtractor = new MediaExtractor();

2、设置数据源

1
mediaExtractor.setDataSource(path);

3、获取数据源的轨道数,切换到想要的轨道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 轨道索引
int videoIndex = -1;
// 视频轨道格式信息
MediaFormat mediaFormat = null;
// 数据源的轨道数
int trackCount = mediaExtractor.getTrackCount();
for (int i = 0; i < trackCount; i++) {
MediaFormat format = mediaExtractor.getTrackFormat(i);
String mimeType = format.getString(MediaFormat.KEY_MIME);
if (mimeType.startsWith("video/")) {
videoIndex = i;
mediaFormat = format;
break;
}
}
// 切换到想要的轨道
mediaExtractor.selectTrack(videoIndex);

4、对所需轨道数据循环读取读取每帧,进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
while (true) {
// 将样本数据存储到字节缓存区
int readSampleSize = mediaExtractor.readSampleData(byteBuffer, 0);
// 如果没有可获取的样本,退出循环
if (readSampleSize < 0) {
mediaExtractor.unselectTrack(videoIndex);
break;
}
...
...
// 读取下一帧数据
mediaExtractor.advance();
}

5、完成后释放资源

1
mediaExtractor.release();

MediaMuxer

MediaMuxer 的作用是生成音频或视频文件;还可以把音频与视频混合成一个音视频文件。

主要是以下几个步骤:

1、创建实例

1
MediaMuxermediaMuxer = new MediaMuxer(path, format);

path: 输出文件的名称;format: 输出文件的格式,当前只支持 MP4 格式。

2、将音频轨或视频轨添加到 MediaMuxer,返回新的轨道

1
int trackIndex = mediaMuxer.addTrack(videoFormat);

3、开始合成

1
mediaMuxer.start();

4、循环将音频轨或视频轨的数据写到文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
while (true) {
// 将样本数据存储到字节缓存区
int readSampleSize = mediaExtractor.readSampleData(byteBuffer, 0);
// 如果没有可获取的样本,退出循环
if (readSampleSize < 0) {
mediaExtractor.unselectTrack(videoIndex);
break;
}
bufferInfo.size = readSampleSize;
bufferInfo.flags = mediaExtractor.getSampleFlags();
bufferInfo.offset = 0;
bufferInfo.presentationTimeUs = mediaExtractor.getSampleTime();
mediaMuxer.writeSampleData(trackIndex, byteBuffer, bufferInfo);
// 读取下一帧数据
mediaExtractor.advance();
}

5、完成后释放资源

1
2
mediaMuxer.stop();
mediaMuxer.release();

实例

从 MP4 文件中分离出视频生成无声视频文件。

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
/**
* 分离视频的视频轨,输入视频 input.mp4,输出 output_video.mp4
*/
private void extractVideo() {
MediaExtractor mediaExtractor = new MediaExtractor();
MediaMuxer mediaMuxer = null;
File fileDir = FileUtil.getExternalAssetsDir(this);
try {
// 设置视频源
mediaExtractor.setDataSource(new File(fileDir, VIDEO_SOURCE).getAbsolutePath());
// 轨道索引
int videoIndex = -1;
// 视频轨道格式信息
MediaFormat mediaFormat = null;
// 数据源的轨道数
int trackCount = mediaExtractor.getTrackCount();
for (int i = 0; i < trackCount; i++) {
MediaFormat format = mediaExtractor.getTrackFormat(i);
String mimeType = format.getString(MediaFormat.KEY_MIME);
if (mimeType.startsWith("video/")) {
videoIndex = i;
mediaFormat = format;
break;
}
}
// 切换到想要的轨道
mediaExtractor.selectTrack(videoIndex);
File outFile = new File(FileUtil.getMuxerAndExtractorDir(this), OUTPUT_VIDEO);
if (outFile.exists()) {
outFile.delete();
}
mediaMuxer = new MediaMuxer(outFile.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
// 将视频轨添加到 MediaMuxer,返回新的轨道
int trackIndex = mediaMuxer.addTrack(mediaFormat);
ByteBuffer byteBuffer = ByteBuffer.allocate(mediaFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE));
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
mediaMuxer.start();
while (true) {
// 将样本数据存储到字节缓存区
int readSampleSize = mediaExtractor.readSampleData(byteBuffer, 0);
// 如果没有可获取的样本,退出循环
if (readSampleSize < 0) {
mediaExtractor.unselectTrack(videoIndex);
break;
}
bufferInfo.size = readSampleSize;
bufferInfo.flags = mediaExtractor.getSampleFlags();
bufferInfo.offset = 0;
bufferInfo.presentationTimeUs = mediaExtractor.getSampleTime();
mediaMuxer.writeSampleData(trackIndex, byteBuffer, bufferInfo);
// 读取下一帧数据
mediaExtractor.advance();
}
Toast.makeText(this, "分离视频完成", Toast.LENGTH_SHORT).show();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (mediaMuxer != null) {
mediaMuxer.stop();
mediaMuxer.release();
}
mediaExtractor.release();
}
}

分离音频、合成音视频的代码类似,详见 GitHub :AndroidMultiMediaLearning

参考

1、Android 视频分离和合成(MediaMuxer和MediaExtractor)

2、Android 音视频开发(五):使用 MediaExtractor 和 MediaMuxer API 解析和封装 mp4 文件

每周分享第 8 期

这里记录过去一周,我看到的值得分享的内容。

本周刊开源(GitHub: weekly),欢迎投稿,分享文章、资源、工具等。

文章

1、想退休,可能没机会了

别再想着三十五岁或四十五岁退休了。好好工作,好好生活,不辜负这样的时代。

2、您可坐稳了,海苔…其实是紫菜做出来的!

涨知识,有意思的公众号,推荐订阅。

资源

1、Learn Git Branching

一个 Git 命令可视化学习项目。能够生动形象的帮助开发人员理解、学习 Git 命令,通过一系列刺激的关卡挑战,逐步深入的学习 Git 的强大功能。

learn_git

2、fucking-algorithm

解 LeetCode 题目集合。号称“手撕 LeetCode 题目”,该项目旨在传递算法思维。

工具

1、WXMarkdown

微信辅助工具,一个帮助你在微信公众号文章中插入不同社区、平台的小工具。

WXMarkdown

2、AR Copy Paste

可以将周围环境的元素剪下,然后粘贴到 Photoshop 中。

3、泥石流海报

根据网址快速创建一张带二维码的海报图。

nishiliu

图片

1、

wisdom

言论

1、

一旦你意识到精力有限,以下这些事情你就再也不会做了:为消费排队、拐弯抹角说话、单恋倒贴死缠难打、分析人际关系和对己看法、在网上跟陌生人吵架……年轻人才有资格挥霍精力,你是成年人了,你的精力要用来挣钱。

—- 反裤衩阵地

每周分享第 7 期

这里记录过去一周,我看到的值得分享的内容。

本周刊开源(GitHub: zywudev/weekly),欢迎投稿,或者推荐好玩的东西。

文章

1、这一年团队的磨合与成长

作者讲述了自己在字节跳动组建团队过程中的一些故事和感悟。

“如果你喜欢一只蝴蝶,千万不要去追,因为你追不上她。你应该去种花、种草,等到春暖花开的时候,等到草长莺飞的时候,蝴蝶自然会飞回来。如果你喜欢的那只蝴蝶没有飞回来,怎么办呢? 你有了花,有了草,有了阳光,有了雨露,有了独特的魅力,那只蝴蝶没有飞回来,其他的蝴蝶会飞回来,比她更好的会飞回来,这就叫做花开蝶自来,爱情如此,生活如此,事业也如此。”

2、那些消失的安卓技术博主们

任何技术都有消失的时候,相聚离开总有时候,没有什么会永垂不朽。唯有经验与思维永存。

3、终端Terminal:程序员是如何查询天气预报的?

作者介绍了如何用终端命令查询天气,很酷。

weather

资源

1、 leetcode 前 300 题详细通俗的题解

前 300 题每道都进行了详细通俗的分析,并且提供多种思路解法。

2、Pragmatic Programmer中译

译者历时两个月将 《Pragmatic Programmer》翻译成中文。

工具

1、Fanyi

命令行词典,无需打开词典应用。fanyi

2、DeepL

一款来自德国的“高质量”人工智能多国语言翻译工具 ,支持中文、英语、德语、法语、日语、西班牙语、意大利语、荷兰语及波兰语之间的全文翻译。

deepl

3、Draw.io

在线图表绘制应用,界面异常简洁高效。

流程图、结构图、网络拓扑图等各种类型的图表都能用它来画。

drawio

图片

1、台上是生活,台下是希望

life-hope

2、陈皓发的推文,从程序员职业发展的角度评论编程语言,有生命力、有市场需求的语言值得投入。

haochen1

haochen2

Android 音视频学习:使用 Camera API 采集视频数据

这篇文章的主要学习内容是:使用 Camera API 采集视频数据并保存到文件,分别使用 SurfaceView、TextureView 来预览 Camera 数据,取到 NV21 的数据回调。

Android 中预览相机画面主要用 SurfaceView 和 TextureView。

SurfaceView:SurfaceView 是一个有自己 Surface 的 View。界面渲染可以放在单独线程而不是主线程中。它更像是一个 Window,自身不能做变形和动画。

TextureView:TextureView 同样也有自己的 Surface。但是它只能在拥有硬件加速层的 Window 中绘制,它更像是一个普通 View,可以做变形和动画。

更多关于 SurfaceView 和 TextureView 的知识可以看这篇文章 Android 5.0(Lollipop)中的SurfaceTexture,TextureView, SurfaceView和GLSurfaceView

Android 5.0 之前系统提供了 Camera API ,5.0 之后提供了 Camera2 API。

不同手机厂商对 Camera2 的支持程度各不相同,即便是 Android 5.0 以上的手机,也存在对 Camera2 支持非常差的情况,这个时候就要降级使用 Camera。

官方的开源库 cameraview 给出的方案:

API Level Camera API Preview View
9-13 Camera1 SurfaceView
14-20 Camera1 TextureView
21-23 Camera2 TextureView
24 Camera2 SurfaceView

接下来,我们使用 SurfaceView 和 TextureView 实现相机预览的功能。

Camera

使用 SurfaceView

SurfaceView 用于展示相机画面,SurfaceView 持有 SurfaceHolder,我们通过 SurfaceHolder 中的回调可以知道 Surface 的状态(创建、变化、销毁)。

继承 SurfaceView,实现 SurfaceHolder.CallBack 接口。在 surfaceCreated 方法中打开相机预览,在 surfaceDestroyed 方法中关闭相机预览就可以了。Camera 的 open 方法有些耗时,为了避免阻塞 UI 线程,可以创建子线程打开相机。

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
90
91
92
93
94
95
public class CameraSurfaceView extends SurfaceView implements SurfaceHolder.Callback {

private Camera mCamera;

public CameraSurfaceView(Context context) {
this(context, null);
}

public CameraSurfaceView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public CameraSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
getHolder().addCallback(this);
}

@Override
public void surfaceCreated(final SurfaceHolder holder) {

ThreadHelper.getInstance().runOnHandlerThread(new Runnable() {
@Override
public void run() {
openCamera();
startPreview(holder);
}
});
}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
ThreadHelper.getInstance().runOnHandlerThread(new Runnable() {
@Override
public void run() {
releaseCamera();
}
});
}

/**
* 打开相机
*/
private void openCamera() {
int number = Camera.getNumberOfCameras();
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
for (int i = 0; i < number; ++i) {
Camera.getCameraInfo(i, cameraInfo);
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
// 打开后置摄像头
mCamera = Camera.open(i);
mCamera.setDisplayOrientation(90);
}
}
}

/**
* 开始预览
* @param holder
*/
private void startPreview(SurfaceHolder holder) {
if (mCamera != null) {
mCamera.setPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
// 取得 NV21 数据,进一步处理
}
});
try {
mCamera.setPreviewDisplay(holder);
mCamera.startPreview();
} catch (IOException e) {
e.printStackTrace();
}
}
}

/**
* 关闭相机
*/
private void releaseCamera() {
if (mCamera != null) {
try {
mCamera.stopPreview();
mCamera.setPreviewDisplay(null);
mCamera.release();
} catch (IOException e) {
e.printStackTrace();
}
mCamera = null;
}
}

使用 TextureView

继承 TextureView,实现TextureView.SurfaceTextureListener 。在 onSurfaceTextureAvailable 方法中打开相机预览,在onSurfaceTextureDestroyed 中关闭预览。

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
90
91
92
93
94
95
96
97
98
99
100
101
public class CameraTextureView extends TextureView implements TextureView.SurfaceTextureListener {

private Camera mCamera;

public CameraTextureView(Context context) {
this(context, null);
}

public CameraTextureView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public CameraTextureView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setSurfaceTextureListener(this);
}

@Override
public void onSurfaceTextureAvailable(final SurfaceTexture surface, int width, int height) {
ThreadHelper.getInstance().runOnHandlerThread(new Runnable() {
@Override
public void run() {
openCamera();
startPreview(surface);
}
});
}

@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {

}

@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
ThreadHelper.getInstance().runOnHandlerThread(new Runnable() {
@Override
public void run() {
releaseCamera();
}
});
return true;
}

@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {

}

/**
* 打开相机
*/
private void openCamera() {
int number = Camera.getNumberOfCameras();
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
for (int i = 0; i < number; ++i) {
Camera.getCameraInfo(i, cameraInfo);
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
// 打开后置摄像头
mCamera = Camera.open(i);
mCamera.setDisplayOrientation(90);
}
}
}

/**
* 开始预览
* @param texture
*/
private void startPreview(SurfaceTexture texture) {
if (mCamera != null) {
mCamera.setPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
// 取得 NV21 数据,进一步处理
}
});
try {
mCamera.setPreviewTexture(texture);
mCamera.startPreview();
} catch (IOException e) {
e.printStackTrace();
}
}
}

/**
* 关闭相机
*/
private void releaseCamera() {
if (mCamera != null) {
try {
mCamera.stopPreview();
mCamera.setPreviewDisplay(null);
mCamera.release();
} catch (IOException e) {
e.printStackTrace();
}
mCamera = null;
}
}

Camera2

使用 SurfaceView

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
public class Camera2SurfaceView extends SurfaceView implements SurfaceHolder.Callback {

private Context mContext;
private SurfaceHolder mSurfaceHolder;
private Handler mWorkHandler;
private String mCameraId;
private CameraDevice mCameraDevice;
private ImageReader mImageReader;
private CameraCaptureSession mCameraCaptureSession;

public Camera2SurfaceView(Context context) {
this(context, null);
}

public Camera2SurfaceView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public Camera2SurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

private void init() {
mContext = getContext();
mSurfaceHolder = getHolder();
mSurfaceHolder.addCallback(this);
}

@Override
public void surfaceCreated(SurfaceHolder holder) {
HandlerThread handlerThread = new HandlerThread("camera2");
handlerThread.start();
mWorkHandler = new Handler(handlerThread.getLooper());
checkCamera();
openCamera();
}

/**
* 检测相机
*/
private void checkCamera() {
CameraManager cameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
try {
String[] cameraIdList = cameraManager.getCameraIdList();
for (String s : cameraIdList) {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(s);
Integer lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING);
Integer sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
Integer supportedHardwareLevel = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
if (lensFacing != null && lensFacing == CameraCharacteristics.LENS_FACING_BACK) {
mCameraId = s;
break;
}
}
} catch (CameraAccessException e) {
e.printStackTrace();
}

}

/**
* 打开相机
*/
private void openCamera() {
if (ActivityCompat.checkSelfPermission(mContext, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
return;
}

if (mCameraId == null) {
return;
}

CameraManager cameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
try {
cameraManager.openCamera(mCameraId, new CameraDevice.StateCallback() {
@Override
public void onOpened(CameraDevice camera) {
mCameraDevice = camera;
mImageReader = ImageReader.newInstance(getWidth(), getHeight(), ImageFormat.YUV_420_888, 8);
mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
Image image = reader.acquireLatestImage();
//我们可以将这帧数据转成字节数组,类似于Camera1的PreviewCallback回调的预览帧数据
//ByteBuffer buffer = image.getPlanes()[0].getBuffer();
//byte[] data = new byte[buffer.remaining()];
//buffer.get(data);
image.close();
}
}, mWorkHandler);

createCameraPreview();
}

@Override
public void onDisconnected(CameraDevice camera) {
camera.close();
mCameraDevice = null;
}

@Override
public void onClosed(CameraDevice camera) {
super.onClosed(camera);
}

@Override
public void onError(CameraDevice camera, int error) {
camera.close();
mCameraDevice = null;
}
}, mWorkHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}

/**
* 相机预览
*/
private void createCameraPreview() {
try {
final CaptureRequest.Builder captureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
Surface surface = mSurfaceHolder.getSurface();
captureRequestBuilder.addTarget(surface);
Surface imageReaderSurface = mImageReader.getSurface();
captureRequestBuilder.addTarget(imageReaderSurface);
captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO);
mCameraDevice.createCaptureSession(Arrays.asList(surface, imageReaderSurface), new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
mCameraCaptureSession = session;
CaptureRequest captureRequest = captureRequestBuilder.build();
try {
session.setRepeatingRequest(captureRequest, null, null);
} catch (CameraAccessException e) {
}
}

@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
}
}, mWorkHandler);
} catch (Exception e) {
}
}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
closeCameraPreview();
if (mCameraDevice != null) {
mCameraDevice.close();
}
if (mImageReader != null) {
mImageReader.close();
}
mWorkHandler.getLooper().quitSafely();
}

private void closeCameraPreview() {
if (mCameraCaptureSession != null) {
try {
mCameraCaptureSession.stopRepeating();
mCameraCaptureSession.abortCaptures();
mCameraCaptureSession.close();
} catch (Exception e) {
}
mCameraCaptureSession = null;
}
}

使用 TextureView

TextureView 与 SurfaceView 类似,这里就不贴代码了。

具体源码放在 GitHub 上:AndroidMultiMediaLearning

以上只是 Camera 和 Camera2 的简单使用,更多细节可以查看官方 API。

参考

Android平台Camera开发实践指南