blob: c13537e1ecb9042abc16c55dbb3158588b647832 [file] [log] [blame]
// 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 <base/bind.h>
#include <base/bind_helpers.h>
#include <log/log.h>
#include <media/stagefright/MediaDefs.h>
#include <ui/GraphicBuffer.h>
#include <fourcc.h>
#include <h264_parser.h>
#include <rect.h>
#include <unaligned_shared_memory.h>
#include <v4l2_codec2/common/Common.h>
#include <v4l2_codec2/common/EncodeHelpers.h>
#include <v4l2_device.h>
#include <video_pixel_format.h>
namespace android {
namespace {
const media::VideoPixelFormat kInputPixelFormat = media::VideoPixelFormat::PIXEL_FORMAT_NV12;
// 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,
media::VideoPixelFormat* format) {
ALOGD("%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 = media::VideoPixelFormat::PIXEL_FORMAT_I420;
} else if (!crcb && semiplanar) {
*format = media::VideoPixelFormat::PIXEL_FORMAT_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 = media::VideoPixelFormat::PIXEL_FORMAT_YV12;
*format = media::VideoPixelFormat::PIXEL_FORMAT_I420;
} else {
*format = media::VideoPixelFormat::PIXEL_FORMAT_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 = media::VideoPixelFormat::PIXEL_FORMAT_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) {
planes.push_back({offsets[i], strides[i]});
}
return planes;
}
const uint8_t kH264StartCode[] = {0, 0, 0, 1};
const size_t kH264StartCodeSize = sizeof(kH264StartCode);
// Copy a H.264 NALU of size |srcSize| (without start code), located at |src|,into a buffer starting
// at |dst| of size |dstSize|, prepending it with a H.264 start code (as long as both fit). After
// copying, update |dst| to point to the address immediately after the copied data, and update
// |dstSize| to contain remaining destination buffer size.
void copyNALUPrependingStartCode(const uint8_t* src, size_t srcSize, uint8_t** dst,
size_t* dstSize) {
ALOGD("%s()", __func__);
size_t sizeToCopy = kH264StartCodeSize + srcSize;
if (sizeToCopy > *dstSize) {
ALOGE("Could not copy a NALU, not enough space in destination buffer");
return;
}
memcpy(*dst, kH264StartCode, kH264StartCodeSize);
memcpy(*dst + kH264StartCodeSize, src, srcSize);
*dst += sizeToCopy;
*dstSize -= sizeToCopy;
}
// The maximum size for output buffer, which is chosen empirically for a 1080p video.
constexpr size_t kMaxBitstreamBufferSizeInBytes = 2 * 1024 * 1024; // 2MB
// The frame size for 1080p (FHD) video in pixels.
constexpr int k1080PSizeInPixels = 1920 * 1080;
// The frame size for 1440p (QHD) video in pixels.
constexpr int k1440PSizeInPixels = 2560 * 1440;
// Use quadruple size of kMaxBitstreamBufferSizeInBytes when the input frame size is larger than
// 1440p, double if larger than 1080p. This is chosen empirically for some 4k encoding use cases and
// the Android CTS VideoEncoderTest (crbug.com/927284).
size_t GetMaxOutputBufferSize(const media::Size& size) {
if (size.GetArea() > k1440PSizeInPixels) return kMaxBitstreamBufferSizeInBytes * 4;
if (size.GetArea() > k1080PSizeInPixels) return kMaxBitstreamBufferSizeInBytes * 2;
return kMaxBitstreamBufferSizeInBytes;
}
// These are rather subjectively tuned.
constexpr size_t kInputBufferCount = 2;
constexpr size_t kOutputBufferCount = 2;
// Define V4L2_CID_MPEG_VIDEO_H264_SPS_PPS_BEFORE_IDR control code if not present in header files.
#ifndef V4L2_CID_MPEG_VIDEO_H264_SPS_PPS_BEFORE_IDR
#define V4L2_CID_MPEG_VIDEO_H264_SPS_PPS_BEFORE_IDR (V4L2_CID_MPEG_BASE + 388)
#endif
} // namespace
// static
std::unique_ptr<V4L2EncodeComponent::InputFrame> V4L2EncodeComponent::InputFrame::Create(
const C2ConstGraphicBlock& block) {
std::vector<::base::ScopedFD> fds;
const C2Handle* const handle = block.handle();
for (int i = 0; i < handle->numFds; i++) {
fds.emplace_back(dup(handle->data[i]));
if (!fds.back().is_valid()) {
ALOGE("Failed to duplicate input graphic block handle %d (errno: %d)", handle->data[i],
errno);
return nullptr;
}
}
return std::unique_ptr<InputFrame>(new InputFrame(std::move(fds)));
}
// 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());
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());
}
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();
}
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());
ALOG_ASSERT(mEncoderState == EncoderState::UNINITIALIZED);
*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 and stop polling and streaming on the V4L2
// device queues.
flush();
// Deallocate all V4L2 device input and output buffers.
destroyInputBuffers();
destroyOutputBuffers();
// Invalidate all weak pointers so no more functions will be executed on the encoder thread.
mWeakThisFactory.InvalidateWeakPtrs();
setEncoderState(EncoderState::UNINITIALIZED);
done->Signal();
}
void V4L2EncodeComponent::queueTask(std::unique_ptr<C2Work> work) {
ALOGV("%s()", __func__);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
ALOG_ASSERT(mEncoderState != EncoderState::UNINITIALIZED);
// If we're in the error state we can immediately return, freeing all buffers in the work item.
if (mEncoderState == EncoderState::ERROR) {
return;
}
ALOGV("Queued work item (index: %llu, timestamp: %llu, EOS: %d)",
work->input.ordinal.frameIndex.peekull(), work->input.ordinal.timestamp.peekull(),
work->input.flags & C2FrameData::FLAG_END_OF_STREAM);
mInputWorkQueue.push(std::move(work));
// If we were waiting for work, start encoding again.
if (mEncoderState == EncoderState::WAITING_FOR_INPUT) {
setEncoderState(EncoderState::ENCODING);
mEncoderTaskRunner->PostTask(
FROM_HERE,
::base::BindOnce(&V4L2EncodeComponent::scheduleNextEncodeTask, mWeakThis));
}
}
// TODO(dstaessens): Investigate improving drain logic after draining the virtio device is fixed.
void V4L2EncodeComponent::drainTask(drain_mode_t /*drainMode*/) {
ALOGV("%s()", __func__);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
// We can only start draining if all the work in our input queue has been queued on the V4L2
// device input queue, so we mark the last item in the input queue as EOS.
if (!mInputWorkQueue.empty()) {
ALOGV("Marking last item in input work queue as EOS");
mInputWorkQueue.back()->input.flags = static_cast<C2FrameData::flags_t>(
mInputWorkQueue.back()->input.flags | C2FrameData::FLAG_END_OF_STREAM);
return;
}
// If the input queue is empty and there is only a single empty EOS work item in the output
// queue we can immediately consider flushing done.
if ((mOutputWorkQueue.size() == 1) && mOutputWorkQueue.back()->input.buffers.empty()) {
ALOG_ASSERT(mOutputWorkQueue.back()->input.flags & C2FrameData::FLAG_END_OF_STREAM);
setEncoderState(EncoderState::DRAINING);
mEncoderTaskRunner->PostTask(
FROM_HERE, ::base::BindOnce(&V4L2EncodeComponent::onDrainDone, mWeakThis, true));
return;
}
// If the input queue is empty all work that needs to be drained has already been queued in the
// V4L2 device, so we can immediately request a drain.
if (!mOutputWorkQueue.empty()) {
// Mark the last item in the output work queue as EOS, so we will only report it as
// finished after draining has completed.
ALOGV("Starting drain and marking last item in output work queue as EOS");
mOutputWorkQueue.back()->input.flags = C2FrameData::FLAG_END_OF_STREAM;
drain();
}
}
void V4L2EncodeComponent::onDrainDone(bool done) {
ALOGV("%s()", __func__);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
ALOG_ASSERT(mEncoderState == EncoderState::DRAINING || mEncoderState == EncoderState::ERROR);
if (mEncoderState == EncoderState::ERROR) {
return;
}
if (!done) {
ALOGE("draining the encoder failed");
reportError(C2_CORRUPTED);
return;
}
// All work that needs to be drained should be ready now, which means the output work queue
// should only contain a single item which is marked as EOS.
if (mOutputWorkQueue.size() != 1u ||
!(mOutputWorkQueue.front()->input.flags & C2FrameData::FLAG_END_OF_STREAM)) {
ALOGE("Output work queue should contain a single EOS item after draining");
reportError(C2_CORRUPTED);
return;
}
std::unique_ptr<C2Work> eosWork = std::move(mOutputWorkQueue.front());
mOutputWorkQueue.pop_front();
eosWork->worklets.front()->output.flags = C2FrameData::FLAG_END_OF_STREAM;
if (!isWorkDone(*eosWork)) {
ALOGE("EOS work item should be done after draining");
reportError(C2_CORRUPTED);
return;
}
reportWork(std::move(eosWork));
// Draining the encoder is now done, we can start encoding again.
if (!mInputWorkQueue.empty()) {
setEncoderState(EncoderState::ENCODING);
mEncoderTaskRunner->PostTask(
FROM_HERE,
::base::BindOnce(&V4L2EncodeComponent::scheduleNextEncodeTask, mWeakThis));
} else {
setEncoderState(EncoderState::WAITING_FOR_INPUT);
}
}
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 (!mInputWorkQueue.empty()) {
std::unique_ptr<C2Work> work = std::move(mInputWorkQueue.front());
work->input.buffers.clear();
flushedWork->push_back(std::move(work));
mInputWorkQueue.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(mEncoderState == EncoderState::UNINITIALIZED);
mVisibleSize = mInterface->getInputVisibleSize();
mKeyFramePeriod = mInterface->getKeyFramePeriod();
mKeyFrameCounter = 0;
mCSDSubmitted = false;
// Open the V4L2 device for encoding to the requested output format.
// TODO(dstaessens): Do we need to close the device first if already opened?
// TODO(dstaessens): Avoid conversion to VideoCodecProfile and use C2Config::profile_t directly.
media::VideoCodecProfile outputProfile =
c2ProfileToVideoCodecProfile(mInterface->getOutputProfile());
uint32_t outputPixelFormat =
media::V4L2Device::VideoCodecProfileToV4L2PixFmt(outputProfile, false);
if (!outputPixelFormat) {
ALOGE("Invalid output profile %s", media::GetProfileName(outputProfile).c_str());
return false;
}
mDevice = media::V4L2Device::Create();
if (!mDevice) {
ALOGE("Failed to create V4L2 device");
return false;
}
if (!mDevice->Open(media::V4L2Device::Type::kEncoder, outputPixelFormat)) {
ALOGE("Failed to open device for profile %s (%s)",
media::GetProfileName(outputProfile).c_str(),
media::FourccToString(outputPixelFormat).c_str());
return false;
}
// Make sure the device has all required capabilities (multi-planar Memory-To-Memory and
// streaming I/O), and whether flushing is supported.
if (!mDevice->HasCapabilities(V4L2_CAP_VIDEO_M2M_MPLANE | V4L2_CAP_STREAMING)) {
ALOGE("Device doesn't have the required capabilities");
return false;
}
if (!mDevice->IsCommandSupported(V4L2_ENC_CMD_STOP)) {
ALOGE("Device does not support flushing (V4L2_ENC_CMD_STOP)");
return false;
}
// Get input/output queues so we can send encode request to the device and get back the results.
mInputQueue = mDevice->GetQueue(V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE);
mOutputQueue = mDevice->GetQueue(V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE);
if (!mInputQueue || !mOutputQueue) {
ALOGE("Failed to get V4L2 device queues");
return false;
}
// First try to configure the specified output format, as changing the output format can affect
// the configured input format.
if (!configureOutputFormat(outputProfile)) return false;
// Configure the input format. If the device doesn't support the specified format we'll use one
// of the device's preferred formats in combination with an input format convertor.
if (!configureInputFormat(kInputPixelFormat)) return false;
// Create input and output buffers.
// TODO(dstaessens): Avoid allocating output buffers, encode directly into blockpool buffers.
if (!createInputBuffers() || !createOutputBuffers()) return false;
// Configure the device, setting all required controls.
uint8_t level = c2LevelToLevelIDC(mInterface->getOutputLevel());
if (!configureDevice(outputProfile, level)) return false;
// We're ready to start encoding now.
setEncoderState(EncoderState::WAITING_FOR_INPUT);
// As initialization is asynchronous work might have already be queued.
if (!mInputWorkQueue.empty()) {
setEncoderState(EncoderState::ENCODING);
mEncoderTaskRunner->PostTask(
FROM_HERE, ::base::Bind(&V4L2EncodeComponent::scheduleNextEncodeTask, mWeakThis));
}
return true;
}
bool V4L2EncodeComponent::configureInputFormat(media::VideoPixelFormat inputFormat) {
ALOGV("%s()", __func__);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
ALOG_ASSERT(mEncoderState == EncoderState::UNINITIALIZED);
ALOG_ASSERT(!mInputQueue->IsStreaming());
ALOG_ASSERT(!mVisibleSize.IsEmpty());
ALOG_ASSERT(!mInputFormatConverter);
// First try to use the requested pixel format directly.
::base::Optional<struct v4l2_format> format;
auto fourcc = media::Fourcc::FromVideoPixelFormat(inputFormat, false);
if (fourcc) {
format = mInputQueue->SetFormat(fourcc->ToV4L2PixFmt(), mVisibleSize, 0);
}
// If the device doesn't support the requested input format we'll try the device's preferred
// input pixel formats and use a format convertor. We need to try all formats as some formats
// might not be supported for the configured output format.
if (!format) {
std::vector<uint32_t> preferredFormats =
mDevice->PreferredInputFormat(media::V4L2Device::Type::kEncoder);
for (uint32_t i = 0; !format && i < preferredFormats.size(); ++i) {
format = mInputQueue->SetFormat(preferredFormats[i], mVisibleSize, 0);
}
}
if (!format) {
ALOGE("Failed to set input format to %s",
media::VideoPixelFormatToString(inputFormat).c_str());
return false;
}
// Check whether the negotiated input format is valid. The coded size might be adjusted to match
// encoder minimums, maximums and alignment requirements of the currently selected formats.
auto layout = media::V4L2Device::V4L2FormatToVideoFrameLayout(*format);
if (!layout) {
ALOGE("Invalid input layout");
return false;
}
mInputLayout = layout.value();
if (!media::Rect(mInputLayout->coded_size()).Contains(media::Rect(mVisibleSize))) {
ALOGE("Input size %s exceeds encoder capability, encoder can handle %s",
mVisibleSize.ToString().c_str(), mInputLayout->coded_size().ToString().c_str());
return false;
}
// Calculate the input coded size from the format.
// TODO(dstaessens): How is this different from mInputLayout->coded_size()?
mInputCodedSize = media::V4L2Device::AllocatedSizeFromV4L2Format(*format);
// Add an input format convertor if the device doesn't support the requested input format.
// Note: The amount of input buffers in the convertor should match the amount of buffers on the
// device input queue, to simplify logic.
// TODO(dstaessens): Currently an input format convertor is always required. Mapping an input
// buffer always seems to fail unless we copy it into a new a buffer first. As a temporary
// workaround the line below is commented, but this should be undone once the issue is fixed.
//if (mInputLayout->format() != inputFormat) {
ALOGV("Creating input format convertor (%s -> %s)",
media::VideoPixelFormatToString(mInputLayout->format()).c_str(),
media::VideoPixelFormatToString(inputFormat).c_str());
mInputFormatConverter =
FormatConverter::Create(inputFormat, mVisibleSize, kInputBufferCount, mInputCodedSize);
if (!mInputFormatConverter) {
ALOGE("Failed to created input format convertor");
return false;
}
//}
// The coded input size might be different from the visible size due to alignment requirements,
// So we need to specify the visible rectangle. Note that this rectangle might still be adjusted
// due to hardware limitations.
// TODO(dstaessens): Overwrite mVisibleSize with the adapted visible size here?
media::Rect visibleRectangle(mVisibleSize.width(), mVisibleSize.height());
struct v4l2_rect rect;
rect.left = visibleRectangle.x();
rect.top = visibleRectangle.y();
rect.width = visibleRectangle.width();
rect.height = visibleRectangle.height();
// Try to adjust the visible rectangle using the VIDIOC_S_SELECTION command. If this is not
// supported we'll try to use the VIDIOC_S_CROP command instead. The visible rectangle might be
// adjusted to conform to hardware limitations (e.g. round to closest horizontal and vertical
// offsets, width and height).
struct v4l2_selection selection_arg;
memset(&selection_arg, 0, sizeof(selection_arg));
selection_arg.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
selection_arg.target = V4L2_SEL_TGT_CROP;
selection_arg.r = rect;
if (mDevice->Ioctl(VIDIOC_S_SELECTION, &selection_arg) == 0) {
visibleRectangle = media::Rect(selection_arg.r.left, selection_arg.r.top,
selection_arg.r.width, selection_arg.r.height);
} else {
struct v4l2_crop crop;
memset(&crop, 0, sizeof(v4l2_crop));
crop.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE;
crop.c = rect;
if (mDevice->Ioctl(VIDIOC_S_CROP, &crop) != 0 ||
mDevice->Ioctl(VIDIOC_G_CROP, &crop) != 0) {
ALOGE("Failed to crop to specified visible rectangle");
return false;
}
visibleRectangle = media::Rect(crop.c.left, crop.c.top, crop.c.width, crop.c.height);
}
ALOGV("Input format set to %s (size: %s, adjusted size: %dx%d, coded size: %s)",
media::VideoPixelFormatToString(mInputLayout->format()).c_str(),
mVisibleSize.ToString().c_str(), visibleRectangle.width(), visibleRectangle.height(),
mInputCodedSize.ToString().c_str());
mVisibleSize.SetSize(visibleRectangle.width(), visibleRectangle.height());
return true;
}
bool V4L2EncodeComponent::configureOutputFormat(media::VideoCodecProfile outputProfile) {
ALOGV("%s()", __func__);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
ALOG_ASSERT(mEncoderState == EncoderState::UNINITIALIZED);
ALOG_ASSERT(!mOutputQueue->IsStreaming());
ALOG_ASSERT(!mVisibleSize.IsEmpty());
auto format = mOutputQueue->SetFormat(
media::V4L2Device::VideoCodecProfileToV4L2PixFmt(outputProfile, false), mVisibleSize,
GetMaxOutputBufferSize(mVisibleSize));
if (!format) {
ALOGE("Failed to set output format to %s", media::GetProfileName(outputProfile).c_str());
return false;
}
// The device might adjust the requested output buffer size to match hardware requirements.
mOutputBufferSize = ::base::checked_cast<size_t>(format->fmt.pix_mp.plane_fmt[0].sizeimage);
ALOGV("Output format set to %s (buffer size: %u)", media::GetProfileName(outputProfile).c_str(),
mOutputBufferSize);
return true;
}
bool V4L2EncodeComponent::configureDevice(media::VideoCodecProfile outputProfile,
std::optional<const uint8_t> outputH264Level) {
ALOGV("%s()", __func__);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
// Enable frame-level bitrate control. This is the only mandatory general control.
if (!mDevice->SetExtCtrls(V4L2_CTRL_CLASS_MPEG,
{media::V4L2ExtCtrl(V4L2_CID_MPEG_VIDEO_FRAME_RC_ENABLE, 1)})) {
ALOGW("Failed enabling bitrate control");
// TODO(b/161508368): V4L2_CID_MPEG_VIDEO_FRAME_RC_ENABLE is currently not supported yet,
// assume the operation was successful for now.
}
// Additional optional controls:
// - Enable macroblock-level bitrate control.
// - Set GOP length to 0 to disable periodic key frames.
mDevice->SetExtCtrls(V4L2_CTRL_CLASS_MPEG,
{media::V4L2ExtCtrl(V4L2_CID_MPEG_VIDEO_MB_RC_ENABLE, 1),
media::V4L2ExtCtrl(V4L2_CID_MPEG_VIDEO_GOP_SIZE, 0)});
// All controls below are H.264-specific, so we can return here if the profile is not H.264.
if (outputProfile < media::H264PROFILE_MIN || outputProfile > media::H264PROFILE_MAX) {
return true;
}
// When encoding H.264 we want to prepend SPS and PPS to each IDR for resilience. Some
// devices support this through the V4L2_CID_MPEG_VIDEO_H264_SPS_PPS_BEFORE_IDR control.
// Otherwise we have to cache the latest SPS and PPS and inject these manually.
if (mDevice->IsCtrlExposed(V4L2_CID_MPEG_VIDEO_H264_SPS_PPS_BEFORE_IDR)) {
if (!mDevice->SetExtCtrls(
V4L2_CTRL_CLASS_MPEG,
{media::V4L2ExtCtrl(V4L2_CID_MPEG_VIDEO_H264_SPS_PPS_BEFORE_IDR, 1)})) {
ALOGE("Failed to configure device to prepend SPS and PPS to each IDR");
return false;
}
mInjectParamsBeforeIDR = false;
ALOGV("Device supports prepending SPS and PPS to each IDR");
} else {
mInjectParamsBeforeIDR = true;
ALOGV("Device doesn't support prepending SPS and PPS to IDR, injecting manually.");
}
std::vector<media::V4L2ExtCtrl> h264Ctrls;
// No B-frames, for lowest decoding latency.
h264Ctrls.emplace_back(V4L2_CID_MPEG_VIDEO_B_FRAMES, 0);
// Quantization parameter maximum value (for variable bitrate control).
h264Ctrls.emplace_back(V4L2_CID_MPEG_VIDEO_H264_MAX_QP, 51);
// Set H.264 profile.
int32_t profile = media::V4L2Device::VideoCodecProfileToV4L2H264Profile(outputProfile);
if (profile < 0) {
ALOGE("Trying to set invalid H.264 profile");
return false;
}
h264Ctrls.emplace_back(V4L2_CID_MPEG_VIDEO_H264_PROFILE, profile);
// Set H.264 output level. Use Level 4.0 as fallback default.
// TODO(dstaessens): Investigate code added by hiroh@ recently to select level in Chrome VEA.
uint8_t h264Level = outputH264Level.value_or(media::H264SPS::kLevelIDC4p0);
h264Ctrls.emplace_back(V4L2_CID_MPEG_VIDEO_H264_LEVEL,
media::V4L2Device::H264LevelIdcToV4L2H264Level(h264Level));
// Ask not to put SPS and PPS into separate bitstream buffers.
h264Ctrls.emplace_back(V4L2_CID_MPEG_VIDEO_HEADER_MODE,
V4L2_MPEG_VIDEO_HEADER_MODE_JOINED_WITH_1ST_FRAME);
// Ignore return value as these controls are optional.
mDevice->SetExtCtrls(V4L2_CTRL_CLASS_MPEG, std::move(h264Ctrls));
return true;
}
bool V4L2EncodeComponent::updateEncodingParameters() {
ALOGV("%s()", __func__);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
// Query the interface for the encoding parameters requested by the codec 2.0 framework.
C2StreamBitrateInfo::output bitrateInfo;
C2StreamFrameRateInfo::output framerateInfo;
c2_status_t status =
mInterface->query({&bitrateInfo, &framerateInfo}, {}, C2_DONT_BLOCK, nullptr);
if (status != C2_OK) {
ALOGE("Failed to query interface for encoding parameters (error code: %d)", status);
reportError(status);
return false;
}
// Ask device to change bitrate if it's different from the currently configured bitrate.
uint32_t bitrate = bitrateInfo.value;
if (mBitrate != bitrate) {
ALOG_ASSERT(bitrate > 0u);
ALOGV("Setting bitrate to %u", bitrate);
if (!mDevice->SetExtCtrls(V4L2_CTRL_CLASS_MPEG,
{media::V4L2ExtCtrl(V4L2_CID_MPEG_VIDEO_BITRATE, bitrate)})) {
// TODO(b/161495749): V4L2_CID_MPEG_VIDEO_BITRATE is currently not supported yet, assume
// the operation was successful for now.
ALOGW("Requesting bitrate change failed");
}
mBitrate = bitrate;
}
// Ask device to change framerate if it's different from the currently configured framerate.
// TODO(dstaessens): Move IOCTL to device and use helper function.
uint32_t framerate = static_cast<uint32_t>(std::round(framerateInfo.value));
if (mFramerate != framerate) {
ALOG_ASSERT(framerate > 0u);
ALOGV("Setting framerate to %u", framerate);
struct v4l2_streamparm parms;
memset(&parms, 0, sizeof(v4l2_streamparm));
parms.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE;
parms.parm.output.timeperframe.numerator = 1;
parms.parm.output.timeperframe.denominator = framerate;
if (mDevice->Ioctl(VIDIOC_S_PARM, &parms) != 0) {
// TODO(b/161499573): VIDIOC_S_PARM is currently not supported yet, assume the operation
// was successful for now.
ALOGW("Requesting framerate change failed");
}
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;
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) {
mKeyFrameCounter = 0;
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;
}
}
// Request the next frame to be a key frame each time the counter reaches 0.
if (mKeyFrameCounter == 0) {
if (!mDevice->SetExtCtrls(V4L2_CTRL_CLASS_MPEG,
{media::V4L2ExtCtrl(V4L2_CID_MPEG_VIDEO_FORCE_KEY_FRAME)})) {
// TODO(b/161498590): V4L2_CID_MPEG_VIDEO_FORCE_KEY_FRAME is currently not supported
// yet, assume the operation was successful for now.
ALOGW("Failed requesting key frame");
}
}
return true;
}
void V4L2EncodeComponent::scheduleNextEncodeTask() {
ALOGV("%s()", __func__);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
ALOG_ASSERT(mEncoderState == EncoderState::ENCODING || mEncoderState == EncoderState::ERROR);
// If we're in the error state we can immediately return.
if (mEncoderState == EncoderState::ERROR) {
return;
}
// Get the next work item. 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(!mInputWorkQueue.empty());
C2Work* work = mInputWorkQueue.front().get();
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("Scheduling next encode (index: %" PRIu64 ", timestamp: %" PRId64 ", EOS: %d)", index,
timestamp, endOfStream);
if (!work->input.buffers.empty()) {
// Check if the device has free input buffers available. If not we'll switch to the
// WAITING_FOR_INPUT_BUFFERS state, and resume encoding once we're notified buffers are
// available in the onInputBufferDone() task. Note: The input buffers are not copied into
// the device's input buffers, but rather a memory pointer is imported. We still have to
// throttle the number of enqueues queued simultaneously on the device however.
if (mInputQueue->FreeBuffersCount() == 0) {
ALOGV("Waiting for device to return input buffers");
setEncoderState(EncoderState::WAITING_FOR_INPUT_BUFFERS);
return;
}
C2ConstGraphicBlock inputBlock =
work->input.buffers.front()->data().graphicBlocks().front();
// If encoding fails, we'll wait for an event (e.g. input buffers available) to start
// encoding again.
if (!encode(inputBlock, index, timestamp)) {
return;
}
}
// 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.
// TODO(dstaessens): Avoid doing this, store CSD request work at start of output queue.
if (work->input.buffers.empty() && !endOfStream) {
ALOGV("Discarding empty CSD request");
reportWork(std::move(mInputWorkQueue.front()));
} else {
mOutputWorkQueue.push_back(std::move(mInputWorkQueue.front()));
}
mInputWorkQueue.pop();
// Drain the encoder if required.
if (endOfStream) {
drainTask(C2Component::DRAIN_COMPONENT_WITH_EOS);
}
if (mEncoderState == EncoderState::DRAINING) {
return;
} else if (mInputWorkQueue.empty()) {
setEncoderState(EncoderState::WAITING_FOR_INPUT);
return;
}
// Queue the next work item to be encoded.
mEncoderTaskRunner->PostTask(
FROM_HERE, ::base::BindOnce(&V4L2EncodeComponent::scheduleNextEncodeTask, mWeakThis));
}
bool V4L2EncodeComponent::encode(C2ConstGraphicBlock block, uint64_t index, int64_t timestamp) {
ALOGV("%s()", __func__);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
ALOG_ASSERT(mEncoderState == EncoderState::ENCODING);
// Update dynamic encoding parameters (bitrate, framerate, key frame) if requested.
if (!updateEncodingParameters()) return false;
mKeyFrameCounter = (mKeyFrameCounter + 1) % mKeyFramePeriod;
// If required convert the data to the V4L2 device's configured input pixel format. We
// allocate the same amount of buffers on the device input queue and the format convertor,
// so we should never run out of conversion buffers if there are free buffers in the input
// queue.
if (mInputFormatConverter) {
if (!mInputFormatConverter->isReady()) {
ALOGE("Input format convertor ran out of buffers");
reportError(C2_CORRUPTED);
return false;
}
ALOGV("Converting input block (index: %" PRIu64 ")", index);
c2_status_t status = C2_CORRUPTED;
block = mInputFormatConverter->convertBlock(index, block, &status);
if (status != C2_OK) {
ALOGE("Failed to convert input block (index: %" PRIu64 ")", index);
reportError(status);
return false;
}
}
ALOGV("Encoding input block (index: %" PRIu64 ", timestamp: %" PRId64 ", size: %dx%d)", index,
timestamp, block.width(), block.height());
// Create a video frame from the graphic block.
std::unique_ptr<InputFrame> frame = InputFrame::Create(block);
if (!frame) {
ALOGE("Failed to create video frame from input block (index: %" PRIu64
", timestamp: %" PRId64 ")",
index, timestamp);
reportError(C2_CORRUPTED);
return false;
}
// Get the video frame layout and pixel format from the graphic block.
// TODO(dstaessens) Integrate getVideoFrameLayout() into InputFrame::Create()
media::VideoPixelFormat format;
std::optional<std::vector<VideoFramePlane>> planes = getVideoFrameLayout(block, &format);
if (!planes) {
ALOGE("Failed to get input block's layout");
reportError(C2_CORRUPTED);
return false;
}
if (!enqueueInputBuffer(std::move(frame), format, *planes, index, timestamp)) {
ALOGE("Failed to enqueue video frame (index: %" PRIu64 ", timestamp: %" PRId64 ")", index,
timestamp);
reportError(C2_CORRUPTED);
return false;
}
// Start streaming on the input and output queue if required.
if (!mInputQueue->IsStreaming()) {
ALOG_ASSERT(!mOutputQueue->IsStreaming());
if (!mOutputQueue->Streamon() || !mInputQueue->Streamon()) {
ALOGE("Failed to start streaming on input and output queue");
reportError(C2_CORRUPTED);
return false;
}
// Start polling on the V4L2 device.
startDevicePoll();
}
// Allocate and queue all buffers on the output queue. These buffers will be used to store the
// encoded bitstreams.
while (mOutputQueue->FreeBuffersCount() > 0) {
if (!enqueueOutputBuffer()) return false;
}
return true;
}
void V4L2EncodeComponent::drain() {
ALOGV("%s()", __func__);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
if (mEncoderState == EncoderState::DRAINING || mEncoderState == EncoderState::ERROR) {
return;
}
ALOG_ASSERT(mInputQueue->IsStreaming() && mOutputQueue->IsStreaming());
ALOG_ASSERT(!mOutputWorkQueue.empty());
// TODO(dstaessens): Move IOCTL to device class.
struct v4l2_encoder_cmd cmd;
memset(&cmd, 0, sizeof(v4l2_encoder_cmd));
cmd.cmd = V4L2_ENC_CMD_STOP;
if (mDevice->Ioctl(VIDIOC_ENCODER_CMD, &cmd) != 0) {
ALOGE("Failed to stop encoder");
onDrainDone(false);
return;
}
ALOGV("%s(): Sent STOP command to encoder", __func__);
setEncoderState(EncoderState::DRAINING);
}
void V4L2EncodeComponent::flush() {
ALOGV("%s()", __func__);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
// Stop the device poll thread.
stopDevicePoll();
// Stop streaming on the V4L2 device, which stops all currently queued work and releases all
// buffers currently in use by the device.
// TODO(b/160540027): Calling streamoff currently results in a bug.
for (auto& queue : {mInputQueue, mOutputQueue}) {
if (queue && queue->IsStreaming() && !queue->Streamoff()) {
ALOGE("Failed to stop streaming on the device queue");
reportError(C2_CORRUPTED);
}
}
// Return all buffers to the input format convertor and clear all references to graphic blocks
// in the input queue.
for (auto& it : mInputBuffersMap) {
if (mInputFormatConverter && it.second) {
mInputFormatConverter->returnBlock(it.first);
}
it.second = nullptr;
}
// Report all queued work items as aborted.
std::list<std::unique_ptr<C2Work>> abortedWorkItems;
while (!mInputWorkQueue.empty()) {
std::unique_ptr<C2Work> work = std::move(mInputWorkQueue.front());
work->result = C2_NOT_FOUND;
work->input.buffers.clear();
abortedWorkItems.push_back(std::move(work));
mInputWorkQueue.pop();
}
while (!mOutputWorkQueue.empty()) {
std::unique_ptr<C2Work> work = std::move(mOutputWorkQueue.front());
work->result = C2_NOT_FOUND;
work->input.buffers.clear();
abortedWorkItems.push_back(std::move(work));
mOutputWorkQueue.pop_front();
}
if (!abortedWorkItems.empty())
mListener->onWorkDone_nb(shared_from_this(), std::move(abortedWorkItems));
// Streaming and polling on the V4L2 device input and output queues will be resumed once new
// encode work is queued.
}
std::shared_ptr<C2LinearBlock> V4L2EncodeComponent::fetchOutputBlock() {
// TODO(dstaessens): fetchLinearBlock() might be blocking.
ALOGV("Fetching linear block (size: %u)", mOutputBufferSize);
std::shared_ptr<C2LinearBlock> outputBlock;
c2_status_t status = mOutputBlockPool->fetchLinearBlock(
mOutputBufferSize, {C2MemoryUsage::CPU_READ, C2MemoryUsage::CPU_WRITE}, &outputBlock);
if (status != C2_OK) {
ALOGE("Failed to fetch linear block (error: %d)", status);
reportError(status);
return nullptr;
}
return outputBlock;
}
void V4L2EncodeComponent::onInputBufferDone(uint64_t index) {
ALOGV("%s(): Input buffer done (index: %" PRIu64 ")", __func__, index);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
ALOG_ASSERT(mEncoderState != EncoderState::UNINITIALIZED);
// 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. If
// using an input format convertor, we also need to return the block to the convertor.
LOG_ASSERT(!work->input.buffers.empty());
work->input.buffers.front().reset();
if (mInputFormatConverter) {
c2_status_t status = mInputFormatConverter->returnBlock(index);
if (status != C2_OK) {
reportError(status);
return;
}
}
// 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 (!mOutputWorkQueue.empty() && isWorkDone(*mOutputWorkQueue.front())) {
reportWork(std::move(mOutputWorkQueue.front()));
mOutputWorkQueue.pop_front();
}
// If we previously used up all input queue buffers we can start encoding again now.
if ((mEncoderState == EncoderState::WAITING_FOR_INPUT_BUFFERS) && !mInputWorkQueue.empty()) {
setEncoderState(EncoderState::ENCODING);
mEncoderTaskRunner->PostTask(
FROM_HERE,
::base::BindOnce(&V4L2EncodeComponent::scheduleNextEncodeTask, mWeakThis));
}
}
void V4L2EncodeComponent::onOutputBufferDone(uint32_t payloadSize, bool keyFrame, int64_t timestamp,
std::shared_ptr<C2LinearBlock> outputBlock) {
ALOGV("%s(): output buffer done (timestamp: %" PRId64 ", size: %u, key frame: %d)", __func__,
timestamp, payloadSize, keyFrame);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
if (mEncoderState == EncoderState::ERROR) {
return;
}
C2ConstLinearBlock constBlock =
outputBlock->share(outputBlock->offset(), payloadSize, 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 (!mCSDSubmitted) {
ALOGV("No CSD submitted yet, extracting CSD");
std::unique_ptr<C2StreamInitDataInfo::output> csd;
C2ReadView view = constBlock.map().get();
extractCSDInfo(&csd, view.data(), view.capacity());
if (!csd) {
ALOGE("Failed to extract CSD");
reportError(C2_CORRUPTED);
return;
}
// Attach the CSD to the first item in our output work queue.
LOG_ASSERT(!mOutputWorkQueue.empty());
C2Work* work = mOutputWorkQueue.front().get();
work->worklets.front()->output.configUpdate.push_back(std::move(csd));
mCSDSubmitted = true;
}
// 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.
// TODO(dstaessens): Investigate handling empty CSD requests.
if (timestamp != 0) {
reportError(C2_CORRUPTED);
}
return;
}
std::shared_ptr<C2Buffer> buffer = C2Buffer::CreateLinearBuffer(std::move(constBlock));
if (keyFrame) {
buffer->setInfo(
std::make_shared<C2StreamPictureTypeMaskInfo::output>(0u, C2Config::SYNC_FRAME));
}
work->worklets.front()->output.buffers.emplace_back(buffer);
// 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 (!mOutputWorkQueue.empty() && isWorkDone(*mOutputWorkQueue.front())) {
reportWork(std::move(mOutputWorkQueue.front()));
mOutputWorkQueue.pop_front();
}
}
C2Work* V4L2EncodeComponent::getWorkByIndex(uint64_t index) {
ALOGD("%s(): getting work item (index: %" PRIu64 ")", __func__, index);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
auto it = std::find_if(mOutputWorkQueue.begin(), mOutputWorkQueue.end(),
[index](const std::unique_ptr<C2Work>& w) {
return w->input.ordinal.frameIndex.peeku() == index;
});
if (it == mOutputWorkQueue.end()) {
ALOGE("Failed to find work (index: %" PRIu64 ")", index);
return nullptr;
}
return it->get();
}
C2Work* V4L2EncodeComponent::getWorkByTimestamp(int64_t timestamp) {
ALOGD("%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(mOutputWorkQueue.begin(), mOutputWorkQueue.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 == mOutputWorkQueue.end()) {
ALOGE("Failed to find work (timestamp: %" PRIu64 ")", timestamp);
return nullptr;
}
return it->get();
}
bool V4L2EncodeComponent::isWorkDone(const C2Work& work) const {
ALOGD("%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 is marked as EOS but draining has not finished yet");
return false;
}
if (!work.input.buffers.empty() && work.input.buffers.front()) {
ALOGV("Input buffer associated with work item not returned yet");
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 not returned yet");
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(shared_from_this(), std::move(finishedWorkList));
}
bool V4L2EncodeComponent::startDevicePoll() {
ALOGV("%s()", __func__);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
if (!mDevice->StartPolling(
::base::BindRepeating(&V4L2EncodeComponent::serviceDeviceTask, mWeakThis),
::base::BindRepeating(&V4L2EncodeComponent::onPollError, mWeakThis))) {
ALOGE("Device poll thread failed to start");
reportError(C2_CORRUPTED);
return false;
}
ALOGV("Device poll started");
return true;
}
bool V4L2EncodeComponent::stopDevicePoll() {
ALOGV("%s()", __func__);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
if (!mDevice->StopPolling()) {
ALOGE("Failed to stop polling on the device");
reportError(C2_CORRUPTED);
return false;
}
ALOGV("Device poll stopped");
return true;
}
void V4L2EncodeComponent::onPollError() {
ALOGV("%s()", __func__);
reportError(C2_CORRUPTED);
}
void V4L2EncodeComponent::serviceDeviceTask(bool /*event*/) {
ALOGV("%s()", __func__);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
ALOG_ASSERT(mEncoderState != EncoderState::UNINITIALIZED);
if (mEncoderState == EncoderState::ERROR) {
return;
}
// Dequeue completed output (VIDEO_CAPTURE) buffers, and recycle to the free list. We dequeue
// from the output queue first. Otherwise there is a very small change that a drain is completed
// between dequeuing from input and output queue, which would cause us to call OnDrainDone()
// without all input buffers being returned yet. This is not a strict requirement but makes the
// OnDrainDone() function a lot simpler.
while (mOutputQueue->QueuedBuffersCount() > 0) {
if (!dequeueOutputBuffer()) break;
}
// Dequeue completed input (VIDEO_OUTPUT) buffers, and recycle to the free list.
while (mInputQueue->QueuedBuffersCount() > 0) {
if (!dequeueInputBuffer()) break;
}
ALOGV("%s() - done", __func__);
}
bool V4L2EncodeComponent::enqueueInputBuffer(std::unique_ptr<InputFrame> frame,
media::VideoPixelFormat format,
const std::vector<VideoFramePlane>& planes,
int64_t index, int64_t timestamp) {
ALOGV("%s(): queuing input buffer (index: %" PRId64 ")", __func__, index);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
ALOG_ASSERT(mInputQueue->FreeBuffersCount() > 0);
ALOG_ASSERT(mEncoderState == EncoderState::ENCODING);
ALOG_ASSERT(mInputLayout->format() == format);
ALOG_ASSERT(mInputLayout->planes().size() == planes.size());
auto buffer = mInputQueue->GetFreeBuffer();
if (!buffer) {
ALOGE("Failed to get free buffer from device input queue");
return false;
}
// Mark the buffer with the frame's timestamp so we can identify the associated output buffers.
buffer->SetTimeStamp(
{.tv_sec = static_cast<time_t>(timestamp / ::base::Time::kMicrosecondsPerSecond),
.tv_usec = static_cast<time_t>(timestamp % ::base::Time::kMicrosecondsPerSecond)});
size_t bufferId = buffer->BufferId();
for (size_t i = 0; i < planes.size(); ++i) {
// Single-buffer input format may have multiple color planes, so bytesUsed of the single
// buffer should be sum of each color planes' size.
size_t bytesUsed = 0;
if (planes.size() == 1) {
bytesUsed = media::VideoFrame::AllocationSize(format, mInputLayout->coded_size());
} else {
bytesUsed = ::base::checked_cast<size_t>(
media::VideoFrame::PlaneSize(format, i, mInputLayout->coded_size()).GetArea());
}
// TODO(crbug.com/901264): The way to pass an offset within a DMA-buf is not defined
// in V4L2 specification, so we abuse data_offset for now. Fix it when we have the
// right interface, including any necessary validation and potential alignment.
buffer->SetPlaneDataOffset(i, planes[i].mOffset);
bytesUsed += planes[i].mOffset;
// Workaround: filling length should not be needed. This is a bug of videobuf2 library.
buffer->SetPlaneSize(i, mInputLayout->planes()[i].size + planes[i].mOffset);
buffer->SetPlaneBytesUsed(i, bytesUsed);
}
std::move(*buffer).QueueDMABuf(frame->getFDs());
ALOGV("Queued buffer in input queue (index: %" PRId64 ", timestamp: %" PRId64
", bufferId: %zu)",
index, timestamp, bufferId);
mInputBuffersMap[bufferId] = {index, std::move(frame)};
return true;
}
bool V4L2EncodeComponent::enqueueOutputBuffer() {
ALOGV("%s()", __func__);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
ALOG_ASSERT(mOutputQueue->FreeBuffersCount() > 0);
auto buffer = mOutputQueue->GetFreeBuffer();
if (!buffer) {
ALOGE("Failed to get free buffer from device output queue");
reportError(C2_CORRUPTED);
return false;
}
size_t buffer_id = buffer->BufferId();
if (!std::move(*buffer).QueueMMap()) {
ALOGE("Failed to queue auto buffer using QueueMMap");
return false;
}
ALOGV("%s(): Queued buffer in output queue (bufferId: %zu)", __func__, buffer_id);
return true;
}
bool V4L2EncodeComponent::dequeueInputBuffer() {
ALOGV("%s()", __func__);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
ALOG_ASSERT(mEncoderState != EncoderState::UNINITIALIZED);
ALOG_ASSERT(mInputQueue->QueuedBuffersCount() > 0);
std::pair<bool, media::V4L2ReadableBufferRef> result = mInputQueue->DequeueBuffer();
if (!result.first) {
ALOGE("Failed to dequeue buffer from input queue");
reportError(C2_CORRUPTED);
return false;
}
if (!result.second) {
// No more buffers ready to be dequeued in input queue.
return false;
}
const media::V4L2ReadableBufferRef buffer = std::move(result.second);
uint64_t index = mInputBuffersMap[buffer->BufferId()].first;
int64_t timestamp = buffer->GetTimeStamp().tv_usec +
buffer->GetTimeStamp().tv_sec * ::base::Time::kMicrosecondsPerSecond;
ALOGV("Dequeued buffer from input queue (index: %" PRId64 ", timestamp: %" PRId64
", bufferId: %zu)",
index, timestamp, buffer->BufferId());
mInputBuffersMap[buffer->BufferId()].second = nullptr;
onInputBufferDone(index);
return true;
}
bool V4L2EncodeComponent::dequeueOutputBuffer() {
ALOGV("%s()", __func__);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
ALOG_ASSERT(mEncoderState != EncoderState::UNINITIALIZED);
ALOG_ASSERT(mOutputQueue->QueuedBuffersCount() > 0);
std::pair<bool, media::V4L2ReadableBufferRef> result = mOutputQueue->DequeueBuffer();
if (!result.first) {
ALOGE("Failed to dequeue buffer from output queue");
reportError(C2_CORRUPTED);
return false;
}
if (!result.second) {
// No more buffers ready to be dequeued in output queue.
return false;
}
media::V4L2ReadableBufferRef buffer = std::move(result.second);
size_t encodedDataSize = buffer->GetPlaneBytesUsed(0) - buffer->GetPlaneDataOffset(0);
::base::TimeDelta timestamp = ::base::TimeDelta::FromMicroseconds(
buffer->GetTimeStamp().tv_usec +
buffer->GetTimeStamp().tv_sec * ::base::Time::kMicrosecondsPerSecond);
ALOGV("Dequeued buffer from output queue (timestamp: %" PRId64
", bufferId: %zu, data size: %zu, EOS: %d)",
timestamp.InMicroseconds(), buffer->BufferId(), encodedDataSize, buffer->IsLast());
// If the output buffer contains encoded data we need to allocate a new output block and copy
// the encoded buffer data.
// TODO(dstaessens): Avoid always performing copy while outputting buffers.
if (encodedDataSize > 0) {
std::shared_ptr<C2LinearBlock> outputBlock = fetchOutputBlock();
if (!outputBlock) {
ALOGE("Failed to create output block");
reportError(C2_CORRUPTED);
return false;
}
size_t outputDataSize = copyIntoOutputBuffer(buffer, *outputBlock);
if (outputDataSize == 0) {
ALOGE("Invalid output buffer size");
reportError(C2_CORRUPTED);
return false;
}
onOutputBufferDone(outputDataSize, buffer->IsKeyframe(), timestamp.InMicroseconds(),
outputBlock);
}
// If the buffer is marked as last and we were flushing the encoder, flushing is now done.
if ((mEncoderState == EncoderState::DRAINING) && buffer->IsLast()) {
ALOG_ASSERT(mInputQueue->QueuedBuffersCount() == 0);
onDrainDone(true);
// Start the encoder again.
struct v4l2_encoder_cmd cmd;
memset(&cmd, 0, sizeof(v4l2_encoder_cmd));
cmd.cmd = V4L2_ENC_CMD_START;
if (mDevice->Ioctl(VIDIOC_ENCODER_CMD, &cmd) != 0) {
ALOGE("Failed to restart encoder after flushing (V4L2_ENC_CMD_START)");
reportError(C2_CORRUPTED);
return false;
}
}
// We copied the result into an output block, so we can immediately free the output buffer and
// enqueue it again so it can be reused.
buffer = nullptr;
enqueueOutputBuffer();
return true;
}
bool V4L2EncodeComponent::createInputBuffers() {
ALOGV("%s()", __func__);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
ALOG_ASSERT(!mInputQueue->IsStreaming());
ALOG_ASSERT(mInputBuffersMap.empty());
// No memory is allocated here, we just generate a list of buffers on the input queue, which
// will hold memory handles to the real buffers.
if (mInputQueue->AllocateBuffers(kInputBufferCount, V4L2_MEMORY_DMABUF) < kInputBufferCount) {
ALOGE("Failed to create V4L2 input buffers.");
return false;
}
mInputBuffersMap.resize(mInputQueue->AllocatedBuffersCount());
return true;
}
bool V4L2EncodeComponent::createOutputBuffers() {
ALOGV("%s()", __func__);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
ALOG_ASSERT(!mOutputQueue->IsStreaming());
// Fetch the output block pool.
C2BlockPool::local_id_t poolId = mInterface->getBlockPoolId();
c2_status_t status = GetCodec2BlockPool(poolId, shared_from_this(), &mOutputBlockPool);
if (status != C2_OK || !mOutputBlockPool) {
ALOGE("Failed to get output block pool, error: %d", status);
return false;
}
// Memory is allocated here to decode into. The encoded result is copied to a blockpool buffer
// upon dequeuing.
if (mOutputQueue->AllocateBuffers(kOutputBufferCount, V4L2_MEMORY_MMAP) < kOutputBufferCount) {
ALOGE("Failed to allocate V4L2 output buffers.");
return false;
}
return true;
}
void V4L2EncodeComponent::destroyInputBuffers() {
ALOGV("%s()", __func__);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
ALOG_ASSERT(!mInputQueue->IsStreaming());
if (!mInputQueue || mInputQueue->AllocatedBuffersCount() == 0) return;
mInputQueue->DeallocateBuffers();
mInputBuffersMap.clear();
}
void V4L2EncodeComponent::destroyOutputBuffers() {
ALOGV("%s()", __func__);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
ALOG_ASSERT(!mOutputQueue->IsStreaming());
if (!mOutputQueue || mOutputQueue->AllocatedBuffersCount() == 0) return;
mOutputQueue->DeallocateBuffers();
mOutputBlockPool.reset();
}
size_t V4L2EncodeComponent::copyIntoOutputBuffer(
scoped_refptr<media::V4L2ReadableBuffer> outputBuffer, const C2LinearBlock& outputBlock) {
ALOGV("%s()", __func__);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
const uint8_t* src = static_cast<const uint8_t*>(outputBuffer->GetPlaneMapping(0)) +
outputBuffer->GetPlaneDataOffset(0);
size_t srcSize = outputBuffer->GetPlaneBytesUsed(0) - outputBuffer->GetPlaneDataOffset(0);
// TODO(dstaessens): Investigate mapping output block directly.
int dupFd = dup(outputBlock.handle()->data[0]);
if (dupFd < 0) {
ALOGE("Failed to duplicate output buffer handle (errno: %d)", errno);
reportError(C2_CORRUPTED);
return 0;
}
::base::SharedMemoryHandle shmHandle(::base::FileDescriptor(dupFd, true), 0u,
::base::UnguessableToken::Create());
auto dest =
std::make_unique<media::UnalignedSharedMemory>(shmHandle, outputBlock.size(), false);
if (!dest->MapAt(outputBlock.offset(), outputBlock.size())) {
ALOGE("Failed to map output buffer");
return 0;
}
uint8_t* dstPtr = static_cast<uint8_t*>(dest->memory());
size_t remainingDstSize = dest->size();
if (!mInjectParamsBeforeIDR) {
if (srcSize <= remainingDstSize) {
memcpy(dstPtr, src, srcSize);
return srcSize;
} else {
ALOGE("Output data did not fit in the BitstreamBuffer");
return 0;
}
}
// Cache the newest SPS and PPS found in the stream, and inject them before each IDR found.
media::H264Parser parser;
parser.SetStream(src, srcSize);
media::H264NALU nalu;
// TODO(dstaessens): Split SPS and PPS case?
bool streamParamsFound = false;
while (parser.AdvanceToNextNALU(&nalu) == media::H264Parser::kOk) {
// nalu.size is always without the start code, regardless of the NALU type.
if (nalu.size + kH264StartCodeSize > remainingDstSize) {
ALOGE("Output data did not fit in the BitstreamBuffer");
break;
}
switch (nalu.nal_unit_type) {
case media::H264NALU::kSPS:
mCachedSPS.resize(nalu.size);
memcpy(mCachedSPS.data(), nalu.data, nalu.size);
streamParamsFound = true;
ALOGE("Updated cached SPS");
break;
case media::H264NALU::kPPS:
mCachedPPS.resize(nalu.size);
memcpy(mCachedPPS.data(), nalu.data, nalu.size);
streamParamsFound = true;
ALOGE("Updated cached PPS");
break;
case media::H264NALU::kIDRSlice:
if (streamParamsFound) {
ALOGE("Not injecting stream header before IDR, already present");
break;
}
// Only inject if we have both headers cached, and enough space for both the headers
// including the H.264 start codes and the NALU itself.
size_t h264HeaderSize =
mCachedSPS.size() + mCachedPPS.size() + (2 * kH264StartCodeSize);
if (mCachedSPS.empty() || mCachedPPS.empty() ||
(h264HeaderSize + nalu.size + kH264StartCodeSize > remainingDstSize)) {
ALOGE("Not enough space to inject a stream header before IDR");
break;
}
copyNALUPrependingStartCode(mCachedSPS.data(), mCachedSPS.size(), &dstPtr,
&remainingDstSize);
copyNALUPrependingStartCode(mCachedPPS.data(), mCachedPPS.size(), &dstPtr,
&remainingDstSize);
ALOGV("Stream header injected before IDR");
break;
}
copyNALUPrependingStartCode(nalu.data, nalu.size, &dstPtr, &remainingDstSize);
}
return dest->size() - remainingDstSize;
}
void V4L2EncodeComponent::reportError(c2_status_t error) {
ALOGV("%s()", __func__);
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
{
std::lock_guard<std::mutex> lock(mComponentLock);
setComponentState(ComponentState::ERROR);
}
// TODO(dstaessens): Report all pending work items as finished upon failure.
if (mEncoderState != EncoderState::ERROR) {
setEncoderState(EncoderState::ERROR);
mListener->onError_nb(shared_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;
}
void V4L2EncodeComponent::setEncoderState(EncoderState state) {
ALOG_ASSERT(mEncoderTaskRunner->RunsTasksInCurrentSequence());
// Check whether the state change is valid.
switch (state) {
case EncoderState::UNINITIALIZED:
// TODO(dstaessens): Check all valid state changes.
break;
case EncoderState::WAITING_FOR_INPUT:
ALOG_ASSERT(mEncoderState == EncoderState::UNINITIALIZED ||
mEncoderState == EncoderState::ENCODING ||
mEncoderState == EncoderState::DRAINING);
break;
case EncoderState::WAITING_FOR_INPUT_BUFFERS:
ALOG_ASSERT(mEncoderState == EncoderState::ENCODING);
break;
case EncoderState::ENCODING:
ALOG_ASSERT(mEncoderState == EncoderState::WAITING_FOR_INPUT ||
mEncoderState == EncoderState::WAITING_FOR_INPUT_BUFFERS ||
mEncoderState == EncoderState::DRAINING);
break;
case EncoderState::DRAINING:
ALOG_ASSERT(mEncoderState == EncoderState::ENCODING);
break;
case EncoderState::ERROR:
break;
}
ALOGV("Changed encoder state from %s to %s", encoderStateToString(mEncoderState),
encoderStateToString(state));
mEncoderState = 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";
}
}
const char* V4L2EncodeComponent::encoderStateToString(V4L2EncodeComponent::EncoderState state) {
switch (state) {
case EncoderState::UNINITIALIZED:
return "UNINITIALIZED";
case EncoderState::WAITING_FOR_INPUT:
return "WAITING_FOR_INPUT";
case EncoderState::WAITING_FOR_INPUT_BUFFERS:
return "WAITING_FOR_INPUT_BUFFERS";
case EncoderState::ENCODING:
return "ENCODING";
case EncoderState::DRAINING:
return "Draining";
case EncoderState::ERROR:
return "ERROR";
}
}
} // namespace android