Android手機(jī)通過rtp發(fā)送aac數(shù)據(jù)給vlc播放的實(shí)現(xiàn)步驟
截屏

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()
}
音頻采集時(shí)需要設(shè)置采集參數(shù),設(shè)置的這些參數(shù)需要與創(chuàng)建MediaCodec時(shí)的參數(shù)一致。
- sampleRate是采樣率:44100
- channelCount是通道數(shù):1
- 單個(gè)采樣數(shù)據(jù)大小格式:AudioFormat.ENCODING_PCM_16BIT
- 最小數(shù)據(jù)buffer:AudioRecord.getMinBufferSize()計(jì)算獲取
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()
}
}
這里采用的阻塞的方式采集數(shù)據(jù),所以AudioRecord依據(jù)設(shè)置的采樣頻率生成數(shù)據(jù)的,我們可以直接把當(dāng)前的時(shí)間設(shè)置為錄制的時(shí)間戳。
MediaCodec編碼音頻數(shù)據(jù)
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創(chuàng)建MediaFormat并設(shè)置參數(shù),這里設(shè)置的音頻參數(shù)必須與AudioRecord一致。
- MIME_TYPE:"audio/mp4a-latm"
- 采樣頻率與AudioRecord一致:44100
- 通道數(shù)與AudioRecord一致:1
- KEY_AAC_PROFILE配置為低帶寬要求類型:AACObjectLC
- KEY_BIT_RATE設(shè)置的大小影響編碼壓縮率: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傳數(shù)據(jù)的時(shí)候設(shè)置的時(shí)間戳是當(dāng)前的系統(tǒng)時(shí)間,由于我們使用rtp發(fā)送實(shí)時(shí)數(shù)據(jù),所以flag不需要設(shè)置結(jié)束標(biāo)志。
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原始的數(shù)據(jù),我們可以根據(jù)具體的需求來決定是否添加au header發(fā)送。這里實(shí)現(xiàn)了有au header和沒有 au header兩種方案。沒有au header的情況我們直接把MediaCode讀出的數(shù)據(jù)通過rtp發(fā)送出去。有au header的情況我們需要在原始的aac數(shù)據(jù)前面追加4個(gè)字節(jié)的au header。是否有au header與vlc播放的sdp內(nèi)容有關(guān)。后面會(huì)詳解介紹sdp內(nèi)容的設(shè)置。
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占用兩個(gè)字節(jié),它會(huì)描述au header的大小,這里設(shè)置為2.
- au header 占用兩個(gè)字節(jié),它描述了aac原始數(shù)據(jù)的大小,這里需要根據(jù)MediaCodec返回的aac原始數(shù)據(jù)大小進(jìn)行設(shè)置。
Rtp發(fā)送數(shù)據(jù)
我們使用jrtplib庫來發(fā)送數(shù)據(jù),這里對庫進(jìn)行簡單的封裝并提供了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方法要指定發(fā)送數(shù)據(jù)使用的端口,payloadType設(shè)置載體類型,sampleRate是采樣率。
addDestinationIp用于添加接收端ip地址,地址格式: "192.168.1.1"。
sendData方法用于發(fā)送數(shù)據(jù),increase是時(shí)間間隔,時(shí)間單位是 sampleRate/秒
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
audioRtpWrapper = RtpWrapper()
audioRtpWrapper?.open(audioRtpPort, audioPayloadType, audioSampleRate)
audioRtpWrapper?.addDestinationIp(ip)
}
MediaCodec返回format的時(shí)候創(chuàng)建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()
}
發(fā)送數(shù)據(jù)的時(shí)候需要指定payloadType,距離上次發(fā)送數(shù)據(jù)的時(shí)間間隔等信息。
(info.presentationTimeUs - lastSendAudioTime)計(jì)算的是以微妙為單位的時(shí)間間隔。
(info.presentationTimeUs - lastSendAudioTime) * audioSampleRate / 1000 / 1000轉(zhuǎn)換成sampleRate/秒為單位的時(shí)間間隔。
rtp發(fā)送aac數(shù)據(jù)使用的payloadType為97。
SDP文件配置
vlc播放器播放rtp音頻數(shù)據(jù)時(shí)需要指定sdp文件,它通過讀取sdp文件中的信息可以了解rpt接收端口、payloadType類型、音頻的格式等信息用于接收數(shù)據(jù)流并解碼播放。這里有兩種配置方式用于支持有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文件配置了端口號(hào)為40020, Rtp payload type為97,音頻的采樣率為44100、通道數(shù)為1。
音頻config配置計(jì)算方式:

比較有au header和沒有au header的兩個(gè)版本,發(fā)現(xiàn)它們的區(qū)別在于是否配置了sizeLength和indexLength。
我這里的au header是兩個(gè)字節(jié)的,sizeLength為13代表占用了13bit,indexLength為3代表占用3bit。配合發(fā)送數(shù)據(jù)時(shí)添加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 。
- 啟動(dòng)Android應(yīng)用指定運(yùn)行vlc的電腦的ip地址。
- 開始錄制,如何vlc打開的是play_audio_auheader.sdp,那么在開始錄制前需要選中auHeader check box
總結(jié)
- AudioRecord的設(shè)置信息與MediaCodec的配置信息必須一致。
- AudioRecord采用block的方式讀取數(shù)據(jù),這樣我們可以直接使用系統(tǒng)時(shí)間來配置encode時(shí)間戳。
- 是否需要添加au header與sdp配置有關(guān),vlc播放器會(huì)按照sdp配置解析au header。
- sdp中的config需要按照實(shí)際的音頻配置信息計(jì)算得出,否則不能正常播放。
工程git地址
https://github.com/mjlong123123/AudioRecorder
以上就是Android手機(jī)通過rtp發(fā)送aac數(shù)據(jù)給vlc播放的實(shí)現(xiàn)步驟的詳細(xì)內(nèi)容,更多關(guān)于Android rtp發(fā)送aac數(shù)據(jù)給vlc播放的資料請關(guān)注腳本之家其它相關(guān)文章!
- java、android可用的rtp封包解包h264案例
- Android下拉刷新ListView——RTPullListView(demo)
- Android GSYVideoPlayer視頻播放器功能的實(shí)現(xiàn)
- Android實(shí)現(xiàn)本地Service方法控制音樂播放
- Android項(xiàng)目實(shí)現(xiàn)視頻播放器
- Android Studio實(shí)現(xiàn)音樂播放器
- Android Studio實(shí)現(xiàn)簡單音樂播放功能的示例代碼
- Android實(shí)現(xiàn)文字滾動(dòng)播放效果的代碼
- Android仿優(yōu)酷視頻的懸浮窗播放效果
- Android10.0實(shí)現(xiàn)本地音樂播放(附源碼下載)
- Android原生視頻播放VideoView的使用
相關(guān)文章
Android 簡單的實(shí)現(xiàn)滑塊拼圖驗(yàn)證碼功能
這篇文章主要介紹了Android 簡單的實(shí)現(xiàn)滑塊拼圖驗(yàn)證碼功能,幫助大家更好的理解和學(xué)習(xí)使用Android開發(fā),感興趣的朋友可以了解下2021-03-03
一個(gè)簡單的toolabar結(jié)合drawlayout使用方法
這篇文章主要為大家詳細(xì)介紹了一個(gè)簡單的toolabar結(jié)合drawlayout的使用方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10
Android 8.0版本更新無法自動(dòng)安裝問題的解決方法
這篇文章主要為大家詳細(xì)介紹了Android 8.0版本更新無法自動(dòng)安裝問題的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-06-06
利用SpannableString和ImageSpan在textview中插入圖片的方法
這篇文章主要為大家詳細(xì)介紹了利用SpannableString和ImageSpan在textview中插入圖片的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-11-11
Android實(shí)現(xiàn)購物車整體頁面邏輯詳解
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)購物車的整體頁面邏輯,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-11-11
Android獲得內(nèi)/外置存儲(chǔ)卡路徑的方法
我們知道Android上一般都有外置的存儲(chǔ)卡,內(nèi)置存儲(chǔ)卡路徑大家都知道怎么獲得的。那么如何獲取外置存儲(chǔ)卡的位置呢?下面小編通過本文給大家分享下2017-01-01

