blob: 60b780fb4e5312d942a830d87c3386e2f2f74afa [file] [log] [blame]
/*
* Copyright (C) 2020 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 "TranscoderWrapper"
#include <aidl/android/media/TranscodingErrorCode.h>
#include <aidl/android/media/TranscodingRequestParcel.h>
#include <media/MediaTranscoder.h>
#include <media/NdkCommon.h>
#include <media/TranscoderWrapper.h>
#include <media/TranscodingRequest.h>
#include <utils/AndroidThreads.h>
#include <utils/Log.h>
#include <thread>
namespace android {
using Status = ::ndk::ScopedAStatus;
using ::aidl::android::media::TranscodingErrorCode;
using ::aidl::android::media::TranscodingVideoCodecType;
using ::aidl::android::media::TranscodingVideoTrackFormat;
static TranscodingErrorCode toTranscodingError(media_status_t status) {
switch (status) {
case AMEDIA_OK:
return TranscodingErrorCode::kNoError;
case AMEDIACODEC_ERROR_INSUFFICIENT_RESOURCE: // FALLTHRU
case AMEDIACODEC_ERROR_RECLAIMED:
return TranscodingErrorCode::kInsufficientResources;
case AMEDIA_ERROR_MALFORMED:
return TranscodingErrorCode::kMalformed;
case AMEDIA_ERROR_UNSUPPORTED:
return TranscodingErrorCode::kUnsupported;
case AMEDIA_ERROR_INVALID_OBJECT: // FALLTHRU
case AMEDIA_ERROR_INVALID_PARAMETER:
return TranscodingErrorCode::kInvalidParameter;
case AMEDIA_ERROR_INVALID_OPERATION:
return TranscodingErrorCode::kInvalidOperation;
case AMEDIA_ERROR_IO:
return TranscodingErrorCode::kErrorIO;
case AMEDIA_ERROR_UNKNOWN: // FALLTHRU
default:
return TranscodingErrorCode::kUnknown;
}
}
static std::shared_ptr<AMediaFormat> getVideoFormat(
const char* originalMime,
const std::optional<TranscodingVideoTrackFormat>& requestedFormat) {
if (requestedFormat == std::nullopt) {
return nullptr;
}
std::shared_ptr<AMediaFormat> format =
std::shared_ptr<AMediaFormat>(AMediaFormat_new(), &AMediaFormat_delete);
bool changed = false;
if (requestedFormat->codecType == TranscodingVideoCodecType::kHevc &&
strcmp(originalMime, AMEDIA_MIMETYPE_VIDEO_HEVC)) {
AMediaFormat_setString(format.get(), AMEDIAFORMAT_KEY_MIME, AMEDIA_MIMETYPE_VIDEO_HEVC);
changed = true;
} else if (requestedFormat->codecType == TranscodingVideoCodecType::kAvc &&
strcmp(originalMime, AMEDIA_MIMETYPE_VIDEO_AVC)) {
AMediaFormat_setString(format.get(), AMEDIAFORMAT_KEY_MIME, AMEDIA_MIMETYPE_VIDEO_AVC);
changed = true;
}
if (requestedFormat->bitrateBps > 0) {
AMediaFormat_setInt32(format.get(), AMEDIAFORMAT_KEY_BIT_RATE, requestedFormat->bitrateBps);
changed = true;
}
// TODO: translate other fields from requestedFormat to the format for MediaTranscoder.
// Also need to determine more settings to expose in TranscodingVideoTrackFormat.
if (!changed) {
// Use null format for passthru.
format.reset();
}
return format;
}
//static
std::string TranscoderWrapper::toString(const Event& event) {
std::string typeStr;
switch (event.type) {
case Event::Start:
typeStr = "Start";
break;
case Event::Pause:
typeStr = "Pause";
break;
case Event::Resume:
typeStr = "Resume";
break;
case Event::Stop:
typeStr = "Stop";
break;
case Event::Finish:
typeStr = "Finish";
break;
case Event::Error:
typeStr = "Error";
break;
case Event::Progress:
typeStr = "Progress";
break;
case Event::HeartBeat:
typeStr = "HeartBeat";
break;
case Event::Abandon:
typeStr = "Abandon";
break;
default:
return "(unknown)";
}
std::string result;
result = "session {" + std::to_string(event.clientId) + "," + std::to_string(event.sessionId) +
"}: " + typeStr;
if (event.type == Event::Error || event.type == Event::Progress) {
result += " " + std::to_string(event.arg);
}
return result;
}
class TranscoderWrapper::CallbackImpl : public MediaTranscoder::CallbackInterface {
public:
CallbackImpl(const std::shared_ptr<TranscoderWrapper>& owner, ClientIdType clientId,
SessionIdType sessionId)
: mOwner(owner), mClientId(clientId), mSessionId(sessionId) {}
virtual void onFinished(const MediaTranscoder* transcoder __unused) override {
auto owner = mOwner.lock();
if (owner != nullptr) {
owner->onFinish(mClientId, mSessionId);
}
}
virtual void onError(const MediaTranscoder* transcoder __unused,
media_status_t error) override {
auto owner = mOwner.lock();
if (owner != nullptr) {
owner->onError(mClientId, mSessionId, error);
}
}
virtual void onProgressUpdate(const MediaTranscoder* transcoder __unused,
int32_t progress) override {
auto owner = mOwner.lock();
if (owner != nullptr) {
owner->onProgress(mClientId, mSessionId, progress);
}
}
virtual void onHeartBeat(const MediaTranscoder* transcoder __unused) override {
auto owner = mOwner.lock();
if (owner != nullptr) {
owner->onHeartBeat(mClientId, mSessionId);
}
}
virtual void onCodecResourceLost(const MediaTranscoder* transcoder __unused,
const std::shared_ptr<ndk::ScopedAParcel>& pausedState
__unused) override {
ALOGV("%s: session {%lld, %d}", __FUNCTION__, (long long)mClientId, mSessionId);
}
private:
std::weak_ptr<TranscoderWrapper> mOwner;
ClientIdType mClientId;
SessionIdType mSessionId;
};
TranscoderWrapper::TranscoderWrapper(const std::shared_ptr<TranscoderCallbackInterface>& cb,
const std::shared_ptr<TranscodingLogger>& logger,
int64_t heartBeatIntervalUs)
: mCallback(cb),
mLogger(logger),
mHeartBeatIntervalUs(heartBeatIntervalUs),
mCurrentClientId(0),
mCurrentSessionId(-1),
mLooperReady(false) {
ALOGV("TranscoderWrapper CTOR: %p", this);
}
TranscoderWrapper::~TranscoderWrapper() {
ALOGV("TranscoderWrapper DTOR: %p", this);
}
static bool isResourceError(media_status_t err) {
return err == AMEDIACODEC_ERROR_RECLAIMED || err == AMEDIACODEC_ERROR_INSUFFICIENT_RESOURCE;
}
void TranscoderWrapper::reportError(ClientIdType clientId, SessionIdType sessionId,
media_status_t err) {
auto callback = mCallback.lock();
if (callback != nullptr) {
if (isResourceError(err)) {
// Add a placeholder pause state to mPausedStateMap. This is required when resuming.
// TODO: remove this when transcoder pause/resume logic is ready. New logic will
// no longer use the pause states.
auto it = mPausedStateMap.find(SessionKeyType(clientId, sessionId));
if (it == mPausedStateMap.end()) {
mPausedStateMap.emplace(SessionKeyType(clientId, sessionId),
new ndk::ScopedAParcel());
}
callback->onResourceLost(clientId, sessionId);
} else {
callback->onError(clientId, sessionId, toTranscodingError(err));
}
}
}
void TranscoderWrapper::start(ClientIdType clientId, SessionIdType sessionId,
const TranscodingRequestParcel& requestParcel, uid_t callingUid,
const std::shared_ptr<ITranscodingClientCallback>& clientCb) {
TranscodingRequest request{requestParcel};
queueEvent(Event::Start, clientId, sessionId, [=] {
media_status_t err = handleStart(clientId, sessionId, request, callingUid, clientCb);
if (err != AMEDIA_OK) {
cleanup();
reportError(clientId, sessionId, err);
} else {
auto callback = mCallback.lock();
if (callback != nullptr) {
callback->onStarted(clientId, sessionId);
}
}
});
}
void TranscoderWrapper::pause(ClientIdType clientId, SessionIdType sessionId) {
queueEvent(Event::Pause, clientId, sessionId, [=] {
media_status_t err = handlePause(clientId, sessionId);
cleanup();
if (err != AMEDIA_OK) {
reportError(clientId, sessionId, err);
} else {
auto callback = mCallback.lock();
if (callback != nullptr) {
callback->onPaused(clientId, sessionId);
}
}
});
}
void TranscoderWrapper::resume(ClientIdType clientId, SessionIdType sessionId,
const TranscodingRequestParcel& requestParcel, uid_t callingUid,
const std::shared_ptr<ITranscodingClientCallback>& clientCb) {
TranscodingRequest request{requestParcel};
queueEvent(Event::Resume, clientId, sessionId, [=] {
media_status_t err = handleResume(clientId, sessionId, request, callingUid, clientCb);
if (err != AMEDIA_OK) {
cleanup();
reportError(clientId, sessionId, err);
} else {
auto callback = mCallback.lock();
if (callback != nullptr) {
callback->onResumed(clientId, sessionId);
}
}
});
}
void TranscoderWrapper::stop(ClientIdType clientId, SessionIdType sessionId, bool abandon) {
queueEvent(Event::Stop, clientId, sessionId, [=] {
if (mTranscoder != nullptr && clientId == mCurrentClientId &&
sessionId == mCurrentSessionId) {
// Cancelling the currently running session.
media_status_t err = mTranscoder->cancel();
if (err != AMEDIA_OK) {
ALOGW("failed to stop transcoder: %d", err);
} else {
ALOGI("transcoder stopped");
}
logSessionEnded(TranscodingLogger::SessionEndedReason::CANCELLED, err);
cleanup();
} else {
// For sessions that's not currently running, release any pausedState for the session.
mPausedStateMap.erase(SessionKeyType(clientId, sessionId));
}
// No callback needed for stop.
});
if (abandon) {
queueEvent(Event::Abandon, 0, 0, nullptr);
}
}
void TranscoderWrapper::onFinish(ClientIdType clientId, SessionIdType sessionId) {
queueEvent(Event::Finish, clientId, sessionId, [=] {
if (mTranscoder != nullptr && clientId == mCurrentClientId &&
sessionId == mCurrentSessionId) {
logSessionEnded(TranscodingLogger::SessionEndedReason::FINISHED, AMEDIA_OK);
cleanup();
}
auto callback = mCallback.lock();
if (callback != nullptr) {
callback->onFinish(clientId, sessionId);
}
});
}
void TranscoderWrapper::onError(ClientIdType clientId, SessionIdType sessionId,
media_status_t error) {
queueEvent(
Event::Error, clientId, sessionId,
[=] {
if (mTranscoder != nullptr && clientId == mCurrentClientId &&
sessionId == mCurrentSessionId) {
logSessionEnded(TranscodingLogger::SessionEndedReason::ERROR, error);
cleanup();
}
reportError(clientId, sessionId, error);
},
error);
}
void TranscoderWrapper::onProgress(ClientIdType clientId, SessionIdType sessionId,
int32_t progress) {
queueEvent(
Event::Progress, clientId, sessionId,
[=] {
auto callback = mCallback.lock();
if (callback != nullptr) {
callback->onProgressUpdate(clientId, sessionId, progress);
}
},
progress);
}
void TranscoderWrapper::onHeartBeat(ClientIdType clientId, SessionIdType sessionId) {
queueEvent(Event::HeartBeat, clientId, sessionId, [=] {
auto callback = mCallback.lock();
if (callback != nullptr) {
callback->onHeartBeat(clientId, sessionId);
}
});
}
media_status_t TranscoderWrapper::setupTranscoder(
ClientIdType clientId, SessionIdType sessionId, const TranscodingRequestParcel& request,
uid_t callingUid, const std::shared_ptr<ITranscodingClientCallback>& clientCb,
TranscodingLogger::SessionEndedReason* failureReason,
const std::shared_ptr<ndk::ScopedAParcel>& pausedState) {
if (clientCb == nullptr) {
ALOGE("client callback is null");
return AMEDIA_ERROR_INVALID_PARAMETER;
}
if (mTranscoder != nullptr) {
ALOGE("transcoder already running");
return AMEDIA_ERROR_INVALID_OPERATION;
}
// Unwrap the callback and send heartbeats to the client after each operation during setup.
auto callback = mCallback.lock();
if (callback == nullptr) {
return AMEDIA_ERROR_INVALID_OPERATION;
}
Status status;
::ndk::ScopedFileDescriptor srcFd, dstFd;
int srcFdInt = request.sourceFd.get();
if (srcFdInt < 0) {
status = clientCb->openFileDescriptor(request.sourceFilePath, "r", &srcFd);
if (!status.isOk() || srcFd.get() < 0) {
ALOGE("failed to open source");
*failureReason = TranscodingLogger::SessionEndedReason::OPEN_SRC_FD_FAILED;
return AMEDIA_ERROR_IO;
}
srcFdInt = srcFd.get();
}
callback->onHeartBeat(clientId, sessionId);
int dstFdInt = request.destinationFd.get();
if (dstFdInt < 0) {
// Open dest file with "rw", as the transcoder could potentially reuse part of it
// for resume case. We might want the further differentiate and open with "w" only
// for start.
status = clientCb->openFileDescriptor(request.destinationFilePath, "rw", &dstFd);
if (!status.isOk() || dstFd.get() < 0) {
ALOGE("failed to open destination");
*failureReason = TranscodingLogger::SessionEndedReason::OPEN_DST_FD_FAILED;
return AMEDIA_ERROR_IO;
}
dstFdInt = dstFd.get();
}
callback->onHeartBeat(clientId, sessionId);
mCurrentClientId = clientId;
mCurrentSessionId = sessionId;
mCurrentCallingUid = callingUid;
mTranscoderCb = std::make_shared<CallbackImpl>(shared_from_this(), clientId, sessionId);
mTranscoder = MediaTranscoder::create(mTranscoderCb, mHeartBeatIntervalUs, request.clientPid,
request.clientUid, pausedState);
if (mTranscoder == nullptr) {
ALOGE("failed to create transcoder");
*failureReason = TranscodingLogger::SessionEndedReason::CREATE_FAILED;
return AMEDIA_ERROR_UNKNOWN;
}
callback->onHeartBeat(clientId, sessionId);
media_status_t err = mTranscoder->configureSource(srcFdInt);
if (err != AMEDIA_OK) {
ALOGE("failed to configure source: %d", err);
*failureReason = TranscodingLogger::SessionEndedReason::CONFIG_SRC_FAILED;
return err;
}
callback->onHeartBeat(clientId, sessionId);
std::vector<std::shared_ptr<AMediaFormat>> trackFormats = mTranscoder->getTrackFormats();
if (trackFormats.size() == 0) {
ALOGE("failed to get track formats!");
*failureReason = TranscodingLogger::SessionEndedReason::NO_TRACKS;
return AMEDIA_ERROR_MALFORMED;
}
callback->onHeartBeat(clientId, sessionId);
for (int i = 0; i < trackFormats.size(); ++i) {
std::shared_ptr<AMediaFormat> format;
const char* mime = nullptr;
AMediaFormat_getString(trackFormats[i].get(), AMEDIAFORMAT_KEY_MIME, &mime);
if (!strncmp(mime, "video/", 6)) {
format = getVideoFormat(mime, request.requestedVideoTrackFormat);
mSrcFormat = trackFormats[i];
mDstFormat = format;
}
err = mTranscoder->configureTrackFormat(i, format.get());
if (err != AMEDIA_OK) {
ALOGE("failed to configure track format for track %d: %d", i, err);
*failureReason = TranscodingLogger::SessionEndedReason::CONFIG_TRACK_FAILED;
return err;
}
callback->onHeartBeat(clientId, sessionId);
}
err = mTranscoder->configureDestination(dstFdInt);
if (err != AMEDIA_OK) {
ALOGE("failed to configure dest: %d", err);
*failureReason = TranscodingLogger::SessionEndedReason::CONFIG_DST_FAILED;
return err;
}
callback->onHeartBeat(clientId, sessionId);
return AMEDIA_OK;
}
media_status_t TranscoderWrapper::handleStart(
ClientIdType clientId, SessionIdType sessionId, const TranscodingRequestParcel& request,
uid_t callingUid, const std::shared_ptr<ITranscodingClientCallback>& clientCb) {
ALOGI("%s: setting up transcoder for start", __FUNCTION__);
TranscodingLogger::SessionEndedReason reason = TranscodingLogger::SessionEndedReason::UNKNOWN;
media_status_t err =
setupTranscoder(clientId, sessionId, request, callingUid, clientCb, &reason);
if (err != AMEDIA_OK) {
ALOGE("%s: failed to setup transcoder", __FUNCTION__);
logSessionEnded(reason, err);
return err;
}
mTranscodeStartTime = std::chrono::steady_clock::now();
err = mTranscoder->start();
if (err != AMEDIA_OK) {
ALOGE("%s: failed to start transcoder: %d", __FUNCTION__, err);
logSessionEnded(TranscodingLogger::SessionEndedReason::START_FAILED, err);
return err;
}
ALOGI("%s: transcoder started", __FUNCTION__);
return AMEDIA_OK;
}
media_status_t TranscoderWrapper::handlePause(ClientIdType clientId, SessionIdType sessionId) {
if (mTranscoder == nullptr) {
ALOGE("%s: transcoder is not running", __FUNCTION__);
return AMEDIA_ERROR_INVALID_OPERATION;
}
if (clientId != mCurrentClientId || sessionId != mCurrentSessionId) {
ALOGW("%s: stopping session {%lld, %d} that's not current session {%lld, %d}", __FUNCTION__,
(long long)clientId, sessionId, (long long)mCurrentClientId, mCurrentSessionId);
}
ALOGI("%s: pausing transcoder", __FUNCTION__);
std::shared_ptr<ndk::ScopedAParcel> pauseStates;
media_status_t err = mTranscoder->pause(&pauseStates);
logSessionEnded(TranscodingLogger::SessionEndedReason::PAUSED, err);
if (err != AMEDIA_OK) {
ALOGE("%s: failed to pause transcoder: %d", __FUNCTION__, err);
return err;
}
mPausedStateMap[SessionKeyType(clientId, sessionId)] = pauseStates;
ALOGI("%s: transcoder paused", __FUNCTION__);
return AMEDIA_OK;
}
media_status_t TranscoderWrapper::handleResume(
ClientIdType clientId, SessionIdType sessionId, const TranscodingRequestParcel& request,
uid_t callingUid, const std::shared_ptr<ITranscodingClientCallback>& clientCb) {
std::shared_ptr<ndk::ScopedAParcel> pausedState;
auto it = mPausedStateMap.find(SessionKeyType(clientId, sessionId));
if (it != mPausedStateMap.end()) {
pausedState = it->second;
mPausedStateMap.erase(it);
} else {
ALOGE("%s: can't find paused state", __FUNCTION__);
return AMEDIA_ERROR_INVALID_OPERATION;
}
ALOGI("%s: setting up transcoder for resume", __FUNCTION__);
TranscodingLogger::SessionEndedReason reason = TranscodingLogger::SessionEndedReason::UNKNOWN;
media_status_t err = setupTranscoder(clientId, sessionId, request, callingUid, clientCb,
&reason, pausedState);
if (err != AMEDIA_OK) {
ALOGE("%s: failed to setup transcoder: %d", __FUNCTION__, err);
logSessionEnded(reason, err);
return err;
}
// Note: For now resume() will just restart transcoding from the beginning, so there is no need
// to distinguish between resume and start from a performance perspective.
mTranscodeStartTime = std::chrono::steady_clock::now();
err = mTranscoder->resume();
if (err != AMEDIA_OK) {
ALOGE("%s: failed to resume transcoder: %d", __FUNCTION__, err);
logSessionEnded(TranscodingLogger::SessionEndedReason::RESUME_FAILED, err);
return err;
}
ALOGI("%s: transcoder resumed", __FUNCTION__);
return AMEDIA_OK;
}
void TranscoderWrapper::cleanup() {
mCurrentClientId = 0;
mCurrentSessionId = -1;
mCurrentCallingUid = -1;
mTranscoderCb = nullptr;
mTranscoder = nullptr;
mSrcFormat = nullptr;
mDstFormat = nullptr;
}
void TranscoderWrapper::logSessionEnded(const TranscodingLogger::SessionEndedReason& reason,
int error) {
std::chrono::microseconds transcodeDuration(-1);
if (reason == TranscodingLogger::SessionEndedReason::FINISHED && error == AMEDIA_OK) {
transcodeDuration = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - mTranscodeStartTime);
}
mLogger->logSessionEnded(reason, mCurrentCallingUid, error, transcodeDuration, mSrcFormat.get(),
mDstFormat.get());
}
void TranscoderWrapper::queueEvent(Event::Type type, ClientIdType clientId, SessionIdType sessionId,
const std::function<void()> runnable, int32_t arg) {
std::scoped_lock lock{mLock};
if (!mLooperReady) {
// A shared_ptr to ourselves is given to the thread's stack, so that the TranscoderWrapper
// object doesn't go away until the thread exits. When a watchdog timeout happens, this
// allows the session controller to release its reference to the TranscoderWrapper object
// without blocking on the thread exits.
std::thread([owner = shared_from_this()]() { owner->threadLoop(); }).detach();
mLooperReady = true;
}
mQueue.push_back({type, clientId, sessionId, runnable, arg});
mCondition.notify_one();
}
void TranscoderWrapper::threadLoop() {
androidSetThreadPriority(0 /*tid (0 = current) */, ANDROID_PRIORITY_BACKGROUND);
std::unique_lock<std::mutex> lock{mLock};
// TranscoderWrapper currently lives in the transcoding service, as long as
// MediaTranscodingService itself.
while (true) {
// Wait for the next event.
while (mQueue.empty()) {
mCondition.wait(lock);
}
Event event = *mQueue.begin();
mQueue.pop_front();
ALOGV("%s: %s", __FUNCTION__, toString(event).c_str());
if (event.type == Event::Abandon) {
break;
}
lock.unlock();
event.runnable();
lock.lock();
}
}
} // namespace android