一、接收数据
rtp包的组包与拆包已经由rtp 库完成,这里可以从rtp库的回调直接接收到原始发送的数据。
videoRtpWrapper.open(40018, 96, 90000);
videoRtpWrapper.setCallback { data, len ->
Log.d("dragon_video", "received video data $len")
nalu.appended(data, len) { buffer, offset, size ->
videoBufferQueue.put(buffer);
videoBufferSizeQueue.put(size);
}
}
我们可以看到rtp payload指定的类型是96,96代表的就是h264视频数据类型。这里监听的是偶数端口40018,奇数端口留给rtcp使用。我们接收到的数据是nalu分片数据,我们还需要把分片数据组成完整的nalu数据。
二、分片组包
我们使用NaluData这个工具类进行组包操作,相应的他也提供了拆包方法给发送端使用。
class NaluData {
private val F_MASK = 0b10000000.toByte()
private val NRI_MASK = 0b01100000.toByte()
private val TYPE_MASK = 0b00011111.toByte()
private val START_MASK = 0b10000000.toByte()
private val END_MASK = 0b01000000.toByte()
private val RESERVE_MASK = 0b00100000.toByte()
private val maxFragmentSize = 65535 - 1000
private val data = ByteArray(1000 * 100)
private var position = 0;
fun appended(buffer: ByteArray, len: Int, sender: (ByteArray, Int, Int) -> Unit) {
val type = buffer[0] and TYPE_MASK
//fu indicator type
if (type == 0b11100.toByte()) {
//fu indicator start/end flag
val isStartFlag = (buffer[1] and START_MASK) == START_MASK
val isEndFlag = (buffer[1] and END_MASK) == END_MASK
when {
isStartFlag -> {
data[0] = 0
data[1] = 0
data[2] = 0
data[3] = 1
position = 4
data[position] = (buffer[0] and (F_MASK or NRI_MASK)) or (buffer[1] and TYPE_MASK)
position++
System.arraycopy(buffer, 2, data, position, len - 2)
position += (len - 2)
}
isEndFlag -> {
System.arraycopy(buffer, 2, data, position, len - 2)
position += (len - 2)
sender.invoke(data, 0, position)
}
else -> {
System.arraycopy(buffer, 2, data, position, len - 2)
position += (len - 2)
}
}
} else {
data[0] = 0
data[1] = 0
data[2] = 0
data[3] = 1
position = 4
System.arraycopy(buffer, 0, data, position, len)
position += len
sender.invoke(data, 0, position)
}
}
......
}
接收到数据后第一步是判断是否是分片数据,因为当数据没有超过udp限制的大小的时候,我们可以直接发送原始的nalu数据,当数据超过限制的时候,我们才进行分片操作。
首先我们获取分片类型,根据是否能获取分片类型判断它是否是分片数据。先来看下分片数据的头部构成。
根据获取到的fu indicator type,fu indicator start/end flag进行组片操作。组片过程涉及到将fu indicator和fu header转换成nalu header。根据插图中的描述,我们可以方便的获取转变后的nalu header。组片后要记得将0001头添加到nalu header前面。
三、解码
分片组装成nalu数据后可以直接提供给MediaCode进行解码,解码后的数据通过output surface渲染。
为了能够正确的解码数据,我们在初始化Mediacode阶段需要设置基本的视频信息。
val videoFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 480, 800)
videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, 750000)
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30)
videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5)
var currentTime = 0L
videoDecodeCodec = object : SurfaceDecodeCodec(videoFormat, outputSurface) {
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {
val buffer = codec.getInputBuffer(index) ?: return;
val data = videoBufferQueue.take()
val size = videoBufferSizeQueue.take()
val time = (System.currentTimeMillis() - currentTime) * 1000
buffer.position(0)
buffer.put(data, 0, size)
codec.queueInputBuffer(index, 0, size, time, 0)
}
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
}
}
这里设置了视频的格式,分辨率,码率等信息。输入解码数据的时候需要指定数据的渲染时间,我简单的使用当前时间与解码器启动时间的差值来计算的。
abstract class SurfaceDecodeCodec(mediaFormat: MediaFormat, val outputSurface: Surface) : BaseCodec("SurfaceDecodeCodec", mediaFormat) {
override fun onCreateMediaCodec(mediaFormat: MediaFormat): MediaCodec {
val mediaCodecList = MediaCodecList(MediaCodecList.ALL_CODECS)
val mediaCodecName = mediaCodecList.findDecoderForFormat(mediaFormat)
return MediaCodec.createByCodecName(mediaCodecName)
}
override fun onConfigMediaCodec(mediaCodec: MediaCodec) {
mediaCodec.configure(mediaFormat, outputSurface, null, 0)
}
override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) {
codec.releaseOutputBuffer(index,true);
}
}
由于我们需要把解码的数据显示在ui上,所以这里配置了outputSurface来输出解码后的数据。这个outputSurface就是SurfaceView构造的surface。
构造outputSurface的代码就比较简单了。
class MainActivity : AppCompatActivity() {
var rtpPlayer: PlayerRtp? = null;
var outputSurface: Surface? = null;
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
surfaceView3.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
outputSurface = holder?.surface;
rtpPlayer = PlayerRtp(outputSurface!!);
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
rtpPlayer?.release();
}
})
}
}
在SurfaceView的callback中直接可以获取surface,我们把这个surface提供给MediaCode渲染使用就可以了。