diff options
author | Samuel Rødal <samuel.rodal@digia.com> | 2013-03-13 15:13:17 +0100 |
---|---|---|
committer | Samuel Rødal <samuel.rodal@digia.com> | 2013-03-13 15:13:17 +0100 |
commit | 98fbbf3af12def9ad0fb1daba9728761859a943b (patch) | |
tree | 7ba05226cada0d71017842709c57ceb52472db51 /src/imports/nativemedia/omx.cpp | |
parent | 71b64b2544f81814078f97f0f52bc31c8ce19296 (diff) |
Added nativemedia for video playback.
See the test.qml in src/imports/nativemedia for usage.
Diffstat (limited to 'src/imports/nativemedia/omx.cpp')
-rw-r--r-- | src/imports/nativemedia/omx.cpp | 648 |
1 files changed, 648 insertions, 0 deletions
diff --git a/src/imports/nativemedia/omx.cpp b/src/imports/nativemedia/omx.cpp new file mode 100644 index 0000000..15307df --- /dev/null +++ b/src/imports/nativemedia/omx.cpp @@ -0,0 +1,648 @@ +#include <OMXAL/OpenMAXAL.h> +#include <OMXAL/OpenMAXAL_Android.h> + +#include <SurfaceTexture.h> +#include <gui/SurfaceTextureClient.h> + +#include "omxplayer.h" + +#include <QtGui> + +#include <cassert> + +// number of required interfaces for the MediaPlayer creation +#define NB_MAXAL_INTERFACES 3 // XAAndroidBufferQueueItf, XAStreamInformationItf and XAPlayItf + +// 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) + +using namespace android; + +// 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 + +OmxPlayer::~OmxPlayer() +{ +} + +struct OmxContext : public OmxPlayer { + // engine interfaces + XAObjectItf engineObject; + XAEngineItf engineEngine; + + // output mix interfaces + XAObjectItf outputMixObject; + + // streaming media player interfaces + XAObjectItf playerObj; + XAPlayItf playerPlayItf; + XAAndroidBufferQueueItf playerBQItf; + XAStreamInformationItf playerStreamInfoItf; + XAVolumeItf playerVolItf; + + // 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 + bool reachedEof; + + // For mutual exclusion between callback thread and application thread(s). + // The mutex protects reachedEof, discontinuity, + // The condition is signalled when a discontinuity is acknowledged. + + pthread_mutex_t mutex; + pthread_cond_t cond; + + sp<SurfaceTexture> surfaceTexture; + sp<SurfaceTexture> surfaceTextureClient; + + int currentFrame; + GLuint textureId; + + // for render, useful for debugging + QOpenGLShaderProgram *program; + + int vertexLocation; + int texCoordLocation; + int textureLocation; + + OmxContext() + : engineObject(0) + , engineEngine(0) + , outputMixObject(0) + , playerObj(0) + , playerPlayItf(0) + , playerBQItf(0) + , playerStreamInfoItf(0) + , playerVolItf(0) + , file(0) + , reachedEof(false) + , mutex(PTHREAD_MUTEX_INITIALIZER) + , cond(PTHREAD_COND_INITIALIZER) + , currentFrame(-1) + , textureId(0) + , program(0) + , vertexLocation(0) + , texCoordLocation(0) + , textureLocation(0) + { + } + + ~OmxContext(); + + void updateTexture() + { + if (surfaceTexture.get()) + surfaceTexture->updateTexImage(); + } + + bool hasFrame() + { + return currentFrame >= 0; + } + + void setPaused(bool paused); + + bool initialize(const QByteArray &filename); + bool enqueueInitialBuffers(bool discontinuity); + + XAresult AndroidBufferQueueCallback( + XAAndroidBufferQueueItf caller, + void *pCallbackContext, + void *pBufferContext, + void *pBufferData, + XAuint32 dataSize, + XAuint32 dataUsed, + const XAAndroidBufferItem *pItems, + XAuint32 itemsLength); + + void StreamChangeCallback(XAStreamInformationItf caller, + XAuint32 eventId, + XAuint32 streamIndex, + void * pEventData, + void * pContext ); + + void render(QOpenGLContext *context, QWindow *surface); +}; + +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 */) +{ + return static_cast<OmxContext *>(pCallbackContext)->AndroidBufferQueueCallback( + caller, pCallbackContext, pBufferContext, pBufferData, dataSize, dataUsed, pItems, itemsLength); +} + +// AndroidBufferQueueItf callback to supply MPEG-2 TS packets to the media player +XAresult OmxContext::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; + + // note there is never any contention on this mutex unless a discontinuity request is active + ok = pthread_mutex_lock(&mutex); + assert(0 == ok); + +#if 0 + // 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; + } +#endif + + if ((pBufferData == NULL) && (pBufferContext != NULL)) { + const int processedCommand = *(int *)pBufferContext; + if (kEosBufferCntxt == processedCommand) { + qDebug("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) { + qDebug("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 = 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 ) +{ + static_cast<OmxContext *>(pContext)->StreamChangeCallback( + caller, eventId, streamIndex, pEventData, pContext); +} + +// callback invoked whenever there is new or changed stream information +void OmxContext::StreamChangeCallback(XAStreamInformationItf caller, + XAuint32 eventId, + XAuint32 streamIndex, + void * pEventData, + void * pContext ) +{ + qDebug("StreamChangeCallback called for stream %u", streamIndex); + + 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); + qDebug("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); + emit videoSize(videoInfo.width, videoInfo.height); + } break; + default: + fprintf(stderr, "Unexpected domain %u\n", domain); + break; + } + } break; + default: + fprintf(stderr, "Unexpected stream event ID %u\n", eventId); + break; + } +} + +// Enqueue the initial buffers, and optionally signal a discontinuity in the first buffer +bool OmxContext::enqueueInitialBuffers(bool 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 false; + } + if ((bytesRead % MPEG2_TS_PACKET_SIZE) != 0) { + qDebug("Dropping last packet because it is not whole"); + } + size_t packetsRead = bytesRead / MPEG2_TS_PACKET_SIZE; + qDebug("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 = false; + } else { + res = (*playerBQItf)->Enqueue(playerBQItf, NULL /*pBufferContext*/, + dataCache + i*BUFFER_SIZE, bufferSize, NULL, 0); + } + assert(XA_RESULT_SUCCESS == res); + packetsRead -= packetsThisBuffer; + } + + return true; +} + +class FrameCallback : public SurfaceTexture::FrameAvailableListener +{ +public: + FrameCallback(OmxContext *ctx) + : context(ctx) + { + } + + void onFrameAvailable() + { + emit context->frameAvailable(); + ++context->currentFrame; + } + +private: + OmxContext *context; +}; + +OmxPlayer *OmxPlayer::create() +{ + return new OmxContext; +} + +bool OmxContext::initialize(const QByteArray &filename) +{ + file = fopen(filename.data(), "rb"); + if (!file) { + qWarning("Failed to open %s", filename.data()); + return false; + } + + XAresult res; + + qDebug() << "Creating engine object"; + + // create engine + res = xaCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL); + assert(XA_RESULT_SUCCESS == res); + + qDebug() << "Creating engine object succeeded?" << (res == XA_RESULT_SUCCESS); + + // realize the engine + res = (*engineObject)->Realize(engineObject, XA_BOOLEAN_FALSE); + assert(XA_RESULT_SUCCESS == res); + + qDebug() << "Realizing engine object succeeded?" << (res == XA_RESULT_SUCCESS); + + // 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); + + qDebug() << "Getting engine interface succeeded?" << (res == XA_RESULT_SUCCESS); + + // create output mix + res = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 0, NULL, NULL); + assert(XA_RESULT_SUCCESS == res); + + qDebug() << "Creating output mix succeeded?" << (res == XA_RESULT_SUCCESS); + + // realize the output mix + res = (*outputMixObject)->Realize(outputMixObject, XA_BOOLEAN_FALSE); + assert(XA_RESULT_SUCCESS == res); + + qDebug() << "Realizing output mix succeeded?" << (res == XA_RESULT_SUCCESS); + + // 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, 0 }; + + qDebug() << "Creating SurfaceTexture"; + + glGenTextures(1, &textureId); + surfaceTexture = new android::SurfaceTexture(textureId); + + qDebug() << "Creating SurfaceTextureClient"; + + android::sp<android::SurfaceTextureClient> surfaceTextureClient = new android::SurfaceTextureClient(surfaceTexture); + + sp<SurfaceTexture::FrameAvailableListener> listener = new FrameCallback(this); + + surfaceTexture->setFrameAvailableListener(listener); + + // 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 + static_cast<ANativeWindow *>(surfaceTextureClient.get()), // hWindow + // must be 0 + 0 // hDisplay + }; + XADataSink imageVideoSink = {&loc_nd, 0}; + + // 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}; + + + qDebug() << "Creating media player for engine" << engineEngine; + + // create media player + res = (*engineEngine)->CreateMediaPlayer(engineEngine, &playerObj, &dataSrc, + 0, &audioSnk, &imageVideoSink, 0, 0, + NB_MAXAL_INTERFACES /*XAuint32 numInterfaces*/, + iidArray /*const XAInterfaceID *pInterfaceIds*/, + required /*const XAboolean *pInterfaceRequired*/); + + qDebug() << "CreateMediaPlayer succeeded?" << (res == XA_RESULT_SUCCESS); + + // realize the player + res = (*playerObj)->Realize(playerObj, XA_BOOLEAN_FALSE); + + qDebug() << "Realize media player succeeded?" << (res == XA_RESULT_SUCCESS); + + // get the play interface + res = (*playerObj)->GetInterface(playerObj, XA_IID_PLAY, &playerPlayItf); + + qDebug() << "Get play interface succeeded?" << (res == XA_RESULT_SUCCESS); + + // get the stream information interface (for video size) + res = (*playerObj)->GetInterface(playerObj, XA_IID_STREAMINFORMATION, &playerStreamInfoItf); + + qDebug() << "Get stream information interface succeeded?" << (res == XA_RESULT_SUCCESS); + + // get the volume interface + res = (*playerObj)->GetInterface(playerObj, XA_IID_VOLUME, &playerVolItf); + + qDebug() << "Get volume interface succeeded?" << (res == XA_RESULT_SUCCESS); + + // get the Android buffer queue interface + res = (*playerObj)->GetInterface(playerObj, XA_IID_ANDROIDBUFFERQUEUESOURCE, &playerBQItf); + + qDebug() << "Get buffer queue interface succeeded?" << (res == XA_RESULT_SUCCESS); + + // specify which events we want to be notified of + res = (*playerBQItf)->SetCallbackEventsMask(playerBQItf, XA_ANDROIDBUFFERQUEUEEVENT_PROCESSED); + + qDebug() << "Setting callback events mask succeeded?" << (res == XA_RESULT_SUCCESS); + + // register the callback from which OpenMAX AL can retrieve the data to play + res = (*playerBQItf)->RegisterCallback(playerBQItf, ::AndroidBufferQueueCallback, this); + + qDebug() << "Registering buffer queue callback succeeded?" << (res == XA_RESULT_SUCCESS); + + // 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, this); + + qDebug() << "Registering stream change callback succeeded?" << (res == XA_RESULT_SUCCESS); + + // enqueue the initial buffers + if (!enqueueInitialBuffers(false)) + return false; + + qDebug() << "Enqueued initial buffers"; + + // prepare the player + res = (*playerPlayItf)->SetPlayState(playerPlayItf, XA_PLAYSTATE_PAUSED); + + qDebug() << "Preparing player (setting play state paused) succeeded?" << (res == XA_RESULT_SUCCESS); + + // set the volume + res = (*playerVolItf)->SetVolumeLevel(playerVolItf, 0); + + qDebug() << "Setting volume level succeeded?" << (res == XA_RESULT_SUCCESS); + + return true; +} + +OmxContext::~OmxContext() +{ + if (!file) + return; + + // 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; + } +} + +void OmxContext::setPaused(bool paused) +{ + XAresult res; + + // make sure the streaming media player was created + if (NULL != playerPlayItf) { + // set the player's state + res = (*playerPlayItf)->SetPlayState(playerPlayItf, paused ? + XA_PLAYSTATE_PAUSED : XA_PLAYSTATE_PLAYING); + } +} + +void OmxContext::render(QOpenGLContext *context, QWindow *surface) +{ + context->makeCurrent(surface); + + glClearColor(0, 0, 0, 1); + glClear(GL_COLOR_BUFFER_BIT); + + if (currentFrame >= 0) { + surfaceTexture->updateTexImage(); + + if (!program) { + qDebug() << "Received first video frame, starting to render"; + + program = new QOpenGLShaderProgram; + + program->addShaderFromSourceCode(QOpenGLShader::Vertex, + "attribute highp vec4 vertex;\n" + "attribute highp vec2 texCoord;\n" + "varying highp vec2 coord;\n" + "void main(void)\n" + "{\n" + " coord = texCoord;\n" + " gl_Position = vertex;\n" + "}"); + program->addShaderFromSourceCode(QOpenGLShader::Fragment, + "#extension GL_OES_EGL_image_external : require\n" + "uniform samplerExternalOES tex;\n" + "varying highp vec2 coord;\n" + "void main(void)\n" + "{\n" + " gl_FragColor = texture2D(tex, coord);\n" + "}"); + + program->link(); + + vertexLocation = program->attributeLocation("vertex"); + texCoordLocation = program->attributeLocation("texCoord"); + textureLocation = program->uniformLocation("tex"); + + qDebug() << "attribute locations" << vertexLocation << texCoordLocation; + qDebug() << "uniform location" << textureLocation; + } + + program->bind(); + + static GLfloat const triangleVertices[] = { + -1, -3, + 3, 1, + -1, 1 + }; + + static GLfloat const triangleTexCoords[] = { + 0, 2, + 2, 0, + 0, 0 + }; + + program->enableAttributeArray(vertexLocation); + program->setAttributeArray(vertexLocation, triangleVertices, 2); + program->enableAttributeArray(texCoordLocation); + program->setAttributeArray(texCoordLocation, triangleTexCoords, 2); + + glDrawArrays(GL_TRIANGLES, 0, 3); + + program->disableAttributeArray(vertexLocation); + program->disableAttributeArray(texCoordLocation); + } + + context->swapBuffers(surface); +} |