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(使用前将#替换为@)