H264解码器移植到OPhone

来源:互联网 发布:python turtle画圆 编辑:程序博客网 时间:2024/03/28 16:46

注:原版发布于OphoneSDN,如需转载请参考OPhoneSDN版权声明

目录

目录... 2

1     移植目标... 3

2     面向人群... 4

3     假定前提... 4

4     开发环境... 5

4.1     安装Eclipse. 5

4.2     安装Cygwin. 5

4.3     安装Android NDK. 6

4.4     安装OPhone SDK. 6

4.5     安装OPhone插件... 6

5     移植过程... 7

5.1     移植流程... 7

5.2     封装Java接口... 7

5.3     使用C实现本地方法... 9

5.3.1       生成头文件... 9

5.3.2       实现本地方法... 10

5.3.3       编译本地方法... 12

5.4     编写库测试程序... 15

5.5     集成测试... 20

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、  你熟悉JavaJNI技术;

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插件ADTWDT,参见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目录下。

唉,这里我又要抱怨一下,OPhoneSymbian的模拟器都做的太不人性化了,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

模拟器黑屏怎么办?

    一个字,等!我之前好几次都认为是模拟器挂了,后来发现模拟器启动就是很慢,希望下个版本能够改进。

原创粉丝点击