天天看點

流媒體程式開發之:H264解碼器移植到OPhone

1.   移植目标

       将H.264解碼器移植到OPhone作業系統之上(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)有一套可供移植的源代碼庫,這裡以H.264解碼庫為例,為了保護我們的知識版權,這裡隻能夠公開頭檔案:

#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/OPhone  

    #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__  

      你不用熟悉OPhone平台,一切從零開始,因為在此之前,我也不熟悉。

5.  移植過程

5.1 移植流程

5.2 封裝Java接口

        在“假定前提”中提到了要移植的函數,接下來會編寫這些 函數的Java Native Interface。

package ophone.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路徑不是源代碼的路徑,并且要加上包名:

        這裡生成了一個ophone_streaming_video_h264_H264decode.h,我們打開來看看:

#include <jni.h>  

#ifndef _Included_ophone_streaming_video_h264_H264decode  

#define _Included_ophone_streaming_video_h264_H264decode  

#ifdef __cplusplus  

extern "C" {  

JNIEXPORT jint JNICALL Java_ophone_streaming_video_h264_H264decode_DecodeOneFrame  

  (JNIEnv *, jclass, jlong, jobject, jobject);  

JNIEXPORT jlong JNICALL Java_ophone_streaming_video_h264_H264decode_Initialize  

  (JNIEnv *, jclass);  

JNIEXPORT void JNICALL Java_ophone_streaming_video_h264_H264decode_Destroy  

  (JNIEnv *, jclass, jlong);  

5.3.2 實作本地方法

        之前已經生成了JNI頭檔案,接下來隻需要實作這個頭檔案的幾個導出函數,這裡以H264解碼器的實作為例:

#include "ophone_streaming_video_h264_H264decode.h"  

#include "H264Decode.h"  

  (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);  

            InJbyte = env->GetByteArrayElements(InByteArrary,0);  

            In = (unsigned char*)InJbyte + InPosition;  

        }  

        //Output  

            jclass ByteBufferClass = env->GetObjectClass(pOutBuffer);  

            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相關屬性  

            jmethodID SetPositionMethodId = env->GetMethodID(ByteBufferClass,"position","(I)Ljava/nio/Buffer;");  

            //設定輸入緩沖區偏移  

            env->CallObjectMethod(pInBuffer,SetPositionMethodId,InPosition + DecodeSize);  

            //設定輸出緩沖區偏移  

            env->CallObjectMethod(pOutBuffer,SetPositionMethodId,OutSize);  

    //清理  

    env->ReleaseByteArrayElements(InByteArrary,InJbyte,0);  

    env->ReleaseByteArrayElements(OutByteArrary,OutJbyte,0);  

    return DecodeSize;  

  (JNIEnv * env, jclass obj) {  

    H264Decode *pDecode = H264Decode::H264DecodeConstruct();  

    return (jlong)pDecode;  

  (JNIEnv * env, jclass obj, jlong 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 ophone_streaming_video_h264_H264decode.cpp  

include $(BUILD_SHARED_LIBRARY)  

        關于Android.mk檔案中,各個字段的解釋,可以參考$NDK"doc下的《OPHONE-MK.TXT》和《OVERVIEW.TXT》,裡面有詳細的介紹。

        最後,我們啟動Cygwin,開始編譯:

       如果你看到了Install:**,這說明你的庫已經編譯好了。

       FAQ 2:

       如果編譯遇到下面錯誤,怎麼辦?

error: redefinition of typedef 'int8_t'  

   需要注釋掉你的代碼中“typedef signed char  int8_t;”,如果你的代碼之前是已經移植到了Mobile/Symbian上的話,很有可能遇到這個問題。

5.4 編寫庫測試程式

    用Eclipse建立一個OPhone工程,在入口類中輸入如下代碼:

/**    

 * @author ophone 

 * @email [email protected] 

 */  

import java.io.File;  

import java.io.FileInputStream;  

import java.io.InputStream;  

import OPhone.app.Activity;  

import OPhone.graphics.BitmapFactory;  

import OPhone.os.Bundle;  

import OPhone.os.Handler;  

import OPhone.os.Message;  

import OPhone.widget.ImageView;  

import OPhone.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 ophone  

 * @email [email protected]  

*/  

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[1] = (byte) ((parValue >> 8) & 0x000000FF);  

        retValue[2] = (byte) ((parValue >> 16) & 0x000000FF);  

        retValue[3] = (byte) ((parValue >> 24) & 0x000000FF);  

    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:"OPhone"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:

        模拟器黑屏怎麼辦?

        這可能是由于模拟器啟動速度比較慢所引起的,是以需要多等一會。希望下個版本能夠改進。

原文位址:http://www.ophonesdn.com/article/show/45;jsessionid=306BD3BE92F43DC693BEB09B0234B036

<a href="http://www.eoeandroid.com/forumdisplay.php?fid=4">國内最棒的Google Android技術社群(eoeandroid),歡迎通路!</a>