Skip to main content

moregeek program

如何实现rtmp或rtsp播放端回调yuv/rgb数据?_daniusdk的博客-多极客编程

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

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

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

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

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

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

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

©著作权归作者所有:来自51CTO博客作者音视频牛哥的原创作品,请联系作者获取转载授权,否则将追究法律责任

android技术分享| 视频通话开发流程(二)_mb60af473914346的博客-多极客编程

多人呼叫 多人呼叫与点对点呼叫区别在于多人呼叫是一次呼叫1个以上的人,中途也可以再呼叫邀请别人加入通话。 整个呼叫的流程跟点对点呼叫类似,但也有些区别,需要添加额外的 API 逻辑来实现功能。下面我们分主叫被叫两种角色来分析。 主叫 发起呼叫 创建多个LocalInvitation 对象 val callArray = arrayOf("1234","5678","8888") callArray

android平台gb28181设备接入端对接编码前后音视频源类型浅析_daniusdk的博客-多极客编程

前言今天主要对Android平台GB28181设备接入模块支持的接入数据类型,做个简单的汇总:编码前数据(目前支持的有YV12/NV21/NV12/I420/RGB24/RGBA32/RGB565等数据类型),其中,Android平台前后摄像头数据,或者屏幕数据,或者Unity拿到的数据,均属编码前数据;编码后数据(如无人机等264/HEVC数据,或者本地解析的MP4音视频数据);拉取RTSP或R

如何在保护用户隐私的同时实现精准广告投放?_hms core的博客-多极客编程

用户在浏览App的页面时,如果经常跳出来不喜欢的弹窗广告不仅损害用户的浏览体验,也让用户对广告内容产生反感。作为App的营销人员,线上投放广告时如何精准捕捉用户需求,同时不引起用户的抵触心理十分重要。当用户不愿意将自己的个人信息,例如年龄、性别、兴趣爱好等隐私数据授权给App时,基于用户正在浏览的页面投放广告是个不错的选择,它决定了一则广告能否高效地定位到目标用户。比如,用户正在新闻App里读一篇

某车联网app 通讯协议加密分析(二) unidbg手把手跑通_奋飞安全的博客-多极客编程

一、目标 有一段时间没有写unidbg相关的文章了,这个样本挺合适,难度适中,还适当给你挖个小坑。所以后面是一个系列文章,包含 unidbg补环境,Trace Block 对比流程,Trace Code定位差异。掌握好这一系列套路,Native分析可以算入门了。 这次先来把so用unidbg跑通 v6.1.0 二、步骤 Dump so IDA打开 libencrypt.so 去到我们要分析的两个函

某车联网app 通讯协议加密分析(三) trace block_奋飞安全的博客-多极客编程

一、目标 之前我们已经用unidbg跑通了libencrypt.so,那么如何判断跑出来的结果是对是错?再如何纠正unidbg跑错误的流程,是我们今天的目标。 v6.1.0 二、步骤 找到明显的接口来判断 checkcode是加密,加密的结果确实不好判断是否正确。不过我们可以试试解密,能解密就是对的,简单粗暴。这里解密函数是 decheckcode 。 public void callB() {

如何让android平台像ipc一样实现gb28181前端设备接入_daniusdk的博客-多极客编程

技术背景好多开发者在做国标对接的时候,首先想到的是IPC摄像头,通过参数化配置,接入到国标平台,实现媒体数据的按需查看等操作。像执法记录仪等智能终端,跑在Android平台,对接GB28181平台的需求也非常大,网上相关demo也不少,但真正设计符合相关协议规范、功能完善、长时间稳定运行的并不多。基于此,我们研发了Android平台GB28181接入模块,目前功能设计,总的来说,IPC有的功能要有

【scan kit】集成扫码服务时android studio总是报错oom如何解决?_华为开发者联盟的博客-多极客编程

【问题描述】1、项目中已经集成了华为推送服务,现在还需要集成华为的统一扫码服务,然后就在app module的build.gradle文件中添加了如下的依赖:implementation 'com.huawei.hms:scan:2.6.0.300'2、然后在编译时出现了如下图中所示的报错信息:​3、错误信息已经拷贝出来了放在了下面,大家可以看下:* What went wrong:Out of

【business touch kit】服务号消息发送接口返回401如何解决?_华为开发者联盟的博客-多极客编程

【Business Touch Kit 简介】华为服务号(Business Touch Kit)提供给商家对应的服务号,让用户快速发现商家服务,建立用户与商家的互动连接。为您提供华为统一的商家服务号中心,通过服务分发,用户互动连接,多样化的营销工具等,促进商家的商业闭环。【问题描述】调用服务号消息发送的接口发起HTTP请求时,返回了401的状态码,如下所示:​请求头参数如下图所示:​请求体参数如下

iphone 下载 testflight,打开软件后怎么是这个界面,求如何使用testflight_wx62d4c604e4fd0的博客-多极客编程

iPhone 下载 testflight,打开软件后怎么是这个界面,求如何使用Testflight 1、安装iOS上架辅助软件Appuploader 2、申请iOS发布证书(p12) 3、申请iOS发布描述文件(mobileprovision) 4、打包ipa 5、上传ipa到iTunes Connect 6、TestFlight方式安装到苹果手机测试 7、设置APP各项信息提交审核 一、下载安

某车联网app 通讯协议加密分析(二) unidbg手把手跑通_奋飞安全的博客-多极客编程

一、目标 有一段时间没有写unidbg相关的文章了,这个样本挺合适,难度适中,还适当给你挖个小坑。所以后面是一个系列文章,包含 unidbg补环境,Trace Block 对比流程,Trace Code定位差异。掌握好这一系列套路,Native分析可以算入门了。 这次先来把so用unidbg跑通 v6.1.0 二、步骤 Dump so IDA打开 libencrypt.so 去到我们要分析的两个函

harmonyos connect faq第四期_华为开发者联盟的博客-多极客编程

在HarmonyOS Connect生态产品的认证测试环节,你是否存在这些疑问:如何获取认证要求、测试标准等信息?如何了解认证流程中的注意事项?如何处理DECC工具使用过程中遇到的问题?……?本期FAQ通过三个部分——认证要求、认证流程、DevEco Certification Centre(以下简称“DECC”)工具,为大家带来19个认证测试常见问题答疑,让你快速get到解决方案。 赶紧往下看吧

hms core热门kit top问题合集,你要问的,这里全都有!_华为开发者联盟的博客-多极客编程

​​HMS Core简介​​HMS Core是华为终端云服务(HUAWEI Mobile Services)开放能力合集,位于开发者应用与操作系统之间,是为应用开发提供基础服务的平台。同时,依托华为云服务,HMS Core也为这些服务提供云端能力,用于各服务的开通、业务实现及运营。对应用的用户来说,集成了HMS Core的应用能为用户提供多样化、多场景、多功能的更佳体验;对应用的开发者来说,HMS