| /* |
| * Copyright (C) 2010 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. |
| */ |
| |
| #include <assert.h> |
| #include <jni.h> |
| #include <pthread.h> |
| #include <string.h> |
| //#define LOG_NDEBUG 0 |
| #define LOG_TAG "NativeMedia" |
| #include <utils/Log.h> |
| |
| #include <OMXAL/OpenMAXAL.h> |
| #include <OMXAL/OpenMAXAL_Android.h> |
| |
| #include <android/native_window_jni.h> |
| |
| // 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 16 |
| |
| // we're streaming MPEG-2 transport stream data, operate on transport stream block size |
| #define MPEG2_TS_BLOCK_SIZE 188 |
| |
| // number of MPEG-2 transport stream blocks per buffer, an arbitrary number |
| #define BLOCKS_PER_BUFFER 20 |
| |
| // determines how much memory we're dedicating to memory caching |
| #define BUFFER_SIZE (BLOCKS_PER_BUFFER*MPEG2_TS_BLOCK_SIZE) |
| |
| // where we cache in memory the data to play |
| // note this memory is re-used by the buffer queue callback |
| char dataCache[BUFFER_SIZE * NB_BUFFERS]; |
| |
| // handle of the file to play |
| FILE *file; |
| |
| // has the app reached the end of the file |
| 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) |
| pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; |
| pthread_cond_t cond = PTHREAD_COND_INITIALIZER; |
| |
| // whether a discontinuity is in progress |
| jboolean discontinuity = JNI_FALSE; |
| |
| static jboolean enqueueInitialBuffers(jboolean discontinuity); |
| |
| // Callback for XAPlayItf through which we receive the XA_PLAYEVENT_HEADATEND event */ |
| void PlayCallback(XAPlayItf caller, void *pContext, XAuint32 event) { |
| if (event & XA_PLAYEVENT_HEADATEND) { |
| ALOGV("XA_PLAYEVENT_HEADATEND received, all MP2TS data has been decoded\n"); |
| } |
| } |
| |
| // AndroidBufferQueueItf callback for an audio player |
| 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) { |
| // FIXME sorry, can't rewind after EOS |
| 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) { |
| ALOGV("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(BUFFER_SIZE == dataSize); |
| assert(dataCache <= (char *) pBufferData && (char *) pBufferData < |
| &dataCache[BUFFER_SIZE * NB_BUFFERS]); |
| assert(0 == (((char *) pBufferData - dataCache) % BUFFER_SIZE)); |
| |
| #if 0 |
| // sample code to use the XAVolumeItf |
| XAAndroidBufferQueueState state; |
| (*caller)->GetState(caller, &state); |
| switch (state.index) { |
| case 300: |
| (*playerVolItf)->SetVolumeLevel(playerVolItf, -600); // -6dB |
| ALOGV("setting volume to -6dB"); |
| break; |
| case 400: |
| (*playerVolItf)->SetVolumeLevel(playerVolItf, -1200); // -12dB |
| ALOGV("setting volume to -12dB"); |
| break; |
| case 500: |
| (*playerVolItf)->SetVolumeLevel(playerVolItf, 0); // full volume |
| ALOGV("setting volume to 0dB (full volume)"); |
| break; |
| case 600: |
| (*playerVolItf)->SetMute(playerVolItf, XA_BOOLEAN_TRUE); // mute |
| ALOGV("muting player"); |
| break; |
| case 700: |
| (*playerVolItf)->SetMute(playerVolItf, XA_BOOLEAN_FALSE); // unmute |
| ALOGV("unmuting player"); |
| break; |
| case 800: |
| (*playerVolItf)->SetStereoPosition(playerVolItf, -1000); |
| (*playerVolItf)->EnableStereoPosition(playerVolItf, XA_BOOLEAN_TRUE); |
| ALOGV("pan sound to the left (hard-left)"); |
| break; |
| case 900: |
| (*playerVolItf)->EnableStereoPosition(playerVolItf, XA_BOOLEAN_FALSE); |
| ALOGV("disabling stereo position"); |
| break; |
| default: |
| break; |
| } |
| #endif |
| |
| // 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 |
| nbRead = fread(pBufferData, BUFFER_SIZE, 1, file); |
| if (nbRead > 0) { |
| assert(1 == nbRead); |
| res = (*caller)->Enqueue(caller, NULL /*pBufferContext*/, |
| pBufferData /*pData*/, |
| nbRead * BUFFER_SIZE /*dataLength*/, |
| NULL /*pMsg*/, |
| 0 /*msgLength*/); |
| assert(XA_RESULT_SUCCESS == res); |
| } else { |
| // 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*/, |
| // FIXME == sizeof(BufferItem)? */ |
| 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; |
| } |
| |
| |
| void StreamChangeCallback (XAStreamInformationItf caller, |
| XAuint32 eventId, |
| XAuint32 streamIndex, |
| void * pEventData, |
| void * pContext ) |
| { |
| ALOGV("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); |
| ALOGI("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 */ |
| size_t nbRead; |
| nbRead = fread(dataCache, BUFFER_SIZE, NB_BUFFERS, file); |
| if (nbRead <= 0) { |
| // could be premature EOF or I/O error |
| ALOGE("Error filling cache, exiting\n"); |
| return JNI_FALSE; |
| } |
| assert(1 <= nbRead && nbRead <= NB_BUFFERS); |
| ALOGV("Initially queueing %zu buffers of %u bytes each", nbRead, BUFFER_SIZE); |
| |
| /* 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 < nbRead; i++) { |
| 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, BUFFER_SIZE, items /*pMsg*/, |
| // FIXME == sizeof(BufferItem)? */ |
| sizeof(XAuint32)*2 /*msgLength*/); |
| discontinuity = JNI_FALSE; |
| } else { |
| res = (*playerBQItf)->Enqueue(playerBQItf, NULL /*pBufferContext*/, |
| dataCache + i*BUFFER_SIZE, BUFFER_SIZE, NULL, 0); |
| } |
| assert(XA_RESULT_SUCCESS == res); |
| } |
| |
| 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) { |
| ALOGE("Failed to open %s", utf8); |
| 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 SurfaceTextureClient |
| (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); |
| |
| // use the play interface to set up a callback for the XA_PLAYEVENT_HEADATEND event */ |
| res = (*playerPlayItf)->SetCallbackEventsMask(playerPlayItf, XA_PLAYEVENT_HEADATEND); |
| assert(XA_RESULT_SUCCESS == res); |
| res = (*playerPlayItf)->RegisterCallback(playerPlayItf, |
| PlayCallback /*callback*/, NULL /*pContext*/); |
| 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); |
| |
| // 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);//-300); |
| 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) |
| { |
| // 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 |
| // FIXME sorry, can't rewind after EOS |
| while (discontinuity && !reachedEof) { |
| ok = pthread_cond_wait(&cond, &mutex); |
| assert(0 == ok); |
| } |
| ok = pthread_mutex_unlock(&mutex); |
| assert(0 == ok); |
| } |
| |
| } |