/* * Copyright (C) 2019 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 "ECOSession" //#define DEBUG_ECO_SESSION #include "eco/ECOSession.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "eco/ECODataKey.h" #include "eco/ECODebug.h" namespace android { namespace media { namespace eco { using android::binder::Status; using android::sp; #define RETURN_IF_ERROR(expr) \ { \ status_t _errorCode = (expr); \ if (_errorCode != true) { \ return _errorCode; \ } \ } // static sp ECOSession::createECOSession(int32_t width, int32_t height, bool isCameraRecording) { // Only support up to 1080P. // TODO: Support the same resolution as in EAF. if (width <= 0 || height <= 0 || width > 5120 || height > 5120 || width > 1920 * 1080 / height) { ECOLOGE("Failed to create ECOSession with w: %d, h: %d, isCameraRecording: %d", width, height, isCameraRecording); return nullptr; } return new ECOSession(width, height, isCameraRecording); } ECOSession::ECOSession(int32_t width, int32_t height, bool isCameraRecording) : BnECOSession(), mStopThread(false), mLastReportedQp(0), mListener(nullptr), mProvider(nullptr), mWidth(width), mHeight(height), mIsCameraRecording(isCameraRecording) { ECOLOGI("ECOSession created with w: %d, h: %d, isCameraRecording: %d", mWidth, mHeight, mIsCameraRecording); mThread = std::thread(startThread, this); // Read the debug properies. mLogStats = property_get_bool(kDebugLogStats, false); mLogStatsEntries = mLogStats ? property_get_int32(kDebugLogStatsSize, 0) : 0; mLogInfo = property_get_bool(kDebugLogStats, false); mLogInfoEntries = mLogInfo ? property_get_int32(kDebugLogInfosSize, 0) : 0; ECOLOGI("ECOSession debug settings: logStats: %s, entries: %d, logInfo: %s entries: %d", mLogStats ? "true" : "false", mLogStatsEntries, mLogInfo ? "true" : "false", mLogInfoEntries); } ECOSession::~ECOSession() { mStopThread = true; mWorkerWaitCV.notify_all(); if (mThread.joinable()) { ECOLOGD("ECOSession: join the thread"); mThread.join(); } ECOLOGI("ECOSession destroyed with w: %d, h: %d, isCameraRecording: %d", mWidth, mHeight, mIsCameraRecording); } // static void ECOSession::startThread(ECOSession* session) { session->run(); } void ECOSession::run() { ECOLOGD("ECOSession: starting main thread"); while (!mStopThread) { std::unique_lock runLock(mStatsQueueLock); mWorkerWaitCV.wait(runLock, [this] { return mStopThread == true || !mStatsQueue.empty() || mNewListenerAdded; }); if (mStopThread) return; std::scoped_lock lock(mSessionLock); if (mNewListenerAdded) { // Check if there is any session info available. ECOData sessionInfo = generateLatestSessionInfoEcoData(); if (!sessionInfo.isEmpty()) { Status status = mListener->onNewInfo(sessionInfo); if (!status.isOk()) { ECOLOGE("%s: Failed to publish info: %s due to binder error", __FUNCTION__, sessionInfo.debugString().c_str()); // Remove the listener. The lock has been acquired outside this function. mListener = nullptr; } } mNewListenerAdded = false; } if (!mStatsQueue.empty()) { ECOData stats = mStatsQueue.front(); mStatsQueue.pop_front(); processStats(stats); // TODO: Handle the error from processStats } } ECOLOGD("ECOSession: exiting main thread"); } bool ECOSession::processStats(const ECOData& stats) { ECOLOGV("%s: receive stats: %s", __FUNCTION__, stats.debugString().c_str()); if (stats.getDataType() != ECOData::DATA_TYPE_STATS) { ECOLOGE("Invalid stats. ECOData with type: %s", stats.getDataTypeString().c_str()); return false; } // Get the type of the stats. std::string statsType; if (stats.findString(KEY_STATS_TYPE, &statsType) != ECODataStatus::OK) { ECOLOGE("Invalid stats ECOData without statsType"); return false; } if (statsType.compare(VALUE_STATS_TYPE_SESSION) == 0) { processSessionStats(stats); } else if (statsType.compare(VALUE_STATS_TYPE_FRAME) == 0) { processFrameStats(stats); } else { ECOLOGE("processStats:: Failed to process stats as ECOData contains unknown stats type"); return false; } return true; } void ECOSession::processSessionStats(const ECOData& stats) { ECOLOGV("processSessionStats"); ECOData info(ECOData::DATA_TYPE_INFO, systemTime(SYSTEM_TIME_BOOTTIME)); info.setString(KEY_INFO_TYPE, VALUE_INFO_TYPE_SESSION); ECODataKeyValueIterator iter(stats); while (iter.hasNext()) { ECOData::ECODataKeyValuePair entry = iter.next(); const std::string& key = entry.first; const ECOData::ECODataValueType value = entry.second; ECOLOGV("Processing key: %s", key.c_str()); if (!key.compare(KEY_STATS_TYPE)) { // Skip the key KEY_STATS_TYPE as that has been parsed already. continue; } else if (!key.compare(ENCODER_TYPE)) { mCodecType = std::get(value); ECOLOGV("codec type is %d", mCodecType); } else if (!key.compare(ENCODER_PROFILE)) { mCodecProfile = std::get(value); ECOLOGV("codec profile is %d", mCodecProfile); } else if (!key.compare(ENCODER_LEVEL)) { mCodecLevel = std::get(value); ECOLOGV("codec level is %d", mCodecLevel); } else if (!key.compare(ENCODER_TARGET_BITRATE_BPS)) { mTargetBitrateBps = std::get(value); ECOLOGV("codec target bitrate is %d", mTargetBitrateBps); } else if (!key.compare(ENCODER_KFI_FRAMES)) { mKeyFrameIntervalFrames = std::get(value); ECOLOGV("codec kfi is %d", mKeyFrameIntervalFrames); } else if (!key.compare(ENCODER_FRAMERATE_FPS)) { mFramerateFps = std::get(value); ECOLOGV("codec framerate is %f", mFramerateFps); } else if (!key.compare(ENCODER_INPUT_WIDTH)) { int32_t width = std::get(value); if (width != mWidth) { ECOLOGW("Codec width: %d, expected: %d", width, mWidth); } ECOLOGV("codec input width is %d", width); } else if (!key.compare(ENCODER_INPUT_HEIGHT)) { int32_t height = std::get(value); if (height != mHeight) { ECOLOGW("Codec height: %d, expected: %d", height, mHeight); } ECOLOGV("codec input height is %d", height); } else if (!key.compare(ENCODER_OUTPUT_WIDTH)) { mOutputWidth = std::get(value); if (mOutputWidth != mWidth) { ECOLOGW("Codec output width: %d, expected: %d", mOutputWidth, mWidth); } ECOLOGV("codec output width is %d", mOutputWidth); } else if (!key.compare(ENCODER_OUTPUT_HEIGHT)) { mOutputHeight = std::get(value); if (mOutputHeight != mHeight) { ECOLOGW("Codec output height: %d, expected: %d", mOutputHeight, mHeight); } ECOLOGV("codec output height is %d", mOutputHeight); } else { ECOLOGW("Unknown session stats key %s from provider.", key.c_str()); continue; } info.set(key, value); } if (mListener != nullptr) { Status status = mListener->onNewInfo(info); if (!status.isOk()) { ECOLOGE("%s: Failed to publish info: %s due to binder error", __FUNCTION__, info.debugString().c_str()); // Remove the listener. The lock has been acquired outside this function. mListener = nullptr; } } } ECOData ECOSession::generateLatestSessionInfoEcoData() { bool hasInfo = false; ECOData info(ECOData::DATA_TYPE_INFO, systemTime(SYSTEM_TIME_BOOTTIME)); if (mOutputWidth != -1) { info.setInt32(ENCODER_OUTPUT_WIDTH, mOutputWidth); hasInfo = true; } if (mOutputHeight != -1) { info.setInt32(ENCODER_OUTPUT_HEIGHT, mOutputHeight); hasInfo = true; } if (mCodecType != -1) { info.setInt32(ENCODER_TYPE, mCodecType); hasInfo = true; } if (mCodecProfile != -1) { info.setInt32(ENCODER_PROFILE, mCodecProfile); hasInfo = true; } if (mCodecLevel != -1) { info.setInt32(ENCODER_LEVEL, mCodecLevel); hasInfo = true; } if (mTargetBitrateBps != -1) { info.setInt32(ENCODER_TARGET_BITRATE_BPS, mTargetBitrateBps); hasInfo = true; } if (mKeyFrameIntervalFrames != -1) { info.setInt32(ENCODER_KFI_FRAMES, mKeyFrameIntervalFrames); hasInfo = true; } if (mFramerateFps > 0) { info.setFloat(ENCODER_FRAMERATE_FPS, mFramerateFps); hasInfo = true; } if (hasInfo) { info.setString(KEY_INFO_TYPE, VALUE_INFO_TYPE_SESSION); } return info; } void ECOSession::processFrameStats(const ECOData& stats) { ECOLOGD("processFrameStats"); bool needToNotifyListener = false; ECOData info(ECOData::DATA_TYPE_INFO, systemTime(SYSTEM_TIME_BOOTTIME)); info.setString(KEY_INFO_TYPE, VALUE_INFO_TYPE_FRAME); ECODataKeyValueIterator iter(stats); while (iter.hasNext()) { ECOData::ECODataKeyValuePair entry = iter.next(); const std::string& key = entry.first; const ECOData::ECODataValueType value = entry.second; ECOLOGD("Processing %s key", key.c_str()); if (!key.compare(KEY_STATS_TYPE)) { // Skip the key KEY_STATS_TYPE as that has been parsed already. continue; } else if (!key.compare(FRAME_NUM) || !key.compare(FRAME_PTS_US) || !key.compare(FRAME_TYPE) || !key.compare(FRAME_SIZE_BYTES) || !key.compare(ENCODER_ACTUAL_BITRATE_BPS) || !key.compare(ENCODER_FRAMERATE_FPS)) { // Only process the keys that are supported by ECOService 1.0. info.set(key, value); } else if (!key.compare(FRAME_AVG_QP)) { // Check the qp to see if need to notify the listener. const int32_t currAverageQp = std::get(value); // Check if the delta between current QP and last reported QP is larger than the // threshold specified by the listener. const bool largeQPChangeDetected = abs(currAverageQp - mLastReportedQp) > mListenerQpCondition.mQpChangeThreshold; // Check if the qp is going from below threshold to beyond threshold. const bool exceedQpBlockinessThreshold = (mLastReportedQp <= mListenerQpCondition.mQpBlocknessThreshold && currAverageQp > mListenerQpCondition.mQpBlocknessThreshold); // Check if the qp is going from beyond threshold to below threshold. const bool fallBelowQpBlockinessThreshold = (mLastReportedQp > mListenerQpCondition.mQpBlocknessThreshold && currAverageQp <= mListenerQpCondition.mQpBlocknessThreshold); // Notify the listener if any of the above three conditions met. if (largeQPChangeDetected || exceedQpBlockinessThreshold || fallBelowQpBlockinessThreshold) { mLastReportedQp = currAverageQp; needToNotifyListener = true; } info.set(key, value); } else { ECOLOGW("Unknown frame stats key %s from provider.", key.c_str()); } } if (needToNotifyListener && mListener != nullptr) { Status status = mListener->onNewInfo(info); if (!status.isOk()) { ECOLOGE("%s: Failed to publish info: %s due to binder error", __FUNCTION__, info.debugString().c_str()); // Remove the listener. The lock has been acquired outside this function. mListener = nullptr; } } } Status ECOSession::getIsCameraRecording(bool* _aidl_return) { std::scoped_lock lock(mSessionLock); *_aidl_return = mIsCameraRecording; return binder::Status::ok(); } Status ECOSession::addStatsProvider( const sp<::android::media::eco::IECOServiceStatsProvider>& provider, const ::android::media::eco::ECOData& config, bool* status) { ::android::String16 name; Status result = provider->getName(&name); if (!result.isOk()) { // This binder transaction failure may due to permission issue. *status = false; ALOGE("Failed to get provider name"); return STATUS_ERROR(ERROR_PERMISSION_DENIED, "Failed to get provider name"); } ECOLOGV("Try to add stats provider name: %s uid: %d pid %d", ::android::String8(name).string(), IPCThreadState::self()->getCallingUid(), IPCThreadState::self()->getCallingPid()); if (provider == nullptr) { ECOLOGE("%s: provider must not be null", __FUNCTION__); *status = false; return STATUS_ERROR(ERROR_ILLEGAL_ARGUMENT, "Null provider given to addStatsProvider"); } std::scoped_lock lock(mSessionLock); if (mProvider != nullptr) { ::android::String16 name; mProvider->getName(&name); String8 errorMsg = String8::format( "ECOService 1.0 only supports one stats provider, current provider: %s", ::android::String8(name).string()); ECOLOGE("%s", errorMsg.string()); *status = false; return STATUS_ERROR(ERROR_ALREADY_EXISTS, errorMsg.string()); } // TODO: Handle the provider config. if (config.getDataType() != ECOData::DATA_TYPE_STATS_PROVIDER_CONFIG) { ECOLOGE("Provider config is invalid"); *status = false; return STATUS_ERROR(ERROR_ILLEGAL_ARGUMENT, "Provider config is invalid"); } mProvider = provider; mProviderName = name; *status = true; return binder::Status::ok(); } Status ECOSession::removeStatsProvider( const sp<::android::media::eco::IECOServiceStatsProvider>& provider, bool* status) { std::scoped_lock lock(mSessionLock); // Check if the provider is the same as current provider for the session. if (IInterface::asBinder(provider) != IInterface::asBinder(mProvider)) { *status = false; ECOLOGE("Failed to remove provider"); return STATUS_ERROR(ERROR_ILLEGAL_ARGUMENT, "Provider does not match"); } mProvider = nullptr; *status = true; return binder::Status::ok(); } Status ECOSession::addInfoListener( const sp<::android::media::eco::IECOServiceInfoListener>& listener, const ::android::media::eco::ECOData& config, bool* status) { ALOGV("%s: Add listener %p", __FUNCTION__, listener.get()); std::scoped_lock lock(mSessionLock); ::android::String16 name; Status result = listener->getName(&name); if (!result.isOk()) { // This binder transaction failure may due to permission issue. *status = false; ALOGE("Failed to get listener name"); return STATUS_ERROR(ERROR_PERMISSION_DENIED, "Failed to get listener name"); } if (mListener != nullptr) { ECOLOGE("ECOService 1.0 only supports one listener"); *status = false; return STATUS_ERROR(ERROR_ALREADY_EXISTS, "ECOService 1.0 only supports one listener"); } if (listener == nullptr) { ECOLOGE("%s: listener must not be null", __FUNCTION__); *status = false; return STATUS_ERROR(ERROR_ILLEGAL_ARGUMENT, "Null listener given to addInfoListener"); } if (config.getDataType() != ECOData::DATA_TYPE_INFO_LISTENER_CONFIG) { *status = false; ECOLOGE("%s: listener config is invalid", __FUNCTION__); return STATUS_ERROR(ERROR_ILLEGAL_ARGUMENT, "listener config is invalid"); } if (config.isEmpty()) { *status = false; ECOLOGE("Listener must provide listening criterion"); return STATUS_ERROR(ERROR_ILLEGAL_ARGUMENT, "listener config is empty"); } // For ECOService 1.0, listener must specify the two threshold in order to receive info. if (config.findInt32(KEY_LISTENER_QP_BLOCKINESS_THRESHOLD, &mListenerQpCondition.mQpBlocknessThreshold) != ECODataStatus::OK || config.findInt32(KEY_LISTENER_QP_CHANGE_THRESHOLD, &mListenerQpCondition.mQpChangeThreshold) != ECODataStatus::OK || mListenerQpCondition.mQpBlocknessThreshold < ENCODER_MIN_QP || mListenerQpCondition.mQpBlocknessThreshold > ENCODER_MAX_QP) { *status = false; ECOLOGE("%s: listener config is invalid", __FUNCTION__); return STATUS_ERROR(ERROR_ILLEGAL_ARGUMENT, "listener config is not valid"); } ECOLOGD("Info listener name: %s uid: %d pid %d", ::android::String8(name).string(), IPCThreadState::self()->getCallingUid(), IPCThreadState::self()->getCallingPid()); mListener = listener; mListenerName = name; mNewListenerAdded = true; mWorkerWaitCV.notify_all(); *status = true; return binder::Status::ok(); } Status ECOSession::removeInfoListener( const sp<::android::media::eco::IECOServiceInfoListener>& listener, bool* _aidl_return) { std::scoped_lock lock(mSessionLock); // Check if the listener is the same as current listener for the session. if (IInterface::asBinder(listener) != IInterface::asBinder(mListener)) { *_aidl_return = false; ECOLOGE("Failed to remove listener"); return STATUS_ERROR(ERROR_ILLEGAL_ARGUMENT, "Listener does not match"); } mListener = nullptr; mNewListenerAdded = false; *_aidl_return = true; return binder::Status::ok(); } Status ECOSession::pushNewStats(const ::android::media::eco::ECOData& stats, bool* _aidl_return) { ECOLOGV("ECOSession get new stats type: %s", stats.getDataTypeString().c_str()); std::unique_lock lock(mStatsQueueLock); mStatsQueue.push_back(stats); mWorkerWaitCV.notify_all(); *_aidl_return = true; return binder::Status::ok(); } Status ECOSession::getWidth(int32_t* _aidl_return) { std::scoped_lock lock(mSessionLock); *_aidl_return = mWidth; return binder::Status::ok(); } Status ECOSession::getHeight(int32_t* _aidl_return) { std::scoped_lock lock(mSessionLock); *_aidl_return = mHeight; return binder::Status::ok(); } Status ECOSession::getNumOfListeners(int32_t* _aidl_return) { std::scoped_lock lock(mSessionLock); *_aidl_return = (mListener == nullptr ? 0 : 1); return binder::Status::ok(); } Status ECOSession::getNumOfProviders(int32_t* _aidl_return) { std::scoped_lock lock(mSessionLock); *_aidl_return = (mProvider == nullptr ? 0 : 1); return binder::Status::ok(); } /*virtual*/ void ECOSession::binderDied(const wp& /*who*/) { ECOLOGV("binderDied"); } status_t ECOSession::dump(int fd, const Vector& /*args*/) { std::scoped_lock lock(mSessionLock); dprintf(fd, "\n== Session Info: ==\n\n"); dprintf(fd, "Width: %d Height: %d isCameraRecording: %d, target-bitrate: %d bps codetype: %d " "profile: %d level: %d\n", mWidth, mHeight, mIsCameraRecording, mTargetBitrateBps, mCodecType, mCodecProfile, mCodecLevel); if (mProvider != nullptr) { dprintf(fd, "Provider: %s \n", ::android::String8(mProviderName).string()); } if (mListener != nullptr) { dprintf(fd, "Listener: %s \n", ::android::String8(mListenerName).string()); } dprintf(fd, "\n===================\n\n"); return NO_ERROR; } void ECOSession::logStats(const ECOData& data) { // Check if mLogStats is true; if (!mLogStats || mLogStatsEntries == 0) return; // Check if we need to remove the old entry. if (mStatsDebugBuffer.size() >= mLogStatsEntries) { mStatsDebugBuffer.pop_front(); } mStatsDebugBuffer.push_back(data); } void ECOSession::logInfos(const ECOData& data) { // Check if mLogInfo is true; if (!mLogInfo || mLogInfoEntries == 0) return; // Check if we need to remove the old entry. if (mInfosDebugBuffer.size() >= mLogInfoEntries) { mInfosDebugBuffer.pop_front(); } mInfosDebugBuffer.push_back(data); } } // namespace eco } // namespace media } // namespace android