刷抖音看美腿中毒后,我决定做一款抖音App
当下抖音非常火热,你是不是也很心动做一个类似的 App?
来源:https://blog.51cto.com/14263171/2373915
短视频内容生产
优质短视频内容的产生依赖于短视频的采集和特效编辑,这就要求在进行抖音 App 开发时,用到基础的美颜、混音、滤镜、变速、图片视频混剪、字幕等功能。
在这些功能基础上,进行预处理,结合 OpenGL、AI、AR 技术,产生很多有趣的动态贴纸玩法,使得短视频内容更具创意。
视频录制的大致实现流程是先由 Camera 、 AudioRecord 进行最原始的相机画面以及声音的采集,然后将采集的数据进行滤镜、降噪等前处理,处理完成后由 MediaCodec 进行硬件编码,最后采用 MediaMuxer 生成最终的 MP4 文件。
短视频处理播放
视频的处理和播放主要是视频的清晰度、观看流畅度方面的体验。在这方面来讲,可以采用“窄带高清”技术,在节省码率的同时能够提供更加清晰的观看体验,经过测试,同等视频质量下最高可以节省 20-40% 带宽。
除了带宽之外,短视频内容的存储和 CDN 优化也尤为重要,通常我们需要上传到云存储服务器的内容是短视频内容和封面内容。
而 CDN 优化带给短视频平台的则是进一步的短视频首次载入和循环播放方面的体验。
比如针对首播慢的问题,像阿里云播放器支持 QUIC 协议,基于 CDN 的调度,可以使短视频首次播放秒开的成功率达到 98%。
此外在循环播放时还可以边播放边缓存,用户反复观看某一短视频时就不用耗费流量了。
录制视频的方式
在 Android 系统当中,如果需要一台 Android 设备来获取到一个 MP4 这样的视频文件的话,主流的方式一共与三种:
MediaRecorder
MediaCodec+MediaMuxer
FFmpeg
MediaRecorder:是 Android 系统直接提供给我们的录制类,用于录制音频和视频的一个类,简单方便。
它不需要理会中间录制过程,结束录制后可以直接得到音频文件进行播放,录制的音频文件是经过压缩的,需要设置编码器,录制的音频文件可以用系统自带的播放器播放。
优点:大部分以及集成,直接调用相关接口即可,代码量小,简单稳定;缺点:无法实时处理音频;输出的音频格式不是很多。
MediaCodec+MediaMuxer:MediaCodec 与 MediaMuxer 结合使用同样能够实现录制的功能。
MediaCodec 是 Android 提供的编解码类,MediaMuxer 则是复用类(生成视频文件)。
从易用性的角度上来说肯定不如 MediaRecorder,但是允许我们进行更加灵活的操作,比如需要给录制的视频添加水印等各种效果。
优点: 与 MediaRecorder 一样低功耗速度快,并且更加灵活;缺点: 支持的格式有限,兼容性问题。
FFmpeg:FFmpeg(Fast forword mpeg,音视频转换器)是一个开源免费跨平台的视频和音频流方案,它提供了录制/音视频编解码、转换以及流化音视频的完整解决方案。
主要的作用在于对多媒体数据进行解协议、解封装、解码以及转码等操作。
优点:格式支持非常的强,十分的灵活,功能强大,兼容性好;缺点:C语言些的音视频编解码程序,使用起来不是很方便。
虽然从数据看来 FFmpeg 是最好的,但是我们得首先排除这种,因为他的易用性是最差的。
其次,MediaRecorder 也是需要排除的,所以在这里我比较推荐 MediaCodec+MediaMuxer 这种方式。
编码器参数
码率:数据传输时单位时间传送的数据位数,KBPS:千位每秒。码率和质量成正比,也和文件体积成正比。码率超过一定数值,对图像的质量没有多大的影响。
帧数:每秒显示多少个画面,FPS。
关键帧间隔:在 H.264 编码中,编码后输出的压缩图像数据有多种,可以简单的分为关键帧和非关键帧。
关键帧能够进行独立解码,看成是一个图像经过压缩的产物。而非关键帧包含了与其他帧的“差异”信息,也可以称呼为“参考帧”,它的解码需要参考关键帧才能够解码出一个图像。非关键帧拥有更高的压缩率。
MediaCodec+MediaMuxer 的使用
MediaMuxer 和 MediaCodec 这两个类,它们的参考文:
http://developer.android.com/reference/android/media/MediaMuxer.html
http://developer.android.com/reference/android/media/MediaCodec.html
里边有使用的框架。这个组合可以实现很多功能,比如音视频文件的编辑(结合 MediaExtractor),用 OpenGL 绘制 Surface 并生成 MP4 文件,屏幕录像以及类似 Camera App 里的录像功能(虽然这个用 MediaRecorder 更合适)等。
它们一个是生成视频,一个生成音频,这里把它们结合一下,同时生成音频和视频。
基本框架和流程如下:
首先是录音线程,主要参考 HWEncoderExperiments。通过 AudioRecord 类接收来自麦克风的采样数据,然后丢给 Encoder 准备编码:
AudioRecord audio_recorder; audio_recorder = new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT, buffer_size); // ... audio_recorder.startRecording(); while (is_recording) { byte[] this_buffer = new byte[frame_buffer_size]; read_result = audio_recorder.read(this_buffer, 0, frame_buffer_size); // read audio raw data // … presentationTimeStamp = System.nanoTime() / 1000; audioEncoder.offerAudioEncoder(this_buffer.clone(), presentationTimeStamp); // feed to audio encoder } new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT, buffer_size); // ... audio_recorder.startRecording(); while (is_recording) { byte[] this_buffer = new byte[frame_buffer_size]; read_result = audio_recorder.read(this_buffer, 0, frame_buffer_size); // read audio raw data // … presentationTimeStamp = System.nanoTime() / 1000; audioEncoder.offerAudioEncoder(this_buffer.clone(), presentationTimeStamp); // feed to audio encoder
}
这里也可以设置 AudioRecord 的回调(通过 setRecordPositionUpdateListener())来触发音频数据的读取。
offerAudioEncoder() 里主要是把 Audio 采样数据送入音频 MediaCodec 的 InputBuffer 进行编码:
ByteBuffer[] inputBuffers = mAudioEncoder.getInputBuffers(); int inputBufferIndex = mAudioEncoder.dequeueInputBuffer(-1); if (inputBufferIndex >= 0) { ByteBuffer inputBuffer = inputBuffers[inputBufferIndex]; inputBuffer.clear(); inputBuffer.put(this_buffer); ... mAudioEncoder.queueInputBuffer(inputBufferIndex, 0, this_buffer.length, presentationTimeStamp, 0); } int inputBufferIndex = mAudioEncoder.dequeueInputBuffer(-1); if (inputBufferIndex >= 0) { ByteBuffer inputBuffer = inputBuffers[inputBufferIndex]; inputBuffer.clear(); inputBuffer.put(this_buffer); ... mAudioEncoder.queueInputBuffer(inputBufferIndex, 0, this_buffer.length, presentationTimeStamp, 0); }
下面,参考 Grafika-SoftInputSurfaceActivity,并加入音频处理。主循环大体分四部分:
try { // Part 1 prepareEncoder(outputFile); ... // Part 2 for (int i = 0; i < NUM_FRAMES; i++) { generateFrame(i); drainVideoEncoder(false); drainAudioEncoder(false); } // Part 3 ... drainVideoEncoder(true); drainAudioEncoder(true); } catch (IOException ioe) { throw new RuntimeException(ioe); } finally { // Part 4 releaseEncoder(); } // Part 1 prepareEncoder(outputFile); ... // Part 2 for (int i = 0; i < NUM_FRAMES; i++) { generateFrame(i); drainVideoEncoder(false); drainAudioEncoder(false); } // Part 3 ... drainVideoEncoder(true); drainAudioEncoder(true); } catch (IOException ioe) { throw new RuntimeException(ioe); } finally { // Part 4 releaseEncoder(); }
第 1 部分是准备工作,除了 Video 的 MediaCodec,这里还初始化了 Audio 的 MediaCodec:
MediaFormat audioFormat = new MediaFormat(); audioFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, 44100); audioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); ... mAudioEncoder = MediaCodec.createEncoderByType(AUDIO_MIME_TYPE); mAudioEncoder.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); mAudioEncoder.start(); audioFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, 44100); audioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); ... mAudioEncoder = MediaCodec.createEncoderByType(AUDIO_MIME_TYPE); mAudioEncoder.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); mAudioEncoder.start();
第 2 部分进入主循环,App 在 Surface 上直接绘图,由于这个 Surface 是从 MediaCodec 中用 createInputSurface() 申请来的,所以画完后不用显式用 queueInputBuffer() 交给 Encoder。
drainVideoEncoder() 和 drainAudioEncoder() 分别将编码好的音视频从 Buffer 中拿出来(通过 dequeueOutputBuffer()),然后交由 MediaMuxer 进行混合(通过 writeSampleData())。
注意音视频通过 PTS(Presentation Time Stamp,决定了某一帧的音视频数据何时显示或播放)来同步,音频的 time stamp 需在 AudioRecord 从 MIC 采集到数据时获取并放到相应的 bufferInfo 中。
视频由于是在 Surface 上画,因此直接用 dequeueOutputBuffer() 出来的 bufferInfo 中的就行,最后将编码好的数据送去 MediaMuxer 进行多路混合。
注意这里 Muxer 要等把 audio track 和 video track 都加入了再开始。
MediaCodec 在一开始调用 dequeueOutputBuffer() 时会返回一次 INFO_OUTPUT_FORMAT_CHANGED消息。
我们只需在这里获取该 MediaCodec 的 format,并注册到 MediaMuxer 里。
接着判断当前 audio track 和 video track 是否都已就绪,如果是的话就启动 Muxer。
总结来说,drainVideoEncoder() 的主逻辑大致如下,drainAudioEncoder 也是类似的,只是把 video 的 MediaCodec 换成 audio 的 MediaCodec 即可:
while(true) { int encoderStatus = mVideoEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC); if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { ... } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { encoderOutputBuffers = mVideoEncoder.getOutputBuffers(); } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { MediaFormat newFormat = mAudioEncoder.getOutputFormat(); mAudioTrackIndex = mMuxer.addTrack(newFormat); mNumTracksAdded++; if (mNumTracksAdded == TOTAL_NUM_TRACKS) { mMuxer.start(); } } else if (encoderStatus < 0) { ... } else { ByteBuffer encodedData = encoderOutputBuffers[encoderStatus]; ... if (mBufferInfo.size != 0) { mMuxer.writeSampleData(mVideoTrackIndex, encodedData, mBufferInfo); } mVideoEncoder.releaseOutputBuffer(encoderStatus, false); if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { break; } } } true) { int encoderStatus = mVideoEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC); if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { ... } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { encoderOutputBuffers = mVideoEncoder.getOutputBuffers(); } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { MediaFormat newFormat = mAudioEncoder.getOutputFormat(); mAudioTrackIndex = mMuxer.addTrack(newFormat); mNumTracksAdded++; if (mNumTracksAdded == TOTAL_NUM_TRACKS) { mMuxer.start(); } } else if (encoderStatus < 0) { ... } else { ByteBuffer encodedData = encoderOutputBuffers[encoderStatus]; ... if (mBufferInfo.size != 0) { mMuxer.writeSampleData(mVideoTrackIndex, encodedData, mBufferInfo); } mVideoEncoder.releaseOutputBuffer(encoderStatus, false); if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { break; } }
}
第 3 部分是结束录制,发送 EOS 信息,这样在 drainVideoEncoder() 和 drainAudioEncoder 中就可以根据 EOS 退出内循环。
第 4 部分为清理工作。把 audio 和 video 的 MediaCodec,MediaCodec 用的 Surface 及 MediaMuxer 对象释放。
最后几点注意:
在 AndroidManifest.xml 里加上录音权限,否则创建 AudioRecord 对象时铁定失败。
音视频通过 PTS 同步,两个的单位要一致。
MediaMuxer 的使用要按照 Constructor->addTrack->start->writeSampleData->Stop 的顺序。如果既有音频又有视频,在 Stop 前两个都要 writeSampleData() 过。
总结
以上就是抖音类 App 的部分内容,其中的步骤和过程是我亲自实践过的,按照上述的过程应该都可以正常运行,写这一篇文章花了很多时间,希望所有看了这篇文章的朋友们都能够有一定的收获。
推荐阅读:
“37岁,年薪50万,一夜被裁”:伪上班,毁掉了多少中国年轻人
从零开始搭建创业公司后台技术栈
互联网公司没有中年人
一个人学习、工作很迷茫?
点击「阅读原文」加入我们的小圈子!
刷抖音看美腿中毒后,我决定做一款抖音App相关推荐
- 创客匠人助力“音体美”培训机构布局线上
"让每个学生都能掌握1-2项艺术特长." 此前,教育部体育卫生与艺术教育司司长王登峰在新闻发布会上提出:美育课堂要教会学生欣赏美.发现美,也要教给学生创造美的能力. 近期,在线少儿 ...
- 服务器中毒怎么找出病毒源文件,电脑中毒后怎样查找出在哪个文件夹
电脑中毒了在哪个文件夹呢!当我们怎么找都找不到的时候,是不是想放弃!下面由学习啦小编给你做出详细的电脑中毒文件夹查找介绍!希望对你有帮助! 电脑中毒后再哪个文件夹: 查找方法一. 要禁用 Dr. Wa ...
- 如何通过看美剧提高英语
看到很多童鞋讨论有关美剧学习英语到底有没有用,以及用哪部美剧练习,我在这里想说这只是一个参考,世界上没有绝对的事情,究竟有没有用看个人,如果你 到现在还无法衡量自己的英语水平或者依旧被英语牵着鼻子走, ...
- 看脚识女人:为什么女人的美腿美脚在某些男人眼中那么重要?
ok, 看到很多人满好奇为什么美腿美脚在某些男人眼中那么重要,那么我就发表一下我的看法吧. 我的想法可能比较典型,为什么我敢这么说,因为我是彻头彻尾属于那种"无脚不欢"的男人,美腿 ...
- @Android程序员今年必看!!拖更了三年带回了一个抖音,虎牙,哔哩哔哩都在用的库|墙裂推荐
作者:yummyLau 链接:https://juejin.im/post/5eddf8456fb9a04804041738 文章是转载的哦 ,感兴趣的可以看看作者其他的文章,我个人觉得挺不错的. 起 ...
- 为什么我从PR里面导出来的视频,在电脑上可以正常播放,但是上传到天猫、淘宝、抖音、微信等平台后就变的模糊,类似于重影的效果
为什么我从PR里面导出来的视频,在电脑上可以正常播放,但是上传到天猫.淘宝.抖音.微信等平台后就变的模糊,类似于重影的效果.尤其是加速后的视频,比如2倍速度.或倍速度,模糊的会越来越严重. 类似于这样 ...
- 超详细抖音运营变现攻略,教你做一个会“赚钱”的vlog博主丨国仁网络
旅行日渐成为大众生活中不可或缺的一部分,大众除了利用常规拍照.拍视频的方式,来记录旅程,volg也走入大众视线以及短视频舞台中心. 打开抖音,你能频繁刷到关于旅行.美食.学习.日常等各种主题的vlog ...
- 最近分享一款抖音上很火的七夕节程序员表白页面_html5七夕表白放烟花动画特效...
html5七夕表白放烟花动画特效 最近分享一款抖音上很火的七夕节程序员表白页面.小姐姐,我好喜欢你,你愿意做我女朋友吗? 表白成功触发烟花背景动画特效. 表白内容:有人说,人的一生会遇到2920万人, ...
- 一场直播获利6亿,看“美ONE”如何打造出“李佳琦”这样一个价值过亿的IP!!!
李佳琦,一个美妆圈让人又爱又恨的男人,一个带货怪物,一个赚钱机器.据说每50个进入李佳琦直播间或者看过他拍的视频的女生,其中90%都会剁手下单. 作为一个男性美妆博主,抖音开通两个月涨粉1400万,淘 ...
- 最近分享一款抖音上很火的七夕节程序员表白页面_html5七夕表白放烟花动画特效
html5七夕表白放烟花动画特效 最近分享一款抖音上很火的七夕节程序员表白页面.小姐姐,我好喜欢你,你愿意做我女朋友吗? 表白成功触发烟花背景动画特效. 表白内容:有人说,人的一生会遇到2920万人, ...
最新文章
- 基于AFNetworking的多张图片上传
- matlab多项式加法运算,matlab多项式运算与代数方程求解解析.ppt
- 在Ubuntu 13.10 下安装支持SSL的Apache
- 编译原理语义分析代码_Pix2Pix原理分析与代码解读
- [深入Python]Alex Martelli的Borg类
- 多变量遗传算法python代码_遗传算法介绍并附上Python代码
- 数据抽取的常见理论方法
- linux数据库删除命令大全,linux删除数据库命令
- (转)mysql explain
- python实现指纹识别毕业论文_指纹识别系统毕业论文.doc
- 小米和联想的“骁龙”之争,首发第一,友谊第二...
- 利用Regsvr32绕过Applocker的限制策略
- ply文件 java_ply之解析java文件,找出包名、类名、依赖类
- lsdyna如何设置set中的node_list_如何画出一幅好看的图
- Anaconda/Conda创建环境时报错的解决方案
- JavaScript 高级程序设计
- tomcat布置前端项目
- 什么是计算机网络AP,无线AP是什么
- mysql定时备份及删除历史数据
- [MBTI]四大维度 – 第二篇