最后
在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。
附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
}
float matrix[] = new float[16];
if (mSurfaceTexture != null) {
//请求刷新最新内容
mSurfaceTexture.updateTexImage();
}
mSurfaceTexture.getTransformMatrix(matrix);
if (mFrameListener != null) {
//通知MediaCodec刷新画面
mFrameListener.onFrameAvailable(new VideoFrameData(mFilter,
matrix, mSurfaceTexture.getTimestamp(), mTextureId));
}
mFilter.init();
if (mOldFilter != null) {
mOldFilter.release();
mOldFilter = null;
}
mSurfaceTexture.getTransformMatrix(mMatrix);
//绘制预览内容
mFilter.draw(mTextureId, mMatrix);
}
复制代码
mFilter 中包含 OpenGL 相关的着色器程序
着色器代码如下:
/**
- 默认代码
/
private static final String FRAGMENT_CODE =
“#extension GL_OES_EGL_image_external : require\n” +
“precision mediump float;\n” +
“varying vec2 vTextureCoord;\n” +
“uniform samplerExternalOES uTexture;\n” +
“void main() {\n” +
" gl_FragColor = texture2D(uTexture, vTextureCoord);\n" +
“}\n”;
/* - 默认代码
*/
private static final String VERTEX_CODE =
“uniform mat4 uTexMatrix;\n” +
“attribute vec2 aPosition;\n” +
“attribute vec4 aTextureCoord;\n” +
“varying vec2 vTextureCoord;\n” +
“void main() {\n” +
" gl_Position = vec4(aPosition,0.0,1.0);\n" +
" vTextureCoord = (uTexMatrix * aTextureCoord).xy;\n" +
“}\n”;
复制代码
外部纹理和普通纹理不同,需要在片段着色器代码头部声明拓展。
#extension GL_OES_EGL_image_external : require
复制代码
着色器代码比较简单,不包含滤镜相关的内容,直接使用相机的纹理绘制一个矩形。
录制关键代码
内容录制编码使用 MediaCodec + MediaMuxer 的组合来实现。MediaCodec 在初始化时,我们可以从中获取一个 Surface,用来往里面填充内容。
MediaFormat format = MediaFormat.createVideoFormat(C.VideoParams.MIME_TYPE,
configuration.getVideoWidth(),
configuration.getVideoHeight());
//设置参数
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, C.VideoParams.BIT_RATE);
format.setInteger(MediaFormat.KEY_FRAME_RATE, C.VideoParams.SAMPLE_RATE);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, C.VideoParams.I_FRAME_INTERVAL);
MediaCodec encoder = MediaCodec.createEncoderByType(C.VideoParams.MIME_TYPE);
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
inputSurface = encoder.createInputSurface();
复制代码
获取 inputSurface 之后,我们新建一个 EGLSurface,到这里编码器的初始化就完成了,当有新的内容时,通知编码器来刷新。之前我们获取了GLSurfaceView 的 GL 上下文,当收到新内容通知时,我们把 GL 环境切到编码器的线程,然后绘制,最后调用 swapBuffers 方法把绘制的内容填充到inputSurface 中,这就是所谓的离屏渲染(听着很高大上,后面讲解短视频后期制作时也会用到这个)。
这里不使用 EOS 纹理也是可以的,我们可以通过 Camera 的setPreviewCallback 方法监听相机的每一帧数据,然后将 YUV 数据转换成ARGB 数据,再转成纹理交给 OpenGL 渲染即可。
最后新建 MediaMuxer
muxer = new MediaMuxer(configuration.getFileName(),
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
此部分内容参考 grafika 实现
视频变速
视频变速相对来说比较容易,在编码之后,我们从 MediaCodec 的缓冲区中获取本次编码内容的 ByteBuffer 和 BufferInfo ,前者是编码后的内容,后者是本次内容的信息,包括时间戳,大小等。我们通过改变视频的时间戳,就可以达到视频变速的要求。比如要加快视频的速度,那么只需要将视频的时间戳间隔缩小一定的倍数即可。放慢操作和这个相反,只需要把时间戳间隔放大一定的倍数即可。
音频录制
音频的录制我们需要使用到 AudioRecord 这个大杀器,大致流程图如下。
音频录制比较简单,参考官方文档即可。这里需要开启两条线程,因为目前使用的编码是同步模式,如果是在一条线程里处理数据,会导致麦克风的数据丢失。
关键代码如下:
初始化AudioRecord
指定单声道模式,采样率为 44100,每个采样点 16 比特
int bufferSize = AudioRecord.getMinBufferSize(
configuration.getSampleRate(), C.AudioParams.CHANNEL,
C.AudioParams.BITS_PER_SAMPLE);
recorder = new AudioRecord(
MediaRecorder.AudioSource.MIC, configuration.getSampleRate(),
C.AudioParams.CHANNEL, C.AudioParams.BITS_PER_SAMPLE, bufferSize);
复制代码
初始化MediaCodec
MediaFormat audioFormat = MediaFormat.createAudioFormat(C.AudioParams.MIME_TYPE,
C.AudioParams.SAMPLE_RATE, C.AudioParams.CHANNEL_COUNT);
audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE,
MediaCodecInfo.CodecProfileLevel.AACObjectLC);
audioFormat.setInteger(MediaFormat.KEY_CHANNEL_MASK, C.AudioParams.CHANNEL);
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, C.AudioParams.BIT_RATE);
audioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, C.AudioParams.CHANNEL_COUNT);
audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 1024 * 4);
encoder = MediaCodec.createEncoderByType(C.AudioParams.MIME_TYPE);
encoder.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
bufferInfo = new MediaCodec.BufferInfo();
mStream = new BufferedOutputStream(new FileOutputStream(configuration.getFileName()));
复制代码
音频编码
读取音频数据
byte[] buffer = new byte[configuration.getSamplePerFrame()];
int bytes = recorder.read(buffer, 0, buffer.length);
if (bytes > 0) {
encode(buffer, bytes);
}
复制代码
塞进MediaCodec缓冲区
private void onEncode(byte[] data, int length) {
final ByteBuffer[] inputBuffers = encoder.getInputBuffers();
while (true) {
final int inputBufferIndex = encoder.dequeueInputBuffer(BUFFER_TIME_OUT);
if (inputBufferIndex >= 0) {
final ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
inputBuffer.clear();
inputBuffer.position(0);
if (data != null) {
inputBuffer.put(data, 0, length);
}
if (length <= 0) {
encoder.queueInputBuffer(inputBufferIndex, 0, 0,
getTimeUs(), MediaCodec.BUFFER_FLAG_END_OF_STREAM);
break;
} else {
encoder.queueInputBuffer(inputBufferIndex, 0, length,
getTimeUs(), 0);
}
break;
}
}
}
复制代码
取出编码后的数据并写入文件
private void drain() {
bufferInfo = new MediaCodec.BufferInfo();
ByteBuffer[] encoderOutputBuffers = encoder.getOutputBuffers();
int encoderStatus = encoder.dequeueOutputBuffer(bufferInfo, C.BUFFER_TIME_OUT);
while (encoderStatus >= 0) {
ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
int outSize = bufferInfo.size;
encodedData.position(bufferInfo.offset);
encodedData.limit(bufferInfo.offset + bufferInfo.size);
byte[] data = new byte[outSize + 7];
addADTSHeader(data, outSize + 7);
encodedData.get(data, 7, outSize);
try {
mStream.write(data, 0, data.length);
} catch (IOException e) {
LogUtil.e(e);
}
if (duration >= configuration.getMaxDuration()) {
stop();
}
encoder.releaseOutputBuffer(encoderStatus, false);
encoderStatus = encoder.dequeueOutputBuffer(bufferInfo, C.BUFFER_TIME_OUT);
}
}
复制代码
aac文件对内容格式有要求,需要在每一帧的内容头部添加内容,代码如下:
private void addADTSHeader(byte[] packet, int length) {
int profile = 2; // AAC LC
int freqIdx = 4; // 44.1KHz
int chanCfg = 1; // CPE
// fill in A D T S 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) + (length >> 11));
packet[4] = (byte) ((length & 0x7FF) >> 3);
packet[5] = (byte) (((length & 7) << 5) + 0x1F);
packet[6] = (byte) 0xFC;
}
复制代码
音频变速
一开始调研短视频方案的时候,对于音频变速这方面,想了很多个方案:
- 音频和视频使用 MediaMuxer 合成,指定变速速率,在录制结束时使用ffmpeg 进行变速
- 视频和音频分开录制,视频实时变速录制,音频在录制结束时使用 ffmpeg 变速,然后再使用 ffmpeg 合并到视频中
- 音频和视频分开录制,音频实时变速,视频实时变速,录制完成后,使用ffmpeg 合成
最终我选择了第三个方案,前两个方案的死因如下:
- 效率差,ffmpeg 如果要对视频进行变速,效率很低,一个视频如果要放慢三倍,最久的时候要十几秒,并且因为使用的是软编,对 cpu 占用率比较高,会导致 UI 卡顿,
- 音频变速耗时比视频变速要少,但是对用户来说,还是可以感知的到的,所以这个方案也 pass。(主要是达不到抖音的效果)
第三个方案需要使用一个第三方库——SoundTouch,它可以改变音频的音调和速度。SoundTouch 由 C++ 实现,因此我们需要用 NDK 工具把它集成到工程当中。集成的方法参照官方文档即可。官方的例子中主要给出了处理 wav 文件的方法,接下来我介绍一下如何使用这个库实时处理 pcm 数据(通过实时处理PCM 数据,我们还可以弄个变声功能噢)。
SoundTouch 使用
新建类—— SoundTouch
public class SoundTouch {
private native final void setTempo(long handle, float tempo);
private native final void setPitchSemiTones(long handle, float pitch);
private native final void putBytes(long handle, byte[] input, int offset, int length);
private native final int getBytes(long handle, byte[] output, int length);
private native final static long newInstance();
private native final void deleteInstance(long handle);
private native final void flush(long handle);
private long handle = 0;
public SoundTouch() {
handle = newInstance();
}
public void putBytes(byte[] input) {
this.putBytes(handle, input, 0, input.length);
}
public int getBytes(byte[] output) {
return this.getBytes(handle, output, output.length);
}
public void close() {
deleteInstance(handle);
handle = 0;
更多学习和讨论,欢迎加入我们!
有许多来自一线的技术大牛,也有在小厂或外包公司奋斗的码农,我们致力打造一个平等,高质量的Android交流圈子,不一定能短期就让每个人的技术突飞猛进,但从长远来说,眼光,格局,长远发展的方向才是最重要的。
这里有2000+小伙伴,让你的学习不寂寞~·
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
等,高质量的Android交流圈子,不一定能短期就让每个人的技术突飞猛进,但从长远来说,眼光,格局,长远发展的方向才是最重要的。
这里有2000+小伙伴,让你的学习不寂寞~·
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!