You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
543 lines
19 KiB
543 lines
19 KiB
/*
|
|
* 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);
|
|
}
|
|
|
|
}
|