这里主要会不断更新,我写的项目的源代码的下载地址。所谓更新就是会不断添加新的项目代码下载链接。虽然会要一点分数,但是这也是我的劳动成果嘛。我要去去下载别人的资料呀,虽然要分可耻,但是希望各位谅解。哈哈
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的。
- 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;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- getWindow().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() {
-
- @Override
- public 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() {
-
- @Override
- public void onClick(View v) {
- finish();
- }
- });
-
-
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
-
- 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");
- }
-
- @Override
- public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
- width = arg2;
- height = arg3;
- }
-
- @Override
- public void surfaceCreated(SurfaceHolder arg0) {
- Log.i("SUr", "play before");
- new Thread(new Runnable() {
-
- @Override
- public void run() {
- Log.i("SUr", "play");
- initialWithUrl(MainActivity.RTSPURL);
- play(bitmap);
- }
- }).start();
- new Thread(new Runnable() {
-
- @Override
- public void run() {
- while (true) {
- if ((bitmap != null)) {
-
- 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();
- }
-
- @Override
- public 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;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- getWindow().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的。
-
-
-
-
-
-
-
- #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;
- }
- 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");
-
- }
- 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");
-
- }
-
- fillPicture(&info,pixels,&picture);
- AndroidBitmap_unlockPixels(env, bitmap);
- }
- }
- }
- return 1;
-
- }
这部分代码其实在上一篇博客QT版的RTSP客户端里面已经说道了,但是有一点不同的地方就是解码后的数据是直接填充bitmap的图像数据的。现在我再重新说一下大体的流程,首先是ffmpeg的初始化获取参数,其中包括了图像尺寸的参数。在获取图像的尺寸参数后,调用JAVA代码初始化bitmap,然后就是解码部分了,先是读取一个packet然后判断是不是视频流,如果是开始解码,解码后的数据格式是420P的,这个就是利用我们前面搭起来的服务器的。然后利用swscale进行格式转化,最后填充bitmap的图像数据,而在JAVA层会有一个单独的线程去刷新这个bitmap显示的。
-
-
-
-
-
-
- #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);
-
-
-
-
-
-
-
-
- jclass cls = env->GetObjectClass(thisz);
- jmethodID mid = env->GetMethodID(cls, "setBitmapSize", "(II)V");
- 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客户端的实现过程。