blob: 344a4fa48c3eb25de035f1d2020f42d9ff2a1cf4 [file] [edit]
/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#define LOG_TAG "VideoFrameSource"
#define ATRACE_TAG ATRACE_TAG_CAMERA
#include "VideoFrameSource.h"
#include <android/hardware/graphics/common/1.2/types.h>
#include <fcntl.h>
#include <libyuv.h>
#include <log/log.h>
#include <media/NdkMediaFormat.h>
#include <media/stagefright/MediaCodecConstants.h>
#include <sys/types.h>
#include <unistd.h>
#include <utils/Trace.h>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <thread>
#include "utils/HWLUtils.h"
namespace android {
namespace framesource {
using namespace std::chrono_literals;
struct MediaFormatDeleter {
void operator()(AMediaFormat* f) {
AMediaFormat_delete(f);
}
};
using MediaFormatPtr = std::unique_ptr<AMediaFormat, MediaFormatDeleter>;
VideoFrameSource::VideoFrameSource(const LogicalCharacteristics& chars,
uint32_t /*camera_id*/,
const std::string& file_path)
: chars_(std::make_unique<LogicalCharacteristics>(chars)),
file_path_(file_path) {
}
VideoFrameSource::~VideoFrameSource() {
if (codec_) {
AMediaCodec_stop(codec_);
AMediaCodec_delete(codec_);
}
if (extractor_) {
AMediaExtractor_delete(extractor_);
}
if (video_fd_ >= 0) {
close(video_fd_);
video_fd_ = -1;
}
}
status_t VideoFrameSource::Initialize() {
video_fd_ = open(file_path_.c_str(), O_RDONLY);
if (video_fd_ < 0) {
ALOGE("%s: Failed to open video file %s", __FUNCTION__, file_path_.c_str());
return BAD_VALUE;
}
extractor_ = AMediaExtractor_new();
off64_t fileSize = lseek64(video_fd_, 0, SEEK_END);
lseek64(video_fd_, 0, SEEK_SET);
media_status_t status =
AMediaExtractor_setDataSourceFd(extractor_, video_fd_, 0, fileSize);
if (status != AMEDIA_OK) {
ALOGE("%s: Failed to set data source", __FUNCTION__);
return BAD_VALUE;
}
if (!InitializeMediaCodec()) {
return BAD_VALUE;
}
if (AMediaCodec_start(codec_) != AMEDIA_OK) {
ALOGI(
"%s: Received error in starting decoder. "
"Trying again after resetting this emulated device.",
__FUNCTION__);
if (!InitializeMediaCodec()) {
ALOGE("%s: Failed to re-configure the media codec.", __FUNCTION__);
return BAD_VALUE;
}
AMediaExtractor_seekTo(extractor_, 0, AMEDIAEXTRACTOR_SEEK_CLOSEST_SYNC);
if (auto status = AMediaCodec_start(codec_); status != AMEDIA_OK) {
ALOGE("%s: Received error again in starting decoder. Error code: %d",
__FUNCTION__, status);
return BAD_VALUE;
}
}
return OK;
}
bool VideoFrameSource::InitializeMediaCodec() {
if (codec_) {
AMediaCodec_stop(codec_);
AMediaCodec_delete(codec_);
codec_ = nullptr;
}
int numTracks = AMediaExtractor_getTrackCount(extractor_);
int selectedTrack = -1;
const char* mime = nullptr;
MediaFormatPtr format;
for (int i = 0; i < numTracks; ++i) {
format.reset(AMediaExtractor_getTrackFormat(extractor_, i));
AMediaFormat_getString(format.get(), AMEDIAFORMAT_KEY_MIME, &mime);
if (strncmp(mime, "video/", 6) == 0) {
selectedTrack = i;
break;
}
}
if (selectedTrack < 0 || !format) {
ALOGE("%s: No video track found", __FUNCTION__);
return false;
}
media_status_t status = AMediaExtractor_selectTrack(extractor_, selectedTrack);
if (status != AMEDIA_OK) {
ALOGE("%s: Failed to select video track: %d", __FUNCTION__, status);
return false;
}
codec_ = AMediaCodec_createDecoderByType(mime);
if (!codec_) {
ALOGE("%s: Failed to create decoder for %s", __FUNCTION__, mime);
return false;
}
AMediaFormat_setInt32(format.get(), AMEDIAFORMAT_KEY_COLOR_FORMAT,
COLOR_FormatYUV420Planar);
status = AMediaCodec_configure(codec_, format.get(), nullptr, nullptr, 0);
if (status != AMEDIA_OK) {
ALOGE("%s: Failed to configure codec", __FUNCTION__);
return false;
}
MediaFormatPtr outFormat(AMediaCodec_getOutputFormat(codec_));
AMediaFormat_getInt32(outFormat.get(), AMEDIAFORMAT_KEY_WIDTH, &video_width_);
AMediaFormat_getInt32(outFormat.get(), AMEDIAFORMAT_KEY_HEIGHT,
&video_height_);
AMediaFormat_getInt32(outFormat.get(), AMEDIAFORMAT_KEY_COLOR_FORMAT,
&video_color_format_);
AMediaFormat_getInt32(outFormat.get(), AMEDIAFORMAT_KEY_STRIDE,
&video_stride_);
AMediaFormat_getInt32(outFormat.get(), AMEDIAFORMAT_KEY_SLICE_HEIGHT,
&video_slice_height_);
return true;
}
int VideoFrameSource::GetNextDecodedFrame(AMediaCodecBufferInfo* out_info) {
int retries = 0;
const int kMaxRetries = 600; // Try for ~600ms total
const int kTimeoutUs = 1000; // 1ms
while (retries < kMaxRetries) {
while (true) {
ssize_t bufIdx = AMediaCodec_dequeueInputBuffer(codec_, 0);
if (bufIdx < 0) {
break;
}
size_t bufSize;
uint8_t* buf = AMediaCodec_getInputBuffer(codec_, bufIdx, &bufSize);
ssize_t sampleSize = AMediaExtractor_getSampleSize(extractor_);
if (sampleSize > (ssize_t)bufSize) {
ALOGE("%s: Input buffer too small for sample size %zd > %zu",
__FUNCTION__, sampleSize, bufSize);
return -1;
}
sampleSize = AMediaExtractor_readSampleData(extractor_, buf, bufSize);
int64_t timeUs = AMediaExtractor_getSampleTime(extractor_);
if (sampleSize < 0) {
// EOF, queue EOS flag
AMediaCodec_queueInputBuffer(codec_, bufIdx, 0, 0, timeUs,
AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM);
ALOGI("%s: End of stream reached, queued EOS", __FUNCTION__);
} else {
AMediaCodec_queueInputBuffer(codec_, bufIdx, 0, sampleSize, timeUs, 0);
AMediaExtractor_advance(extractor_);
}
}
// Check output
ssize_t outBufIdx =
AMediaCodec_dequeueOutputBuffer(codec_, out_info, kTimeoutUs);
if (outBufIdx >= 0) {
return outBufIdx;
} else if (outBufIdx == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) {
// On format change, we just loop and retry.
MediaFormatPtr newFormat(AMediaCodec_getOutputFormat(codec_));
AMediaFormat_getInt32(newFormat.get(), AMEDIAFORMAT_KEY_WIDTH,
&video_width_);
AMediaFormat_getInt32(newFormat.get(), AMEDIAFORMAT_KEY_HEIGHT,
&video_height_);
AMediaFormat_getInt32(newFormat.get(), AMEDIAFORMAT_KEY_STRIDE,
&video_stride_);
AMediaFormat_getInt32(newFormat.get(), AMEDIAFORMAT_KEY_SLICE_HEIGHT,
&video_slice_height_);
AMediaFormat_getInt32(newFormat.get(), AMEDIAFORMAT_KEY_COLOR_FORMAT,
&video_color_format_);
ALOGI("%s: Format changed: %dx%d, stride %d, slice %d, color %d",
__FUNCTION__, video_width_, video_height_, video_stride_,
video_slice_height_, video_color_format_);
} else if (outBufIdx == AMEDIACODEC_INFO_TRY_AGAIN_LATER) {
retries++;
} else {
ALOGE("%s: AMediaCodec_dequeueOutputBuffer failed: %zd", __FUNCTION__,
outBufIdx);
return -1;
}
}
ALOGE("%s: Timed out waiting for decoded frame", __FUNCTION__);
return -1;
}
status_t VideoFrameSource::GetFrameForTimestamp(nsecs_t timestamp,
uint8_t** buffer,
size_t* buffer_size,
AMediaCodecBufferInfo* out_info) {
// Check if we can reuse the cached frame
if (timestamp == last_request_timestamp_ && !cached_frame_.empty()) {
*buffer = cached_frame_.data();
*buffer_size = cached_frame_.size();
*out_info = cached_info_;
return OK;
}
// Decode new frame
size_t outSize = 0;
AMediaCodecBufferInfo info = {};
int bufIdx = GetNextDecodedFrame(&info);
if (bufIdx < 0) {
ALOGE("%s: Failed to decode frame", __FUNCTION__);
return UNKNOWN_ERROR;
}
bool loop_reset = ((info.flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) != 0);
if (info.size > 0) {
uint8_t* codec_out = AMediaCodec_getOutputBuffer(codec_, bufIdx, &outSize);
codec_out += info.offset;
// Update cache
if (cached_frame_.size() < static_cast<size_t>(info.size)) {
cached_frame_.resize(info.size);
}
memcpy(cached_frame_.data(), codec_out, info.size);
cached_info_ = info;
last_request_timestamp_ = timestamp;
*buffer = cached_frame_.data();
*buffer_size = cached_frame_.size();
*out_info = cached_info_;
} else {
// Empty frame (likely EOS without data).
*buffer = nullptr;
*buffer_size = 0;
*out_info = info;
}
AMediaCodec_releaseOutputBuffer(codec_, bufIdx, false);
if (loop_reset) {
Reset();
// If the EOS frame had no data, reuse the last cached frame to avoid
// returning a null buffer (which causes black/green frames).
if (*buffer == nullptr && !cached_frame_.empty()) {
*buffer = cached_frame_.data();
*buffer_size = cached_frame_.size();
*out_info = cached_info_;
}
}
return OK;
}
void VideoFrameSource::Reset() {
ALOGI("Start video playback from the beginning.");
AMediaExtractor_seekTo(extractor_, 0, AMEDIAEXTRACTOR_SEEK_CLOSEST_SYNC);
AMediaCodec_flush(codec_);
}
status_t VideoFrameSource::ProduceFrame(uint32_t /*camera_id*/,
nsecs_t timestamp,
const SensorSettings& /*settings*/,
SensorBuffer* buffer,
const SensorBuffer* /*input_buffer*/) {
ATRACE_CALL();
if (buffer == nullptr) return BAD_VALUE;
uint8_t* src_ptr = nullptr;
size_t src_size = 0;
AMediaCodecBufferInfo info = {};
status_t res = GetFrameForTimestamp(timestamp, &src_ptr, &src_size, &info);
if (res != OK) {
return res;
}
if (src_ptr != nullptr && info.size > 0) {
CopyFrame(src_ptr, info.size, info, buffer);
}
return OK;
}
uint8_t* VideoFrameSource::GetBufferFromFrame(uint8_t* src, int src_width,
int src_height, int dst_width,
int dst_height, int* dst_stride_y,
int* dst_stride_u,
int* dst_stride_v) {
// Assume default stride if not set
int stride = (video_stride_ > 0) ? video_stride_ : src_width;
int slice_height =
(video_slice_height_ > 0) ? video_slice_height_ : src_height;
size_t y_size = stride * slice_height;
uint8_t* src_y = src;
uint8_t* src_u = nullptr;
uint8_t* src_v = nullptr;
int src_stride_y = stride;
int src_stride_u = 0;
int src_stride_v = 0;
switch (video_color_format_) {
case COLOR_FormatYUV420SemiPlanar: // NV12
src_u = src + y_size;
src_v = nullptr; // Not used for NV12ToABGR
src_stride_u = stride;
src_stride_v = stride;
break;
case COLOR_FormatYUV420Planar: // I420 or YV12
default:
// Assume I420/YV12 as default for software decoders
src_u = src + y_size;
src_v = src + y_size + (stride / 2 * slice_height / 2);
src_stride_u = stride / 2;
src_stride_v = stride / 2;
break;
}
// If dimensions match, return directly
if (src_width == dst_width && src_height == dst_height) {
*dst_stride_y = src_stride_y;
*dst_stride_u = src_stride_u;
*dst_stride_v = src_stride_v;
return src;
}
// Scaling logic if dimensions mismatch
size_t required_size = dst_width * dst_height * 3 / 2;
if (scaled_buffer_.size() < required_size) {
scaled_buffer_.resize(required_size);
}
// Calculate Aspect Ratio and Crop Region
int crop_x = 0;
int crop_y = 0;
int crop_w = src_width;
int crop_h = src_height;
float video_ar = (float)src_width / src_height;
float dst_ar = (float)dst_width / dst_height;
// Allow a small epsilon for float comparison
if (std::abs(video_ar - dst_ar) > 0.001f) {
if (video_ar > dst_ar) {
// Video is wider than Dest: Crop Left/Right
crop_w = (int)(src_height * dst_ar);
crop_x = (src_width - crop_w) / 2;
} else {
// Video is taller than Dest: Crop Top/Bottom
crop_h = (int)(src_width / dst_ar);
crop_y = (src_height - crop_h) / 2;
}
}
// Ensure crop dimensions are even to maintain color plane alignment
crop_x &= ~1;
crop_y &= ~1;
crop_w &= ~1;
crop_h &= ~1;
uint8_t* dst_y = scaled_buffer_.data();
uint8_t* dst_u = nullptr;
uint8_t* dst_v = nullptr;
*dst_stride_y = dst_width;
*dst_stride_u = 0;
*dst_stride_v = 0;
if (video_color_format_ == COLOR_FormatYUV420SemiPlanar) {
// NV12 Scale
// Apply crop offset to source pointers
const uint8_t* src_y_crop = src_y + (crop_y * src_stride_y) + crop_x;
const uint8_t* src_u_crop = src_u + ((crop_y / 2) * src_stride_u) + crop_x;
*dst_stride_u = dst_width;
dst_u = dst_y + dst_width * dst_height;
libyuv::NV12Scale(src_y_crop, src_stride_y, src_u_crop, src_stride_u,
crop_w, crop_h, dst_y, *dst_stride_y, dst_u,
*dst_stride_u, dst_width, dst_height, libyuv::kFilterBox);
} else {
// I420 Scale
// Apply crop offset to source pointers
const uint8_t* src_y_crop = src_y + (crop_y * src_stride_y) + crop_x;
const uint8_t* src_u_crop =
src_u + ((crop_y / 2) * src_stride_u) + (crop_x / 2);
const uint8_t* src_v_crop =
src_v + ((crop_y / 2) * src_stride_v) + (crop_x / 2);
*dst_stride_u = dst_width / 2;
*dst_stride_v = dst_width / 2;
dst_u = dst_y + dst_width * dst_height;
dst_v = dst_u + (dst_width / 2) * (dst_height / 2);
libyuv::I420Scale(src_y_crop, src_stride_y, src_u_crop, src_stride_u,
src_v_crop, src_stride_v, crop_w, crop_h, dst_y,
*dst_stride_y, dst_u, *dst_stride_u, dst_v, *dst_stride_v,
dst_width, dst_height, libyuv::kFilterBox);
}
return scaled_buffer_.data();
}
void VideoFrameSource::CopyFrame(uint8_t* src, size_t /*src_size*/,
AMediaCodecBufferInfo /*info*/,
SensorBuffer* buffer) {
int src_stride_y, src_stride_u, src_stride_v;
uint8_t* src_buffer = GetBufferFromFrame(
src, video_width_, video_height_, buffer->width, buffer->height,
&src_stride_y, &src_stride_u, &src_stride_v);
// Set up plane pointers for the (possibly scaled) source buffer
uint8_t* src_y = src_buffer;
uint8_t* src_u = nullptr;
uint8_t* src_v = nullptr;
if (video_color_format_ == COLOR_FormatYUV420SemiPlanar) {
// Check if we returned the original src or scaled buffer
if (src_buffer == src) {
// Original buffer logic
int stride = (video_stride_ > 0) ? video_stride_ : video_width_;
int slice_height =
(video_slice_height_ > 0) ? video_slice_height_ : video_height_;
size_t y_size = stride * slice_height;
src_u = src_y + y_size;
// src_v not used for NV12
} else {
// Scaled buffer logic
// Scaled buffer is packed.
size_t y_size = buffer->width * buffer->height;
if (video_color_format_ == COLOR_FormatYUV420SemiPlanar) {
src_u = src_y + y_size;
} else {
src_u = src_y + y_size;
src_v = src_u + (buffer->width / 2) * (buffer->height / 2);
}
}
} else {
// I420
if (src_buffer == src) {
int stride = (video_stride_ > 0) ? video_stride_ : video_width_;
int slice_height =
(video_slice_height_ > 0) ? video_slice_height_ : video_height_;
size_t y_size = stride * slice_height;
src_u = src_y + y_size;
src_v = src_y + y_size + (stride / 2 * slice_height / 2);
} else {
size_t y_size = buffer->width * buffer->height;
src_u = src_y + y_size;
src_v = src_u + (buffer->width / 2) * (buffer->height / 2);
}
}
// NOTE: We assume buffer dimensions match video dimensions. If they don't,
// libyuv will handle the region of interest defined by the buffer's
// width/height.
if (buffer->format == PixelFormat::RGBA_8888) {
if (video_color_format_ == COLOR_FormatYUV420SemiPlanar) {
// NV12
libyuv::NV12ToABGR(
src_y, src_stride_y, src_u, src_stride_u, buffer->plane.img.img,
buffer->plane.img.stride_in_bytes, buffer->width, buffer->height);
} else {
// I420
libyuv::I420ToABGR(src_y, src_stride_y, src_u, src_stride_u, src_v,
src_stride_v, buffer->plane.img.img,
buffer->plane.img.stride_in_bytes, buffer->width,
buffer->height);
}
} else if (buffer->format == PixelFormat::RGB_888) {
if (video_color_format_ == COLOR_FormatYUV420SemiPlanar) {
// NV12
libyuv::NV12ToRGB24(
src_y, src_stride_y, src_u, src_stride_u, buffer->plane.img.img,
buffer->plane.img.stride_in_bytes, buffer->width, buffer->height);
} else {
// I420
libyuv::I420ToRGB24(src_y, src_stride_y, src_u, src_stride_u, src_v,
src_stride_v, buffer->plane.img.img,
buffer->plane.img.stride_in_bytes, buffer->width,
buffer->height);
}
} else if (buffer->format == PixelFormat::YCBCR_420_888 ||
buffer->format == PixelFormat::YCRCB_420_SP) {
// Copy/Convert to I420 or NV12/NV21 destination
auto& planes = buffer->plane.img_y_crcb;
bool dst_is_nv21 = (planes.cbcr_step == 2 && planes.img_cr < planes.img_cb);
bool dst_is_nv12 = (planes.cbcr_step == 2 && planes.img_cb < planes.img_cr);
if (video_color_format_ == COLOR_FormatYUV420SemiPlanar) {
// Source NV12
if (dst_is_nv12) {
// Dest NV12 - Copy
libyuv::CopyPlane(src_y, src_stride_y, planes.img_y, planes.y_stride,
buffer->width, buffer->height);
libyuv::CopyPlane(src_u, src_stride_u, planes.img_cb,
planes.cbcr_stride, buffer->width, buffer->height / 2);
} else if (dst_is_nv21) {
// Dest NV21 - Swap UV (NV12 -> NV21)
libyuv::CopyPlane(src_y, src_stride_y, planes.img_y, planes.y_stride,
buffer->width, buffer->height);
libyuv::SwapUVPlane(src_u, src_stride_u, planes.img_cr,
planes.cbcr_stride, buffer->width,
buffer->height / 2);
} else {
// Dest I420 - Split
libyuv::NV12ToI420(src_y, src_stride_y, src_u, src_stride_u,
planes.img_y, planes.y_stride, planes.img_cb,
planes.cbcr_stride, planes.img_cr,
planes.cbcr_stride, buffer->width, buffer->height);
}
} else {
// Source I420
if (dst_is_nv12) {
// Dest NV12 - Merge
libyuv::I420ToNV12(src_y, src_stride_y, src_u, src_stride_u, src_v,
src_stride_v, planes.img_y, planes.y_stride,
planes.img_cb, planes.cbcr_stride, buffer->width,
buffer->height);
} else if (dst_is_nv21) {
// Dest NV21 - Merge (Swap U/V args)
libyuv::I420ToNV21(src_y, src_stride_y, src_u, src_stride_u, src_v,
src_stride_v, planes.img_y, planes.y_stride,
planes.img_cr, planes.cbcr_stride, buffer->width,
buffer->height);
} else {
// Dest I420 - Copy
libyuv::I420Copy(src_y, src_stride_y, src_u, src_stride_u, src_v,
src_stride_v, planes.img_y, planes.y_stride,
planes.img_cb, planes.cbcr_stride, planes.img_cr,
planes.cbcr_stride, buffer->width, buffer->height);
}
}
}
}
void VideoFrameSource::CalculateAndAppendNoiseProfile(
float /*gain*/, float /*max_raw_value*/, HalCameraMetadata* result) {
// Zero noise for video
double noise_profile[8] = {0.0};
result->Set(ANDROID_SENSOR_NOISE_PROFILE, noise_profile, 8);
}
} // namespace framesource
} // namespace android