diff --git a/ndk/platforms/android-14/samples/native-media/AndroidManifest.xml b/ndk/platforms/android-14/samples/native-media/AndroidManifest.xml new file mode 100644 index 000000000..07668cff2 --- /dev/null +++ b/ndk/platforms/android-14/samples/native-media/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/ndk/platforms/android-14/samples/native-media/NativeMedia.ts b/ndk/platforms/android-14/samples/native-media/NativeMedia.ts new file mode 100644 index 000000000..d9d54b2e4 Binary files /dev/null and b/ndk/platforms/android-14/samples/native-media/NativeMedia.ts differ diff --git a/ndk/platforms/android-14/samples/native-media/README.txt b/ndk/platforms/android-14/samples/native-media/README.txt index 516698a61..f900e31a9 100644 --- a/ndk/platforms/android-14/samples/native-media/README.txt +++ b/ndk/platforms/android-14/samples/native-media/README.txt @@ -1,2 +1,8 @@ -The documentation for Android native media based on OpenMAX AL 1.0.1 -references this directory, but the example is not yet available. +This sample app requires an MPEG-2 Transport Stream file to be +placed in /sdcard/NativeMedia.ts and encoded as: + + video: H.264 baseline profile + audio: AAC LC stereo + +For demonstration purposes we have supplied such a .ts file. +Any actual stream must be created according to the MPEG-2 specification. diff --git a/ndk/platforms/android-14/samples/native-media/default.properties b/ndk/platforms/android-14/samples/native-media/default.properties new file mode 100644 index 000000000..2d6991797 --- /dev/null +++ b/ndk/platforms/android-14/samples/native-media/default.properties @@ -0,0 +1,4 @@ +# Indicates whether an apk should be generated for each density. +split.density=false +# Project target. +target=android-14 diff --git a/ndk/platforms/android-14/samples/native-media/jni/Android.mk b/ndk/platforms/android-14/samples/native-media/jni/Android.mk new file mode 100644 index 000000000..369ccf834 --- /dev/null +++ b/ndk/platforms/android-14/samples/native-media/jni/Android.mk @@ -0,0 +1,30 @@ +# Copyright (C) 2011 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := native-media-jni +LOCAL_SRC_FILES := native-media-jni.c +# for native multimedia +LOCAL_LDLIBS += -lOpenMAXAL +# for logging +LOCAL_LDLIBS += -llog +# for native windows +LOCAL_LDLIBS += -landroid + +LOCAL_CFLAGS += -UNDEBUG + +include $(BUILD_SHARED_LIBRARY) diff --git a/ndk/platforms/android-14/samples/native-media/jni/native-media-jni.c b/ndk/platforms/android-14/samples/native-media/jni/native-media-jni.c new file mode 100644 index 000000000..bdf568a4f --- /dev/null +++ b/ndk/platforms/android-14/samples/native-media/jni/native-media-jni.c @@ -0,0 +1,526 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* This is a JNI example where we use native methods to play video + * using OpenMAX AL. See the corresponding Java source file located at: + * + * src/com/example/nativemedia/NativeMedia/NativeMedia.java + * + * In this example we use assert() for "impossible" error conditions, + * and explicit handling and recovery for more likely error conditions. + */ + +#include +#include +#include +#include +#include + +// for __android_log_print(ANDROID_LOG_INFO, "YourApp", "formatted message"); +#include +#define TAG "NativeMedia" +#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, TAG, __VA_ARGS__) + +// for native media +#include +#include + +// for native window JNI +#include + +// engine interfaces +static XAObjectItf engineObject = NULL; +static XAEngineItf engineEngine = NULL; + +// output mix interfaces +static XAObjectItf outputMixObject = NULL; + +// streaming media player interfaces +static XAObjectItf playerObj = NULL; +static XAPlayItf playerPlayItf = NULL; +static XAAndroidBufferQueueItf playerBQItf = NULL; +static XAStreamInformationItf playerStreamInfoItf = NULL; +static XAVolumeItf playerVolItf = NULL; + +// number of required interfaces for the MediaPlayer creation +#define NB_MAXAL_INTERFACES 3 // XAAndroidBufferQueueItf, XAStreamInformationItf and XAPlayItf + +// video sink for the player +static ANativeWindow* theNativeWindow; + +// number of buffers in our buffer queue, an arbitrary number +#define NB_BUFFERS 8 + +// we're streaming MPEG-2 transport stream data, operate on transport stream block size +#define MPEG2_TS_PACKET_SIZE 188 + +// number of MPEG-2 transport stream blocks per buffer, an arbitrary number +#define PACKETS_PER_BUFFER 10 + +// determines how much memory we're dedicating to memory caching +#define BUFFER_SIZE (PACKETS_PER_BUFFER*MPEG2_TS_PACKET_SIZE) + +// where we cache in memory the data to play +// note this memory is re-used by the buffer queue callback +static char dataCache[BUFFER_SIZE * NB_BUFFERS]; + +// handle of the file to play +static FILE *file; + +// has the app reached the end of the file +static jboolean reachedEof = JNI_FALSE; + +// constant to identify a buffer context which is the end of the stream to decode +static const int kEosBufferCntxt = 1980; // a magic value we can compare against + +// For mutual exclusion between callback thread and application thread(s). +// The mutex protects reachedEof, discontinuity, +// The condition is signalled when a discontinuity is acknowledged. + +static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t cond = PTHREAD_COND_INITIALIZER; + +// whether a discontinuity is in progress +static jboolean discontinuity = JNI_FALSE; + +static jboolean enqueueInitialBuffers(jboolean discontinuity); + +// AndroidBufferQueueItf callback to supply MPEG-2 TS packets to the media player +static XAresult AndroidBufferQueueCallback( + XAAndroidBufferQueueItf caller, + void *pCallbackContext, /* input */ + void *pBufferContext, /* input */ + void *pBufferData, /* input */ + XAuint32 dataSize, /* input */ + XAuint32 dataUsed, /* input */ + const XAAndroidBufferItem *pItems,/* input */ + XAuint32 itemsLength /* input */) +{ + XAresult res; + int ok; + + // pCallbackContext was specified as NULL at RegisterCallback and is unused here + assert(NULL == pCallbackContext); + + // note there is never any contention on this mutex unless a discontinuity request is active + ok = pthread_mutex_lock(&mutex); + assert(0 == ok); + + // was a discontinuity requested? + if (discontinuity) { + // Note: can't rewind after EOS, which we send when reaching EOF + // (don't send EOS if you plan to play more content through the same player) + if (!reachedEof) { + // clear the buffer queue + res = (*playerBQItf)->Clear(playerBQItf); + assert(XA_RESULT_SUCCESS == res); + // rewind the data source so we are guaranteed to be at an appropriate point + rewind(file); + // Enqueue the initial buffers, with a discontinuity indicator on first buffer + (void) enqueueInitialBuffers(JNI_TRUE); + } + // acknowledge the discontinuity request + discontinuity = JNI_FALSE; + ok = pthread_cond_signal(&cond); + assert(0 == ok); + goto exit; + } + + if ((pBufferData == NULL) && (pBufferContext != NULL)) { + const int processedCommand = *(int *)pBufferContext; + if (kEosBufferCntxt == processedCommand) { + LOGV("EOS was processed\n"); + // our buffer with the EOS message has been consumed + assert(0 == dataSize); + goto exit; + } + } + + // pBufferData is a pointer to a buffer that we previously Enqueued + assert((dataSize > 0) && ((dataSize % MPEG2_TS_PACKET_SIZE) == 0)); + assert(dataCache <= (char *) pBufferData && (char *) pBufferData < + &dataCache[BUFFER_SIZE * NB_BUFFERS]); + assert(0 == (((char *) pBufferData - dataCache) % BUFFER_SIZE)); + + // don't bother trying to read more data once we've hit EOF + if (reachedEof) { + goto exit; + } + + size_t nbRead; + // note we do call fread from multiple threads, but never concurrently + size_t bytesRead; + bytesRead = fread(pBufferData, 1, BUFFER_SIZE, file); + if (bytesRead > 0) { + if ((bytesRead % MPEG2_TS_PACKET_SIZE) != 0) { + LOGV("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 /*pBufferContext*/, + pBufferData /*pData*/, + bufferSize /*dataLength*/, + NULL /*pMsg*/, + 0 /*msgLength*/); + assert(XA_RESULT_SUCCESS == res); + } else { + // EOF or I/O error, signal EOS + XAAndroidBufferItem msgEos[1]; + msgEos[0].itemKey = XA_ANDROID_ITEMKEY_EOS; + msgEos[0].itemSize = 0; + // EOS message has no parameters, so the total size of the message is the size of the key + // plus the size if itemSize, both XAuint32 + res = (*caller)->Enqueue(caller, (void *)&kEosBufferCntxt /*pBufferContext*/, + NULL /*pData*/, 0 /*dataLength*/, + msgEos /*pMsg*/, + sizeof(XAuint32)*2 /*msgLength*/); + assert(XA_RESULT_SUCCESS == res); + reachedEof = JNI_TRUE; + } + +exit: + ok = pthread_mutex_unlock(&mutex); + assert(0 == ok); + return XA_RESULT_SUCCESS; +} + + +// callback invoked whenever there is new or changed stream information +static void StreamChangeCallback(XAStreamInformationItf caller, + XAuint32 eventId, + XAuint32 streamIndex, + void * pEventData, + void * pContext ) +{ + LOGV("StreamChangeCallback called for stream %u", streamIndex); + // pContext was specified as NULL at RegisterStreamChangeCallback and is unused here + assert(NULL == pContext); + switch (eventId) { + case XA_STREAMCBEVENT_PROPERTYCHANGE: { + /** From spec 1.0.1: + "This event indicates that stream property change has occurred. + The streamIndex parameter identifies the stream with the property change. + The pEventData parameter for this event is not used and shall be ignored." + */ + + XAresult res; + XAuint32 domain; + res = (*caller)->QueryStreamType(caller, streamIndex, &domain); + assert(XA_RESULT_SUCCESS == res); + switch (domain) { + case XA_DOMAINTYPE_VIDEO: { + XAVideoStreamInformation videoInfo; + res = (*caller)->QueryStreamInformation(caller, streamIndex, &videoInfo); + assert(XA_RESULT_SUCCESS == res); + LOGV("Found video size %u x %u, codec ID=%u, frameRate=%u, bitRate=%u, duration=%u ms", + videoInfo.width, videoInfo.height, videoInfo.codecId, videoInfo.frameRate, + videoInfo.bitRate, videoInfo.duration); + } break; + default: + fprintf(stderr, "Unexpected domain %u\n", domain); + break; + } + } break; + default: + fprintf(stderr, "Unexpected stream event ID %u\n", eventId); + break; + } +} + + +// create the engine and output mix objects +void Java_com_example_nativemedia_NativeMedia_createEngine(JNIEnv* env, jclass clazz) +{ + XAresult res; + + // create engine + res = xaCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL); + assert(XA_RESULT_SUCCESS == res); + + // realize the engine + res = (*engineObject)->Realize(engineObject, XA_BOOLEAN_FALSE); + assert(XA_RESULT_SUCCESS == res); + + // get the engine interface, which is needed in order to create other objects + res = (*engineObject)->GetInterface(engineObject, XA_IID_ENGINE, &engineEngine); + assert(XA_RESULT_SUCCESS == res); + + // create output mix + res = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 0, NULL, NULL); + assert(XA_RESULT_SUCCESS == res); + + // realize the output mix + res = (*outputMixObject)->Realize(outputMixObject, XA_BOOLEAN_FALSE); + assert(XA_RESULT_SUCCESS == res); + +} + + +// Enqueue the initial buffers, and optionally signal a discontinuity in the first buffer +static jboolean enqueueInitialBuffers(jboolean discontinuity) +{ + + /* Fill our cache. + * We want to read whole packets (integral multiples of MPEG2_TS_PACKET_SIZE). + * fread returns units of "elements" not bytes, so we ask for 1-byte elements + * and then check that the number of elements is a multiple of the packet size. + */ + size_t bytesRead; + bytesRead = fread(dataCache, 1, BUFFER_SIZE * NB_BUFFERS, file); + if (bytesRead <= 0) { + // could be premature EOF or I/O error + return JNI_FALSE; + } + if ((bytesRead % MPEG2_TS_PACKET_SIZE) != 0) { + LOGV("Dropping last packet because it is not whole"); + } + size_t packetsRead = bytesRead / MPEG2_TS_PACKET_SIZE; + LOGV("Initially queueing %u packets", packetsRead); + + /* Enqueue the content of our cache before starting to play, + we don't want to starve the player */ + size_t i; + for (i = 0; i < NB_BUFFERS && packetsRead > 0; i++) { + // compute size of this buffer + size_t packetsThisBuffer = packetsRead; + if (packetsThisBuffer > PACKETS_PER_BUFFER) { + packetsThisBuffer = PACKETS_PER_BUFFER; + } + size_t bufferSize = packetsThisBuffer * MPEG2_TS_PACKET_SIZE; + XAresult res; + if (discontinuity) { + // signal discontinuity + XAAndroidBufferItem items[1]; + items[0].itemKey = XA_ANDROID_ITEMKEY_DISCONTINUITY; + items[0].itemSize = 0; + // DISCONTINUITY message has no parameters, + // so the total size of the message is the size of the key + // plus the size if itemSize, both XAuint32 + res = (*playerBQItf)->Enqueue(playerBQItf, NULL /*pBufferContext*/, + dataCache + i*BUFFER_SIZE, bufferSize, items /*pMsg*/, + sizeof(XAuint32)*2 /*msgLength*/); + discontinuity = JNI_FALSE; + } else { + res = (*playerBQItf)->Enqueue(playerBQItf, NULL /*pBufferContext*/, + dataCache + i*BUFFER_SIZE, bufferSize, NULL, 0); + } + assert(XA_RESULT_SUCCESS == res); + packetsRead -= packetsThisBuffer; + } + + return JNI_TRUE; +} + + +// create streaming media player +jboolean Java_com_example_nativemedia_NativeMedia_createStreamingMediaPlayer(JNIEnv* env, + jclass clazz, jstring filename) +{ + XAresult res; + + // convert Java string to UTF-8 + const char *utf8 = (*env)->GetStringUTFChars(env, filename, NULL); + assert(NULL != utf8); + + // open the file to play + file = fopen(utf8, "rb"); + if (file == NULL) { + return JNI_FALSE; + } + + // configure data source + 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}; + + // configure audio sink + XADataLocator_OutputMix loc_outmix = { XA_DATALOCATOR_OUTPUTMIX, outputMixObject }; + XADataSink audioSnk = { &loc_outmix, NULL }; + + // configure image video sink + XADataLocator_NativeDisplay loc_nd = { + XA_DATALOCATOR_NATIVEDISPLAY, // locatorType + // the video sink must be an ANativeWindow created from a Surface or SurfaceTexture + (void*)theNativeWindow, // hWindow + // must be NULL + NULL // hDisplay + }; + XADataSink imageVideoSink = {&loc_nd, NULL}; + + // declare interfaces to use + XAboolean required[NB_MAXAL_INTERFACES] + = {XA_BOOLEAN_TRUE, XA_BOOLEAN_TRUE, XA_BOOLEAN_TRUE}; + XAInterfaceID iidArray[NB_MAXAL_INTERFACES] + = {XA_IID_PLAY, XA_IID_ANDROIDBUFFERQUEUESOURCE, + XA_IID_STREAMINFORMATION}; + + // create media player + res = (*engineEngine)->CreateMediaPlayer(engineEngine, &playerObj, &dataSrc, + NULL, &audioSnk, &imageVideoSink, NULL, NULL, + NB_MAXAL_INTERFACES /*XAuint32 numInterfaces*/, + iidArray /*const XAInterfaceID *pInterfaceIds*/, + required /*const XAboolean *pInterfaceRequired*/); + assert(XA_RESULT_SUCCESS == res); + + // release the Java string and UTF-8 + (*env)->ReleaseStringUTFChars(env, filename, utf8); + + // realize the player + res = (*playerObj)->Realize(playerObj, XA_BOOLEAN_FALSE); + assert(XA_RESULT_SUCCESS == res); + + // get the play interface + res = (*playerObj)->GetInterface(playerObj, XA_IID_PLAY, &playerPlayItf); + assert(XA_RESULT_SUCCESS == res); + + // get the stream information interface (for video size) + res = (*playerObj)->GetInterface(playerObj, XA_IID_STREAMINFORMATION, &playerStreamInfoItf); + assert(XA_RESULT_SUCCESS == res); + + // get the volume interface + res = (*playerObj)->GetInterface(playerObj, XA_IID_VOLUME, &playerVolItf); + assert(XA_RESULT_SUCCESS == res); + + // get the Android buffer queue interface + res = (*playerObj)->GetInterface(playerObj, XA_IID_ANDROIDBUFFERQUEUESOURCE, &playerBQItf); + assert(XA_RESULT_SUCCESS == res); + + // specify which events we want to be notified of + res = (*playerBQItf)->SetCallbackEventsMask(playerBQItf, XA_ANDROIDBUFFERQUEUEEVENT_PROCESSED); + assert(XA_RESULT_SUCCESS == res); + + // register the callback from which OpenMAX AL can retrieve the data to play + res = (*playerBQItf)->RegisterCallback(playerBQItf, AndroidBufferQueueCallback, NULL); + assert(XA_RESULT_SUCCESS == res); + + // we want to be notified of the video size once it's found, so we register a callback for that + res = (*playerStreamInfoItf)->RegisterStreamChangeCallback(playerStreamInfoItf, + StreamChangeCallback, NULL); + assert(XA_RESULT_SUCCESS == res); + + // enqueue the initial buffers + if (!enqueueInitialBuffers(JNI_FALSE)) { + return JNI_FALSE; + } + + // prepare the player + res = (*playerPlayItf)->SetPlayState(playerPlayItf, XA_PLAYSTATE_PAUSED); + assert(XA_RESULT_SUCCESS == res); + + // set the volume + res = (*playerVolItf)->SetVolumeLevel(playerVolItf, 0); + assert(XA_RESULT_SUCCESS == res); + + // start the playback + res = (*playerPlayItf)->SetPlayState(playerPlayItf, XA_PLAYSTATE_PLAYING); + assert(XA_RESULT_SUCCESS == res); + + return JNI_TRUE; +} + + +// set the playing state for the streaming media player +void Java_com_example_nativemedia_NativeMedia_setPlayingStreamingMediaPlayer(JNIEnv* env, + jclass clazz, jboolean isPlaying) +{ + XAresult res; + + // make sure the streaming media player was created + if (NULL != playerPlayItf) { + + // set the player's state + res = (*playerPlayItf)->SetPlayState(playerPlayItf, isPlaying ? + XA_PLAYSTATE_PLAYING : XA_PLAYSTATE_PAUSED); + assert(XA_RESULT_SUCCESS == res); + + } + +} + + +// shut down the native media system +void Java_com_example_nativemedia_NativeMedia_shutdown(JNIEnv* env, jclass clazz) +{ + // destroy streaming media player object, and invalidate all associated interfaces + if (playerObj != NULL) { + (*playerObj)->Destroy(playerObj); + playerObj = NULL; + playerPlayItf = NULL; + playerBQItf = NULL; + playerStreamInfoItf = NULL; + playerVolItf = NULL; + } + + // destroy output mix object, and invalidate all associated interfaces + if (outputMixObject != NULL) { + (*outputMixObject)->Destroy(outputMixObject); + outputMixObject = NULL; + } + + // destroy engine object, and invalidate all associated interfaces + if (engineObject != NULL) { + (*engineObject)->Destroy(engineObject); + engineObject = NULL; + engineEngine = NULL; + } + + // close the file + if (file != NULL) { + fclose(file); + file = NULL; + } + + // make sure we don't leak native windows + if (theNativeWindow != NULL) { + ANativeWindow_release(theNativeWindow); + theNativeWindow = NULL; + } +} + + +// set the surface +void Java_com_example_nativemedia_NativeMedia_setSurface(JNIEnv *env, jclass clazz, jobject surface) +{ + // obtain a native window from a Java surface + theNativeWindow = ANativeWindow_fromSurface(env, surface); +} + + +// rewind the streaming media player +void Java_com_example_nativemedia_NativeMedia_rewindStreamingMediaPlayer(JNIEnv *env, jclass clazz) +{ + XAresult res; + + // make sure the streaming media player was created + if (NULL != playerBQItf && NULL != file) { + // first wait for buffers currently in queue to be drained + int ok; + ok = pthread_mutex_lock(&mutex); + assert(0 == ok); + discontinuity = JNI_TRUE; + // wait for discontinuity request to be observed by buffer queue callback + // Note: can't rewind after EOS, which we send when reaching EOF + // (don't send EOS if you plan to play more content through the same player) + while (discontinuity && !reachedEof) { + ok = pthread_cond_wait(&cond, &mutex); + assert(0 == ok); + } + ok = pthread_mutex_unlock(&mutex); + assert(0 == ok); + } + +} diff --git a/ndk/platforms/android-14/samples/native-media/project.properties b/ndk/platforms/android-14/samples/native-media/project.properties new file mode 100644 index 000000000..8f51418b2 --- /dev/null +++ b/ndk/platforms/android-14/samples/native-media/project.properties @@ -0,0 +1,13 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system use, +# "ant.properties", and override values to adapt the script to your +# project structure. + +# Indicates whether an apk should be generated for each density. +split.density=false +# Project target. +target=android-14 diff --git a/ndk/platforms/android-14/samples/native-media/res/drawable/icon.png b/ndk/platforms/android-14/samples/native-media/res/drawable/icon.png new file mode 100644 index 000000000..a07c69fa5 Binary files /dev/null and b/ndk/platforms/android-14/samples/native-media/res/drawable/icon.png differ diff --git a/ndk/platforms/android-14/samples/native-media/res/layout/main.xml b/ndk/platforms/android-14/samples/native-media/res/layout/main.xml new file mode 100644 index 000000000..0e41339ad --- /dev/null +++ b/ndk/platforms/android-14/samples/native-media/res/layout/main.xml @@ -0,0 +1,133 @@ + + + + + + + + + +