今天某乎收到个问题推荐,如何实现RTSP回调YUV数据,用于二次处理?
正好前些年我们做RTSP和RTMP直播播放的时候,实现过相关的需求,本文就以Android为例,大概说说具体实现吧。
先说回调yuv或rgb这块意义吧,不管是RTSP还是RTMP直播播放模块,解码后的yuv/rgb数据,可以实现比如快照(编码保存png或jpeg)、回调给第三方用于比如视频分析、亦或比如回调给Unity,实现Unity平台下的绘制。
为了图文并茂,让大家有个基本的认识,先上张图,demo展示的是本地播放的同时,可把yuv或rgb回上来,供上层做二次处理:
我们把协议栈这块处理,放到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 ;