| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| //#define LOG_NDEBUG 0 |
| #define LOG_TAG "V4L2EncodeComponent" |
| |
| #include <v4l2_codec2/components/V4L2EncodeComponent.h> |
| |
| #include <inttypes.h> |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include <C2AllocatorGralloc.h> |
| #include <C2PlatformSupport.h> |
| #include <C2Work.h> |
| #include <android/hardware/graphics/common/1.0/types.h> |
| #include <base/bind.h> |
| #include <base/bind_helpers.h> |
| #include <cutils/properties.h> |
| #include <log/log.h> |
| #include <media/stagefright/MediaDefs.h> |
| #include <ui/GraphicBuffer.h> |
| #include <ui/Size.h> |
| |
| #include <v4l2_codec2/common/Common.h> |
| #include <v4l2_codec2/common/EncodeHelpers.h> |
| #include <v4l2_codec2/common/FormatConverter.h> |
| #include <v4l2_codec2/common/VideoPixelFormat.h> |
| #include <v4l2_codec2/components/BitstreamBuffer.h> |
| #include <v4l2_codec2/components/V4L2EncodeInterface.h> |
| #include <v4l2_codec2/components/V4L2Encoder.h> |
| |
| using android::hardware::graphics::common::V1_0::BufferUsage; |
| |
| namespace android { |
| |
| namespace { |
| |
| const VideoPixelFormat kInputPixelFormat = VideoPixelFormat::NV12; |
| |
| // The peak bitrate in function of the target bitrate, used when the bitrate mode is VBR. |
| constexpr uint32_t kPeakBitrateMultiplier = 2u; |
| |
| // Get the video frame layout from the specified |inputBlock|. |
| // TODO(dstaessens): Clean up code extracting layout from a C2GraphicBlock. |
| std::optional<std::vector<VideoFramePlane>> getVideoFrameLayout(const C2ConstGraphicBlock& block, |
| VideoPixelFormat* format) { |
| ALOGV("%s()", __func__); |
| |
| // Get the C2PlanarLayout from the graphics block. The C2GraphicView returned by block.map() |
| // needs to be released before calling getGraphicBlockInfo(), or the lockYCbCr() call will block |
| // Indefinitely. |
| C2PlanarLayout layout = block.map().get().layout(); |
| |
| // The above layout() cannot fill layout information and memset 0 instead if the input format is |
| // IMPLEMENTATION_DEFINED and its backed format is RGB. We fill the layout by using |
| // ImplDefinedToRGBXMap in the case. |
| if (layout.type == C2PlanarLayout::TYPE_UNKNOWN) { |
| std::unique_ptr<ImplDefinedToRGBXMap> idMap = ImplDefinedToRGBXMap::Create(block); |
| if (idMap == nullptr) { |
| ALOGE("Unable to parse RGBX_8888 from IMPLEMENTATION_DEFINED"); |
| return std::nullopt; |
| } |
| layout.type = C2PlanarLayout::TYPE_RGB; |
| // These parameters would be used in TYPE_GRB case below. |
| layout.numPlanes = 3; // same value as in C2AllocationGralloc::map() |
| layout.rootPlanes = 1; // same value as in C2AllocationGralloc::map() |
| layout.planes[C2PlanarLayout::PLANE_R].offset = idMap->offset(); |
| layout.planes[C2PlanarLayout::PLANE_R].rowInc = idMap->rowInc(); |
| } |
| |
| std::vector<uint32_t> offsets(layout.numPlanes, 0u); |
| std::vector<uint32_t> strides(layout.numPlanes, 0u); |
| switch (layout.type) { |
| case C2PlanarLayout::TYPE_YUV: { |
| android_ycbcr ycbcr = getGraphicBlockInfo(block); |
| offsets[C2PlanarLayout::PLANE_Y] = |
| static_cast<uint32_t>(reinterpret_cast<uintptr_t>(ycbcr.y)); |
| offsets[C2PlanarLayout::PLANE_U] = |
| static_cast<uint32_t>(reinterpret_cast<uintptr_t>(ycbcr.cb)); |
| offsets[C2PlanarLayout::PLANE_V] = |
| static_cast<uint32_t>(reinterpret_cast<uintptr_t>(ycbcr.cr)); |
| strides[C2PlanarLayout::PLANE_Y] = static_cast<uint32_t>(ycbcr.ystride); |
| strides[C2PlanarLayout::PLANE_U] = static_cast<uint32_t>(ycbcr.cstride); |
| strides[C2PlanarLayout::PLANE_V] = static_cast<uint32_t>(ycbcr.cstride); |
| |
| bool crcb = false; |
| if (offsets[C2PlanarLayout::PLANE_U] > offsets[C2PlanarLayout::PLANE_V]) { |
| // Swap offsets, no need to swap strides as they are identical for both chroma planes. |
| std::swap(offsets[C2PlanarLayout::PLANE_U], offsets[C2PlanarLayout::PLANE_V]); |
| crcb = true; |
| } |
| |
| bool semiplanar = false; |
| if (ycbcr.chroma_step > |
| offsets[C2PlanarLayout::PLANE_V] - offsets[C2PlanarLayout::PLANE_U]) { |
| semiplanar = true; |
| } |
| |
| if (!crcb && !semiplanar) { |
| *format = VideoPixelFormat::I420; |
| } else if (!crcb && semiplanar) { |
| *format = VideoPixelFormat::NV12; |
| } else if (crcb && !semiplanar) { |
| // HACK: pretend YV12 is I420 now since VEA only accepts I420. (YV12 will be used |
| // for input byte-buffer mode). |
| // TODO(dstaessens): Is this hack still necessary now we're not using the VEA directly? |
| //format = VideoPixelFormat::YV12; |
| *format = VideoPixelFormat::I420; |
| } else { |
| *format = VideoPixelFormat::NV21; |
| } |
| break; |
| } |
| case C2PlanarLayout::TYPE_RGB: { |
| offsets[C2PlanarLayout::PLANE_R] = layout.planes[C2PlanarLayout::PLANE_R].offset; |
| strides[C2PlanarLayout::PLANE_R] = |
| static_cast<uint32_t>(layout.planes[C2PlanarLayout::PLANE_R].rowInc); |
| *format = VideoPixelFormat::ARGB; |
| break; |
| } |
| default: |
| ALOGW("Unknown layout type: %u", static_cast<uint32_t>(layout.type)); |
| return std::nullopt; |
| } |
| |
| std::vector<VideoFramePlane> planes; |
| for (uint32_t i = 0; i < layout.rootPlanes; ++i) { |
| // The mSize field is not used in our case, so we can safely set it to zero. |
| planes.push_back({strides[i], offsets[i], 0}); |
| } |
| return planes; |
| } |
| |
| // Get the video frame stride for the specified |format| and |size|. |
| std::optional<uint32_t> getVideoFrameStride(VideoPixelFormat format, ui::Size size) { |
| // Fetch a graphic block from the pool to determine the stride. |
| std::shared_ptr<C2BlockPool> pool; |
| c2_status_t status = GetCodec2BlockPool(C2BlockPool::BASIC_GRAPHIC, nullptr, &pool); |
| if (status != C2_OK) { |
| ALOGE("Failed to get basic graphic block pool (err=%d)", status); |
| return std::nullopt; |
| } |
| |
| // Android HAL format doesn't have I420, we use YV12 instead and swap the U and V planes when |
| // converting to NV12. YCBCR_420_888 will allocate NV12 by minigbm. |
| HalPixelFormat halFormat = (format == VideoPixelFormat::I420) ? HalPixelFormat::YV12 |
| : HalPixelFormat::YCBCR_420_888; |
| |
| std::shared_ptr<C2GraphicBlock> block; |
| status = pool->fetchGraphicBlock(size.width, size.height, static_cast<uint32_t>(halFormat), |
| C2MemoryUsage(C2MemoryUsage::CPU_READ), &block); |
| if (status != C2_OK) { |
| ALOGE("Failed to fetch graphic block (err=%d)", status); |
| return std::nullopt; |
| } |
| |
| const C2ConstGraphicBlock constBlock = block->share(C2Rect(size.width, size.height), C2Fence()); |
| VideoPixelFormat pixelFormat; |
| std::optional<std::vector<VideoFramePlane>> planes = |
| getVideoFrameLayout(constBlock, &pixelFormat); |
| if (!planes || planes.value().empty()) { |
| ALOGE("Failed to get video frame layout from block"); |
| return std::nullopt; |
| } |
| |
| return planes.value()[0].mStride; |
| } |
| |
| // Create an input frame from the specified graphic block. |
| std::unique_ptr<V4L2Encoder::InputFrame> CreateInputFrame(const C2ConstGraphicBlock& block, |
| uint64_t index, int64_t timestamp) { |
| VideoPixelFormat format; |
| std::optional<std::vector<VideoFramePlane>> planes = getVideoFrameLayout(block, &format); |
| if (!planes) { |
| ALOGE("Failed to get input block's layout"); |
| return nullptr; |
| } |
| |
| std::vector<int> fds; |
| const C2Handle* const handle = block.handle(); |
| for (int i = 0; i < handle->numFds; i++) { |
| fds.emplace_back(handle->data[i]); |
| } |
| |
| return std::make_unique<V4L2Encoder::InputFrame>(std::move(fds), std::move(planes.value()), |
| format, index, timestamp); |
| } |
| |
| // Check whether the specified |profile| is an H.264 profile. |
| bool IsH264Profile(C2Config::profile_t profile) { |
| return (profile >= C2Config::PROFILE_AVC_BASELINE && |
| profile <= C2Config::PROFILE_AVC_ENHANCED_MULTIVIEW_DEPTH_HIGH); |
| } |
| |
| } // namespace |
| |
| // static |
| std::atomic<int32_t> V4L2EncodeComponent::sConcurrentInstances = 0; |
| |
| // static |
| std::shared_ptr<C2Component> V4L2EncodeComponent::create( |
| C2String name, c2_node_id_t id, std::shared_ptr<C2ReflectorHelper> helper, |
| C2ComponentFactory::ComponentDeleter deleter) { |
| ALOGV("%s(%s)", __func__, name.c_str()); |
| |
| static const int32_t kMaxConcurrentInstances = |
| property_get_int32("ro.vendor.v4l2_codec2.encode_concurrent_instances", -1); |
| |
| static std::mutex mutex; |
| std::lock_guard<std::mutex> lock(mutex); |
| if (kMaxConcurrentInstances >= 0 && sConcurrentInstances.load() >= kMaxConcurrentInstances) { |
| ALOGW("Cannot create additional encoder, maximum number of instances reached: %d", |
| kMaxConcurrentInstances); |
| return nullptr; |
| } |
| |
| auto interface = std::make_shared<V4L2EncodeInterface>(name, std::move(helper)); |
| if (interface->status() != C2_OK) { |
| ALOGE("Component interface initialization failed (error code %d)", interface->status()); |
| return nullptr; |
| } |
| |
| return std::shared_ptr<C2Component>(new V4L2EncodeComponent(name, id, std::move(interface)), |
| deleter); |
| } |
| |
| V4L2EncodeComponent::V4L2EncodeComponent(C2String name, c2_node_id_t id, |
| std::shared_ptr<V4L2EncodeInterface> interface) |
| : mName(name), |
| mId(id), |
| mInterface(std::move(interface)), |
| mComponentState(ComponentState::LOADED) { |
| ALOGV("%s(%s)", __func__, name.c_str()); |
| |
| sConcurrentInstances.fetch_add(1, std::memory_order_relaxed); |
| } |
| |
| V4L2EncodeComponent::~V4L2EncodeComponent() { |
| ALOGV("%s()", __func__); |
| |
| // Stop encoder thread and invalidate pointers if component wasn't stopped before destroying. |
| if (mEncoderThread.IsRunning()) { |
| mEncoderTaskRunner->PostTask( |
| FROM_HERE, ::base::BindOnce( |
| [](::base::WeakPtrFactory<V4L2EncodeComponent>* weakPtrFactory) { |
| weakPtrFactory->InvalidateWeakPtrs(); |
| }, |
| &mWeakThisFactory)); |
| mEncoderThread.Stop(); |
| } |
| |
| sConcurrentInstances.fetch_sub(1, std::memory_order_relaxed); |
| ALOGV("%s(): done", __func__); |
| } |
| |
| c2_status_t V4L2EncodeComponent::start() { |
| ALOGV("%s()", __func__); |
| |
| // Lock while starting, to synchronize start/stop/reset/release calls. |
| std::lock_guard<std::mutex> lock(mComponentLock); |
| |
| // According to the specification start() should only be called in the LOADED state. |
| if (mComponentState != ComponentState::LOADED) { |
| return C2_BAD_STATE; |
| } |
| |
| if (!mEncoderThread.Start()) { |
| ALOGE("Failed to start encoder thread"); |
| return C2_CORRUPTED; |
| } |
| mEncoderTaskRunner = mEncoderThread.task_runner(); |
| mWeakThis = mWeakThisFactory.GetWeakPtr(); |
| |
| // Initialize the encoder on the encoder thread. |
| ::base::WaitableEvent done; |
| bool success = false; |
| mEncoderTaskRunner->PostTask( |
| FROM_HERE, ::base::Bind(&V4L2EncodeComponent::startTask, mWeakThis, &success, &done)); |
| done.Wait(); |
| |
| if (!success) { |
| ALOGE("Failed to initialize encoder"); |
| return C2_CORRUPTED; |
| } |
| |
| setComponentState(ComponentState::RUNNING); |
| return C2_OK; |
| } |
| |
| c2_status_t V4L2EncodeComponent::stop() { |
| ALOGV("%s()", __func__); |
| |
| // Lock while stopping, to synchronize start/stop/reset/release calls. |
| std::lock_guard<std::mutex> lock(mComponentLock); |
| |
| if (mComponentState != ComponentState::RUNNING && mComponentState != ComponentState::ERROR) { |
| return C2_BAD_STATE; |
| } |
| |
| // Return immediately if the component is already stopped. |
| if (!mEncoderThread.IsRunning()) { |
| return C2_OK; |
| } |
| |
| // Wait for the component to stop. |
| ::base::WaitableEvent done; |
| mEncoderTaskRunner->PostTask( |
| FROM_HERE, ::base::BindOnce(&V4L2EncodeComponent::stopTask, mWeakThis, &done)); |
| done.Wait(); |
| mEncoderThread.Stop(); |
| |
| setComponentState(ComponentState::LOADED); |
| |
| ALOGV("%s() - done", __func__); |
| return C2_OK; |
| } |
| |
| c2_status_t V4L2EncodeComponent::reset() { |
| ALOGV("%s()", __func__); |
| |
| // The interface specification says: "This method MUST be supported in all (including tripped) |
| // states other than released". |
| if (mComponentState == ComponentState::UNLOADED) { |
| return C2_BAD_STATE; |
| } |
| |
| // TODO(dstaessens): Reset the component's interface to default values. |
| stop(); |
| |
| return C2_OK; |
| } |
| |
| c2_status_t V4L2EncodeComponent::release() { |
| ALOGV("%s()", __func__); |
| |
| // The interface specification says: "This method MUST be supported in stopped state.", but the |
| // release method seems to be called in other states as well. |
| reset(); |
| |
| setComponentState(ComponentState::UNLOADED); |
| return C2_OK; |
| } |
| |
| c2_status_t V4L2EncodeComponent::queue_nb(std::list<std::unique_ptr<C2Work>>* const items) { |
| ALOGV("%s()", __func__); |
| |
| if (mComponentState != ComponentState::RUNNING) { |
| ALOGE("Trying to queue work item while component is not running"); |
| return C2_BAD_STATE; |
| } |
| |
| while (!items->empty()) { |
| mEncoderTaskRunner->PostTask(FROM_HERE, |
| ::base::BindOnce(&V4L2EncodeComponent::queueTask, mWeakThis, |
| std::move(items->front()))); |
| items->pop_front(); |
| } |
| |
| return C2_OK; |
| } |
| |
| c2_status_t V4L2EncodeComponent::drain_nb(drain_mode_t mode) { |
| ALOGV("%s()", __func__); |
| |
| if (mode == DRAIN_CHAIN) { |
| return C2_OMITTED; // Tunneling is not supported for now. |
| } |
| |
| if (mComponentState != ComponentState::RUNNING) { |
| return C2_BAD_STATE; |
| } |
| |
| mEncoderTaskRunner->PostTask( |
| FROM_HERE, ::base::BindOnce(&V4L2EncodeComponent::drainTask, mWeakThis, mode)); |
| return C2_OK; |
| } |
| |
| c2_status_t V4L2EncodeComponent::flush_sm(flush_mode_t mode, |
| std::list<std::unique_ptr<C2Work>>* const flushedWork) { |
| ALOGV("%s()", __func__); |
| |
| if (mode != FLUSH_COMPONENT) { |
| return C2_OMITTED; // Tunneling is not supported by now |
| } |
| |
| if (mComponentState != ComponentState::RUNNING) { |
| return C2_BAD_STATE; |
| } |
| |
| // Work that can be immediately discarded should be returned in |flushedWork|. This method may |
| // be momentarily blocking but must return within 5ms, which should give us enough time to |
| // immediately abandon all non-started work on the encoder thread. We can return all work that |
| // can't be immediately discarded using onWorkDone() later. |
| ::base::WaitableEvent done; |
| mEncoderTaskRunner->PostTask(FROM_HERE, ::base::BindOnce(&V4L2EncodeComponent::flushTask, |
| mWeakThis, &done, flushedWork)); |
| done.Wait(); |
| |
| return C2_OK; |
| } |
| |
| c2_status_t V4L2EncodeComponent::announce_nb(const std::vector<C2WorkOutline>& items) { |
| return C2_OMITTED; // Tunneling is not supported by now |
| } |
| |
| c2_status_t V4L2EncodeComponent::setListener_vb(const std::shared_ptr<Listener>& listener, |
| c2_blocking_t mayBlock) { |
| ALOG_ASSERT(mComponentState != ComponentState::UNLOADED); |
| |
| // Lock so we're sure the component isn't currently starting or stopping. |
| std::lock_guard<std::mutex> lock(mComponentLock); |
| |
| // If the encoder thread is not running it's safe to update the listener directly. |
| if (!mEncoderThread.IsRunning()) { |
| mListener = listener; |
| return C2_OK; |
| } |
| |
| // The listener should be updated before exiting this function. If called while the component is |
| // currently running we should be allowed to block, as we can only change the listener on the |
| // encoder thread. |
| ALOG_ASSERT(mayBlock == c2_blocking_t::C2_MAY_BLOCK); |
| |
| ::base::WaitableEvent done; |
| mEncoderTaskRunner->PostTask(FROM_HERE, ::base::BindOnce(&V4L2EncodeComponent::setListenerTask, |
| mWeakThis, listener, &done)); |
| done.Wait(); |
| |
| return C2_OK; |
| } |
| |
| std::shared_ptr<C2ComponentInterface> V4L2EncodeComponent::intf() { |
| return std::make_shared<SimpleInterface<V4L2EncodeInterface>>(mName.c_str(), mId, mInterface); |
| } |
| |
| void V4L2EncodeComponent::startTask(bool* success, ::base::WaitableEvent* done) { |
| ALOGV("%s()", __func__); |
| ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence()); |
| |
| *success = initializeEncoder(); |
| done->Signal(); |
| } |
| |
| void V4L2EncodeComponent::stopTask(::base::WaitableEvent* done) { |
| ALOGV("%s()", __func__); |
| ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence()); |
| |
| // Flushing the encoder will abort all pending work. |
| flush(); |
| |
| mInputFormatConverter.reset(); |
| |
| mEncoder.reset(); |
| mOutputBlockPool.reset(); |
| |
| // Invalidate all weak pointers so no more functions will be executed on the encoder thread. |
| mWeakThisFactory.InvalidateWeakPtrs(); |
| |
| done->Signal(); |
| } |
| |
| void V4L2EncodeComponent::queueTask(std::unique_ptr<C2Work> work) { |
| ALOGV("%s()", __func__); |
| ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence()); |
| ALOG_ASSERT(mEncoder); |
| |
| // Currently only a single worklet per work item is supported. An input buffer should always be |
| // supplied unless this is a drain or CSD request. |
| ALOG_ASSERT(work->input.buffers.size() <= 1u && work->worklets.size() == 1u); |
| |
| // Set the default values for the output worklet. |
| work->worklets.front()->output.flags = static_cast<C2FrameData::flags_t>(0); |
| work->worklets.front()->output.buffers.clear(); |
| work->worklets.front()->output.ordinal = work->input.ordinal; |
| |
| uint64_t index = work->input.ordinal.frameIndex.peeku(); |
| int64_t timestamp = static_cast<int64_t>(work->input.ordinal.timestamp.peeku()); |
| bool endOfStream = work->input.flags & C2FrameData::FLAG_END_OF_STREAM; |
| ALOGV("Queuing next encode (index: %" PRIu64 ", timestamp: %" PRId64 ", EOS: %d)", index, |
| timestamp, endOfStream); |
| |
| // The codec 2.0 framework might queue an empty CSD request, but this is currently not |
| // supported. We will return the CSD with the first encoded buffer work. |
| if (work->input.buffers.empty() && !endOfStream) { |
| ALOGV("Discarding empty CSD request"); |
| reportWork(std::move(work)); |
| return; |
| } |
| |
| // By the time we get an input buffer, the output block pool should be configured. |
| if (!mOutputBlockPool && !getBlockPool()) { |
| reportError(C2_CORRUPTED); |
| return; |
| } |
| |
| // If conversion is required but no free buffers are available we queue the work item. |
| if (mInputFormatConverter && !mInputFormatConverter->isReady()) { |
| ALOGV("Input format convertor ran out of buffers"); |
| mInputConverterQueue.push(std::move(work)); |
| return; |
| } |
| |
| // If we have data to encode send it to the encoder. If conversion is required we will first |
| // convert the data to the requested pixel format. |
| if (!work->input.buffers.empty()) { |
| C2ConstGraphicBlock inputBlock = |
| work->input.buffers.front()->data().graphicBlocks().front(); |
| if (mInputFormatConverter) { |
| ALOGV("Converting input block (index: %" PRIu64 ")", index); |
| c2_status_t status = C2_CORRUPTED; |
| inputBlock = mInputFormatConverter->convertBlock(index, inputBlock, &status); |
| if (status != C2_OK) { |
| ALOGE("Failed to convert input block (index: %" PRIu64 ")", index); |
| reportError(status); |
| return; |
| } |
| } |
| if (!encode(inputBlock, index, timestamp)) { |
| return; |
| } |
| } |
| |
| mWorkQueue.push_back(std::move(work)); |
| if (endOfStream) { |
| mEncoder->drain(); |
| } |
| } |
| |
| void V4L2EncodeComponent::drainTask(drain_mode_t /*drainMode*/) { |
| ALOGV("%s()", __func__); |
| ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence()); |
| |
| // We can only start draining if all work has been queued in the encoder, so we mark the last |
| // item waiting for conversion as EOS if required. |
| if (!mInputConverterQueue.empty()) { |
| C2Work* work = mInputConverterQueue.back().get(); |
| work->input.flags = static_cast<C2FrameData::flags_t>(work->input.flags | |
| C2FrameData::FLAG_END_OF_STREAM); |
| return; |
| } |
| |
| // Mark the last item in the output work queue as EOS, so we will only report it as finished |
| // after draining has completed. |
| if (!mWorkQueue.empty()) { |
| ALOGV("Starting drain and marking last item in output work queue as EOS"); |
| C2Work* work = mWorkQueue.back().get(); |
| work->input.flags = static_cast<C2FrameData::flags_t>(work->input.flags | |
| C2FrameData::FLAG_END_OF_STREAM); |
| mEncoder->drain(); |
| } |
| } |
| |
| void V4L2EncodeComponent::onDrainDone(bool success) { |
| ALOGV("%s()", __func__); |
| ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence()); |
| ALOG_ASSERT(!mWorkQueue.empty()); |
| |
| if (!success) { |
| ALOGE("draining the encoder failed"); |
| reportError(C2_CORRUPTED); |
| return; |
| } |
| |
| // Find the first work item marked as EOS. This might not be the first item in the queue, as |
| // previous buffers in the queue might still be waiting for their associated input buffers. |
| auto it = std::find_if( |
| mWorkQueue.cbegin(), mWorkQueue.cend(), [](const std::unique_ptr<C2Work>& work) { |
| return ((work->input.flags & C2FrameData::FLAG_END_OF_STREAM) && |
| !(work->worklets.back()->output.flags & C2FrameData::FLAG_END_OF_STREAM)); |
| }); |
| if (it == mWorkQueue.end()) { |
| ALOGW("No EOS work item found in queue"); |
| return; |
| } |
| |
| // Mark the item in the output work queue as EOS done. |
| C2Work* eosWork = it->get(); |
| eosWork->worklets.back()->output.flags = C2FrameData::FLAG_END_OF_STREAM; |
| |
| // Draining is done which means all buffers on the device output queue have been returned, but |
| // not all buffers on the device input queue might have been returned yet. |
| if ((eosWork != mWorkQueue.front().get()) || !isWorkDone(*eosWork)) { |
| ALOGV("Draining done, waiting for input buffers to be returned"); |
| return; |
| } |
| |
| ALOGV("Draining done"); |
| reportWork(std::move(mWorkQueue.front())); |
| mWorkQueue.pop_front(); |
| } |
| |
| void V4L2EncodeComponent::flushTask(::base::WaitableEvent* done, |
| std::list<std::unique_ptr<C2Work>>* const flushedWork) { |
| ALOGV("%s()", __func__); |
| ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence()); |
| |
| // Move all work that can immediately be aborted to flushedWork, and notify the caller. |
| if (flushedWork) { |
| while (!mInputConverterQueue.empty()) { |
| std::unique_ptr<C2Work> work = std::move(mInputConverterQueue.front()); |
| work->input.buffers.clear(); |
| flushedWork->push_back(std::move(work)); |
| mInputConverterQueue.pop(); |
| } |
| } |
| done->Signal(); |
| |
| flush(); |
| } |
| |
| void V4L2EncodeComponent::setListenerTask(const std::shared_ptr<Listener>& listener, |
| ::base::WaitableEvent* done) { |
| ALOGV("%s()", __func__); |
| ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence()); |
| |
| mListener = listener; |
| done->Signal(); |
| } |
| |
| bool V4L2EncodeComponent::initializeEncoder() { |
| ALOGV("%s()", __func__); |
| ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence()); |
| ALOG_ASSERT(!mInputFormatConverter); |
| ALOG_ASSERT(!mEncoder); |
| |
| mLastFrameTime = std::nullopt; |
| |
| // Get the requested profile and level. |
| C2Config::profile_t outputProfile = mInterface->getOutputProfile(); |
| |
| // CSD only needs to be extracted when using an H.264 profile. |
| mExtractCSD = IsH264Profile(outputProfile); |
| |
| std::optional<uint8_t> h264Level; |
| if (IsH264Profile(outputProfile)) { |
| h264Level = c2LevelToV4L2Level(mInterface->getOutputLevel()); |
| } |
| |
| // Get the stride used by the C2 framework, as this might be different from the stride used by |
| // the V4L2 encoder. |
| std::optional<uint32_t> stride = |
| getVideoFrameStride(kInputPixelFormat, mInterface->getInputVisibleSize()); |
| if (!stride) { |
| ALOGE("Failed to get video frame stride"); |
| reportError(C2_CORRUPTED); |
| return false; |
| } |
| |
| // Get the requested bitrate mode and bitrate. The C2 framework doesn't offer a parameter to |
| // configure the peak bitrate, so we use a multiple of the target bitrate. |
| mBitrateMode = mInterface->getBitrateMode(); |
| if (property_get_bool("persist.vendor.v4l2_codec2.disable_vbr", false)) { |
| // NOTE: This is a workaround for b/235771157. |
| ALOGW("VBR is disabled on this device"); |
| mBitrateMode = C2Config::BITRATE_CONST; |
| } |
| |
| mBitrate = mInterface->getBitrate(); |
| |
| mEncoder = V4L2Encoder::create( |
| outputProfile, h264Level, mInterface->getInputVisibleSize(), *stride, |
| mInterface->getKeyFramePeriod(), mBitrateMode, mBitrate, |
| mBitrate * kPeakBitrateMultiplier, |
| ::base::BindRepeating(&V4L2EncodeComponent::fetchOutputBlock, mWeakThis), |
| ::base::BindRepeating(&V4L2EncodeComponent::onInputBufferDone, mWeakThis), |
| ::base::BindRepeating(&V4L2EncodeComponent::onOutputBufferDone, mWeakThis), |
| ::base::BindRepeating(&V4L2EncodeComponent::onDrainDone, mWeakThis), |
| ::base::BindRepeating(&V4L2EncodeComponent::reportError, mWeakThis, C2_CORRUPTED), |
| mEncoderTaskRunner); |
| if (!mEncoder) { |
| ALOGE("Failed to create V4L2Encoder (profile: %s)", profileToString(outputProfile)); |
| return false; |
| } |
| |
| // Add an input format convertor if the device doesn't support the requested input format. |
| ALOGV("Creating input format convertor (%s)", |
| videoPixelFormatToString(mEncoder->inputFormat()).c_str()); |
| mInputFormatConverter = |
| FormatConverter::Create(mEncoder->inputFormat(), mEncoder->visibleSize(), |
| V4L2Encoder::kInputBufferCount, mEncoder->codedSize()); |
| if (!mInputFormatConverter) { |
| ALOGE("Failed to created input format convertor"); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool V4L2EncodeComponent::updateEncodingParameters() { |
| ALOGV("%s()", __func__); |
| ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence()); |
| |
| // Ask device to change bitrate if it's different from the currently configured bitrate. The C2 |
| // framework doesn't offer a parameter to configure the peak bitrate, so we'll use a multiple of |
| // the target bitrate here. The peak bitrate is only used if the bitrate mode is set to VBR. |
| uint32_t bitrate = mInterface->getBitrate(); |
| if (mBitrate != bitrate) { |
| ALOG_ASSERT(bitrate > 0u); |
| ALOGV("Setting bitrate to %u", bitrate); |
| if (!mEncoder->setBitrate(bitrate)) { |
| reportError(C2_CORRUPTED); |
| return false; |
| } |
| mBitrate = bitrate; |
| |
| if (mBitrateMode == C2Config::BITRATE_VARIABLE) { |
| ALOGV("Setting peak bitrate to %u", bitrate * kPeakBitrateMultiplier); |
| // TODO(b/190336806): Our stack doesn't support dynamic peak bitrate changes yet, ignore |
| // errors for now. |
| mEncoder->setPeakBitrate(bitrate * kPeakBitrateMultiplier); |
| } |
| } |
| |
| // Ask device to change framerate if it's different from the currently configured framerate. |
| uint32_t framerate = static_cast<uint32_t>(std::round(mInterface->getFramerate())); |
| if (mFramerate != framerate) { |
| ALOG_ASSERT(framerate > 0u); |
| ALOGV("Setting framerate to %u", framerate); |
| if (!mEncoder->setFramerate(framerate)) { |
| ALOGE("Requesting framerate change failed"); |
| reportError(C2_CORRUPTED); |
| return false; |
| } |
| mFramerate = framerate; |
| } |
| |
| // Check whether an explicit key frame was requested, if so reset the key frame counter to |
| // immediately request a key frame. |
| C2StreamRequestSyncFrameTuning::output requestKeyFrame; |
| c2_status_t status = mInterface->query({&requestKeyFrame}, {}, C2_DONT_BLOCK, nullptr); |
| if (status != C2_OK) { |
| ALOGE("Failed to query interface for key frame request (error code: %d)", status); |
| reportError(status); |
| return false; |
| } |
| if (requestKeyFrame.value == C2_TRUE) { |
| mEncoder->requestKeyframe(); |
| requestKeyFrame.value = C2_FALSE; |
| std::vector<std::unique_ptr<C2SettingResult>> failures; |
| status = mInterface->config({&requestKeyFrame}, C2_MAY_BLOCK, &failures); |
| if (status != C2_OK) { |
| ALOGE("Failed to reset key frame request on interface (error code: %d)", status); |
| reportError(status); |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| bool V4L2EncodeComponent::encode(C2ConstGraphicBlock block, uint64_t index, int64_t timestamp) { |
| ALOGV("%s()", __func__); |
| ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence()); |
| ALOG_ASSERT(mEncoder); |
| |
| ALOGV("Encoding input block (index: %" PRIu64 ", timestamp: %" PRId64 ", size: %dx%d)", index, |
| timestamp, block.width(), block.height()); |
| |
| // Dynamically adjust framerate based on the frame's timestamp if required. |
| constexpr int64_t kMaxFramerateDiff = 5; |
| if (mLastFrameTime && (timestamp > *mLastFrameTime)) { |
| int64_t newFramerate = std::max( |
| static_cast<int64_t>(std::round(1000000.0 / (timestamp - *mLastFrameTime))), |
| static_cast<int64_t>(1LL)); |
| if (abs(mFramerate - newFramerate) > kMaxFramerateDiff) { |
| ALOGV("Adjusting framerate to %" PRId64 " based on frame timestamps", newFramerate); |
| mInterface->setFramerate(static_cast<uint32_t>(newFramerate)); |
| } |
| } |
| mLastFrameTime = timestamp; |
| |
| // Update dynamic encoding parameters (bitrate, framerate, key frame) if requested. |
| if (!updateEncodingParameters()) return false; |
| |
| // Create an input frame from the graphic block. |
| std::unique_ptr<V4L2Encoder::InputFrame> frame = CreateInputFrame(block, index, timestamp); |
| if (!frame) { |
| ALOGE("Failed to create video frame from input block (index: %" PRIu64 |
| ", timestamp: %" PRId64 ")", |
| index, timestamp); |
| reportError(C2_CORRUPTED); |
| return false; |
| } |
| |
| if (!mEncoder->encode(std::move(frame))) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void V4L2EncodeComponent::flush() { |
| ALOGV("%s()", __func__); |
| ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence()); |
| |
| mEncoder->flush(); |
| |
| // Report all queued work items as aborted. |
| std::list<std::unique_ptr<C2Work>> abortedWorkItems; |
| while (!mInputConverterQueue.empty()) { |
| std::unique_ptr<C2Work> work = std::move(mInputConverterQueue.front()); |
| work->result = C2_NOT_FOUND; |
| work->input.buffers.clear(); |
| abortedWorkItems.push_back(std::move(work)); |
| mInputConverterQueue.pop(); |
| } |
| while (!mWorkQueue.empty()) { |
| std::unique_ptr<C2Work> work = std::move(mWorkQueue.front()); |
| // Return buffer to the input format convertor if required. |
| if (mInputFormatConverter && work->input.buffers.empty()) { |
| mInputFormatConverter->returnBlock(work->input.ordinal.frameIndex.peeku()); |
| } |
| work->result = C2_NOT_FOUND; |
| work->input.buffers.clear(); |
| abortedWorkItems.push_back(std::move(work)); |
| mWorkQueue.pop_front(); |
| } |
| if (!abortedWorkItems.empty()) { |
| mListener->onWorkDone_nb(weak_from_this(), std::move(abortedWorkItems)); |
| } |
| } |
| |
| void V4L2EncodeComponent::fetchOutputBlock(uint32_t size, |
| std::unique_ptr<BitstreamBuffer>* buffer) { |
| ALOGV("Fetching linear block (size: %u)", size); |
| std::shared_ptr<C2LinearBlock> block; |
| c2_status_t status = mOutputBlockPool->fetchLinearBlock( |
| size, |
| C2MemoryUsage(C2MemoryUsage::CPU_READ | |
| static_cast<uint64_t>(BufferUsage::VIDEO_ENCODER)), |
| &block); |
| if (status != C2_OK) { |
| ALOGE("Failed to fetch linear block (error: %d)", status); |
| reportError(status); |
| } |
| |
| *buffer = std::make_unique<BitstreamBuffer>(std::move(block), 0, size); |
| } |
| |
| void V4L2EncodeComponent::onInputBufferDone(uint64_t index) { |
| ALOGV("%s(): Input buffer done (index: %" PRIu64 ")", __func__, index); |
| ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence()); |
| ALOG_ASSERT(mEncoder); |
| |
| // There are no guarantees the input buffers are returned in order, so we need to find the work |
| // item which this buffer belongs to. |
| C2Work* work = getWorkByIndex(index); |
| if (!work) { |
| ALOGE("Failed to find work associated with input buffer %" PRIu64, index); |
| reportError(C2_CORRUPTED); |
| return; |
| } |
| |
| // We're done using the input block, release reference to return the block to the client. |
| LOG_ASSERT(!work->input.buffers.empty()); |
| work->input.buffers.front().reset(); |
| |
| // Return the block to the convertor if required. If we have buffers awaiting conversion, we can |
| // now attempt to convert and encode them again. |
| if (mInputFormatConverter) { |
| c2_status_t status = mInputFormatConverter->returnBlock(index); |
| if (status != C2_OK) { |
| reportError(status); |
| return; |
| } |
| while (!mInputConverterQueue.empty() && mInputFormatConverter->isReady()) { |
| std::unique_ptr<C2Work> work = std::move(mInputConverterQueue.front()); |
| mInputConverterQueue.pop(); |
| queueTask(std::move(work)); |
| } |
| } |
| |
| // Return all completed work items. The work item might have been waiting for it's input buffer |
| // to be returned, in which case we can report it as completed now. As input buffers are not |
| // necessarily returned in order we might be able to return multiple ready work items now. |
| while (!mWorkQueue.empty() && isWorkDone(*mWorkQueue.front())) { |
| reportWork(std::move(mWorkQueue.front())); |
| mWorkQueue.pop_front(); |
| } |
| } |
| |
| void V4L2EncodeComponent::onOutputBufferDone(size_t dataSize, int64_t timestamp, bool keyFrame, |
| std::unique_ptr<BitstreamBuffer> buffer) { |
| ALOGV("%s(): output buffer done (timestamp: %" PRId64 ", size: %zu, keyframe: %d)", __func__, |
| timestamp, dataSize, keyFrame); |
| ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence()); |
| ALOG_ASSERT(buffer->dmabuf); |
| |
| C2ConstLinearBlock constBlock = |
| buffer->dmabuf->share(buffer->dmabuf->offset(), dataSize, C2Fence()); |
| |
| // If no CSD (content-specific-data, e.g. SPS for H.264) has been submitted yet, we expect this |
| // output block to contain CSD. We only submit the CSD once, even if it's attached to each key |
| // frame. |
| if (mExtractCSD) { |
| ALOGV("No CSD submitted yet, extracting CSD"); |
| std::unique_ptr<C2StreamInitDataInfo::output> csd; |
| C2ReadView view = constBlock.map().get(); |
| if (!extractCSDInfo(&csd, view.data(), view.capacity())) { |
| ALOGE("Failed to extract CSD"); |
| reportError(C2_CORRUPTED); |
| return; |
| } |
| |
| // Attach the CSD to the first item in our output work queue. |
| LOG_ASSERT(!mWorkQueue.empty()); |
| C2Work* work = mWorkQueue.front().get(); |
| work->worklets.front()->output.configUpdate.push_back(std::move(csd)); |
| mExtractCSD = false; |
| } |
| |
| // Get the work item associated with the timestamp. |
| C2Work* work = getWorkByTimestamp(timestamp); |
| if (!work) { |
| // It's possible we got an empty CSD request with timestamp 0, which we currently just |
| // discard. |
| if (timestamp != 0) { |
| reportError(C2_CORRUPTED); |
| } |
| return; |
| } |
| |
| std::shared_ptr<C2Buffer> linearBuffer = C2Buffer::CreateLinearBuffer(std::move(constBlock)); |
| if (!linearBuffer) { |
| ALOGE("Failed to create linear buffer from block"); |
| reportError(C2_CORRUPTED); |
| return; |
| } |
| |
| if (keyFrame) { |
| linearBuffer->setInfo( |
| std::make_shared<C2StreamPictureTypeMaskInfo::output>(0u, C2Config::SYNC_FRAME)); |
| } |
| work->worklets.front()->output.buffers.emplace_back(std::move(linearBuffer)); |
| |
| // We can report the work item as completed if its associated input buffer has also been |
| // released. As output buffers are not necessarily returned in order we might be able to return |
| // multiple ready work items now. |
| while (!mWorkQueue.empty() && isWorkDone(*mWorkQueue.front())) { |
| reportWork(std::move(mWorkQueue.front())); |
| mWorkQueue.pop_front(); |
| } |
| } |
| |
| C2Work* V4L2EncodeComponent::getWorkByIndex(uint64_t index) { |
| ALOGV("%s(): getting work item (index: %" PRIu64 ")", __func__, index); |
| ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence()); |
| |
| auto it = std::find_if(mWorkQueue.begin(), mWorkQueue.end(), |
| [index](const std::unique_ptr<C2Work>& w) { |
| return w->input.ordinal.frameIndex.peeku() == index; |
| }); |
| if (it == mWorkQueue.end()) { |
| ALOGE("Failed to find work (index: %" PRIu64 ")", index); |
| return nullptr; |
| } |
| return it->get(); |
| } |
| |
| C2Work* V4L2EncodeComponent::getWorkByTimestamp(int64_t timestamp) { |
| ALOGV("%s(): getting work item (timestamp: %" PRId64 ")", __func__, timestamp); |
| ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence()); |
| ALOG_ASSERT(timestamp >= 0); |
| |
| // Find the work with specified timestamp by looping over the output work queue. This should be |
| // very fast as the output work queue will never be longer then a few items. Ignore empty work |
| // items that are marked as EOS, as their timestamp might clash with other work items. |
| auto it = std::find_if( |
| mWorkQueue.begin(), mWorkQueue.end(), [timestamp](const std::unique_ptr<C2Work>& w) { |
| return !(w->input.flags & C2FrameData::FLAG_END_OF_STREAM) && |
| w->input.ordinal.timestamp.peeku() == static_cast<uint64_t>(timestamp); |
| }); |
| if (it == mWorkQueue.end()) { |
| ALOGE("Failed to find work (timestamp: %" PRIu64 ")", timestamp); |
| return nullptr; |
| } |
| return it->get(); |
| } |
| |
| bool V4L2EncodeComponent::isWorkDone(const C2Work& work) const { |
| ALOGV("%s()", __func__); |
| ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence()); |
| |
| if ((work.input.flags & C2FrameData::FLAG_END_OF_STREAM) && |
| !(work.worklets.front()->output.flags & C2FrameData::FLAG_END_OF_STREAM)) { |
| ALOGV("Work item %" PRIu64 " is marked as EOS but draining has not finished yet", |
| work.input.ordinal.frameIndex.peeku()); |
| return false; |
| } |
| |
| if (!work.input.buffers.empty() && work.input.buffers.front()) { |
| ALOGV("Input buffer associated with work item %" PRIu64 " not returned yet", |
| work.input.ordinal.frameIndex.peeku()); |
| return false; |
| } |
| |
| // If the work item had an input buffer to be encoded, it should have an output buffer set. |
| if (!work.input.buffers.empty() && work.worklets.front()->output.buffers.empty()) { |
| ALOGV("Output buffer associated with work item %" PRIu64 " not returned yet", |
| work.input.ordinal.frameIndex.peeku()); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void V4L2EncodeComponent::reportWork(std::unique_ptr<C2Work> work) { |
| ALOG_ASSERT(work); |
| ALOGV("%s(): Reporting work item as finished (index: %llu, timestamp: %llu)", __func__, |
| work->input.ordinal.frameIndex.peekull(), work->input.ordinal.timestamp.peekull()); |
| ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence()); |
| |
| work->result = C2_OK; |
| work->workletsProcessed = static_cast<uint32_t>(work->worklets.size()); |
| |
| std::list<std::unique_ptr<C2Work>> finishedWorkList; |
| finishedWorkList.emplace_back(std::move(work)); |
| mListener->onWorkDone_nb(weak_from_this(), std::move(finishedWorkList)); |
| } |
| |
| bool V4L2EncodeComponent::getBlockPool() { |
| ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence()); |
| |
| auto sharedThis = weak_from_this().lock(); |
| if (!sharedThis) { |
| ALOGI("%s(): V4L2EncodeComponent instance is already destroyed", __func__); |
| return false; |
| } |
| |
| C2BlockPool::local_id_t poolId = mInterface->getBlockPoolId(); |
| if (poolId == C2BlockPool::BASIC_LINEAR) { |
| ALOGW("Using unoptimized linear block pool"); |
| } |
| c2_status_t status = GetCodec2BlockPool(poolId, std::move(sharedThis), &mOutputBlockPool); |
| if (status != C2_OK || !mOutputBlockPool) { |
| ALOGE("Failed to get output block pool, error: %d", status); |
| return false; |
| } |
| return true; |
| } |
| |
| void V4L2EncodeComponent::reportError(c2_status_t error) { |
| ALOGV("%s()", __func__); |
| ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence()); |
| |
| // TODO(dstaessens): Report all pending work items as finished upon failure. |
| std::lock_guard<std::mutex> lock(mComponentLock); |
| if (mComponentState != ComponentState::ERROR) { |
| setComponentState(ComponentState::ERROR); |
| mListener->onError_nb(weak_from_this(), static_cast<uint32_t>(error)); |
| } |
| } |
| |
| void V4L2EncodeComponent::setComponentState(ComponentState state) { |
| // Check whether the state change is valid. |
| switch (state) { |
| case ComponentState::UNLOADED: |
| ALOG_ASSERT(mComponentState == ComponentState::LOADED); |
| break; |
| case ComponentState::LOADED: |
| ALOG_ASSERT(mComponentState == ComponentState::UNLOADED || |
| mComponentState == ComponentState::RUNNING || |
| mComponentState == ComponentState::ERROR); |
| break; |
| case ComponentState::RUNNING: |
| ALOG_ASSERT(mComponentState == ComponentState::LOADED); |
| break; |
| case ComponentState::ERROR: |
| break; |
| } |
| |
| ALOGV("Changed component state from %s to %s", componentStateToString(mComponentState), |
| componentStateToString(state)); |
| mComponentState = state; |
| } |
| |
| const char* V4L2EncodeComponent::componentStateToString(V4L2EncodeComponent::ComponentState state) { |
| switch (state) { |
| case ComponentState::UNLOADED: |
| return "UNLOADED"; |
| case ComponentState::LOADED: |
| return "LOADED"; |
| case ComponentState::RUNNING: |
| return "RUNNING"; |
| case ComponentState::ERROR: |
| return "ERROR"; |
| } |
| } |
| |
| } // namespace android |