VLC RTSP视频播放终极解决方案

2023-05-16

VLC播放RTSP视频流遇到的问题

摄像头是RTSP协议的,需要在Android端实时显示摄像头视频流,这里采用了开源的VLC播放器,可能会有如下需求:

一、有截屏的需求

二、有屏幕录制的需求

三、视频本来是横的,但是现在要竖屏显示,如何旋转视频,另外旋转后视频会拉伸,因此需要截取一段显示

四、显示的视频可能需要做额外处理,比如识别出人脸后框出来

先说说直接用VLC播放器的SDK会遇到的问题,利用SDK显示视频通常是如下写法:

private MediaPlayer mMediaPlayer;
private LibVLC mVlc;

void createPlayer(String url, int width, int height) {
    ArrayList<String> options = new ArrayList<>();
    options.add("--aout=opensles");
    options.add("--audio-time-stretch"); 
    options.add("-vvv"); 
    mVlc = new LibVLC(context, options);

    mMediaPlayer = new MediaPlayer(mVlc);
    IVLCVout vout = mMediaPlayer.getVLCVout();
    vout.setVideoView(textureView);
    vout.attachViews();

    vout.setWindowSize(width, height);

    Media m = new Media(mVlc, Uri.parse(url));
    int cache = 1000;
    m.addOption(":network-caching=" + cache);
    m.addOption(":file-caching=" + cache);
    m.addOption(":live-cacheing=" + cache);
    m.addOption(":sout-mux-caching=" + cache);
    m.addOption(":codec=mediacodec,iomx,all");
    mMediaPlayer.setMedia(m);
    mMediaPlayer.play();
}

public void releasePlayer() {
    mMediaPlayer.setVideoCallback(null, null);
    mMediaPlayer.stop();
    IVLCVout vout = mMediaPlayer.getVLCVout();
    vout.detachViews();
    mVlc.release();
    mVlc = null;
}

这里值得一提的是调setVideoView设置视频输出,可以是TextureView,也可以是SurfaceView,也可以是SurfaceTexture。我尝试过使用SurfaceTexture,然后当Frame Available时再从SurfaceTexture绘制到Window Surface上,结果显示出来的是一团糟,原因尚未查明。

另外为了避免视频播放时卡顿,最好加上各种Cache。注意Cache设得太大会增加延时。


接下来说说这种方式的局限:

1,对于截屏的需求,如果采用的SurfaceView,是无法getDrawingCache的。采用TextureView的话系统提供了接口获取截屏Bitmap的。尝试过采用SurfaceTexture绘制再glReadPixels获取RGBA这种办法失败了,最终的图像是混沌的。

2,对于视频录制,除非能拿到每一帧视频数据,否则无解,如果能从SurfaceTexture上拷出数据就行了,但是实践中发现拷出来的图像是混沌的,原因未明。

3,对于横竖屏切换,假如视频是横的,手机分辨率是1920 * 1080,如果要竖屏显示,需要对视频进行旋转,对于TextureView可以采用setTransform(Matrix),为了避免视频拉伸需要截取一部分来显示,但是默认截取是从左到右或从上到下的,假如我要截取视频中间的部分就不行了。

4,对于额外处理的需求最靠谱的办法还是拿到视频流,离线渲染完成后再显示。

综上,解决一切问题的核心就是拿到视频流。网上关于截屏和视频录制的方案都是抄来抄去的,VLC的native层本来是有截屏和视频录制功能的,只是没开放给Java层,所以自己加几行代码开放出来重新编译一下就OK了。但是仍然没解决根本问题:拿到视频流。

为了解决这个问题,我们只能翻vlc的代码。首先给vlc-android的代码同步下来,然后编译一遍,建议在linux下编,过程中会遇到各种各样的问题,google并解决之。编译完后会在libvlc目录下生成一堆so文件,包括jni目录中的libc++_shared.so, libvlc.so, libvlcjni.so,还有private_libs目录中的libiomx.so和libanw.xo,另外还会output一个aar文件,我们直接用这个aar文件就好了,里面已经给so都打包了。

接下来正式看vlc的代码了,libvlc是重点,这个相当于一个中间层,是封装了给Android端用的。里面最终还是调用底层的vlc框架,我们就不用关注了。libvlc里有两个文件是重点,一个是libvlcjni.c,一个是libvlcjni-mediaplayer.c。

先看看libvlcjni-media_player.h头文件,里面介绍了一些关键的接口,注释非常详细,需要仔细阅读,获取视频流的答案就在里面。就是这两个函数:

LIBVLC_API
void libvlc_video_set_callbacks( libvlc_media_player_t *mp,
                                 libvlc_video_lock_cb lock,
                                 libvlc_video_unlock_cb unlock,
                                 libvlc_video_display_cb display,
                                 void *opaque );

LIBVLC_API
void libvlc_video_set_format( libvlc_media_player_t *mp, const char *chroma,
                              unsigned width, unsigned height,
                              unsigned pitch );

为了获取视频流,首先要调用libvlc_video_set_format设置视频流编码格式和宽高,然后调用libvlc_video_set_callbacks设置回调,里面有三个回调,我们用到的是lock和display,在lock中传入buffer,解码后的视频流会写到该buffer中,然后在display中将buffer回调到java层。

胜利的曙光依稀就在眼前,我们在libvlcjni-mediaplayer.c中插入以下代码:

void
Java_org_videolan_libvlc_MediaPlayer_nativeSetVideoFormat(JNIEnv *env, jobject thiz, 
    jstring format, jint width, jint height, jint pitch) {

    vlcjni_object *p_obj = VLCJniObject_getInstance(env, thiz);

    if (!p_obj)
        return;

    const char *formatStr = (*env)->GetStringUTFChars(env, format, NULL);

    libvlc_video_set_format(p_obj->u.p_mp, formatStr, width, height, pitch);

    (*env)->ReleaseStringUTFChars(env, format, formatStr);
}

struct myfield {
    jclass mediaPlayerClazz;
    jmethodID onDisplayCallback;
    jobject thiz;
    void *buffer;
} myfield;

static void *lock(void *data, void ** p_pixels) {
    *p_pixels = myfield.buffer;
    return NULL;
}

static void unlock(void *data, void *id, void * const * p_pixels) {

}

static pthread_mutex_t myMutex = PTHREAD_MUTEX_INITIALIZER;

static void display(void *data, void *id) {
    JavaVM *jvm = fields.jvm;

    JNIEnv *env;

    int stat = (*jvm)->GetEnv(jvm, (void **)&env, JNI_VERSION_1_2);
    if (stat == JNI_EDETACHED) {
        if ((*jvm)->AttachCurrentThread(jvm, (void **) &env, NULL) != 0) {
            return;
        }
    } else if (stat == JNI_OK) {
        //
    } else if (stat == JNI_EVERSION) {
        return;
    }

    pthread_mutex_lock(&myMutex);

    if (myfield.thiz != NULL) {
        (*env)->CallVoidMethod(env, myfield.thiz, myfield.onDisplayCallback);
    }

    pthread_mutex_unlock(&myMutex);

    (*jvm)->DetachCurrentThread(jvm);
}

void
Java_org_videolan_libvlc_MediaPlayer_nativeSetVideoBuffer(JNIEnv *env, jobject thiz, jobject buffer) {
    vlcjni_object *p_obj = VLCJniObject_getInstance(env, thiz);

    libvlc_media_player_t *mp = p_obj->u.p_mp;
    if (!mp) {
        return;
    }

    if (buffer == NULL) {
        (*env)->DeleteGlobalRef(env, myfield.mediaPlayerClazz);
        pthread_mutex_lock(&myMutex);
        (*env)->DeleteGlobalRef(env, myfield.thiz);
        myfield.thiz = NULL;
        pthread_mutex_unlock(&myMutex);
        return;
    }

    myfield.mediaPlayerClazz = (*env)->FindClass(env, "org/videolan/libvlc/MediaPlayer");
    myfield.mediaPlayerClazz = (jclass) (*env)->NewGlobalRef(env, myfield.mediaPlayerClazz);
    myfield.onDisplayCallback = (*env)->GetMethodID(env, myfield.mediaPlayerClazz, "onDisplay", "()V");
    myfield.thiz = (*env)->NewGlobalRef(env, thiz);
    myfield.buffer = (*env)->GetDirectBufferAddress(env, buffer);

    libvlc_video_set_callbacks(mp, lock, NULL, display, NULL);
}

要注意的是这里用到了fields.jvm,是在libvlcjni.c中的Jni_OnLoad时保存的全局JavaVM,要在struct fields中添加成员jvm。此外org.videolan.libvlc.MediaPlayer.java中添加代码如下:

public void setVideoFormat(String format, int width, int height, int pitch) {
    nativeSetVideoFormat(format, width, height, pitch);
}

private native void nativeSetVideoFormat(String format, int width, int height, int pitch);

private ByteBuffer mBuffer;
private MediaPlayCallback mCallback;

public void setVideoCallback(ByteBuffer buffer, MediaPlayCallback callback) {
    mBuffer = buffer;
    mCallback = callback;
    nativeSetVideoBuffer(buffer);
}

private native void nativeSetVideoBuffer(ByteBuffer buffer);

private void onDisplay() {
    if (mCallback != null) {
        mCallback.onDisplay(mBuffer);
    }
}

MediaPlayerCallback.java定义如下,直接返回了视频流buffer。

public interface MediaPlayCallback {
    public void onDisplay(ByteBuffer buffer);
}

这个视频流是RGBA的,我们可以用OpenGL来渲染。如果直接转成Bitmap再显示性能就堪忧了。


接下来再来说说以上jni中要注意的一些问题,

一,内存泄露,这里setVideoCallback时会从java层传下来buffer和MediaPlayer对象,其中MediaPlayer由于之后在Display回调中要用到,因此NewGlobalRef保存下来,这里如果没有释放的话,当
MediaPlayer重建后,如手机横竖屏切换或多次退出进来,之前的MediaPlayer会一直被JNI层持有,包括MediaPlayer中的Buffer就泄露了,这个Buffer通常都不小。所以释放MediaPlayer时
要解除jni层的引用。

二,回调在子线程,Display回调是在子线程的,这里需要获取子线程的JNIEnv,需要AttachCurrentThread,调用完后在Detach。另外考虑到线程同步,要加上锁。

三,这里的Buffer是Java层传下来的,考虑到性能,buffer是通过ByteBuffer.allocDirect创建的,这样可以直接被native层使用,通过GetDirectBufferAddress获取到buffer的地址。
当数据更新完后通知Java层直接读就好了,不用多余的拷贝。


最后再来谈谈文章开始提到的四个需求,

一,对于截屏,拿到了视频的Buffer数据后,可以通过如下方式生成Bitmap,然后保存文件

Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bitmap.copyPixelsFromBuffer(buffer);

二,对于录屏,可以参考我的如下项目中的视频录制部分
https://github.com/dingjikerbo/Android-Camera

三,关于横竖屏切换及视频裁剪,同样可参考我的项目
https://github.com/dingjikerbo/Android-Camera

大致思路是通过OpenGL渲染来处理视频,先渲染到Offscreen Surface上,然后再Blit到Window Surface上显示,Blit时可以指定要裁剪的区域。

四,关于视频的额外处理,如滤镜或者人脸识别同样可以参考我的Android-Camera项目


Demo项目地址:https://github.com/dingjikerbo/Android-RTSP

我的博客

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

VLC RTSP视频播放终极解决方案 的相关文章

  • vscode同步设置&扩展插件

    首先安装同步插件 xff1a Settings Sync 第二步 xff1a 进入你的github如图 xff1a 打开设置选项 xff1a 新建一个token xff1a 如图 xff1a 记住这个token值 转到vscode 按shi

随机推荐

  • mysql 常用语句使用

    1 查询语句 SELECT FROM table 2 更改语句 UPDATE table SET name 61 39 123456 39 WHERE id 61 100 3 插入语句 INSERT INTO table VALUES 1
  • STM32学习第七天--串口调试助手没弄懂

    啊啊 啊 今天真的好沮丧 调代码足足调了一晚上 xff0c 不知道什么原因工程就是错 xff0c 最后好不容易啊 xff0c 在主函数加了个 include 34 stm32f10x lib h 34 就好使了 xff0c 真不知道为什么
  • swoole 相关

    安装虚拟机 VMware Workstation Pro 安装CentOS CentOS 7 x86 64 Minimal 1708 iso 安装FinalShell 教程地址 安装lnmp 教程地址 服务状态管理命令 1 安装lnmp 2
  • ffmpeg编程:读取摄像头信息,保存为裸yuv420p、yuyv422视频流

    1 源码下载 xff1a https download csdn net download dijkstar 10898462 2 编程环境使用Windows下的QT5 11 minGW32 xff0c 源码中已经放好了fmpeg的bin
  • C中__FILE__ __LINE__的用法

    include lt stdio h gt void main void printf 34 File s Successfully reached line d n 34 FILE LINE Other statements here l
  • ubuntu中添加和删除源

    添加PPA源的命令为 xff1a br sudo add apt repository ppa user ppa name 添加好更新一下 xff1a sudo apt get update 删除命令格式则为 xff1a br sudo a
  • jetson nano 部署yoloV3,yoloV4,yoloV3-tiny,yoloV4-tiny

    系统 ubuntu nbsp nbsp 自带cuda10 0 nbsp 1 下载与安装darknet git clone https github com AlexeyAB darknet cd darknet 2 以下步骤我都在直接进入c
  • 对比first-fit/best-fit/worst-fit/slab以及buddy这几种算法的特点

    以下均为自己对这些算法的理解 xff1a fitst fit算法 First fit算法 xff1a 连续物理内存分配算法的一种 xff0c 将空闲内存块按照地址从小到大的方式连起来 xff0c 具体实现时使用了双向链表的方式 当分配内存时
  • MIT6_0001F16_Pset2

    MIT6 0001F16 Pset2 完成Hangman Game的编写 xff0c 就是一个猜词游戏 span class token comment Problem Set 2 hangman py span span class to
  • 利用python进行数据分析 pdf

    利用python进行数据分析 链接 xff1a https pan baidu com s 1mFg7kB0WG6edKnhumMbbJg 提取码 xff1a 6kos 如果带来帮助 xff0c 可点赞或关注博主 xff01
  • 数据库系统概念第六版 第六章练习题6.11

    6 11 考虑下图所示的关系数据库 xff0c 主码加了下划线 给出关系代数表达式来表达下列的每一个查询 xff1a a 找出First Bank Corporation的所有员工姓名 b 找出First Bank Corporation所
  • 数据库实验4 SQL语言-SELECT查询操作

    数据库实验4 SQL语言 SELECT查询操作 1 首先按照第三章的jxgl数据库的模板创建jxgl数据库并插入数据 xff1a 创建数据库jxgl create database jxgl 创建相应的表 xff1a 创建student表
  • Docker安装RabbitMQ,RabbitMQ Management使用

    上一篇文章介绍了RabbitMQ的一些基本概念 xff0c 如果没看过没了解 xff0c 推荐先去了解一下 xff1a 下面我们一起来安装一下RabbitMQ并且访问一下它的Management吧 我这边是使用了Docker容器来安装和启动
  • 数据库系统概念第六版 第七章练习题 15 22

    数据库第七章理论习题 7 15 为医院构造一个包含一组病人和一组医生的E R图 为每个病人关联一组不同的检查和化验记录 说明 xff1a 医生和病人间存在联系 xff0c 同时病人有特有的检查和化验记录 xff0c 病人有病床 xff0c
  • 操作系统设计思想--主奴思想

    操作系统设计思想 主奴思想 对于操作系统而言 xff0c 如何稳定可靠运行无疑是最重要的 目前的方案均为将用户进程和其他用户进程 将用户进程和操作系统进行分离 实现操作系统可以管理用户进程 xff0c 但用户进程不能侵入内核 xff0c 同
  • 从零开始学GO ---- 错误处理

    从零开始学GO 错误处理 Go 语言通过内置的错误接口提供了非常简单的错误处理机制 error类型接口 在这个接口类型的声明中只包含了一个方法Error Error方法不接受任何参数 xff0c 但是会返回一个string类型的结果 它的作
  • 从零开始学GO ---- 接口

    从零开始学GO 接口 接口是一个编程规范 xff0c 一组方法签名的集合 Go的接口是非侵入式的设置 xff0c 一个具体类型实现接口不需要在语法上显式地声明 xff0c 只要具体类型的方法集是接口方法集的超集 xff0c 就代表该类型实现
  • Golang通过反射对结构体进行赋值和其他操作

    Golang通过反射对结构体进行赋值和其他操作 通过反射可以方便快捷地访问Golang的结构体字段 xff0c 并进行赋值或其他操作 当两个结构体内容相同但名字不同时 xff0c 如何快捷地进行赋值 xff1f 或者内容有交集 xff0c
  • 如何使用github来下载你需要使用的ros资源

    使用ros系统 xff0c 需要用到许多数据包 xff0c 有些时候你需要使用的ROS数据包并没有Debian包的形式 xff0c 这时你需要从数据源安装它 代码开发人员通常使用的有三种主流的版本控制系统 xff1a SVN xff0c G
  • VLC RTSP视频播放终极解决方案

    VLC播放RTSP视频流遇到的问题 摄像头是RTSP协议的 xff0c 需要在Android端实时显示摄像头视频流 xff0c 这里采用了开源的VLC播放器 xff0c 可能会有如下需求 xff1a 一 有截屏的需求 二 有屏幕录制的需求