这里主要会不断更新,我写的项目的源代码的下载地址。所谓更新就是会不断添加新的项目代码下载链接。虽然会要一点分数,但是这也是我的劳动成果嘛。我要去去下载别人的资料呀,虽然要分可耻,但是希望各位谅解。哈哈
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);
-
// 去除title
-
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) {
-
// 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");
-
}
-
-
@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)) {
-
// 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();
-
}
-
-
@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);
-
// 去除title
-
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的。
-
/*
-
* 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客户端的实现过程。