今天某乎收到个问题推荐,如何实现RTSP回调YUV数据,用于二次处理?
正好前些年我们做RTSP和RTMP直播播放的时候,实现过相关的需求,本文就以Android为例,大概说说具体实现吧。
先说回调yuv或rgb这块意义吧,不管是RTSP还是RTMP直播播放模块,解码后的yuv/rgb数据,可以实现比如快照(编码保存png或jpeg)、回调给第三方用于比如视频分析、亦或比如回调给Unity,实现Unity平台下的绘制。
为了图文并茂,让大家有个基本的认识,先上张图,demo展示的是本地播放的同时,可把yuv或rgb回上来,供上层做二次处理:
我们把协议栈这块处理,放到JNI下,播放之前,设置回调:
代码语言:javascript复制libPlayer.SmartPlayerSetExternalRender(playerHandle, new I420ExternalRender());
I420ExternalRender()具体实现:
代码语言:javascript复制/*
* 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类型:
代码语言:javascript复制/*
设置视频回调, 吐视频数据出来, 可以指定吐出来的视频宽高
*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);
相关视频帧图像格式和帧结构:
代码语言:javascript复制//定义视频帧图像格式
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;
感兴趣的开发者可以酌情参考,实现自己的业务逻辑。