场景说明
公司要做个实时视频的项目,用的RK3399的开发板,通过开发板的网口接收H264裸流数据并展示。开发板上跑Android 9,因为3399这个板子上有ARM 的mali T860MP4 GPU,T860主要面向中端的高分辨率需求,支持4K视频功耗也比此前更低,性能还是不错的,所以选择使用MediaCodec进行硬解。
获取UDP数据
通过开发板的网口获取UDP数据这块,网上代码很多一搜一大堆,这里简单罗列下我的代码:
//初始化udp连接
try {
//6666是指定的推送到的网络端口
udpClient = new DatagramSocket(6666);
if (receivePacket == null) {
// 创建接受数据的 packet
receivePacket = new DatagramPacket(receiveByte, BUFFER_LENGTH);
}
} catch (SocketException e) {
e.printStackTrace();
}
/**
* 接收Udp数据
*/
private void startUdpSocketThread() {
clientUdphread = new Thread(new Runnable() {
@Override
public void run() {
receiveMessage();
}
});
isThreadRunning = true;
clientUdphread.start();
}
/**
* 处理接受到的UDP消息
*/
private void receiveMessage() {
Log.d(TAG, "receiveMessage start");
while (isThreadRunning) {
try {
if (udpClient != null) {
udpClient.receive(receivePacket);
}
} catch (IOException e) {
Log.e(TAG, "UDP数据包接收失败!stopUDPSocket");
stopUDPSocket();
e.printStackTrace();
return;
}
if (receivePacket == null || receivePacket.getLength() == 0) {
Log.e(TAG, "无法接收UDP数据或者接收到的UDP数据为空");
continue;
}
//H264数据是以00 00 00 01开头的,通过这个标志判断是新的一帧,将数据放入tempByteBuffer,tempByteBuffer设置的大小是200*1024,receiveByte设置的是8192
if("0".equals(Integer.toHexString(receivePacket.getData()[0] & 0x0FF))
&& "0".equals(Integer.toHexString(receivePacket.getData()[1] & 0x0FF))
&& "0".equals(Integer.toHexString(receivePacket.getData()[2] & 0x0FF))
&& "1".equals(Integer.toHexString(receivePacket.getData()[3] & 0x0FF))) {
//Log.d(TAG, "帧类型="+Integer.toHexString(receivePacket.getData()[4] & 0x0FF)+",tempByteBuffer大小="+tempByteBuffer.position());
//这里将一帧数据传给mediacodec
onFrame(tempByteBuffer);
tempByteBuffer.clear();
tempByteBuffer.put(receivePacket.getData(),0,receivePacket.getLength());
//Log.d(TAG, "tempByteBuffer大小="+tempByteBuffer.position());
}else{
tempByteBuffer.put(receivePacket.getData(),0,receivePacket.getLength());
//Log.d(TAG, "tempByteBuffer大小="+tempByteBuffer.position());
//onFrame()
}
//onFrame(receivePacket.getData(),0,BUFFER_LENGTH);
// 每次接收完UDP数据后,重置长度。否则可能会导致下次收到数据包被截断。
if (receivePacket != null) {
receivePacket.setLength(BUFFER_LENGTH);
}
}
}
初始化解码器
这里要注意的是mediacodec的configure方法必须在surfaceview准备后之后才可以,所以写在监听里了。
try {
//创建解码器
zMediaCodec = MediaCodec.createDecoderByType(MIME_TYPE);
}catch (IOException e){
e.printStackTrace();
Log.d(TAG, "MediaCodec decode IOException");
}
mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, 1920, 1080);
//设置帧率
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
zSurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
Log.d(TAG, "surfaceCreated");
zMediaCodec.configure(mediaFormat, zSurfaceView.getHolder().getSurface(), null, 0);
zMediaCodec.start();
Log.d(TAG, "MediaCodec start");
startUdpSocketThread();
}
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
Log.d(TAG, "surfaceChanged");
}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
mediaCodecstop();
}
});
解码一帧
最开始犯了一个错误,把udp接收到的数据调用onframe,onframe接收的是一帧的数据,所以保证传输的数据是一帧的
/**
* 处理帧
*/
public boolean onFrame(ByteBuffer buf) {
//Log.e("Media", "onFrame start");
//Log.e("Media", "onFrame Thread:" + Thread.currentThread().getId());
// Get input buffer index
@SuppressWarnings("deprecation")
ByteBuffer[] inputBuffers = zMediaCodec.getInputBuffers();
int inputBufferIndex = zMediaCodec.dequeueInputBuffer(0);
//Log.e("Media", "onFrame index:" + inputBufferIndex);
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
inputBuffer.clear();
//这个用法挺不好
for(int i=0;i<buf.position();i++){
inputBuffer.put(buf.get(i));
}
zMediaCodec.queueInputBuffer(inputBufferIndex, 0, inputBuffer.position(), 0, 0);
//mCount++;
} else {
return false;
}
// Get output buffer index
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = zMediaCodec.dequeueOutputBuffer(bufferInfo, 100);
while (outputBufferIndex >= 0) {
zMediaCodec.releaseOutputBuffer(outputBufferIndex, true);
outputBufferIndex = zMediaCodec.dequeueOutputBuffer(bufferInfo, 0);
}
//Log.e("Media", "onFrame end");
return true;
}
后续
后续还会加推流的功能,把获取到的视频流通过rtmp或rtsp推送到流媒体服务器或公共的直播平台上。