前言

当一个Android开发者玩抖音玩疯了之后,就会绞尽脑汁思考自己是否也能开发出一款相同的APP来呢?

滴,滴滴!

本篇文章将介绍自己总结的短视频录制的相关内容,主要分为三个部分:

  • 摄像头内容录制
  • 音频录制
  • 视频合成

先上效果图

  • 录制过程

  • 录制结果

1.摄像头内容录制

录制流程大致如上图所示。

渲染关键代码

新建外部纹理

    @Overridepublic void onSurfaceCreated(GL10 gl, EGLConfig config) {mTextureId = GLUtils.createTextureObject(GLES11Ext.GL_TEXTURE_EXTERNAL_OES);mSurfaceTexture = new SurfaceTexture(mTextureId);...}

新建了外部纹理之后,传入 Camera

mCamera.setPreviewTexture(mSurfaceTexture);
mCamera.startPreview();

GLSurfaceView 渲染时,请求 SurfaceTexture 更新,获取最新的内容

    @Overridepublic void onDrawFrame(GL10 gl) {if (mFilter == null) {return;}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 LCint freqIdx = 4; // 44.1KHzint chanCfg = 1; // CPE// fill in A D T S datapacket[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;}public void flush() {this.flush(handle);}public void setTempo(float tempo) {setTempo(handle, tempo);}public void setPitchSemiTones(float pitch) {setPitchSemiTones(handle, pitch);}static {System.loadLibrary("soundtouch");}}

主要有四个方法

  • setTempo —— 设置音频变速 大于1为加速,小于1为减速
  • setPitchSemiTones —— 设置音频声调
  • putBytes —— 将 pcm 数据添加到 SoundTouch 管道中
  • getBytes —— 从 SoundTouch 管道中取出处理过的 pcm 数据

新建对应的 cpp 文件,关键代码如下:

void Java_com_netease_soundtouch_SoundTouch_setTempo(JNIEnv *env, jobject thiz, jlong handle, jfloat tempo)
{SoundTouch *ptr = (SoundTouch *)handle;ptr->setTempo(tempo);
}
void Java_com_netease_soundtouch_SoundTouch_setPitchSemiTones(JNIEnv *env, jobject thiz, jlong handle, jfloat pitch)
{SoundTouch *ptr = (SoundTouch *)handle;ptr->setPitchSemiTones(pitch);
}
void Java_com_netease_soundtouch_SoundTouch_putBytes(JNIEnv *env, jobject thiz, jlong handle, jbyteArray input, jint offset, jint length)
{SoundTouch *soundTouch = (SoundTouch *)handle;jbyte *data;data = env->GetByteArrayElements(input, JNI_FALSE);soundTouch->putSamples((SAMPLETYPE *)data, length/2);env->ReleaseByteArrayElements(input, data, 0);
}
jint Java_com_netease_soundtouch_SoundTouch_getBytes(JNIEnv *env, jobject thiz, jlong handle, jbyteArray output, jint length)
{int receiveSamples = 0;int maxReceiveSamples = length/2;SoundTouch *soundTouch = (SoundTouch *)handle;jbyte *data;data = env->GetByteArrayElements(output, JNI_FALSE);receiveSamples = soundTouch->receiveSamples((SAMPLETYPE *)data,maxReceiveSamples);env->ReleaseByteArrayElements(output, data, 0);return receiveSamples;
}

处理 pcm 数据

    //在将pcm导入MediaCodec之前,先由SoundTouch处理一遍private void encode(final byte[] data, final int length) {encodeHandler.post(new Runnable() {@Overridepublic void run() {if (soundTouch != null) {soundTouch.putBytes(data);while (true) {//如果是用MediaMuxer来生成音频,我们每次只能写入一帧数据,那么这里缓冲区就不能用4096,只能用1024byte[] modified = new byte[4096];int count = soundTouch.getBytes(modified);if (count > 0) {onEncode(modified, count * 2);drain();} else {break;}}} else {onEncode(data, length);drain();}}});}

音频和视频合成

录制完视频和音频之后,我们需要将音频和视频进行合成,这一步直接使用FFMPEG 工具即可,命令行如下:

ffmpeg -y -i audioFile -ss 0 -t duration -i videoFile -acodec copy -vcodec copy output

其中,audioFile 为我们的 aac 文件的路径,videoFile 为 mp4 文件的路径,output 为最终生成的 mp4 文件的路径,duration 为音频文件的长度,使用MediaExtractor 获取即可。

ffmpeg 不会自动帮我们创建文件,在合成之前,需要先创建output文件

执行完这个命令后,音频和视频就合成完毕了,15秒的视频,合成一次大概只需要100ms左右。我们只需要在每小段视频录制完毕时合成一次即可,对用户来说没什么影响。视频的码率越高,合成所需要的时间越久。

视频合成

多段视频拼接使用 ffmpeg 即可,无需重新解码,我们在点击 app 中的下一步按钮时进行视频的拼接。关键代码如下:

    public static VideoCommand mergeVideo(List<String> videos, String output) {String appDir = StorageUtil.getExternalStoragePath() + File.separator;String fileName = "ffmpeg_concat.txt";FileUtils.writeTxtToFile(videos, appDir, fileName);VideoCommand cmd = new VideoCommand();cmd.append("ffmpeg").append("-y").append("-f").append("concat").append("-safe").append("0").append("-i").append(appDir + fileName).append("-c").append("copy").append(output);return cmd;}

命令行为:

ffmpeg -y -f concat -safe 0 -i concatFile -c copy output

其中,concatFile 是一个 txt 文件,内容为我们要拼接的文件的路径列表,output 为最终输出的 mp4 文件。

总结

整个短视频的录制方案大概就是如此,关于视频录制方面,因为没有具体线上项目实践过,所以可能会存在机型不兼容的情况,大家如果有更好的方案,欢迎在评论区提出来噢,一起探讨下。有些地方讲解不对或者觉得不清楚的,欢迎大家在评论区指出。后面会发关于短视频后期处理的文章,敬请关注!

滴,滴滴!

福利链接: https://pan.baidu.com/s/13PzmECPep3H7eyysGEdQew

以上链接是抖音短视频开发视频教程,有需要本视频的欢迎加入Android开发技术交流群:150923287获取教程提取码!

Android 基于MediaCodec开发抖音短视频录制(壹)相关推荐

  1. Android 基于MediaCodec开发抖音短视频录制(贰)

    前言 上一篇文章中,我大概介绍了一下短视频的拍摄,主要就是音视频的加减速.这篇文章我将介绍下抖音视频特效的实现,废话不多说,进入正题. 1.特效概览 抖音上目前有这九种视频特效,本文将介绍前面六种的实 ...

  2. java开发抖音短视频无水印下载工具

    思路分析 首先在抖音APP段选择你喜欢的视频进行分享,一般分享链接都是这样的:"http://v.douyin.com/111113/",这并不是真的下载地址,在浏览器中输入上述地 ...

  3. 类似于快手、美拍、抖音短视频录制SDK接入教程

    这几年抖音短视频非常火爆,所以很多公司也希望开发短视频录制功能,现在大部分企业是通过第三方视频SDK来完成的,当然不考虑成本的,公司也可以自己开发. 先给大家看下短视频效果,没法插入视频,直接贴视频地 ...

  4. Android 基于MediaCodec+MediaMuxer实现音视频录制合成

    AudioVideoCodec 一款视频录像机,支持AudioRecord录音.MediaCodec输出AAC.MediaMuxer合成音频视频并输出mp4,支持自动对焦.屏幕亮度调节.录制视频时长监 ...

  5. 仿抖音短视频录制按钮动画

    最近找工作,面试遇到了一个问题问抖音的那个录制按钮效果如何实现.说到这里熟悉自定义View的话,很容易就想到这里关键是考Xfermode的了解及使用. 话不多说先上图 项目源码 现在就让我们来动手实现 ...

  6. Android逆向之旅---抖音短视频的Native注册混淆函数获取方法

    一.静态分析 最近在小密圈中有很多同学都在咨询有时候有些应用的动态注册Native函数,在分析so之后发现找不到真的实现函数功能地方,我们知道有时候为了安全考虑会动态注册Native函数,但是如果只是 ...

  7. 仿抖音短视频APP源码如何开发抖音类似特效

    仿抖音短视频APP源码如何开发抖音类似特效 1.特效概览 特效列表 特效列表 2.『灵魂出窍』 抖音的实现效果如下: 灵魂出窍 我的实现效果如下: ezgif.com-rotate.gif 代码实现 ...

  8. 上车短视频赛道:基于uniapp框架快速搭建自己的仿抖音短视频APP

    在今年也就是第48次发布的<中国互联网络发展状况统计报告>有这样一个数据,21年的上半年以来,我国我国网民规模达10.11亿,其中短视频用户达8.88亿.碎片化的生活场景下,短视频成为人们 ...

  9. 仿抖音短视频系统源码,android 时间戳转换

    仿抖音短视频系统源码,android 时间戳转换相关的代码 package util;import java.text.SimpleDateFormat; import java.util.Calen ...

最新文章

  1. dsp之BF531笔记
  2. ICML 2021文章引发热议:矩阵乘法无需相乘,速度提升100倍
  3. html文字粒子效果简陋,5个很棒的CSS3文本粒子动画特效
  4. java学习(102):arraylist的查询和删除
  5. 深度系统如何安装mysql_deepin 安装mysql apache
  6. 用spring搭建微信公众号开发者模式下服务器处理用户消息的加密传输构架(java)
  7. 织梦自适应php源码,DEDE织梦PHP源码响应式建筑设计类网站织梦模板(自适应手机端)...
  8. Android通过tcpdump抓包
  9. poj 1273 最大流
  10. mo文件等不能上传到git上
  11. linux下多线程断点下载工具-axel
  12. mysql几百万的表关联_mysql SQL优化,百万级2张表关联,从40分钟到3秒
  13. SiamFC复现结果
  14. 职言 | 单纯做业务测试真的行得通吗?
  15. 2019年全国大学生电子设计大赛(简单电路特性测试仪)
  16. 企业微信品牌私域运营案例合集
  17. 程序员用代码求救, 同事“秒懂”
  18. xiaomi 小米6刷ubuntu touch
  19. Lwip 奔溃掉线内存申请不出来也许大部分是竞争问题!
  20. Qt将选中的字体加粗下划线

热门文章

  1. 阿里云服务器如何搭建ip
  2. linux内核memset,linux内核API每天来一发(2)
  3. 餐饮业改善寻源战略的6个实践
  4. 读书笔记-Greenplum企业应用实战:简介
  5. element-ui 的去背景----持续跟新
  6. POI进行Excel的合并单元格数据处理
  7. php+jq+添加css,jQuery添加/改变/移除CSS类
  8. Linux gpg --加密和数字签名工具
  9. 华为云焕新数字生活,打造情景化智能产业链条
  10. Python 之父 Guido van Rossum 称退休太无聊,正式加入微软搞开源!