OpenMAX AL is an application-level multimedia library in C. Android NDK multimedia APIs are based on the OpenMAX AL 1.0.1 standard with Android-specific extensions. The API is available for Android 4.0 or higher. We should note that the API is evolving and the Android NDK team mentioned that the future version of OpenMAX AL API may require developers to change their code.
Before we start coding with the OpenMAX AL library, it is important to understand some basics about the library. We will briefly describe the library in the following text.
OpenMAX AL refers to the Application Layer interface of the Open Media Acceleration (OpenMAX) library. It is a royalty-free, cross-platform, C-language application level API for developers to create multimedia applications. Its main features include media recording, media playback, media controls (for example, brightness control), and effects. Compared to OpenSL ES library, OpenMAX AL provides features for both video and audio, but it lacks certain audio features like 3D audio and audio effects which OpenSL ES can provide. Some applications may need to use both libraries.
OpenMAX AL defines two profiles, namely media playback and media player/recorder. Android does not implement all features required by either profile, therefore the OpenMAX AL library in Android does not conform either profile. In addition, Android implements some features specific to Android.
The main features provided by Android OpenMAX AL implementation is the ability to process the MPEG-2 transport stream. We can demultiplex the stream, decode the video and audio, and render them as audio output or to the phone screen. This library allows us to have complete control over the media data before it is passed for presentation. For example, we can call OpenGL ES functions to apply graphics effect on video data before rendering it.
For a detailed description of what is supported on Android, we can refer to the OpenMAX AL for Android documentation available with the Android NDK under the docs/openmaxal/
folder.
The design of OpenMAX AL library is similar to OpenSL ES library. They both adopt an object-oriented approach and the fundamental concepts including objects and interfaces are the same. Readers should refer to the previous recipe for a detailed explanation on these concepts.
The following steps describe how to create a simple Android video playback application using the OpenMAX AL functions:
OpenMAXSLDemo
. Set the package name as cookbook.chapter7.openmaxsldemo
. Refer to the Loading native libraries and registering native methods recipe of Chapter 2, Java Native Interface for more detailed instructions.MainActivity.java
in the package cookbook.chapter7.openmaxsldemo
. This Java file loads the native library OpenMAXSLDemo
, sets the view, and calls the native methods to play the video.mylog.h
and OpenMAXSLDemo.c
files in the jni
folder. A part of the code in OpenMAXSLDemo.c
is showed in the following code snippet.naCreateEngine
creates and realizes the engine object and the output mix object.
void naCreateEngine(JNIEnv* env, jclass clazz) { XAresult res; res = xaCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL); res = (*engineObject)->Realize(engineObject, XA_BOOLEAN_FALSE); res = (*engineObject)->GetInterface(engineObject, XA_IID_ENGINE, &engineEngine); res = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 0, NULL, NULL); res = (*outputMixObject)->Realize(outputMixObject, XA_BOOLEAN_FALSE); }
naCreateStreamingMediaPlayer
creates and realizes a media player object with the data source and data sink. It obtains the buffer queue interface and registers the AndroidBufferQueueCallback
function as the callback function. The callback function will be invoked after a buffer is processed:
jboolean naCreateStreamingMediaPlayer(JNIEnv* env, jclass clazz, jstring filename) { XAresult res; const char *utf8FileName = (*env)->GetStringUTFChars(env, filename, NULL); file = fopen(utf8FileName, "rb"); XADataLocator_AndroidBufferQueue loc_abq = { XA_DATALOCATOR_ANDROIDBUFFERQUEUE, NB_BUFFERS }; XADataFormat_MIME format_mime = {XA_DATAFORMAT_MIME, XA_ANDROID_MIME_MP2TS, XA_CONTAINERTYPE_MPEG_TS }; XADataSource dataSrc = {&loc_abq, &format_mime}; XADataLocator_OutputMix loc_outmix = { XA_DATALOCATOR_OUTPUTMIX, outputMixObject }; XADataSink audioSnk = { &loc_outmix, NULL }; XADataLocator_NativeDisplay loc_nd = {XA_DATALOCATOR_NATIVEDISPLAY, (void*)theNativeWindow, NULL}; XADataSink imageVideoSink = {&loc_nd, NULL}; XAboolean required[NB_MAXAL_INTERFACES] = {XA_BOOLEAN_TRUE, XA_BOOLEAN_TRUE}; XAInterfaceID iidArray[NB_MAXAL_INTERFACES] = {XA_IID_PLAY, XA_IID_ANDROIDBUFFERQUEUESOURCE}; res = (*engineEngine)->CreateMediaPlayer(engineEngine, &playerObj, &dataSrc, NULL, &audioSnk, &imageVideoSink, NULL, NULL, NB_MAXAL_INTERFACES, iidArray, required ); (*env)->ReleaseStringUTFChars(env, filename, utf8FileName); res = (*playerObj)->Realize(playerObj, XA_BOOLEAN_FALSE); res = (*playerObj)->GetInterface(playerObj, XA_IID_PLAY, &playerPlayItf); res = (*playerObj)->GetInterface(playerObj, XA_IID_ANDROIDBUFFERQUEUESOURCE, &playerBQItf); res = (*playerBQItf)->SetCallbackEventsMask(playerBQItf, XA_ANDROIDBUFFERQUEUEEVENT_PROCESSED); res = (*playerBQItf)->RegisterCallback(playerBQItf, AndroidBufferQueueCallback, NULL); if (!enqueueInitialBuffers(JNI_FALSE)) { return JNI_FALSE; } res = (*playerPlayItf)->SetPlayState(playerPlayItf, XA_PLAYSTATE_PAUSED); res = (*playerPlayItf)->SetPlayState(playerPlayItf, XA_PLAYSTATE_PLAYING); return JNI_TRUE; }
AndroidBufferQueueCallback
is the callback function registered to refill the buffer with media data or handle commands:
XAresult AndroidBufferQueueCallback(XAAndroidBufferQueueItf caller, void *pCallbackContext, void *pBufferContext, void *pBufferData, XAuint32 dataSize, XAuint32 dataUsed, const XAAndroidBufferItem *pItems, XAuint32 itemsLength) { XAresult res; int ok; ok = pthread_mutex_lock(&mutex); if (discontinuity) { if (!reachedEof) { res = (*playerBQItf)->Clear(playerBQItf); rewind(file); (void) enqueueInitialBuffers(JNI_TRUE); } discontinuity = JNI_FALSE; ok = pthread_cond_signal(&cond); goto exit; } if ((pBufferData == NULL) && (pBufferContext != NULL)) { const int processedCommand = *(int *)pBufferContext; if (kEosBufferCntxt == processedCommand) { goto exit; } } if (reachedEof) { goto exit; } size_t nbRead; size_t bytesRead; bytesRead = fread(pBufferData, 1, BUFFER_SIZE, file); if (bytesRead > 0) { if ((bytesRead % MPEG2_TS_PACKET_SIZE) != 0) { LOGI(2, "Dropping last packet because it is not whole"); } size_t packetsRead = bytesRead / MPEG2_TS_PACKET_SIZE; size_t bufferSize = packetsRead * MPEG2_TS_PACKET_SIZE; res = (*caller)->Enqueue(caller, NULL, pBufferData, bufferSize, NULL, 0); } else { XAAndroidBufferItem msgEos[1]; msgEos[0].itemKey = XA_ANDROID_ITEMKEY_EOS; msgEos[0].itemSize = 0; res = (*caller)->Enqueue(caller, (void *)&kEosBufferCntxt, NULL, 0, msgEos, sizeof(XAuint32)*2); reachedEof = JNI_TRUE; } exit: ok = pthread_mutex_unlock(&mutex); return XA_RESULT_SUCCESS; }
Android.mk
file in the jni
folder with the following content:LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := OpenMAXSLDemo LOCAL_SRC_FILES := OpenMAXSLDemo.c LOCAL_LDLIBS := -llog LOCAL_LDLIBS += -landroid LOCAL_LDLIBS += -lOpenMAXAL include $(BUILD_SHARED_LIBRARY)
NativeMedia.ts
video file available in the samples/native-media/
directory for testing. The following command can be used to put the video file into the /sdcard/
directory of the testing Android device:$ adb push NativeMedia.ts /sdcard/
We can press Play to start playing the video.
In this recipe, we used the OpenMAX AL library to implement a simple video player.
In order to call the API functions, we must add the following line to our code:
#include <OMXAL/OpenMAXAL.h>
If we are also using Android-specific features, we should include another header:
#include <OMXAL/OpenMAXAL_Android.h>
In the Android.mk
file, we must add the following line to link to the OpenMAX AL multimedia library:
LOCAL_LDLIBS += libOpenMAXAL
Our sample project that is a simplified version of the native media project comes with the Android NDK. The following diagram illustrates how the application works:
In our code, we created and realized the engine and output mix objects at naCreateEngine
function. At the naCreateStreamingMediaPlayerfunction
function, we created and realized the media player object with the audio data sink set as output mix, video data sink set as native display, and data source set as the Android buffer queue.
When a buffer is consumed, the callback function AndroidBufferQueueCallback
is invoked, where we refill the buffer with data from the NativeMedia.ts
file and enqueue it to the buffer queue.