Android手机通过rtp发送aac数据给vlc播放的实现步骤

作者:mjlong123 时间:2023-11-10 00:48:02 

目录
  • 截屏

  • AudioRecord音频采集

  • MediaCodec编码音频数据

  • Rtp发送数据

  • SDP文件配置

  • 音频config配置计算方式:

  • vlc测试播放

  • 总结

  • 工程git地址

截屏

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配置计算方式:

Android手机通过rtp发送aac数据给vlc播放的实现步骤

比较有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测试播放

  1. vlc打开工程目录下的play_audio.sdp/play_audio_auheader.sdp 。

  2. 启动Android应用指定运行vlc的电脑的ip地址。

  3. 开始录制,如何vlc打开的是play_audio_auheader.sdp,那么在开始录制前需要选中auHeader check box

总结

  1. AudioRecord的设置信息与MediaCodec的配置信息必须一致。

  2. AudioRecord采用block的方式读取数据,这样我们可以直接使用系统时间来配置encode时间戳。

  3. 是否需要添加au header与sdp配置有关,vlc播放器会按照sdp配置解析au header。

  4. sdp中的config需要按照实际的音频配置信息计算得出,否则不能正常播放。

工程git地址

https://github.com/mjlong123123/AudioRecorder

来源:https://segmentfault.com/a/1190000039804006

标签:Android,rtp,aac数据,vlc播放
0
投稿

猜你喜欢

  • Java SQL注入案例教程及html基础入门

    2023-09-22 08:34:12
  • OpenCV画任意圆弧曲线

    2023-06-22 19:28:44
  • 详解java 中泛型中的类型擦除和桥方法

    2021-07-10 23:55:08
  • 关于Android 4.4相机预览、录像花屏的问题的解决方法

    2023-01-21 11:50:26
  • 超简洁java实现双色球若干注随机号码生成(实例代码)

    2023-12-03 22:39:08
  • 基于WPF实现控件轮廓跑马灯动画效果

    2022-05-03 20:10:08
  • java使用Dijkstra算法实现单源最短路径

    2022-02-16 23:13:26
  • 浅谈JVM垃圾回收有哪些常用算法

    2022-02-28 16:51:56
  • Spring框架初始化解析

    2021-06-09 16:46:59
  • Android巧用ActionBar实现下拉式导航

    2023-12-02 04:00:59
  • 深入学习java中的Groovy 和 Scala 类

    2023-04-09 10:51:29
  • Android实现LED发光字效果

    2021-09-14 21:58:12
  • Android Fragment概述及用法

    2022-08-23 15:54:22
  • SpringBoot+Prometheus+Grafana实现应用监控和报警的详细步骤

    2023-10-02 06:40:26
  • Mybatis-Plus注入SQL原理分析

    2022-11-09 21:17:22
  • c#中SqlTransaction——事务详解

    2022-12-16 15:40:54
  • Android MaterialCardView的使用介绍与示例

    2023-06-23 06:31:56
  • struts2实现文件上传显示进度条效果

    2021-11-19 16:16:21
  • Java基础之异常处理详解

    2021-07-14 03:03:49
  • Android系统音量条实例代码

    2022-05-26 15:20:48
  • asp之家 软件编程 m.aspxhome.com