H264解码器移植到OPhone
来源:互联网 发布:python turtle画圆 编辑:程序博客网 时间:2024/03/28 16:46
注:原版发布于OphoneSDN,如需转载请参考OPhoneSDN版权声明
目录
目录
1 移植目标
2 面向人群
3 假定前提
4 开发环境
4.1 安装Eclipse
4.2 安装Cygwin
4.3 安装Android NDK
4.4 安装OPhone SDK
4.5 安装OPhone插件
5 移植过程
5.1 移植流程
5.2 封装Java接口
5.3 使用C实现本地方法
5.3.1 生成头文件
5.3.2 实现本地方法
5.3.3 编译本地方法
5.4 编写库测试程序
5.5 集成测试
1 移植目标
将H264解码器移植到Android操作系统之上(NDK+C),并写一个测试程序(OPhoneSDK+Java)测试解码库是否正常运行,下面是解码时的截图:
OPhone的模拟器和Mobile的模拟器一样是模拟ARM指令的,不像Symbian模拟器一样执行的是本地代码,所以在模拟器上模拟出来的效率会比真实手机上的效率要低,之前这款解码器已经优化到在nokia 6600(相当低端的一款手机,CPU主频才120HZ)上做到在线播放。
2 面向人群
本文面向有一定的手机应用开发经验(例如:S60/Mobile/MTK)和有一定的跨手机平台移植经验的人员,帮助她们了解一个企业的核心库(C/C++)是怎么移植到OPhone之上的。
3 假定前提
1、 你熟悉Java/C/C++语言;
2、 你熟悉Java的JNI技术;
3、 你有一定的跨手机平台移植经验;
4、 你有一套可供移植的源代码库,这里我以H264解码库为例,这里因为版权问题,这里只能够公开头文件:
#ifndef __H264DECODE_H__
#define __H264DECODE_H__
#if defined(__SYMBIAN32__) //S602rd/3rd/UIQ
#include <e32base.h>
#include <libc/stdio.h>
#include <libc/stdlib.h>
#include <libc/string.h>
#else //Windows/Mobile/MTK/Android
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#endif
class H264Decode
{
public:
/***************************************************************************/
/* 构造解码器 */
/* @return H264Decode解码器实例 */
/***************************************************************************/
static H264Decode *H264DecodeConstruct();
/***************************************************************************/
/* 解码一帧 */
/* @pInBuffer 指向H264的视频流 */
/* @iInSize H264视频流的大小 */
/* @pOutBuffer 解码后的视频视频 */
/* @iOutSize 解码后的视频大小 */
/* @return 已解码的H264视频流的尺寸 */
/***************************************************************************/
int DecodeOneFrame(unsigned char *pInBuffer,unsigned int iInSize,unsigned char *pOutBuffer,unsigned int &iOutSize);
~H264Decode();
};
#endif // __H264DECODE_H__
你不用熟悉Android系统,因为在此之前,我也不熟悉;
4 开发环境
4.1 安装Eclipse
下载Eclipse Classic 3.5.0 (162 MB) + CDT,并安装它,安装非常简单,这里就不详细介绍了。
4.2 安装Cygwin
Cygwin是一个在windows平台上运行的unix模拟环境,这个东东是移植常用到的工具,比较大,下载安装很简单。点击下载
FAQ 1:
测试NDK环境,如果遇到下面问题怎么办?
请检查你的make版本是否为3.81,如果不是,请升级你的Cygwin版本,我就是因为这个原因,折腾了半天。
4.3 安装Android NDK
简单来说NDK是让开发者使用传统的C/C++语言编写Android模块的一个开发包,目前NDK还不支持编写有界面的应用程序,只能够用来编写一些动态链接库。点击下载
4.4 安装OPhone SDK
参见OPhone SDN官方教程《安装OPhone SDK》
4.5 安装OPhone插件
安装Eclipse插件ADT和WDT,参见OPhone SDK官方教程《安装Eclipse插件ADT》《安装Eclipse插件WDT》
5 移植过程
5.1 移植流程
5.2 封装Java接口
在《假定前提》中提到了要移植的函数,接下来会编写这些函数的Java Native Interface。
/**
* @author panzhenyu
* @email panzhenyu88@gmail.com
*/
package panzhenyu.streaming.video.h264;
import java.nio.ByteBuffer;
public class H264decode {
//H264解码库指针,因为Java没有指针一说,所以这里用一个32位的数来存放指针的值
private long H264decode = 0;
static{
System.loadLibrary("H264Decode");
}
public H264decode() {
this.H264decode = Initialize();
}
public void Cleanup() {
Destroy(H264decode);
}
public int DecodeOneFrame(ByteBuffer pInBuffer,ByteBuffer pOutBuffer) {
return DecodeOneFrame(H264decode, pInBuffer, pOutBuffer);
}
private native static int DecodeOneFrame(long H264decode,ByteBuffer pInBuffer,ByteBuffer pOutBuffer);
private native static long Initialize();
private native static void Destroy(long H264decode);
}
这块没什么好说的,就是按照H264解码库的函数,封装的一层接口,如果你熟悉Java JNI,会发现原来是这么类似。
这里插入一句:我一直认为技术都是相通的,底层的技术就那么几种,学懂了,其它技术都是一通百通。
5.3 使用C实现本地方法
5.3.1 生成头文件
使用javah命令生成JNI头文件,这里需要注意是class路径不是源代码的路径,并且要加上包名:
这里生成了一个panzhenyu_streaming_video_h264_H264decode.h,我们打开来看看:
#include <jni.h>
#ifndef _Included_panzhenyu_streaming_video_h264_H264decode
#define _Included_panzhenyu_streaming_video_h264_H264decode
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT jint JNICALL Java_panzhenyu_streaming_video_h264_H264decode_DecodeOneFrame
(JNIEnv *, jclass, jlong, jobject, jobject);
JNIEXPORT jlong JNICALL Java_panzhenyu_streaming_video_h264_H264decode_Initialize
(JNIEnv *, jclass);
JNIEXPORT void JNICALL Java_panzhenyu_streaming_video_h264_H264decode_Destroy
(JNIEnv *, jclass, jlong);
#ifdef __cplusplus
}
#endif
#endif
5.3.2 实现本地方法
之前,我们已经生成了JNI头文件,接下来我们只需要实现这个头文件的几个导出函数,这里我以H264解码器的实现为例:
/*
============================================================================
Name : panzhenyu_streaming_video_h264_H264decode.cpp
Author : PanZhenyu
Copyright : PanZhenyu
MSN&Email : panzhenyu88@gmail.com
============================================================================
*/
#include "panzhenyu_streaming_video_h264_H264decode.h"
#include "H264Decode.h"
JNIEXPORT jint JNICALL Java_panzhenyu_streaming_video_h264_H264decode_DecodeOneFrame
(JNIEnv * env, jclass obj, jlong decode, jobject pInBuffer, jobject pOutBuffer) {
H264Decode *pDecode = (H264Decode *)decode;
unsigned char *In = NULL;unsigned char *Out = NULL;
unsigned int InPosition = 0;unsigned int InRemaining = 0;unsigned int InSize = 0;
unsigned int OutSize = 0;
jint DecodeSize = -1;
jbyte *InJbyte = 0;
jbyte *OutJbyte = 0;
jbyteArray InByteArrary = 0;
jbyteArray OutByteArrary = 0;
//获取Input/Out ByteBuffer相关属性
{
//Input
{
jclass ByteBufferClass = env->GetObjectClass(pInBuffer);
jmethodID PositionMethodId = env->GetMethodID(ByteBufferClass,"position","()I");
jmethodID RemainingMethodId = env->GetMethodID(ByteBufferClass,"remaining","()I");
jmethodID ArraryMethodId = env->GetMethodID(ByteBufferClass,"array","()[B");
InPosition = env->CallIntMethod(pInBuffer,PositionMethodId);
InRemaining = env->CallIntMethod(pInBuffer,RemainingMethodId);
InSize = InPosition + InRemaining;
InByteArrary = (jbyteArray)env->CallObjectMethod(pInBuffer,ArraryMethodId);
//这里让我很郁闷,为什么每次取出来的地址都是不一样的,难道OPhone实现的时候,每次都把jbyteArray数组中的内容,都复制到一个新分配的内存空间中去么?这么来回分配内存,效率岂不是相当低?那个大哥知道有更好的解决方法,欢迎联系我。
InJbyte = env->GetByteArrayElements(InByteArrary,0);
In = (unsigned char*)InJbyte + InPosition;
}
//Output
{
jclass ByteBufferClass = env->GetObjectClass(pOutBuffer);
jmethodID ArraryMethodId = env->GetMethodID(ByteBufferClass,"array","()[B");
jmethodID ClearMethodId = env->GetMethodID(ByteBufferClass,"clear","()Ljava/nio/Buffer;");
//清理输出缓存区
env->CallObjectMethod(pOutBuffer,ClearMethodId);
OutByteArrary = (jbyteArray)env->CallObjectMethod(pOutBuffer,ArraryMethodId);
OutJbyte = env->GetByteArrayElements(OutByteArrary,0);
Out = (unsigned char*)OutJbyte;
}
}
//解码
DecodeSize = pDecode->DecodeOneFrame(In,InRemaining,Out,OutSize);
//设置Input/Output ByteBuffer相关属性
{
//Input
{
jclass ByteBufferClass = env->GetObjectClass(pInBuffer);
jmethodID SetPositionMethodId = env->GetMethodID(ByteBufferClass,"position","(I)Ljava/nio/Buffer;");
//设置输入缓冲区偏移
env->CallObjectMethod(pInBuffer,SetPositionMethodId,InPosition + DecodeSize);
}
//Output
{
jclass ByteBufferClass = env->GetObjectClass(pOutBuffer);
jmethodID SetPositionMethodId = env->GetMethodID(ByteBufferClass,"position","(I)Ljava/nio/Buffer;");
//设置输出缓冲区偏移
env->CallObjectMethod(pOutBuffer,SetPositionMethodId,OutSize);
}
}
//清理
env->ReleaseByteArrayElements(InByteArrary,InJbyte,0);
env->ReleaseByteArrayElements(OutByteArrary,OutJbyte,0);
return DecodeSize;
}
JNIEXPORT jlong JNICALL Java_panzhenyu_streaming_video_h264_H264decode_Initialize
(JNIEnv * env, jclass obj) {
H264Decode *pDecode = H264Decode::H264DecodeConstruct();
return (jlong)pDecode;
}
JNIEXPORT void JNICALL Java_panzhenyu_streaming_video_h264_H264decode_Destroy
(JNIEnv * env, jclass obj, jlong decode) {
H264Decode *pDecode = (H264Decode *)decode;
if (pDecode)
{
delete pDecode;
pDecode = NULL;
}
}
5.3.3 编译本地方法
哈哈,激动人心的时候到了,接下来,我们只需要把用C实现的本地方法编译为动态链接库,如果之前你用于移植的那个库曾经移植到Symbian上过,那么编译会相当简单,因为NDK的编译器和Symbian的编译器一样,都是采用GCC做交叉编译器。
首先,我们需要在$NDK/apps目录下,创建一个项目目录,这里我创建了一个H264Decode目录,在H264Decode目录中,创建一个Android.mk文件:
APP_PROJECT_PATH := $(call my-dir)
APP_MODULES := H264Decode
接下来,我们需要在$NDK/source目录下,创建源代码目录(这里的目录名要和上面创建的项目目录文件名相同),这里我创建一个H264Decode目录,然后把之前生成的JNI头文件和你实现的本地方法相关头文件和源代码,都拷贝到这个目录下面。
然后,我们编辑Android.mk文件:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := H264Decode
LOCAL_SRC_FILES := common.c cabac.c utils.c golomb.c mpegvideo.c mem.c imgconvert.c h264decode.cpp h264.c dsputil.c panzhenyu_streaming_video_h264_H264decode.cpp
include $(BUILD_SHARED_LIBRARY)
关于Android.mk文件中,各个字段的解释,可以参考$NDK/doc下的《ANDROID-MK.TXT》和《OVERVIEW.TXT》,里面有详细的介绍。
最后,我们启动Cygwin,开始编译:
如果你看到了Install:**,那么恭喜你,你的库已经编译好了。
FAQ 2:
如果编译遇到下面错误,怎么办?
error: redefinition of typedef 'int8_t'
需要注释掉你的代码中“typedef signed char int8_t;”,如果你的代码之前是已经移植到了Mobile/Symbian上的话,很有可能遇到这个问题。
5.4 编写库测试程序
用Eclipse创建一个Android工程,在入口类中输入如下代码:
/**
* @author panzhenyu
* @email panzhenyu@188.com
*/
package panzhenyu.streaming.video.h264;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.ByteBuffer;
import android.app.Activity;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.ImageView;
import android.widget.TextView;
public class H264Example extends Activity {
private static final int VideoWidth = 352;
private static final int VideoHeight = 288;
private ImageView ImageLayout = null;
private TextView FPSLayout = null;
private H264decode Decode = null;
private Handler H = null;
private byte[] Buffer = null;
private int DecodeCount = 0;
private long StartTime = 0;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
ImageLayout = (ImageView) findViewById(R.id.ImageView);
FPSLayout = (TextView) findViewById(R.id.TextView);
Decode = new H264decode();
StartTime = System.currentTimeMillis();
new Thread(new Runnable(){
public void run() {
StartDecode();
}
}).start();
H = new Handler(){
public void handleMessage(Message msg) {
ImageLayout.invalidate();
ImageLayout.setImageBitmap(BitmapFactory.decodeByteArray(Buffer, 0, Buffer.length));
long Time = (System.currentTimeMillis()-StartTime)/1000;
if(Time > 0){
FPSLayout.setText("花费时间:" + Time + "秒 解码帧数:" + DecodeCount + " FPS:" + (DecodeCount/Time) );
}
}
};
}
private void StartDecode(){
File h264file = new File("/tmp/Demo.264");
InputStream h264stream = null;
try {
h264stream = new FileInputStream(h264file);
ByteBuffer pInBuffer = ByteBuffer.allocate(51200);//分配50k的缓存
ByteBuffer pRGBBuffer = ByteBuffer.allocate(VideoWidth*VideoHeight*3);
while (h264stream.read(pInBuffer.array(), pInBuffer.position(), pInBuffer.remaining()) >= 0) {
pInBuffer.position(0);
do{
int DecodeLength = Decode.DecodeOneFrame(pInBuffer, pRGBBuffer);
//如果解码成功,把解码出来的图片显示出来
if(DecodeLength > 0 && pRGBBuffer.position() > 0){
//转换RGB字节为BMP
BMPImage bmp = new BMPImage(pRGBBuffer.array(),VideoWidth,VideoHeight);
Buffer = bmp.getByte();
H.sendMessage(H.obtainMessage());
Thread.sleep(1);
DecodeCount ++;
}
}while(pInBuffer.remaining() > 10240);//确保缓存区里面的数据始终大于10k
//清理已解码缓冲区
int Remaining = pInBuffer.remaining();
System.arraycopy(pInBuffer.array(), pInBuffer.position(), pInBuffer.array(), 0, Remaining);
pInBuffer.position(Remaining);
}
} catch (Exception e1) {
e1.printStackTrace();
} finally {
try{h264stream.close();} catch(Exception e){}
}
}
protected void onDestroy() {
super.onDestroy();
Decode.Cleanup();
}
}
BMPImage是一个工具类,主要用于把RGB序列,转换为BMP图象用于显示:
/**
* @author panzhenyu
* @email panzhenyu@188.com
*/
package panzhenyu.streaming.video.h264;
import java.nio.ByteBuffer;
public class BMPImage {
// --- 私有常量
private final static int BITMAPFILEHEADER_SIZE = 14;
private final static int BITMAPINFOHEADER_SIZE = 40;
// --- 位图文件标头
private byte bfType[] = { 'B', 'M' };
private int bfSize = 0;
private int bfReserved1 = 0;
private int bfReserved2 = 0;
private int bfOffBits = BITMAPFILEHEADER_SIZE + BITMAPINFOHEADER_SIZE;
// --- 位图信息标头
private int biSize = BITMAPINFOHEADER_SIZE;
private int biWidth = 176;
private int biHeight = 144;
private int biPlanes = 1;
private int biBitCount = 24;
private int biCompression = 0;
private int biSizeImage = biWidth*biHeight*3;
private int biXPelsPerMeter = 0x0;
private int biYPelsPerMeter = 0x0;
private int biClrUsed = 0;
private int biClrImportant = 0;
ByteBuffer bmpBuffer = null;
public BMPImage(byte[] Data,int Width,int Height){
biWidth = Width;
biHeight = Height;
biSizeImage = biWidth*biHeight*3;
bfSize = BITMAPFILEHEADER_SIZE + BITMAPINFOHEADER_SIZE + biWidth*biHeight*3;
bmpBuffer = ByteBuffer.allocate(BITMAPFILEHEADER_SIZE + BITMAPINFOHEADER_SIZE + biWidth*biHeight*3);
writeBitmapFileHeader();
writeBitmapInfoHeader();
bmpBuffer.put(Data);
}
public byte[] getByte(){
return bmpBuffer.array();
}
private byte[] intToWord(int parValue) {
byte retValue[] = new byte[2];
retValue[0] = (byte) (parValue & 0x00FF);
retValue[1] = (byte) ((parValue >> 8) & 0x00FF);
return (retValue);
}
private byte[] intToDWord(int parValue) {
byte retValue[] = new byte[4];
retValue[0] = (byte) (parValue & 0x00FF);
retValue[1] = (byte) ((parValue >> 8) & 0x000000FF);
retValue[2] = (byte) ((parValue >> 16) & 0x000000FF);
retValue[3] = (byte) ((parValue >> 24) & 0x000000FF);
return (retValue);
}
private void writeBitmapFileHeader () {
bmpBuffer.put(bfType);
bmpBuffer.put(intToDWord (bfSize));
bmpBuffer.put(intToWord (bfReserved1));
bmpBuffer.put(intToWord (bfReserved2));
bmpBuffer.put(intToDWord (bfOffBits));
}
private void writeBitmapInfoHeader () {
bmpBuffer.put(intToDWord (biSize));
bmpBuffer.put(intToDWord (biWidth));
bmpBuffer.put(intToDWord (biHeight));
bmpBuffer.put(intToWord (biPlanes));
bmpBuffer.put(intToWord (biBitCount));
bmpBuffer.put(intToDWord (biCompression));
bmpBuffer.put(intToDWord (biSizeImage));
bmpBuffer.put(intToDWord (biXPelsPerMeter));
bmpBuffer.put(intToDWord (biYPelsPerMeter));
bmpBuffer.put(intToDWord (biClrUsed));
bmpBuffer.put(intToDWord (biClrImportant));
}
}
5.5 集成测试
集成测试有两点需要注意,在运行程序前,我们需要把动态库复制到模拟器的/system/lib目录下面,还需要把需要解码的视频传到模拟器的/tmp目录下。
唉,这里我又要抱怨一下,OPhone和Symbian的模拟器都做的太不人性化了,Symbian复制一个文件到模拟器中,要进一堆很深的目录,OPhone的更恼火,需要敲命令把文件传递到模拟器里,说实话,在这点上,Mobile的模拟器做的还是非常人性化的。
牢骚发够了,直接上命令:
PATH=D:/Android/OPhone SDK/tools/
adb.exe remount
adb.exe push D:/Eclipse/workspace/H264Example/libs/armeabi/libH264Decode.so /system/lib
adb.exe push D:/Eclipse/workspace/H264Example/Demo.264 /tmp
pause
这里解释一下abd push命令:
adb push <本地文件路径> <远程文件路径> - 复制文件或者目录到模拟器
在Eclipse中,启动库测试程序,哈哈,激动人心的画面终于出来了:
FAQ 3:
模拟器黑屏怎么办?
一个字,等!我之前好几次都认为是模拟器挂了,后来发现模拟器启动就是很慢,希望下个版本能够改进。
- H264解码器移植到OPhone
- 流媒体程序开发之:H264解码器移植到OPhone
- 流媒体程序开发之:H264解码器移植到OPhone
- 流媒体程序开发之H264解码器移植到OPhone
- 流媒体程序开发之:H264解码器移植到OPhone
- J2ME移植到ophone
- J2ME移植到ophone
- J2ME移植到ophone
- J2ME如何移植到ophone
- J2ME如何移植到ophone
- J2ME API 移植到OPhone经验谈
- Android应用移植到OPhone平台指南
- j2me程序如何移植到ophone
- 纯净版基于FFMPEG解码器(H264到YUV)
- 将J2ME游戏移植到OPhone上的指导
- 无缝移植J2ME程序到OPhone平台解决方案
- ffmpeg h264解码器提取
- h264解码器原理
- GIS的那些牛人
- 写dll应该注意的问题
- 服务器管理规范
- Oracledbconsole 服务无法启动
- 孙鑫的XML视频教程
- H264解码器移植到OPhone
- 李开复创新工场发布会演讲
- Google gae jsp错误
- 程序员误区:做软件开发要加班熬夜的工作
- 开发规划
- mysql -- mysql直接在两个数据库服务器之间备份数据库
- 实现多版本 IE 共存的几种解决方案
- Delphi 2007体验!
- Symbian下遍历所有接入点,并动态显示在PopupSettingItem中