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

简介: 今天某乎收到个问题推荐,如何实现RTSP回调YUV数据,用于二次处理?正好前些年我们做RTSP和RTMP直播播放的时候,实现过相关的需求,本文就以Android为例,大概说说具体实现吧。

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


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


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


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

7945299877f749a1a4397032602d9f77.jpg我们把协议栈这块处理,放到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;

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

相关文章
从rtsp视频流中截取图片
从rtsp视频流中截取图片
1772 0
|
2月前
|
编解码 算法 图形学
同一路RTSP|RTMP流如何同时回调YUV和RGB数据实现渲染和算法分析
我们播放RTSP|RTMP流,如果需要同时做渲染和算法分析的话,特别是渲染在上层实现(比如Unity),算法是python这种情况,拉两路流,更耗费带宽和性能,拉一路流,同时回调YUV和RGB数据也可以,但是更灵活的是本文提到的按需转算法期望的RGB数据,然后做算法处理
|
3月前
|
算法 数据处理 开发工具
Android平台RTSP|RTMP播放器如何回调YUV或RGB数据
在开发Android平台上的RTSP或RTMP播放器时,开发者不仅追求低延迟播放,还希望获取解码后的视频数据(如YUV或RGB格式),以便进行视觉算法分析。使用大牛直播SDK中的SmartPlayer,可在确保播放流畅的同时,通过设置外部渲染器(`SmartPlayerSetExternalRender`)来高效地回调原始视频数据。例如,对于RGBA数据,需实现`NTExternalRender`接口,并重写相关方法以处理数据和尺寸变化。同样地,对于I420(YUV)数据,也需要相应地实现接口以满足需求。这种方式使得开发者能在不影响常规播放功能的情况下,进行定制化的视频处理任务。
|
3月前
|
iOS开发 开发者
iOS平台RTMP|RTSP播放器如何实时回调YUV数据
我们在做RTMP、RTSP播放器的时候,有开发者需要自己处理拉取到的YUV数据,做二次分析之用,为此,我们做了以下的设计:InitPlayer之后,再调用SmartPlayerStart()接口之前,设置yuv数据回调即可。
|
3月前
|
Web App开发 编解码 前端开发
怎么把rtsp视频流转成webrtc接口
【8月更文挑战第8天】将RTSP视频流转换为WebRTC接口涉及多个步骤:首先使用工具如webrtc-streamer、ffmpeg等从RTSP源获取视频流;接着对流进行解码与编码至WebRTC兼容格式(如VP8、H.264);然后利用WebSocket建立客户端与服务器间的双向信令通道;再通过JavaScript创建WebRTC PeerConnection并交换SDP信息;最后添加Track传输视频数据,并在接收端解析显示。使用webrtc-streamer可简化这一过程,其API支持多种操作如添加ICE候选、创建Offer等。
562 2
|
Linux 开发工具 图形学
Unity下如何实现RTMP或RTSP播放端录像?
Unity下如何实现RTMP或RTSP播放端录像?
264 0
|
Linux 开发工具 图形学
Unity下如何实现RTMP或RTSP流播放和录制
在探讨Unity平台RTMP或RTSP直播流数据播放和录制之前,我们先简单回顾下RTSP或RTMP直播流数据在Unity平台的播放流程: 1. 通过Native RTSP或RTSP直播播放SDK回调RGB/YUV420/NV12等其中的一种未压缩的图像格式; 2. Unity下创建相应的RGB/YUV420等Shader; 1. Unity从各个平台获取图像数据来填充纹理即可。
311 0
|
前端开发 Android开发 开发者
Android平台RTSP、RTMP播放端如何实现YUV或ARGB数据按设定角度旋转
做音视频RTSP或RTMP直播播放器的时候,不免会遇到这样的诉求,实时播放或快照的时候,由于前端摄像头安装角度不一定是正向,导致播放或快照的时候,视频view显示的画面是呈90° 180°甚至270°旋转的。
172 0
|
数据处理 开发工具 Android开发
Android平台RTMP/RTSP播放器开发系列之解码和绘制
本文主要抛砖引玉,粗略介绍下Android平台RTMP/RTSP播放器中解码和绘制相关的部分(Github)。
|
编解码 开发工具 Android开发
RTSP播放器或RTMP播放器常用的事件回调设计
很多开发者在开发RTSP或RTMP播放器的时候,不晓得哪些event回调事件是有意义的,针对此,我们以大牛直播SDK(github)的Android平台RTSP/RTMP直播播放端为例,简单介绍下常用的event id,总的来说,有以下几个部分组成: