| /* |
| * Copyright (C) 2012 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. |
| */ |
| |
| //#define LOG_NDEBUG 0 |
| #define LOG_TAG "TimedTextPlayer" |
| #include <utils/Log.h> |
| |
| #include <inttypes.h> |
| #include <limits.h> |
| #include <media/stagefright/foundation/ADebug.h> |
| #include <media/stagefright/foundation/AMessage.h> |
| #include <media/stagefright/timedtext/TimedTextDriver.h> |
| #include <media/stagefright/MediaErrors.h> |
| #include <media/MediaPlayerInterface.h> |
| |
| #include "TimedTextPlayer.h" |
| |
| #include "TimedTextSource.h" |
| |
| namespace android { |
| |
| // Event should be fired a bit earlier considering the processing time till |
| // application actually gets the notification message. |
| static const int64_t kAdjustmentProcessingTimeUs = 100000ll; |
| static const int64_t kMaxDelayUs = 5000000ll; |
| static const int64_t kWaitTimeUsToRetryRead = 100000ll; |
| static const int64_t kInvalidTimeUs = INT_MIN; |
| |
| TimedTextPlayer::TimedTextPlayer(const wp<MediaPlayerBase> &listener) |
| : mListener(listener), |
| mSource(NULL), |
| mPendingSeekTimeUs(kInvalidTimeUs), |
| mPaused(false), |
| mSendSubtitleGeneration(0) { |
| } |
| |
| TimedTextPlayer::~TimedTextPlayer() { |
| if (mSource != NULL) { |
| mSource->stop(); |
| mSource.clear(); |
| mSource = NULL; |
| } |
| } |
| |
| void TimedTextPlayer::start() { |
| (new AMessage(kWhatStart, this))->post(); |
| } |
| |
| void TimedTextPlayer::pause() { |
| (new AMessage(kWhatPause, this))->post(); |
| } |
| |
| void TimedTextPlayer::resume() { |
| (new AMessage(kWhatResume, this))->post(); |
| } |
| |
| void TimedTextPlayer::seekToAsync(int64_t timeUs) { |
| sp<AMessage> msg = new AMessage(kWhatSeek, this); |
| msg->setInt64("seekTimeUs", timeUs); |
| msg->post(); |
| } |
| |
| void TimedTextPlayer::setDataSource(sp<TimedTextSource> source) { |
| sp<AMessage> msg = new AMessage(kWhatSetSource, this); |
| msg->setObject("source", source); |
| msg->post(); |
| } |
| |
| void TimedTextPlayer::onMessageReceived(const sp<AMessage> &msg) { |
| switch (msg->what()) { |
| case kWhatPause: { |
| mPaused = true; |
| break; |
| } |
| case kWhatResume: { |
| mPaused = false; |
| if (mPendingSeekTimeUs != kInvalidTimeUs) { |
| seekToAsync(mPendingSeekTimeUs); |
| mPendingSeekTimeUs = kInvalidTimeUs; |
| } else { |
| doRead(); |
| } |
| break; |
| } |
| case kWhatStart: { |
| sp<MediaPlayerBase> listener = mListener.promote(); |
| if (listener == NULL) { |
| ALOGE("Listener is NULL when kWhatStart is received."); |
| break; |
| } |
| mPaused = false; |
| mPendingSeekTimeUs = kInvalidTimeUs; |
| int32_t positionMs = 0; |
| listener->getCurrentPosition(&positionMs); |
| int64_t seekTimeUs = positionMs * 1000ll; |
| |
| notifyListener(); |
| mSendSubtitleGeneration++; |
| doSeekAndRead(seekTimeUs); |
| break; |
| } |
| case kWhatRetryRead: { |
| int32_t generation = -1; |
| CHECK(msg->findInt32("generation", &generation)); |
| if (generation != mSendSubtitleGeneration) { |
| // Drop obsolete msg. |
| break; |
| } |
| int64_t seekTimeUs; |
| int seekMode; |
| if (msg->findInt64("seekTimeUs", &seekTimeUs) && |
| msg->findInt32("seekMode", &seekMode)) { |
| MediaSource::ReadOptions options; |
| options.setSeekTo( |
| seekTimeUs, |
| static_cast<MediaSource::ReadOptions::SeekMode>(seekMode)); |
| doRead(&options); |
| } else { |
| doRead(); |
| } |
| break; |
| } |
| case kWhatSeek: { |
| int64_t seekTimeUs = kInvalidTimeUs; |
| // Clear a displayed timed text before seeking. |
| notifyListener(); |
| msg->findInt64("seekTimeUs", &seekTimeUs); |
| if (seekTimeUs == kInvalidTimeUs) { |
| sp<MediaPlayerBase> listener = mListener.promote(); |
| if (listener != NULL) { |
| int32_t positionMs = 0; |
| listener->getCurrentPosition(&positionMs); |
| seekTimeUs = positionMs * 1000ll; |
| } |
| } |
| if (mPaused) { |
| mPendingSeekTimeUs = seekTimeUs; |
| break; |
| } |
| mSendSubtitleGeneration++; |
| doSeekAndRead(seekTimeUs); |
| break; |
| } |
| case kWhatSendSubtitle: { |
| int32_t generation; |
| CHECK(msg->findInt32("generation", &generation)); |
| if (generation != mSendSubtitleGeneration) { |
| // Drop obsolete msg. |
| break; |
| } |
| // If current time doesn't reach to the fire time, |
| // re-post the message with the adjusted delay time. |
| int64_t fireTimeUs = kInvalidTimeUs; |
| if (msg->findInt64("fireTimeUs", &fireTimeUs)) { |
| // TODO: check if fireTimeUs is not kInvalidTimeUs. |
| int64_t delayUs = delayUsFromCurrentTime(fireTimeUs); |
| if (delayUs > 0) { |
| msg->post(delayUs); |
| break; |
| } |
| } |
| sp<RefBase> obj; |
| if (msg->findObject("subtitle", &obj)) { |
| sp<ParcelEvent> parcelEvent; |
| parcelEvent = static_cast<ParcelEvent*>(obj.get()); |
| notifyListener(&(parcelEvent->parcel)); |
| doRead(); |
| } else { |
| notifyListener(); |
| } |
| break; |
| } |
| case kWhatSetSource: { |
| mSendSubtitleGeneration++; |
| sp<RefBase> obj; |
| msg->findObject("source", &obj); |
| if (mSource != NULL) { |
| mSource->stop(); |
| mSource.clear(); |
| mSource = NULL; |
| } |
| // null source means deselect track. |
| if (obj == NULL) { |
| mPendingSeekTimeUs = kInvalidTimeUs; |
| mPaused = false; |
| notifyListener(); |
| break; |
| } |
| mSource = static_cast<TimedTextSource*>(obj.get()); |
| status_t err = mSource->start(); |
| if (err != OK) { |
| notifyError(err); |
| break; |
| } |
| Parcel parcel; |
| err = mSource->extractGlobalDescriptions(&parcel); |
| if (err != OK) { |
| notifyError(err); |
| break; |
| } |
| notifyListener(&parcel); |
| break; |
| } |
| } |
| } |
| |
| void TimedTextPlayer::doSeekAndRead(int64_t seekTimeUs) { |
| MediaSource::ReadOptions options; |
| options.setSeekTo(seekTimeUs, MediaSource::ReadOptions::SEEK_PREVIOUS_SYNC); |
| doRead(&options); |
| } |
| |
| void TimedTextPlayer::doRead(MediaSource::ReadOptions* options) { |
| int64_t startTimeUs = 0; |
| int64_t endTimeUs = 0; |
| sp<ParcelEvent> parcelEvent = new ParcelEvent(); |
| CHECK(mSource != NULL); |
| status_t err = mSource->read(&startTimeUs, &endTimeUs, |
| &(parcelEvent->parcel), options); |
| if (err == WOULD_BLOCK) { |
| sp<AMessage> msg = new AMessage(kWhatRetryRead, this); |
| if (options != NULL) { |
| int64_t seekTimeUs = kInvalidTimeUs; |
| MediaSource::ReadOptions::SeekMode seekMode = |
| MediaSource::ReadOptions::SEEK_PREVIOUS_SYNC; |
| CHECK(options->getSeekTo(&seekTimeUs, &seekMode)); |
| msg->setInt64("seekTimeUs", seekTimeUs); |
| msg->setInt32("seekMode", seekMode); |
| } |
| msg->setInt32("generation", mSendSubtitleGeneration); |
| msg->post(kWaitTimeUsToRetryRead); |
| return; |
| } else if (err != OK) { |
| notifyError(err); |
| return; |
| } |
| |
| postTextEvent(parcelEvent, startTimeUs); |
| if (endTimeUs > 0) { |
| CHECK_GE(endTimeUs, startTimeUs); |
| // send an empty timed text to clear the subtitle when it reaches to the |
| // end time. |
| postTextEvent(NULL, endTimeUs); |
| } |
| } |
| |
| void TimedTextPlayer::postTextEvent(const sp<ParcelEvent>& parcel, int64_t timeUs) { |
| int64_t delayUs = delayUsFromCurrentTime(timeUs); |
| sp<AMessage> msg = new AMessage(kWhatSendSubtitle, this); |
| msg->setInt32("generation", mSendSubtitleGeneration); |
| if (parcel != NULL) { |
| msg->setObject("subtitle", parcel); |
| } |
| msg->setInt64("fireTimeUs", timeUs); |
| msg->post(delayUs); |
| } |
| |
| int64_t TimedTextPlayer::delayUsFromCurrentTime(int64_t fireTimeUs) { |
| sp<MediaPlayerBase> listener = mListener.promote(); |
| if (listener == NULL) { |
| // TODO: it may be better to return kInvalidTimeUs |
| ALOGE("%s: Listener is NULL. (fireTimeUs = %" PRId64" )", |
| __FUNCTION__, fireTimeUs); |
| return 0; |
| } |
| int32_t positionMs = 0; |
| listener->getCurrentPosition(&positionMs); |
| int64_t positionUs = positionMs * 1000ll; |
| |
| if (fireTimeUs <= positionUs + kAdjustmentProcessingTimeUs) { |
| return 0; |
| } else { |
| int64_t delayUs = fireTimeUs - positionUs - kAdjustmentProcessingTimeUs; |
| if (delayUs > kMaxDelayUs) { |
| return kMaxDelayUs; |
| } |
| return delayUs; |
| } |
| } |
| |
| void TimedTextPlayer::notifyError(int error) { |
| sp<MediaPlayerBase> listener = mListener.promote(); |
| if (listener == NULL) { |
| ALOGE("%s(error=%d): Listener is NULL.", __FUNCTION__, error); |
| return; |
| } |
| listener->sendEvent(MEDIA_INFO, MEDIA_INFO_TIMED_TEXT_ERROR, error); |
| } |
| |
| void TimedTextPlayer::notifyListener(const Parcel *parcel) { |
| sp<MediaPlayerBase> listener = mListener.promote(); |
| if (listener == NULL) { |
| ALOGE("%s: Listener is NULL.", __FUNCTION__); |
| return; |
| } |
| if (parcel != NULL && (parcel->dataSize() > 0)) { |
| listener->sendEvent(MEDIA_TIMED_TEXT, 0, 0, parcel); |
| } else { // send an empty timed text to clear the screen |
| listener->sendEvent(MEDIA_TIMED_TEXT); |
| } |
| } |
| |
| } // namespace android |