Android版 RTSP客户端

来源:互联网 发布:刻绘大师端口怎么设置 编辑:程序博客网 时间:2024/05/18 06:26

 

    这里主要会不断更新,我写的项目的源代码的下载地址。所谓更新就是会不断添加新的项目代码下载链接。虽然会要一点分数,但是这也是我的劳动成果嘛。我要去去下载别人的资料呀,虽然要分可耻,但是希望各位谅解。哈哈

    Qt版的Rtsp客户端 : http://download.csdn.net/detail/nieyongs/6989317

    V4L2+swscale+X264+live555实现流媒体服务端:http://download.csdn.net/detail/nieyongs/6989335

   NDK编译的最新ffmpeg,支持RTSP流: http://download.csdn.net/detail/nieyongs/7061277

   Android版 RTSP客户端 : http://download.csdn.net/detail/nieyongs/7061291






在介绍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的。





[java] view plain copy
  1. package com.ny.rtspclient;  
  2.   
  3. import android.app.Activity;  
  4. import android.content.Intent;  
  5. import android.os.Bundle;  
  6. import android.view.Menu;  
  7. import android.view.View;  
  8. import android.view.View.OnClickListener;  
  9. import android.view.Window;  
  10. import android.view.WindowManager;  
  11. import android.widget.Button;  
  12. import android.widget.EditText;  
  13.   
  14. public class MainActivity extends Activity {  
  15.     public static String RTSPURL="";  
  16.     private EditText text_rtsp;  
  17.     private Button btn_play,btn_cancle;  
  18.     @Override  
  19.     protected void onCreate(Bundle savedInstanceState) {  
  20.         super.onCreate(savedInstanceState);  
  21.         // 去除title  
  22.         getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,  
  23.                 WindowManager.LayoutParams.FLAG_FULLSCREEN);  
  24.         requestWindowFeature(Window.FEATURE_NO_TITLE);  
  25.         setContentView(R.layout.activity_main);  
  26.         text_rtsp=(EditText)findViewById(R.id.rtspurl);  
  27.         btn_play=(Button)findViewById(R.id.btn_play);  
  28.         btn_cancle=(Button)findViewById(R.id.btn_cancle);  
  29.         btn_play.setOnClickListener(new OnClickListener() {  
  30.               
  31.             @Override  
  32.             public void onClick(View v) {  
  33.                 RTSPURL=text_rtsp.getText().toString();  
  34.                 Intent i = new Intent(MainActivity.this, VideoActivity.class);  
  35.                 startActivity(i);  
  36.                 finish();  
  37.             }  
  38.         });  
  39.           
  40.         btn_cancle.setOnClickListener(new OnClickListener() {  
  41.               
  42.             @Override  
  43.             public void onClick(View v) {  
  44.                 finish();  
  45.             }  
  46.         });  
  47.           
  48.           
  49.     }  
  50.   
  51.     @Override  
  52.     public boolean onCreateOptionsMenu(Menu menu) {  
  53.         // Inflate the menu; this adds items to the action bar if it is present.  
  54.         getMenuInflater().inflate(R.menu.main, menu);  
  55.         return true;  
  56.     }  
  57.   
  58. }  

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


[java] view plain copy
  1. package com.ny.rtspclient;  
  2.   
  3. import android.content.Context;  
  4. import android.graphics.Bitmap;  
  5. import android.graphics.Canvas;  
  6. import android.graphics.Matrix;  
  7. import android.graphics.Paint;  
  8. import android.graphics.Paint.Style;  
  9. import android.util.Log;  
  10. import android.view.SurfaceHolder;  
  11. import android.view.SurfaceHolder.Callback;  
  12. import android.view.SurfaceView;  
  13.   
  14. public class VideoDisplay extends SurfaceView implements Callback {  
  15.     private Bitmap bitmap;  
  16.     private Matrix matrix;  
  17.     private SurfaceHolder sfh;  
  18.     private int width = 0;  
  19.     private int height = 0;  
  20.     public native void initialWithUrl(String url);  
  21.     public native void play( Bitmap bitmap);  
  22.       
  23.     public VideoDisplay(Context context) {  
  24.         super(context);  
  25.         sfh = this.getHolder();  
  26.         sfh.addCallback(this);  
  27.         matrix=new Matrix();  
  28.         bitmap = Bitmap.createBitmap(640480, Bitmap.Config.ARGB_8888);  
  29.         Log.i("SUr""begin");  
  30.     }  
  31.   
  32.     @Override  
  33.     public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {  
  34.         width = arg2;  
  35.         height = arg3;  
  36.     }  
  37.   
  38.     @Override  
  39.     public void surfaceCreated(SurfaceHolder arg0) {  
  40.         Log.i("SUr""play before");  
  41.         new Thread(new Runnable() {  
  42.   
  43.             @Override  
  44.             public void run() {  
  45.                 Log.i("SUr""play");  
  46.                 initialWithUrl(MainActivity.RTSPURL);  
  47.                 play(bitmap);  
  48.             }  
  49.         }).start();  
  50.         new Thread(new Runnable() {  
  51.   
  52.             @Override  
  53.             public void run() {  
  54.                 while (true) {  
  55.                     if ((bitmap != null)) {  
  56.                         // System.out.println("begin");  
  57.                         Canvas canvas = sfh.lockCanvas(null);  
  58.                         Paint paint = new Paint();  
  59.                         paint.setAntiAlias(true);  
  60.                         paint.setStyle(Style.FILL);  
  61.                         int mWidth = bitmap.getWidth();  
  62.                         int mHeight = bitmap.getHeight();  
  63.                         matrix.reset();  
  64.                         matrix.setScale((float) width / mWidth, (float) height  
  65.                                 / mHeight);  
  66.                         canvas.drawBitmap(bitmap, matrix, paint);  
  67.                         sfh.unlockCanvasAndPost(canvas);  
  68.                     }  
  69.                 }  
  70.             }  
  71.         }).start();  
  72.     }  
  73.   
  74.     @Override  
  75.     public void surfaceDestroyed(SurfaceHolder arg0) {  
  76.   
  77.     }  
  78.   
  79.     public void setBitmapSize(int width, int height) {  
  80.         Log.i("Sur""setsize");  
  81.         bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);  
  82.     }  
  83.   
  84.     static {  
  85.         System.loadLibrary("rtspclient");  
  86.     }  
  87. }  

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


[java] view plain copy
  1. package com.ny.rtspclient;  
  2.   
  3. import android.app.Activity;  
  4. import android.os.Bundle;  
  5. import android.view.Window;  
  6. import android.view.WindowManager;  
  7.   
  8. public class VideoActivity extends Activity {  
  9.     private VideoDisplay video;  
  10.     @Override  
  11.     protected void onCreate(Bundle savedInstanceState) {  
  12.         super.onCreate(savedInstanceState);  
  13.         // 去除title  
  14.         getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,  
  15.                 WindowManager.LayoutParams.FLAG_FULLSCREEN);  
  16.         requestWindowFeature(Window.FEATURE_NO_TITLE);  
  17.         setContentView(R.layout.activity_main);  
  18.         video=new VideoDisplay(this);  
  19.         setContentView(video);  
  20.     }  
  21. }  

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


[cpp] view plain copy
  1. /* 
  2.  * FFmpeg.cpp 
  3.  * 
  4.  *  Created on: 2014年2月25日 
  5.  *      Author: ny 
  6.  */  
  7.   
  8. #include "FFmpeg.h"  
  9.   
  10. FFmpeg::FFmpeg() {  
  11.     pCodecCtx = NULL;  
  12.     videoStream = -1;  
  13.   
  14. }  
  15.   
  16. FFmpeg::~FFmpeg() {  
  17.     sws_freeContext(pSwsCtx);  
  18.     avcodec_close(pCodecCtx);  
  19.     avformat_close_input(&pFormatCtx);  
  20. }  
  21.   
  22. int FFmpeg::initial(char * url, JNIEnv * e) {  
  23.     int err;  
  24.     env = e;  
  25.     rtspURL = url;  
  26.     AVCodec *pCodec;  
  27.     av_register_all();  
  28.     avformat_network_init();  
  29.     pFormatCtx = avformat_alloc_context();  
  30.     pFrame = avcodec_alloc_frame();  
  31.     err = avformat_open_input(&pFormatCtx, rtspURL, NULL, NULL);  
  32.     if (err < 0) {  
  33.         printf("Can not open this file");  
  34.         return -1;  
  35.     }  
  36.     if (av_find_stream_info(pFormatCtx) < 0) {  
  37.         printf("Unable to get stream info");  
  38.         return -1;  
  39.     }  
  40.     int i = 0;  
  41.     videoStream = -1;  
  42.     for (i = 0; i < pFormatCtx->nb_streams; i++) {  
  43.         if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {  
  44.             videoStream = i;  
  45.             break;  
  46.         }  
  47.     }  
  48.     if (videoStream == -1) {  
  49.         printf("Unable to find video stream");  
  50.         return -1;  
  51.     }  
  52.     pCodecCtx = pFormatCtx->streams[videoStream]->codec;  
  53.   
  54.     width = pCodecCtx->width;  
  55.     height = pCodecCtx->height;  
  56.     avpicture_alloc(&picture, PIX_FMT_RGB24, pCodecCtx->width,  
  57.             pCodecCtx->height);  
  58.     pCodec = avcodec_find_decoder(pCodecCtx->codec_id);  
  59.     pSwsCtx = sws_getContext(width, height, PIX_FMT_YUV420P, width, height,  
  60.             PIX_FMT_RGB24, SWS_BICUBIC, 0, 0, 0);  
  61.   
  62.     if (pCodec == NULL) {  
  63.         printf("Unsupported codec");  
  64.         return -1;  
  65.     }  
  66.     printf("video size : width=%d height=%d \n", pCodecCtx->width,  
  67.             pCodecCtx->height);  
  68.     if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {  
  69.         printf("Unable to open codec");  
  70.         return -1;  
  71.     }  
  72.     printf("initial successfully");  
  73.   
  74.     return 0;  
  75. }  
  76.   
  77. void FFmpeg::fillPicture(AndroidBitmapInfo* info, void *pixels,  
  78.         AVPicture *rgbPicture) {  
  79.     uint8_t *frameLine;  
  80.   
  81.     int yy;  
  82.     for (yy = 0; yy < info->height; yy++) {  
  83.         uint8_t* line = (uint8_t*) pixels;  
  84.         frameLine = (uint8_t *) rgbPicture->data[0] + (yy * rgbPicture->linesize[0]);  
  85.   
  86.         int xx;  
  87.         for (xx = 0; xx < info->width; xx++) {  
  88.             int out_offset = xx * 4;  
  89.             int in_offset = xx * 3;  
  90.   
  91.             line[out_offset] = frameLine[in_offset];  
  92.             line[out_offset + 1] = frameLine[in_offset + 1];  
  93.             line[out_offset + 2] = frameLine[in_offset + 2];  
  94.             line[out_offset + 3] = 0xff; //主要是A值  
  95.         }  
  96.         pixels = (char*) pixels + info->stride;  
  97.     }  
  98. }  
  99.   
  100. int FFmpeg::h264Decodec(jobject & bitmap) {  
  101.     int frameFinished = 0;  
  102.     AndroidBitmapInfo info;  
  103.     void * pixels;  
  104.     int ret = -1;  
  105.     if ((ret = AndroidBitmap_getInfo(env, bitmap, &info)) < 0) {  
  106.         LOGE("AndroidBitmap_getInfo() failed ! error");  
  107.         //return -1;  
  108.     }  
  109.     while (av_read_frame(pFormatCtx, &packet) >= 0) {  
  110.         if (packet.stream_index == videoStream) {  
  111.             avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);  
  112.             if (frameFinished) {  
  113.                 LOGI("***************ffmpeg decodec*******************\n");  
  114.                 int rs = sws_scale(pSwsCtx,  
  115.                         (const uint8_t* const *) pFrame->data, pFrame->linesize,  
  116.                         0, height, picture.data, picture.linesize);  
  117.   
  118.                 if (rs == -1) {  
  119.                     LOGE(  
  120.                             "__________Can open to change to des imag_____________e\n");  
  121.                     return -1;  
  122.                 }  
  123.                 if ((ret = AndroidBitmap_lockPixels(env, bitmap, &pixels))  
  124.                         < 0) {  
  125.                     LOGE("AndroidBitmap_lockPixels() failed ! error");  
  126.                     //return -1;  
  127.                 }  
  128.                 //pixels = picture.data[0] + info.stride;  
  129.                 fillPicture(&info,pixels,&picture);  
  130.                 AndroidBitmap_unlockPixels(env, bitmap);  
  131.             }  
  132.         }  
  133.     }  
  134.     return 1;  
  135.   
  136. }  

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




[cpp] view plain copy
  1. /* 
  2.  * RtspClient.cpp 
  3.  * 
  4.  *  Created on: 2014-3-16 
  5.  *      Author: ny 
  6.  */  
  7. #include <jni.h>  
  8. #include <android/log.h>  
  9. #define  LOG_TAG    "jniTest"  
  10. #define  LOGI(...)  __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)  
  11. #define  LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)  
  12.   
  13. extern "C" {  
  14.   
  15. #include "ffmpeg/libavcodec/avcodec.h"  
  16. #include "ffmpeg/libavformat/avformat.h"  
  17. #include "ffmpeg/libswscale/swscale.h"  
  18. #include "FFmpeg.h"  
  19.   
  20. }  
  21. const char * rtspURL;  
  22. FFmpeg * ffmpeg;  
  23. extern "C" {  
  24.   
  25. void Java_com_ny_rtspclient_VideoDisplay_initialWithUrl(JNIEnv *env,  
  26.         jobject thisz, jstring url) {  
  27.     rtspURL = env->GetStringUTFChars(url, NULL);  
  28.     LOGI("%s", rtspURL);  
  29.     ffmpeg = new FFmpeg();  
  30.     ffmpeg->initial((char *) rtspURL, env);  
  31.     //调用java的方法,设置bitmap的wdith和height  
  32.     /** 
  33.      *public void setBitmapSize(int width, int height) { 
  34.      *    Log.i(TAG, "setsize"); 
  35.      *    mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 
  36.      *} 
  37.      *调用这个方法之后bitmpa!=null,绘图线程就会启动 
  38.      */  
  39.     jclass cls = env->GetObjectClass(thisz);  
  40.     jmethodID mid = env->GetMethodID(cls, "setBitmapSize""(II)V"); //调用java的方法  
  41.     env->CallVoidMethod(thisz, mid, (int) ffmpeg->width, (int) ffmpeg->height);  
  42. }  
  43.   
  44. void Java_com_ny_rtspclient_VideoDisplay_play(JNIEnv *env, jobject thisz,  
  45.         jobject bitmap) {  
  46.   
  47.     ffmpeg->h264Decodec(bitmap);  
  48. }  
  49. }  

 

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

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


原创粉丝点击