刷抖音看美腿中毒后,我決定做一款抖音App
當下抖音非常火熱,你是不是也很心動做一個類似的 App?
短視頻內容生產
優質短視頻內容的產生依賴于短視頻的采集和特效編輯,這就要求在進行抖音 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
- }
這里也可以設置 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);
- }
下面,參考 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();
- }
第 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();
第 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;
- }
- }
- }
第 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 的部分內容,其中的步驟和過程是我親自實踐過的,按照上述的過程應該都可以正常運行,寫這一篇文章花了很多時間,希望所有看了這篇文章的朋友們都能夠有一定的收獲。