• Blog
  • 流媒体程序开发之:H264解码器移植到OPhone

流媒体程序开发之:H264解码器移植到OPhone

OPhone平台开发, 2009-08-27 17:12:45

Tags : 流媒体程序 移植 解码器 视频播放 OPhone

        导语:本文用通俗的语言描述在OPhone上如何实现视频的流畅播放及解码器的调用方法。(创作:Pan、指导:Don)
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平台,一切从零开始,因为在此之前,我也不熟悉。
 

4.  开发环境(请参考: http://www.ophonesdn.com/documentation/)
 

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" {
#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 实现本地方法

        之前已经生成了JNI头文件,接下来只需要实现这个头文件的几个导出函数,这里以H264解码器的实现为例:

 

#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相关属性
	{
		//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;");
			
			//清理输出缓存区
			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_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实现的本地方法编译为动态链接库,如果之前你用于移植的那个库曾经移植到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 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("花费时间:" + 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 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;

	// --- 位图文件标头
	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:\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:

        模拟器黑屏怎么办?
        这可能是由于模拟器启动速度比较慢所引起的,所以需要多等一会。希望下个版本能够改进。

(声明:本网的新闻及文章版权均属OPhone SDN网站所有,如需转载请与我们编辑团队联系。任何媒体、网站或个人未经本网书面协议授权,不得进行任何形式的转载。已经取得本网协议授权的媒体、网站,在转载使用时请注明稿件来源。)