Android版 RTSP客户端

来源:互联网 发布:windows subst 编辑:程序博客网 时间:2024/05/17 14:16

   在介绍Android版 RTSP客户端之前先吐槽一下ffmpeg的移植。虽然网上的教程已经很多了,但是本人能力有限。花费了一周的时间来移植ffmpeg,花费3小时左右的时间来编写了Android版的RTSP客户端。我要吐槽的就是网上的那些ffmpeg移植教程,我很奇怪那么多人移植没人发现问题吗?我碰到的问题是这杨的,一开始我按照网上的教程一步一步的做,但是最后一步出错了。就是这一步,把每一个lib.a 静态库编译成一个ffmpeg.so出错了。出错内容是一些undefine 'uncompress'之类的提示。这个明显是libz库出问题了,我各种百度google,但是最终还是没有解决掉。网上的教程也没有提到关于这个的问题的解决方法,倒是看到不少人提出了这个错误。最后还是自己去分析了Android.mk,最终发现了解决问题的办法了。这里面我就提出和那些教程不一样的地方,错误的地方是在ffmpeg目录下的Android.mk 里面少了一句LOCAL_LDLIBS := -llog -lz 。其实去耐心去分析的话,应该很快去解决这个问题。所以有时候一处问题去百度google不一定是一个好办法,有时候冷静下来去思考不是一个解决的办法。因为是最后一步出错了,而且从提示来看应该是libz库出问题了,和最后一步密切相关的.mk 文件就是ffmpeg目录下的Android.mk文件了。 然后修改了一下编译选项,这样我们的android版的ffmpeg就可以支持rtsp数据流了。吐槽就吐槽到这里了。下面开始介绍我们的Android版的RTSP客户端。其实实现过程类似与qt版的RTSP客户端,主要是显示部分不同。

   由于ffmpeg是一个C库,而Android的开发是用JAVA开发的。这样就有一个语言不兼容的问题了。但是我们使用了NDK就可以实现JAVA代码和C代码的交互使用了。主要是使用了JAVA的JNI技术。对这块知识点不熟悉的朋友可以去百度google学习一下。这里不介绍JNI和NDK的知识点。所以本项目的分为两块,一块是JAVA层代码,一块是C代码。这里我使用了c++来写了。JAVA层代码主要是我们对Android的编程,C层代码主要是我们对rtsp数据流的接受和解码过程。大体框架了解了,我们开始分析一下代码了。主要是测试版的,没有华丽的界面。源代码我会在源代码下载地址里面更新,包括ffmpeg的最新移植支持rtsp的。





package com.ny.rtspclient;import android.app.Activity;import android.content.Intent;import android.os.Bundle;import android.view.Menu;import android.view.View;import android.view.View.OnClickListener;import android.view.Window;import android.view.WindowManager;import android.widget.Button;import android.widget.EditText;public class MainActivity extends Activity {public static String RTSPURL="";private EditText text_rtsp;private Button btn_play,btn_cancle;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);// 去除titlegetWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);requestWindowFeature(Window.FEATURE_NO_TITLE);setContentView(R.layout.activity_main);text_rtsp=(EditText)findViewById(R.id.rtspurl);btn_play=(Button)findViewById(R.id.btn_play);btn_cancle=(Button)findViewById(R.id.btn_cancle);btn_play.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {RTSPURL=text_rtsp.getText().toString();Intent i = new Intent(MainActivity.this, VideoActivity.class);startActivity(i);finish();}});btn_cancle.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {finish();}});}@Overridepublic boolean onCreateOptionsMenu(Menu menu) {// Inflate the menu; this adds items to the action bar if it is present.getMenuInflater().inflate(R.menu.main, menu);return true;}}

这个就是我们的MainActivity程序的入口,主要界面就是一个EditText用于接受用户输入的rtsp地址。两个按键,一个确认一个退出。


package com.ny.rtspclient;import android.content.Context;import android.graphics.Bitmap;import android.graphics.Canvas;import android.graphics.Matrix;import android.graphics.Paint;import android.graphics.Paint.Style;import android.util.Log;import android.view.SurfaceHolder;import android.view.SurfaceHolder.Callback;import android.view.SurfaceView;public class VideoDisplay extends SurfaceView implements Callback {private Bitmap bitmap;private Matrix matrix;private SurfaceHolder sfh;private int width = 0;private int height = 0;public native void initialWithUrl(String url);public native void play( Bitmap bitmap);public VideoDisplay(Context context) {super(context);sfh = this.getHolder();sfh.addCallback(this);matrix=new Matrix();bitmap = Bitmap.createBitmap(640, 480, Bitmap.Config.ARGB_8888);Log.i("SUr", "begin");}@Overridepublic void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {width = arg2;height = arg3;}@Overridepublic void surfaceCreated(SurfaceHolder arg0) {Log.i("SUr", "play before");new Thread(new Runnable() {@Overridepublic void run() {Log.i("SUr", "play");initialWithUrl(MainActivity.RTSPURL);play(bitmap);}}).start();new Thread(new Runnable() {@Overridepublic void run() {while (true) {if ((bitmap != null)) {// System.out.println("begin");Canvas canvas = sfh.lockCanvas(null);Paint paint = new Paint();paint.setAntiAlias(true);paint.setStyle(Style.FILL);int mWidth = bitmap.getWidth();int mHeight = bitmap.getHeight();matrix.reset();matrix.setScale((float) width / mWidth, (float) height/ mHeight);canvas.drawBitmap(bitmap, matrix, paint);sfh.unlockCanvasAndPost(canvas);}}}}).start();}@Overridepublic void surfaceDestroyed(SurfaceHolder arg0) {}public void setBitmapSize(int width, int height) {Log.i("Sur", "setsize");bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);}static {System.loadLibrary("rtspclient");}}

这个是我的重点类,这个类继承于SurfaceView实现了Surfaceholder的三个方法。一个是surface创建的时候,一个是改变的时候,一个销毁的时候调用的。还有我们在这个类里面声明了两个本地的方法,一个是initialWithUrl(String url),主要是对ffmpeg的初始化,后去rtsp数据流的一些参数,获取了图像的尺寸之后在C层代码调用JAVA层代码初始化bitmap对面。因为bitmap初始化需要知道他的尺寸。还有一个本地方法就是play( Bitmap bitmap)这个就是我们ffmpeg里面的一个循环读取数据解码的过程。我们在C层代码那里会讲到。这里我们采用了多线程方式,ffmpeg的数据处理一个线程。surfaceview现实bitmap是一个线程。


package com.ny.rtspclient;import android.app.Activity;import android.os.Bundle;import android.view.Window;import android.view.WindowManager;public class VideoActivity extends Activity {private VideoDisplay video;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);// 去除titlegetWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);requestWindowFeature(Window.FEATURE_NO_TITLE);setContentView(R.layout.activity_main);video=new VideoDisplay(this);setContentView(video);}}

这个类就没什么可说的了,主要是用来装在上面的surfaceview的。


/* * FFmpeg.cpp * *  Created on: 2014年2月25日 *      Author: ny */#include "FFmpeg.h"FFmpeg::FFmpeg() {pCodecCtx = NULL;videoStream = -1;}FFmpeg::~FFmpeg() {sws_freeContext(pSwsCtx);avcodec_close(pCodecCtx);avformat_close_input(&pFormatCtx);}int FFmpeg::initial(char * url, JNIEnv * e) {int err;env = e;rtspURL = url;AVCodec *pCodec;av_register_all();avformat_network_init();pFormatCtx = avformat_alloc_context();pFrame = avcodec_alloc_frame();err = avformat_open_input(&pFormatCtx, rtspURL, NULL, NULL);if (err < 0) {printf("Can not open this file");return -1;}if (av_find_stream_info(pFormatCtx) < 0) {printf("Unable to get stream info");return -1;}int i = 0;videoStream = -1;for (i = 0; i < pFormatCtx->nb_streams; i++) {if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {videoStream = i;break;}}if (videoStream == -1) {printf("Unable to find video stream");return -1;}pCodecCtx = pFormatCtx->streams[videoStream]->codec;width = pCodecCtx->width;height = pCodecCtx->height;avpicture_alloc(&picture, PIX_FMT_RGB24, pCodecCtx->width,pCodecCtx->height);pCodec = avcodec_find_decoder(pCodecCtx->codec_id);pSwsCtx = sws_getContext(width, height, PIX_FMT_YUV420P, width, height,PIX_FMT_RGB24, SWS_BICUBIC, 0, 0, 0);if (pCodec == NULL) {printf("Unsupported codec");return -1;}printf("video size : width=%d height=%d \n", pCodecCtx->width,pCodecCtx->height);if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {printf("Unable to open codec");return -1;}printf("initial successfully");return 0;}void FFmpeg::fillPicture(AndroidBitmapInfo* info, void *pixels,AVPicture *rgbPicture) {uint8_t *frameLine;int yy;for (yy = 0; yy < info->height; yy++) {uint8_t* line = (uint8_t*) pixels;frameLine = (uint8_t *) rgbPicture->data[0] + (yy * rgbPicture->linesize[0]);int xx;for (xx = 0; xx < info->width; xx++) {int out_offset = xx * 4;int in_offset = xx * 3;line[out_offset] = frameLine[in_offset];line[out_offset + 1] = frameLine[in_offset + 1];line[out_offset + 2] = frameLine[in_offset + 2];line[out_offset + 3] = 0xff; //主要是A值}pixels = (char*) pixels + info->stride;}}int FFmpeg::h264Decodec(jobject & bitmap) {int frameFinished = 0;AndroidBitmapInfo info;void * pixels;int ret = -1;if ((ret = AndroidBitmap_getInfo(env, bitmap, &info)) < 0) {LOGE("AndroidBitmap_getInfo() failed ! error");//return -1;}while (av_read_frame(pFormatCtx, &packet) >= 0) {if (packet.stream_index == videoStream) {avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);if (frameFinished) {LOGI("***************ffmpeg decodec*******************\n");int rs = sws_scale(pSwsCtx,(const uint8_t* const *) pFrame->data, pFrame->linesize,0, height, picture.data, picture.linesize);if (rs == -1) {LOGE("__________Can open to change to des imag_____________e\n");return -1;}if ((ret = AndroidBitmap_lockPixels(env, bitmap, &pixels))< 0) {LOGE("AndroidBitmap_lockPixels() failed ! error");//return -1;}//pixels = picture.data[0] + info.stride;fillPicture(&info,pixels,&picture);AndroidBitmap_unlockPixels(env, bitmap);}}}return 1;}

这部分代码其实在上一篇博客QT版的RTSP客户端里面已经说道了,但是有一点不同的地方就是解码后的数据是直接填充bitmap的图像数据的。现在我再重新说一下大体的流程,首先是ffmpeg的初始化获取参数,其中包括了图像尺寸的参数。在获取图像的尺寸参数后,调用JAVA代码初始化bitmap,然后就是解码部分了,先是读取一个packet然后判断是不是视频流,如果是开始解码,解码后的数据格式是420P的,这个就是利用我们前面搭起来的服务器的。然后利用swscale进行格式转化,最后填充bitmap的图像数据,而在JAVA层会有一个单独的线程去刷新这个bitmap显示的。




/* * RtspClient.cpp * *  Created on: 2014-3-16 *      Author: ny */#include <jni.h>#include <android/log.h>#define  LOG_TAG    "jniTest"#define  LOGI(...)  __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)#define  LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)extern "C" {#include "ffmpeg/libavcodec/avcodec.h"#include "ffmpeg/libavformat/avformat.h"#include "ffmpeg/libswscale/swscale.h"#include "FFmpeg.h"}const char * rtspURL;FFmpeg * ffmpeg;extern "C" {void Java_com_ny_rtspclient_VideoDisplay_initialWithUrl(JNIEnv *env,jobject thisz, jstring url) {rtspURL = env->GetStringUTFChars(url, NULL);LOGI("%s", rtspURL);ffmpeg = new FFmpeg();ffmpeg->initial((char *) rtspURL, env);//调用java的方法,设置bitmap的wdith和height/** *public void setBitmapSize(int width, int height) { *    Log.i(TAG, "setsize"); *    mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); *} *调用这个方法之后bitmpa!=null,绘图线程就会启动 */jclass cls = env->GetObjectClass(thisz);jmethodID mid = env->GetMethodID(cls, "setBitmapSize", "(II)V"); //调用java的方法env->CallVoidMethod(thisz, mid, (int) ffmpeg->width, (int) ffmpeg->height);}void Java_com_ny_rtspclient_VideoDisplay_play(JNIEnv *env, jobject thisz,jobject bitmap) {ffmpeg->h264Decodec(bitmap);}}

 

像Java_com_ny_rtspclient_VideoDisplay_play这种函数就是在JAVA里面声明的本地方法。这里我们实现了刚才我们在JAVA层定义的本地方法。主要是初始化的时候,在获取了图像尺寸的时候,在C层去调用JAVA代码初始化bitmap。

 这就是整个Android版的RTSP客户端的实现过程。



0 0