今天某乎收到个问题推荐,如何实现RTSP回调YUV数据,用于二次处理?

正好前些年我们做RTSP和RTMP直播播放的时候,实现过相关的需求,本文就以Android为例,大概说说具体实现吧。

先说回调yuv或rgb这块意义吧,不管是RTSP还是RTMP直播播放模块,解码后的yuv/rgb数据,可以实现比如快照(编码保存png或jpeg)、回调给第三方用于比如视频分析、亦或比如回调给Unity,实现Unity平台下的绘制。

为了图文并茂,让大家有个基本的认识,先上张图,demo展示的是本地播放的同时,可把yuv或rgb回上来,供上层做二次处理:

如何实现RTMP或RTSP播放端回调YUV/RGB数据?_RTMP播放器

我们把协议栈这块处理,放到JNI下,播放之前,设置回调:

libPlayer.SmartPlayerSetExternalRender(playerHandle, new I420ExternalRender());

I420ExternalRender()具体实现:

/*
* SmartPlayer.java
* SmartPlayer
*
* Github: https://github.com/daniulive/SmarterStreaming
*
* Created by DaniuLive on 2015/09/26.
*/

class I420ExternalRender implements NTExternalRender {
// public static final int NT_FRAME_FORMAT_RGBA = 1;
// public static final int NT_FRAME_FORMAT_ABGR = 2;
// public static final int NT_FRAME_FORMAT_I420 = 3;

private int width_ = 0;
private int height_ = 0;

private int y_row_bytes_ = 0;
private int u_row_bytes_ = 0;
private int v_row_bytes_ = 0;

private ByteBuffer y_buffer_ = null;
private ByteBuffer u_buffer_ = null;
private ByteBuffer v_buffer_ = null;

@Override
public int getNTFrameFormat() {
Log.i(TAG, "I420ExternalRender::getNTFrameFormat return "
+ NT_FRAME_FORMAT_I420);
return NT_FRAME_FORMAT_I420;
}

@Override
public void onNTFrameSizeChanged(int width, int height) {
width_ = width;
height_ = height;

y_row_bytes_ = (width_ + 15) & (~15);
u_row_bytes_ = ((width_ + 1) / 2 + 15) & (~15);
v_row_bytes_ = ((width_ + 1) / 2 + 15) & (~15);

y_buffer_ = ByteBuffer.allocateDirect(y_row_bytes_ * height_);
u_buffer_ = ByteBuffer.allocateDirect(u_row_bytes_
* ((height_ + 1) / 2));
v_buffer_ = ByteBuffer.allocateDirect(v_row_bytes_
* ((height_ + 1) / 2));

Log.i(TAG, "I420ExternalRender::onNTFrameSizeChanged width_="
+ width_ + " height_=" + height_ + " y_row_bytes_="
+ y_row_bytes_ + " u_row_bytes_=" + u_row_bytes_
+ " v_row_bytes_=" + v_row_bytes_);
}

@Override
public ByteBuffer getNTPlaneByteBuffer(int index) {
if (index == 0) {
return y_buffer_;
} else if (index == 1) {
return u_buffer_;
} else if (index == 2) {
return v_buffer_;
} else {
Log.e(TAG, "I420ExternalRender::getNTPlaneByteBuffer index error:" + index);
return null;
}
}

@Override
public int getNTPlanePerRowBytes(int index) {
if (index == 0) {
return y_row_bytes_;
} else if (index == 1) {
return u_row_bytes_;
} else if (index == 2) {
return v_row_bytes_;
} else {
Log.e(TAG, "I420ExternalRender::getNTPlanePerRowBytes index error:" + index);
return 0;
}
}

public void onNTRenderFrame(int width, int height, long timestamp)
{
if ( y_buffer_ == null )
return;

if ( u_buffer_ == null )
return;

if ( v_buffer_ == null )
return;


y_buffer_.rewind();

u_buffer_.rewind();

v_buffer_.rewind();

/*
if ( !is_saved_image )
{
is_saved_image = true;

int y_len = y_row_bytes_*height_;

int u_len = u_row_bytes_*((height_+1)/2);
int v_len = v_row_bytes_*((height_+1)/2);

int data_len = y_len + (y_row_bytes_*((height_+1)/2));

byte[] nv21_data = new byte[data_len];

byte[] u_data = new byte[u_len];
byte[] v_data = new byte[v_len];

y_buffer_.get(nv21_data, 0, y_len);
u_buffer_.get(u_data, 0, u_len);
v_buffer_.get(v_data, 0, v_len);

int[] strides = new int[2];
strides[0] = y_row_bytes_;
strides[1] = y_row_bytes_;


int loop_row_c = ((height_+1)/2);
int loop_c = ((width_+1)/2);

int dst_row = y_len;
int src_v_row = 0;
int src_u_row = 0;

for ( int i = 0; i < loop_row_c; ++i)
{
int dst_pos = dst_row;

for ( int j = 0; j <loop_c; ++j )
{
nv21_data[dst_pos++] = v_data[src_v_row + j];
nv21_data[dst_pos++] = u_data[src_u_row + j];
}

dst_row += y_row_bytes_;
src_v_row += v_row_bytes_;
src_u_row += u_row_bytes_;
}

String imagePath = "/sdcard" + "/" + "testonv21" + ".jpeg";

Log.e(TAG, "I420ExternalRender::begin test save iamge++ image_path:" + imagePath);

try
{
File file = new File(imagePath);

FileOutputStream image_os = new FileOutputStream(file);

YuvImage image = new YuvImage(nv21_data, ImageFormat.NV21, width_, height_, strides);

image.compressToJpeg(new android.graphics.Rect(0, 0, width_, height_), 50, image_os);

image_os.flush();
image_os.close();
}
catch(IOException e)
{
e.printStackTrace();
}

Log.e(TAG, "I420ExternalRender::begin test save iamge--");
}

*/


Log.i(TAG, "I420ExternalRender::onNTRenderFrame w=" + width + " h=" + height + " timestamp=" + timestamp);

// copy buffer

// test
// byte[] test_buffer = new byte[16];
// y_buffer_.get(test_buffer);

// Log.i(TAG, "I420ExternalRender::onNTRenderFrame y data:" + bytesToHexString(test_buffer));

// u_buffer_.get(test_buffer);
// Log.i(TAG, "I420ExternalRender::onNTRenderFrame u data:" + bytesToHexString(test_buffer));

// v_buffer_.get(test_buffer);
// Log.i(TAG, "I420ExternalRender::onNTRenderFrame v data:" + bytesToHexString(test_buffer));
}
}

为了验证回上来的数据是否正常,我们加了保存jpeg文件的代码。

当然,回调yuv或rgb,可以做的更精细,比如我们windows的RTMP或RTSP播放器,回调数据,可以指定分辨率(比如缩放)和frame类型:

/*
设置视频回调, 吐视频数据出来, 可以指定吐出来的视频宽高
*handle: 播放句柄
*scale_width:缩放宽度(必须是偶数,建议是 16 的倍数)
*scale_height:缩放高度(必须是偶数
*scale_filter_mode: 缩放质量, 0 的话 SDK 将使用默认值, 目前可设置范围为[1, 3], 值越大 缩放质量越好,但越耗性能
*frame_format: 只能是NT_SP_E_VIDEO_FRAME_FORMAT_RGB32, NT_SP_E_VIDEO_FRAME_FROMAT_I420
成功返回NT_ERC_OK
*/
NT_UINT32(NT_API *SetVideoFrameCallBackV2)(NT_HANDLE handle,
NT_INT32 scale_width, NT_INT32 scale_height,
NT_INT32 scale_filter_mode, NT_INT32 frame_format,
NT_PVOID call_back_data, SP_SDKVideoFrameCallBack call_back);

相关视频帧图像格式和帧结构:

//定义视频帧图像格式
typedef enum _NT_SP_E_VIDEO_FRAME_FORMAT
{
NT_SP_E_VIDEO_FRAME_FORMAT_RGB32 = 1, // 32位的rgb格式, r, g, b各占8, 另外一个字节保留, 内存字节格式为: bb gg rr xx, 主要是和windows位图匹配, 在小端模式下,按DWORD类型操作,最高位是xx, 依次是rr, gg, bb
NT_SP_E_VIDEO_FRAME_FORMAT_ARGB = 2, // 32位的argb格式,内存字节格式是: bb gg rr aa 这种类型,和windows位图匹配
NT_SP_E_VIDEO_FRAME_FROMAT_I420 = 3, // YUV420格式, 三个分量保存在三个面上
} NT_SP_E_VIDEO_FRAME_FORMAT;


// 定义视频帧结构.
typedef struct _NT_SP_VideoFrame
{
NT_INT32 format_; // 图像格式, 请参考NT_SP_E_VIDEO_FRAME_FORMAT
NT_INT32 width_; // 图像宽
NT_INT32 height_; // 图像高

NT_UINT64 timestamp_; // 时间戳, 一般是0,不使用, 以ms为单位的

// 具体的图像数据, argb和rgb32只用第一个, I420用前三个
NT_UINT8* plane0_;
NT_UINT8* plane1_;
NT_UINT8* plane2_;
NT_UINT8* plane3_;

// 每一个平面的每一行的字节数,对于argb和rgb32,为了保持和windows位图兼容,必须是width_*4
// 对于I420, stride0_ 是y的步长, stride1_ 是u的步长, stride2_ 是v的步长,
NT_INT32 stride0_;
NT_INT32 stride1_;
NT_INT32 stride2_;
NT_INT32 stride3_;

} NT_SP_VideoFrame;

感兴趣的开发者可以酌情参考,实现自己的业务逻辑。