Android手機通過rtp傳送aac資料給vlc播放
截圖
AudioRecord音訊採集
private val sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE) private val channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT) private val minBufferSize = AudioRecord.getMinBufferSize(sampleRate, if (channelCount == 1) CHANNEL_IN_MONO else CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT); runInBackground { audioRecord = AudioRecord( MediaRecorder.AudioSource.MIC, sampleRate, if (channelCount == 1) CHANNEL_IN_MONO else CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT, 2 * minBufferSize ) audioRecord.startRecording() }
音訊採集時需要設定採集引數,設定的這些引數需要與建立MediaCodec時的引數一致。
- sampleRate是取樣率:44100
- channelCount是通道數:1
- 單個取樣資料大小格式:AudioFormat.ENCODING_PCM_16BIT
- 最小資料buffer:AudioRecord.getMinBufferSize()計算獲取
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) { try { codec.getInputBuffer(index)?.let { bb -> var startTime = System.currentTimeMillis(); var readSize = audioRecord.read(bb, bb.capacity()) log { "read time ${System.currentTimeMillis() - startTime} read size $readSize" } if (readSize < 0) { readSize = 0 } codec.queueInputBuffer(index, 0, readSize, System.nanoTime() / 1000, 0) } }catch (e:Exception){ e.printStackTrace() } }
這裡採用的阻塞的方式採集資料,所以AudioRecord依據設定的取樣頻率生成資料的,我們可以直接把當前的時間設定為錄製的時間戳。
MediaCodec編碼音訊資料
val mediaFormat = MediaFormat.createAudioFormat( MediaFormat.MIMETYPE_AUDIO_AAC, audioSampleRate, audioChannelCount ) mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, audioBitRate) mediaFormat.setInteger( MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC ) mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, audioMaxBufferSize)
為MediaCodec建立MediaFormat並設定引數,這裡設定的音訊引數必須與AudioRecord一致。
- MIME_TYPE:"audio/mp4a-latm"
- 取樣頻率與AudioRecord一致:44100
- 通道數與AudioRecord一致:1
- KEY_AAC_PROFILE配置為低頻寬要求型別:AACObjectLC
- KEY_BIT_RATE設定的大小影響編碼壓縮率:128 * 1024
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) { try { codec.getInputBuffer(index)?.let { bb -> var startTime = System.currentTimeMillis(); var readSize = audioRecord.read(bb, bb.capacity()) log { "read time ${System.currentTimeMillis() - startTime} read size $readSize" } if (readSize < 0) { readSize = 0 } codec.queueInputBuffer(index, 0, readSize, System.nanoTime() / 1000, 0) } }catch (e:Exception){ e.printStackTrace() } }
給MediaCodec傳資料的時候設定的時間戳是當前的系統時間,由於我們使用rtp傳送實時資料,所以flag不需要設定結束標誌。
audioCodec = object : AudioEncodeCodec(mediaFormat) { override fun onOutputBufferAvailable( codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo ) { try { val buffer = codec.getOutputBuffer(index) ?: return if (lastSendAudioTime == 0L) { lastSendAudioTime = info.presentationTimeUs; } val increase = (info.presentationTimeUs - lastSendAudioTime) * audioSampleRate / 1000 / 1000 if (hasAuHeader) { buffer.position(info.offset) buffer.get(bufferArray, 4, info.size) auHeaderLength.apply { bufferArray[0] = this[0] bufferArray[1] = this[1] } auHeader(info.size).apply { bufferArray[2] = this[0] bufferArray[3] = this[1] } audioRtpWrapper?.sendData(bufferArray, info.size + 4, 97, true, increase.toInt()) } else { buffer.position(info.offset) buffer.get(bufferArray, 0, info.size) audioRtpWrapper?.sendData(bufferArray, info.size, 97, true, increase.toInt()) } lastSendAudioTime = info.presentationTimeUs codec.releaseOutputBuffer(index, false) } catch (e: Exception) { e.printStackTrace() } } }
從MediaCodec讀出的是aac原始的資料,我們可以根據具體的需求來決定是否新增au header傳送。這裡實現了有au header和沒有 au header兩種方案。沒有au header的情況我們直接把MediaCode讀出的資料通過rtp傳送出去。有au header的情況我們需要在原始的aac資料前面追加4個位元組的au header。是否有au header與vlc播放的sdp內容有關。後面會詳解介紹sdp內容的設定。
private val auHeaderLength = ByteArray(2).apply { this[0] = 0 this[1] = 0x10 } private fun auHeader(len: Int): ByteArray { return ByteArray(2).apply { this[0] = (len and 0x1fe0 shr 5).toByte() this[1] = (len and 0x1f shl 3).toByte() } }
- au header length佔用兩個位元組,它會描述au header的大小,這裡設定為2.
- au header 佔用兩個位元組,它描述了aac原始資料的大小,這裡需要根據MediaCodec返回的aac原始資料大小進行設定。
Rtp傳送資料
我們使用jrtplib庫來發送資料,這裡對庫進行簡單的封裝並提供了java封裝類RtpWrapper。
public class RtpWrapper { private long nativeObject = 0; private IDataCallback callback; public RtpWrapper() { init(); } @Override protected void finalize() throws Throwable { release(); super.finalize(); } public void setCallback(IDataCallback callback) { this.callback = callback; } void receivedData(byte[] buffer, int len) { if(this.callback != null) this.callback.onReceivedData(buffer, len); } public interface IDataCallback { void onReceivedData(byte[] buffer, int len); } static { try { System.loadLibrary("rtp-lib"); initLib(); } catch (Throwable e) { e.printStackTrace(); } } private native static void initLib(); private native boolean init(); private native boolean release(); public native boolean open(int port, int payloadType, int sampleRate); public native boolean close(); /** * @param ip "192.168.1.1" * @return */ public native boolean addDestinationIp(String ip); public native int sendData(byte[] buffer, int len, int payloadType, boolean mark, int increase); }
open方法要指定傳送資料使用的埠,payloadType設定載體型別,sampleRate是取樣率。
addDestinationIp用於新增接收端ip地址,地址格式: "192.168.1.1"。
sendData方法用於傳送資料,increase是時間間隔,時間單位是 sampleRate/秒
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) { audioRtpWrapper = RtpWrapper() audioRtpWrapper?.open(audioRtpPort, audioPayloadType, audioSampleRate) audioRtpWrapper?.addDestinationIp(ip) }
MediaCodec返回format的時候建立rtp連線並指定目的地址。
try { val buffer = codec.getOutputBuffer(index) ?: return if (lastSendAudioTime == 0L) { lastSendAudioTime = info.presentationTimeUs; } val increase = (info.presentationTimeUs - lastSendAudioTime) * audioSampleRate / 1000 / 1000 if (hasAuHeader) { buffer.position(info.offset) buffer.get(bufferArray, 4, info.size) auHeaderLength.apply { bufferArray[0] = this[0] bufferArray[1] = this[1] } auHeader(info.size).apply { bufferArray[2] = this[0] bufferArray[3] = this[1] } audioRtpWrapper?.sendData(bufferArray, info.size + 4, 97, true, increase.toInt()) } else { buffer.position(info.offset) buffer.get(bufferArray, 0, info.size) audioRtpWrapper?.sendData(bufferArray, info.size, 97, true, increase.toInt()) } lastSendAudioTime = info.presentationTimeUs codec.releaseOutputBuffer(index, false) } catch (e: Exception) { e.printStackTrace() }
傳送資料的時候需要指定payloadType,距離上次傳送資料的時間間隔等資訊。
(info.presentationTimeUs - lastSendAudioTime)計算的是以微妙為單位的時間間隔。
(info.presentationTimeUs - lastSendAudioTime) * audioSampleRate / 1000 / 1000轉換成sampleRate/秒為單位的時間間隔。
rtp傳送aac資料使用的payloadType為97。
SDP檔案配置
vlc播放器播放rtp音訊資料時需要指定sdp檔案,它通過讀取sdp檔案中的資訊可以瞭解rpt接收埠、payloadType型別、音訊的格式等資訊用於接收資料流並解碼播放。這裡有兩種配置方式用於支援有au header和沒有au header的情況。
- 有au header
m=audio 40020 RTP/AVP 97 a=rtpmap:97 mpeg4-generic/44100/1 a=fmtp: 97 streamtype=5;config=1208;sizeLength=13; indexLength=3
- 沒有au header
m=audio 40020 RTP/AVP 97 a=rtpmap:97 mpeg4-generic/44100/1 a=fmtp: 97 streamtype=5;config=1208
sdp檔案配置了埠號為40020, Rtp payload type為97,音訊的取樣率為44100、通道數為1。
config是用於描述音訊資訊的,它的生成方式如下:
private val audioChannelCount = 1; private val audioProfile = 2 /** * 97000, 88200, 64000, 48000,44100, 32000, 24000, 22050,16000, 12000, 11025, 8000,7350, 0, 0, 0 */ private val audioIndex = 4 private val audioSpecificConfig = ByteArray(2).apply { this[0] = ((audioProfile).shl(3).and(0xff)).or(audioIndex.ushr(1).and(0xff)).toByte() this[1] = ((audioIndex.shl(7).and(0xff)).or(audioChannelCount.shl(3).and(0xff))).toByte() }
audioProfile 與設定給MediaCodec的Profile一致,這裡設定的是AACObjectLC(2),所以audioProfile是2。
audioChannelCount通道數是1 。
audioIndex通過註釋的取樣率表格找到對應44100取樣率的index為4 。
比較有au header和沒有au header的兩個版本,發現它們的區別在於是否配置了sizeLength和indexLength。
我這裡的au header是兩個位元組的,sizeLength為13代表佔用了13bit,indexLength為3代表佔用3bit。配合傳送資料時新增au header的程式碼就容易理解了。
private fun auHeader(len: Int): ByteArray { return ByteArray(2).apply { this[0] = (len and 0x1fe0 shr 5).toByte() this[1] = (len and 0x1f shl 3).toByte() } }
vlc測試播放
- vlc開啟工程目錄下的play_audio.sdp/play_audio_auheader.sdp 。
- 啟動Android應用指定執行vlc的電腦的ip地址。
- 開始錄製,如何vlc開啟的是play_audio_auheader.sdp,那麼在開始錄製前需要選中auHeader check box
總結
- AudioRecord的設定資訊與MediaCodec的配置資訊必須一致。
- AudioRecord採用block的方式讀取資料,這樣我們可以直接使用系統時間來配置encode時間戳。
- 是否需要新增au header與sdp配置有關,vlc播放器會按照sdp配置解析au header。
- sdp中的config需要按照實際的音訊配置資訊計算得出,否則不能正常播放。
工程git地址
- 天翼雲全場景業務無縫替換至國產原生作業系統CTyunOS!
- 以羊了個羊為例,淺談小程式抓包與響應報文修改
- 這幾種常見的 JVM 調優場景,你知道嗎?
- 如此狂妄,自稱高效能佇列的Disruptor有啥來頭?
- 為什麼要學習GoF設計模式?
- 827. 最大人工島 : 簡單「並查集 列舉」運用題
- 手把手教你如何使用 Timestream 實現物聯網時序資料儲存和分析
- 850. 矩形面積 II : 掃描線模板題
- Java 併發程式設計解析 | 基於JDK原始碼解析Java領域中的併發鎖,我們可以從中學習到什麼內容?
- 【手把手】光說不練假把式,這篇全鏈路壓測實踐探索
- 大廠鍾愛的全鏈路壓測有什麼意義?四種壓測方案詳細對比分析
- 寫個續集,填坑來了!關於“Thread.sleep(0)這一行‘看似無用’的程式碼”裡面留下的坑。
- 857. 僱傭 K 名工人的最低成本 : 列舉 優先佇列(堆)運用題
- Vue3 實現一個自定義toast(小彈窗)
- 669. 修剪二叉搜尋樹 : 常規樹的遍歷與二叉樹性質
- 讀完 RocketMQ 原始碼,我學會了如何優雅的建立執行緒
- 效能調優——小小的log大大的坑
- 1582. 二進位制矩陣中的特殊位置 : 簡單模擬題
- elementui原始碼學習之仿寫一個el-switch
- 646. 最長數對鏈 : 常規貪心 DP 運用題