本文主要抛砖引玉,粗略介绍下Android平台RTMP/RTSP播放器中解码和绘制相关的部分(Github)。

解码

提到解码,大家都知道软硬解,甚至一些公司觉得硬解码已经足够通用,慢慢抛弃软解了,如果考虑到设备匹配,软硬解码都支持,是个不错的选择,为此,大牛直播SDK在开发这块的时候,分类是这样的:

1. 软解码:解码后获取到原始数据,可进行后续的原始数据回调和快照等操作;

2. 硬解码:解码后获取到原始数据,可进行后续的原始数据回调和快照等操作;

3. 硬解码:设置surface模式,直接render到设置的surface上,不可进行快照和解码后数据回调操作。

大家可能会疑惑,有了模式2,干嘛要再支持模式3呢?模式2和3分别有什么优势呢?

硬解码直接设置surface模式,相对来说,大多芯片支持更好,解码通用性更好,而且减少了数据拷贝,资源占用更低,缺点是无法获得解码后的原始数据,更像个黑盒操作;模式2兼顾了硬解码资源占用(相对软解)和二次操作原始数据能力(如针对解码后的yuv/rgb数据二次处理),解码通用性相对模式3略差,但数据处理更灵活。

相关接口:

	/**
* Set Video H.264 HW decoder(设置H.264硬解码)
*
* @param handle: return value from SmartPlayerOpen()
*
* @param isHWDecoder: 0: software decoder; 1: hardware decoder.
*
* @return {0} if successful
*/
public native int SetSmartPlayerVideoHWDecoder(long handle, int isHWDecoder); /**
* Set Video H.265(hevc) HW decoder(设置H.265硬解码)
*
* @param handle: return value from SmartPlayerOpen()
*
* @param isHevcHWDecoder: 0: software decoder; 1: hardware decoder.
*
* @return {0} if successful
*/
public native int SetSmartPlayerVideoHevcHWDecoder(long handle, int isHevcHWDecoder); /**
* Set Surface view(设置播放的surfaceview).
*
* @param handle: return value from SmartPlayerOpen()
*
* @param surface: surface view
*
* <pre> NOTE: if not set or set surface with null, it will playback audio only. </pre>
*
* @return {0} if successful
*/
public native int SmartPlayerSetSurface(long handle, Object surface);

考虑到不是所有设备都支持硬解,大牛直播SDK的设计思路是先做硬解检测,检测到不支持,直接切换到软解模式。

绘制

大牛直播SDK的RTMP和RTSP播放器绘制这块,支持两种模式,普通的SurfaceView和GLSurface,普通的surface兼容性更好,GLSurface绘制相对来说更细腻,此外,普通的surface模式下,还支持了一些抗锯齿参数设置。两种模式下,都设计了视频画面的填充模式设置选项(是否等比例显示),具体接口设计如下:

	/**
* 设置视频画面的填充模式,如填充整个view、等比例填充view,如不设置,默认填充整个view
* @param handle: return value from SmartPlayerOpen()
* @param render_scale_mode 0: 填充整个view; 1: 等比例填充view, 默认值是0
* @return {0} if successful
*/
public native int SmartPlayerSetRenderScaleMode(long handle, int render_scale_mode); /**
* 设置SurfaceView模式下(NTRenderer.CreateRenderer第二个参数传false的情况),render类型
*
* @param handle: return value from SmartPlayerOpen()
*
* @param format: 0: RGB565格式,如不设置,默认此模式; 1: ARGB8888格式
*
* @return {0} if successful
*/
public native int SmartPlayerSetSurfaceRenderFormat(long handle, int format); /**
* 设置SurfaceView模式下(NTRenderer.CreateRenderer第二个参数传false的情况),抗锯齿效果,注意:抗锯齿模式开启后,可能会影像性能,请慎用
*
* @param handle: return value from SmartPlayerOpen()
*
* @param isEnableAntiAlias: 0: 如不设置,默认不开启抗锯齿模式; 1: 开启抗锯齿模式
*
* @return {0} if successful
*/
public native int SmartPlayerSetSurfaceAntiAlias(long handle, int isEnableAntiAlias);

音频输出这块,可以考虑audiotrack和opensl es,考虑到通用性,可以选择audiotrack模式,当然最好是设置个选项,用户自行选择:

	/**
* Set AudioOutput Type(设置audio输出类型)
*
* @param handle: return value from SmartPlayerOpen()
*
* @param use_audiotrack:
*
* <pre> NOTE: if use_audiotrack with 0: it will use auto-select output devices; if with 1: will use audio-track mode. </pre>
*
* @return {0} if successful
*/
public native int SmartPlayerSetAudioOutputType(long handle, int use_audiotrack);

视频view反转/旋转

	/**
* 设置视频垂直反转
*
* @param handle: return value from SmartPlayerOpen()
*
* @param is_flip: 0: 不反转, 1: 反转
*
* @return {0} if successful
*/
public native int SmartPlayerSetFlipVertical(long handle, int is_flip); /**
* 设置视频水平反转
*
* @param handle: return value from SmartPlayerOpen()
*
* @param is_flip: 0: 不反转, 1: 反转
*
* @return {0} if successful
*/
public native int SmartPlayerSetFlipHorizontal(long handle, int is_flip); /**
* 设置顺时针旋转, 注意除了0度之外, 其他角度都会额外消耗性能
*
* @param handle: return value from SmartPlayerOpen()
*
* @param degress: 当前支持 0度,90度, 180度, 270度 旋转
*
* @return {0} if successful
*/
public native int SmartPlayerSetRotation(long handle, int degress);

解码后原始数据回调

在有些场景下,开发者需要针对解码后的YUV/RGB或者PCM数据进行处理,这个时候,需要设计针对解码后数据回调的接口模型:

	/**
* Set External Render(设置回调YUV/RGB数据)
*
* @param handle: return value from SmartPlayerOpen()
*
* @param external_render: External Render
*
* @return {0} if successful
*/
public native int SmartPlayerSetExternalRender(long handle, Object external_render); /**
* Set External Audio Output(设置回调PCM数据)
*
* @param handle: return value from SmartPlayerOpen()
*
* @param external_audio_output: External Audio Output
*
* @return {0} if successful
*/
public native int SmartPlayerSetExternalAudioOutput(long handle, Object external_audio_output);

具体调用实例:

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

拿到原始数据,进行二次操作(如人脸识别等):

    class RGBAExternalRender 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 row_bytes_ = 0;
private ByteBuffer rgba_buffer_ = null; @Override
public int getNTFrameFormat() {
Log.i(TAG, "RGBAExternalRender::getNTFrameFormat return "
+ NT_FRAME_FORMAT_RGBA);
return NT_FRAME_FORMAT_RGBA;
} @Override
public void onNTFrameSizeChanged(int width, int height) {
width_ = width;
height_ = height; row_bytes_ = width_ * 4; Log.i(TAG, "RGBAExternalRender::onNTFrameSizeChanged width_:"
+ width_ + " height_:" + height_); rgba_buffer_ = ByteBuffer.allocateDirect(row_bytes_ * height_);
} @Override
public ByteBuffer getNTPlaneByteBuffer(int index) {
if (index == 0) {
return rgba_buffer_;
} else {
Log.e(TAG,
"RGBAExternalRender::getNTPlaneByteBuffer index error:"
+ index);
return null;
}
} @Override
public int getNTPlanePerRowBytes(int index) {
if (index == 0) {
return row_bytes_;
} else {
Log.e(TAG,
"RGBAExternalRender::getNTPlanePerRowBytes index error:"
+ index);
return 0;
}
} public void onNTRenderFrame(int width, int height, long timestamp) {
if (rgba_buffer_ == null)
return; rgba_buffer_.rewind(); // copy buffer // test
// byte[] test_buffer = new byte[16];
// rgba_buffer_.get(test_buffer); Log.i(TAG, "RGBAExternalRender:onNTRenderFrame w=" + width + " h="
+ height + " timestamp=" + timestamp); // Log.i(TAG, "RGBAExternalRender:onNTRenderFrame rgba:" +
// bytesToHexString(test_buffer));
}
} 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));
}
}

总结

以上就是Android平台开发RTMP/RTSP播放器时,针对解码和绘制部分的一点考量,算是抛砖引玉,感兴趣的开发者可酌情参考。

Android平台RTMP/RTSP播放器开发系列--解码和绘制的更多相关文章

  1. Android、iOS平台RTMP/RTSP播放器实时音量调节

    介绍移动端RTMP.RTSP播放器实时音量调节之前,我们之前也写过,为什么windows播放端加这样的接口,windows端播放器在多窗口大屏显示的场景下尤其需要,尽管我们老早就有了实时静音接口,相对 ...

  2. Windows平台RTMP/RTSP播放器实现实时音量调节

    为什么要做实时音量调节 RTMP或RTSP直播播放音量调节,主要用于多实例(多窗口)播放场景下,比如同时播放4路RTMP或RTSP流,如果音频全部打开,几路audio同时打开,可能会影响用户体验,我们 ...

  3. RTSP播放器开发填坑之道

    好多开发者提到,在目前开源播放器如此泛滥的情况下,为什么还需要做自研框架的RTSP播放器,自研和开源播放器,到底好在哪些方面?以下大概聊聊我们的一点经验,感兴趣的,可以关注 github: 1. 低延 ...

  4. 基于VLC的播放器开发

    VLC的C++封装 因为工作需要,研究了一段时间的播放器开发,如果从头开始做,可以学习下FFmpeg(http://www.ffmpeg.org/),很多播放器都是基于FFmpeg开发的,但是这样工作 ...

  5. Android流媒体开发之路三:基于NDK开发Android平台RTSP播放器

    基于NDK开发Android平台RTSP播放器 最近做了不少android端的开发,有推流.播放.直播.对讲等各种应用,做了RTMP.RTSP.HTTP-FLV.自定义等各种协议,还是有不少收获和心得 ...

  6. 如何实现Windows平台RTMP播放器/RTSP播放器播放窗口添加OSD文字叠加

    好多开发者在做Windows平台特别是单屏多画面显示时,希望像监控摄像机一样,可以在播放画面添加OSD台标,以实现字符叠加效果,大多开发者可很轻松的实现以上效果,针对此,本文以大牛直播SDK (Git ...

  7. android音乐播放器开发教程

    android音乐播放器开发教程 Android扫描sd卡和系统文件 Android 关于录音文件的编解码 实现米聊 微信一类的录音上传的功能 android操作sdcard中的多媒体文件——音乐列表 ...

  8. EasyPlayer RTSP播放器运行出现: Unable to load DLL 找不到指定的模块。exception from HRESULT 0x8007007E 解决方案

    最近有EasyPlayer RTSP播放器的开发者反馈,在一台新装的Windows Server 2008的操作系统上运行EasyPlayer RTSP播放器出现"Unable to loa ...

  9. Android实现简单音乐播放器(MediaPlayer)

    Android实现简单音乐播放器(MediaPlayer) 开发工具:Andorid Studio 1.3 运行环境:Android 4.4 KitKat 工程内容 实现一个简单的音乐播放器,要求功能 ...

随机推荐

  1. React.js中JSX的原理与关键实现

    在开始开发之前,我们需要创建一个空项目文件夹.安装 初始化 npm init -y 2.安装webpack相关依赖 npm install webpack webpack-cli -D 安装babel ...

  2. Vue引入vuetify框架你需要知道的几点

    1.命令行安装 npm install vuetify --save 2.在src目录中创建一个名为的文件夹plugins在里面,添加一个vuetify.js文件.代码如下 import Vue fr ...

  3. zabbix通过invoke调用监控服务可用性

    1. 通过脚本判断线上服务是否可用 telnet 127.0.0.1 端口 #线上调用的是使用的dubbo端口 通过invoke 抓取返回的code值,脚本如下 #返回code:0则视为正常,返回其他 ...

  4. Linux文本三剑客-sed

    sed工作原理: sed: Stream Editor.流编辑器 --- 属于行编辑工具 sed和vim一样都是文本编辑工具. 行编辑工具:一行一行处理文件内容 全屏编辑工具:一次性将文件内容加载到内 ...

  5. bat-命令行安装软件

    批处理 执行的两种方式 1.直接右键以管理员身份运行 2.在管理员身份的cmd窗口中 .\xxx.bat 执行 区别 第一种方式 当前cmd默认路径为 C:\windows\system32 第二种方 ...

  6. 实践GoF的23种设计模式:装饰者模式

    摘要:装饰者模式通过组合的方式,提供了能够动态地给对象/模块扩展新功能的能力.理论上,只要没有限制,它可以一直把功能叠加下去,具有很高的灵活性. 本文分享自华为云社区<[Go实现]实践GoF的2 ...

  7. 使用net core 6 c# 的 NPOI 包,读取excel..xlsx单元格内的图片,并存储到指定服务器

    这个是记录,单元格的图片. 直接上代码,直接新建一个 net core api 解决方案,引用一下nuget包.本地创建一个 .xlsx 格式的excel文件 using ICSharpCode.Sh ...

  8. 详解HashMap源码解析(下)

    上文详解HashMap源码解析(上)介绍了HashMap整体介绍了一下数据结构,主要属性字段,获取数组的索引下标,以及几个构造方法.本文重点讲解元素的添加.查找.扩容等主要方法. 添加元素 put(K ...

  9. Codeforces Round #802 (Div. 2)

    题集链接 A Optimal Path 水 代码 #include <bits/stdc++.h> #define endl "\n" using namespace ...

  10. 数据结构-查找-二叉排序查找(平衡二叉树,B树,B+树概念)

    0.为什么需要二叉排序树 1)数组存储方式: 优点:通过下标访问元素,速度快,对于有序数组,可以通过二分查找提高检索效率: 缺点:如果检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低: 2 ...