流媒體程序開發(fā)之:H264解碼器移植到OPhone
將H.264解碼器移植到OPhone操作系統(tǒng)之上(NDK+C),并寫一個(gè)測(cè)試程序(OPhoneSDK+Java)測(cè)試解碼庫(kù)是否正常運(yùn)行,下面是解碼時(shí)的截圖:
OPhone的模擬器和Mobile的模擬器一樣是模擬ARM指令的,不像Symbian模擬器一樣執(zhí)行的是本地代碼,所以在模擬器上模擬出來的效率會(huì)比
真實(shí)手機(jī)上的效率要低,之前這款解碼器已經(jīng)優(yōu)化到在nokia 6600(相當(dāng)?shù)投说囊豢钍謾C(jī),CPU主頻才120Hz)上做到在線播放。
2. 面向人群
本文面向有一定的手機(jī)應(yīng)用開發(fā)經(jīng)驗(yàn)(例如:S60/Mobile/MTK)和有一定的跨手機(jī)平臺(tái)移植經(jīng)驗(yàn)的人員,幫助她們了解一個(gè)企業(yè)的核心庫(kù)(C/C++)是怎么移植到OPhone之上的。
3. 假定前提
1)熟悉Java/C/C++語(yǔ)言;
2)熟悉Java的JNI技術(shù);
3)有一定的跨手機(jī)平臺(tái)移植經(jīng)驗(yàn);
4)有一套可供移植的源代碼庫(kù),這里以H.264解碼庫(kù)為例,為了保護(hù)我們的知識(shí)版權(quán),這里只能夠公開頭文件:
- #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:
- /***************************************************************************/
- /* 構(gòu)造解碼器 */
- /* @return H264Decode解碼器實(shí)例 */
- /***************************************************************************/
- 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平臺(tái),一切從零開始,因?yàn)樵诖酥埃乙膊皇煜ぁ?br />
4. 開發(fā)環(huán)境(請(qǐng)參考: http://www.ophonesdn.com/documentation/)
5. 移植過程
5.1 移植流程
5.2 封裝Java接口
在“假定前提”中提到了要移植的函數(shù),接下來會(huì)編寫這些 函數(shù)的Java Native Interface。
- package ophone.streaming.video.h264;
- import java.nio.ByteBuffer;
- public class H264decode {
- //H264解碼庫(kù)指針,因?yàn)镴ava沒有指針一說,所以這里用一個(gè)32位的數(shù)來存放指針的值
- 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解碼庫(kù)的函數(shù),封裝的一層接口,如果你熟悉Java JNI,會(huì)發(fā)現(xiàn)原來是這么類似。這里插入一句:我一直認(rèn)為技術(shù)都是相通的,底層的技術(shù)就那么幾種,學(xué)懂了,其它技術(shù)都是一通百通。
5.3 使用C實(shí)現(xiàn)本地方法
5.3.1生成頭文件
使用javah命令生成JNI頭文件,這里需要注意是class路徑不是源代碼的路徑,并且要加上包名:
這里生成了一個(gè)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" {
- #endif
- 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);
- #ifdef __cplusplus
- }
- #endif
- #endif
5.3.2 實(shí)現(xiàn)本地方法
之前已經(jīng)生成了JNI頭文件,接下來只需要實(shí)現(xiàn)這個(gè)頭文件的幾個(gè)導(dǎo)出函數(shù),這里以H264解碼器的實(shí)現(xiàn)為例:
- #include "ophone_streaming_video_h264_H264decode.h"
- #include "H264Decode.h"
- JNIEXPORT jint JNICALL Java_ophone_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相關(guān)屬性
- {
- //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 ArraryMethodId = env->GetMethodID(ByteBufferClass,"array","()[B");
- jmethodID ClearMethodId = env->GetMethodID(ByteBufferClass,"clear","()Ljava/nio/Buffer;");
- //清理輸出緩存區(qū)
- 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);
- //設(shè)置Input/Output ByteBuffer相關(guān)屬性
- {
- //Input
- {
- jclass ByteBufferClass = env->GetObjectClass(pInBuffer);
- jmethodID SetPositionMethodId = env->GetMethodID(ByteBufferClass,"position","(I)Ljava/nio/Buffer;");
- //設(shè)置輸入緩沖區(qū)偏移
- env->CallObjectMethod(pInBuffer,SetPositionMethodId,InPosition + DecodeSize);
- }
- //Output
- {
- jclass ByteBufferClass = env->GetObjectClass(pOutBuffer);
- jmethodID SetPositionMethodId = env->GetMethodID(ByteBufferClass,"position","(I)Ljava/nio/Buffer;");
- //設(shè)置輸出緩沖區(qū)偏移
- env->CallObjectMethod(pOutBuffer,SetPositionMethodId,OutSize);
- }
- }
- //清理
- env->ReleaseByteArrayElements(InByteArrary,InJbyte,0);
- env->ReleaseByteArrayElements(OutByteArrary,OutJbyte,0);
- return DecodeSize;
- }
- JNIEXPORT jlong JNICALL Java_ophone_streaming_video_h264_H264decode_Initialize
- (JNIEnv * env, jclass obj) {
- H264Decode *pDecode = H264Decode::H264DecodeConstruct();
- return (jlong)pDecode;
- }
- JNIEXPORT void JNICALL Java_ophone_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實(shí)現(xiàn)的本地方法編譯為動(dòng)態(tài)鏈接庫(kù),如果之前你用于移植的那個(gè)庫(kù)曾經(jīng)移植到Symbian上過,那么編譯會(huì)相當(dāng)簡(jiǎn)單,因?yàn)镹DK的編譯器和Symbian的編譯器一樣,都是采用GCC做交叉編譯器。
首先,需要在$NDK"apps目錄下,創(chuàng)建一個(gè)項(xiàng)目目錄,這里創(chuàng)建了一個(gè)H264Decode目錄,在H264Decode目錄中,創(chuàng)建一個(gè)Android.mk文件:
- APP_PROJECT_PATH := $(call my-dir)
- APP_MODULES := H264Decode
接下來,需要在$NDK"source目錄下,創(chuàng)建源代碼目錄(這里的目錄名要和上面創(chuàng)建的項(xiàng)目目錄文件名相同),這里創(chuàng)建一個(gè)H264Decode目錄,然后把之前生成的JNI頭文件和你實(shí)現(xiàn)的本地方法相關(guān)頭文件和源代碼,都拷貝到 這個(gè)目錄下面。
然后,我們編輯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)
關(guān)于Android.mk文件中,各個(gè)字段的解釋,可以參考$NDK"doc下的《OPHONE-MK.TXT》和《OVERVIEW.TXT》,里面有詳細(xì)的介紹。
最后,我們啟動(dòng)Cygwin,開始編譯:
如果你看到了Install:**,這說明你的庫(kù)已經(jīng)編譯好了。
FAQ 2:
如果編譯遇到下面錯(cuò)誤,怎么辦?
- error: redefinition of typedef 'int8_t'
需要注釋掉你的代碼中“typedef signed char int8_t;”,如果你的代碼之前是已經(jīng)移植到了Mobile/Symbian上的話,很有可能遇到這個(gè)問題。
5.4 編寫庫(kù)測(cè)試程序
用Eclipse創(chuàng)建一個(gè)OPhone工程,在入口類中輸入如下代碼:
- /**
- * @author ophone
- * @email 3751624@qq.com
- */
- package ophone.streaming.video.h264;
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.InputStream;
- import java.nio.ByteBuffer;
- 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("花費(fèi)時(shí)間:" + Time + "秒 解碼幀數(shù):" + 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){
- //轉(zhuǎn)換RGB字節(jié)為BMP
- BMPImage bmp = new BMPImage(pRGBBuffer.array(),VideoWidth,VideoHeight);
- Buffer = bmp.getByte();
- H.sendMessage(H.obtainMessage());
- Thread.sleep(1);
- DecodeCount ++;
- }
- }while(pInBuffer.remaining() > 10240);//確保緩存區(qū)里面的數(shù)據(jù)始終大于10k
- //清理已解碼緩沖區(qū)
- 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是一個(gè)工具類,主要用于把RGB序列,轉(zhuǎn)換為BMP圖象用于顯示:
- @author ophone
- * @email 3751624@qq.com
- */
- package ophone.streaming.video.h264;
- import java.nio.ByteBuffer;
- public class BMPImage {
- // --- 私有常量
- private final static int BITMAPFILEHEADER_SIZE = 14;
- private final static int BITMAPINFOHEADER_SIZE = 40;
- // --- 位圖文件標(biāo)頭
- private byte bfType[] = { 'B', 'M' };
- private int bfSize = 0;
- private int bfReserved1 = 0;
- private int bfReserved2 = 0;
- private int bfOffBits = BITMAPFILEHEADER_SIZE + BITMAPINFOHEADER_SIZE;
- // --- 位圖信息標(biāo)頭
- 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));
- }
- }
測(cè)試程序完整工程在此暫不提供。
5.5集成測(cè)試
集成測(cè)試有兩點(diǎn)需要注意,在運(yùn)行程序前,需要把動(dòng)態(tài)庫(kù)復(fù)制到模擬器的/system/lib目錄下面,還需要把需要解碼的視頻傳到模擬器的/tmp目錄下。
這里要明確的是,OPhone和Symbian的模擬器都做的太不人性化了,Symbian復(fù)制一個(gè)文件到模擬器中,要進(jìn)一堆很深的目錄,OPhone的
更惱火,需要敲命令把文件傳遞到模擬器里,說實(shí)話,僅在這點(diǎn)上,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 <本地文件路徑> <遠(yuǎn)程文件路徑> - 復(fù)制文件或者目錄到模擬器
在Eclipse中,啟動(dòng)庫(kù)測(cè)試程序,得到畫面如下:
FAQ 3:
模擬器黑屏怎么辦?
這可能是由于模擬器啟動(dòng)速度比較慢所引起的,所以需要多等一會(huì)。希望下個(gè)版本能夠改進(jìn)。
原文地址:http://www.ophonesdn.com/article/show/45;jsessionid=306BD3BE92F43DC693BEB09B0234B036
《Android開發(fā)完全講義(第2版)》(本書版權(quán)已輸出到臺(tái)灣)
http://product.dangdang.com/product.aspx?product_id=22741502
《Android高薪之路:Android程序員面試寶典 》http://book.360buy.com/10970314.html
新浪微博:http://t.sina.com.cn/androidguy 昵稱:李寧_Lining
posted on 2009-09-20 17:54 銀河使者 閱讀(2641) 評(píng)論(1) 編輯 收藏 所屬分類: java 、 原創(chuàng) 、移動(dòng)(mobile)