Merge "Add support for DCT-unaligned JPEG compression." into main
diff --git a/services/camera/virtualcamera/VirtualCameraRenderThread.cc b/services/camera/virtualcamera/VirtualCameraRenderThread.cc
index 164580f..a8d2455 100644
--- a/services/camera/virtualcamera/VirtualCameraRenderThread.cc
+++ b/services/camera/virtualcamera/VirtualCameraRenderThread.cc
@@ -46,6 +46,7 @@
#include "android/hardware_buffer.h"
#include "system/camera_metadata.h"
#include "ui/GraphicBuffer.h"
+#include "ui/Rect.h"
#include "util/EglFramebuffer.h"
#include "util/JpegUtil.h"
#include "util/MetadataUtil.h"
@@ -535,8 +536,9 @@
ALOGV("%s: Creating thumbnail with size %d x %d, quality %d", __func__,
resolution.width, resolution.height, quality);
+ Resolution bufferSize = roundTo2DctSize(resolution);
std::shared_ptr<EglFrameBuffer> framebuffer = allocateTemporaryFramebuffer(
- mEglDisplayContext->getEglDisplay(), resolution.width, resolution.height);
+ mEglDisplayContext->getEglDisplay(), bufferSize.width, bufferSize.height);
if (framebuffer == nullptr) {
ALOGE(
"Failed to allocate temporary framebuffer for JPEG thumbnail "
@@ -547,38 +549,23 @@
// TODO(b/324383963) Add support for letterboxing if the thumbnail size
// doesn't correspond
// to input texture aspect ratio.
- if (!renderIntoEglFramebuffer(*framebuffer).isOk()) {
+ if (!renderIntoEglFramebuffer(*framebuffer, /*fence=*/nullptr,
+ Rect(resolution.width, resolution.height))
+ .isOk()) {
ALOGE(
"Failed to render input texture into temporary framebuffer for JPEG "
"thumbnail");
return {};
}
- std::shared_ptr<AHardwareBuffer> inHwBuffer = framebuffer->getHardwareBuffer();
- GraphicBuffer* gBuffer = GraphicBuffer::fromAHardwareBuffer(inHwBuffer.get());
-
- if (gBuffer->getPixelFormat() != HAL_PIXEL_FORMAT_YCbCr_420_888) {
- // This should never happen since we're allocating the temporary buffer
- // with YUV420 layout above.
- ALOGE("%s: Cannot compress non-YUV buffer (pixelFormat %d)", __func__,
- gBuffer->getPixelFormat());
- return {};
- }
-
- YCbCrLockGuard yCbCrLock(inHwBuffer, AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN);
- if (yCbCrLock.getStatus() != NO_ERROR) {
- ALOGE("%s: Failed to lock graphic buffer while generating thumbnail: %d",
- __func__, yCbCrLock.getStatus());
- return {};
- }
-
std::vector<uint8_t> compressedThumbnail;
compressedThumbnail.resize(kJpegThumbnailBufferSize);
- ALOGE("%s: Compressing thumbnail %d x %d", __func__, gBuffer->getWidth(),
- gBuffer->getHeight());
- std::optional<size_t> compressedSize = compressJpeg(
- gBuffer->getWidth(), gBuffer->getHeight(), quality, *yCbCrLock, {},
- compressedThumbnail.size(), compressedThumbnail.data());
+ ALOGE("%s: Compressing thumbnail %d x %d", __func__, resolution.width,
+ resolution.height);
+ std::optional<size_t> compressedSize =
+ compressJpeg(resolution.width, resolution.height, quality,
+ framebuffer->getHardwareBuffer(), {},
+ compressedThumbnail.size(), compressedThumbnail.data());
if (!compressedSize.has_value()) {
ALOGE("%s: Failed to compress jpeg thumbnail", __func__);
return {};
@@ -609,15 +596,22 @@
// Let's create YUV framebuffer and render the surface into this.
// This will take care about rescaling as well as potential format conversion.
+ // The buffer dimensions need to be rounded to nearest multiple of JPEG DCT
+ // size, however we pass the viewport corresponding to size of the stream so
+ // the image will be only rendered to the area corresponding to the stream
+ // size.
+ Resolution bufferSize =
+ roundTo2DctSize(Resolution(stream->width, stream->height));
std::shared_ptr<EglFrameBuffer> framebuffer = allocateTemporaryFramebuffer(
- mEglDisplayContext->getEglDisplay(), stream->width, stream->height);
+ mEglDisplayContext->getEglDisplay(), bufferSize.width, bufferSize.height);
if (framebuffer == nullptr) {
ALOGE("Failed to allocate temporary framebuffer for JPEG compression");
return cameraStatus(Status::INTERNAL_ERROR);
}
// Render into temporary framebuffer.
- ndk::ScopedAStatus status = renderIntoEglFramebuffer(*framebuffer);
+ ndk::ScopedAStatus status = renderIntoEglFramebuffer(
+ *framebuffer, /*fence=*/nullptr, Rect(stream->width, stream->height));
if (!status.isOk()) {
ALOGE("Failed to render input texture into temporary framebuffer");
return status;
@@ -629,38 +623,14 @@
return cameraStatus(Status::INTERNAL_ERROR);
}
- std::shared_ptr<AHardwareBuffer> inHwBuffer = framebuffer->getHardwareBuffer();
- GraphicBuffer* gBuffer = GraphicBuffer::fromAHardwareBuffer(inHwBuffer.get());
-
- if (gBuffer == nullptr) {
- ALOGE(
- "%s: Encountered invalid temporary buffer while rendering JPEG "
- "into BLOB stream",
- __func__);
- return cameraStatus(Status::INTERNAL_ERROR);
- }
-
- if (gBuffer->getPixelFormat() != HAL_PIXEL_FORMAT_YCbCr_420_888) {
- // This should never happen since we're allocating the temporary buffer
- // with YUV420 layout above.
- ALOGE("%s: Cannot compress non-YUV buffer (pixelFormat %d)", __func__,
- gBuffer->getPixelFormat());
- return cameraStatus(Status::INTERNAL_ERROR);
- }
-
- YCbCrLockGuard yCbCrLock(inHwBuffer, AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN);
- if (yCbCrLock.getStatus() != OK) {
- return cameraStatus(Status::INTERNAL_ERROR);
- }
-
std::vector<uint8_t> app1ExifData =
createExif(Resolution(stream->width, stream->height), resultMetadata,
createThumbnail(requestSettings.thumbnailResolution,
requestSettings.thumbnailJpegQuality));
std::optional<size_t> compressedSize = compressJpeg(
- gBuffer->getWidth(), gBuffer->getHeight(), requestSettings.jpegQuality,
- *yCbCrLock, app1ExifData, stream->bufferSize - sizeof(CameraBlob),
- (*planesLock).planes[0].data);
+ stream->width, stream->height, requestSettings.jpegQuality,
+ framebuffer->getHardwareBuffer(), app1ExifData,
+ stream->bufferSize - sizeof(CameraBlob), (*planesLock).planes[0].data);
if (!compressedSize.has_value()) {
ALOGE("%s: Failed to compress JPEG image", __func__);
@@ -714,7 +684,7 @@
}
ndk::ScopedAStatus VirtualCameraRenderThread::renderIntoEglFramebuffer(
- EglFrameBuffer& framebuffer, sp<Fence> fence) {
+ EglFrameBuffer& framebuffer, sp<Fence> fence, std::optional<Rect> viewport) {
ALOGV("%s", __func__);
// Wait for fence to clear.
if (fence != nullptr && fence->isValid()) {
@@ -728,6 +698,11 @@
mEglDisplayContext->makeCurrent();
framebuffer.beforeDraw();
+ Rect viewportRect =
+ viewport.value_or(Rect(framebuffer.getWidth(), framebuffer.getHeight()));
+ glViewport(viewportRect.leftTop().x, viewportRect.leftTop().y,
+ viewportRect.getWidth(), viewportRect.getHeight());
+
sp<GraphicBuffer> textureBuffer = mEglSurfaceTexture->getCurrentBuffer();
if (textureBuffer == nullptr) {
// If there's no current buffer, nothing was written to the surface and
diff --git a/services/camera/virtualcamera/VirtualCameraRenderThread.h b/services/camera/virtualcamera/VirtualCameraRenderThread.h
index 2576556..90757a0 100644
--- a/services/camera/virtualcamera/VirtualCameraRenderThread.h
+++ b/services/camera/virtualcamera/VirtualCameraRenderThread.h
@@ -170,8 +170,9 @@
// If fence is specified, this function will block until the fence is cleared
// before writing to the buffer.
// Always called on the render thread.
- ndk::ScopedAStatus renderIntoEglFramebuffer(EglFrameBuffer& framebuffer,
- sp<Fence> fence = nullptr);
+ ndk::ScopedAStatus renderIntoEglFramebuffer(
+ EglFrameBuffer& framebuffer, sp<Fence> fence = nullptr,
+ std::optional<Rect> viewport = std::nullopt);
// Camera callback
const std::shared_ptr<
diff --git a/services/camera/virtualcamera/tests/Android.bp b/services/camera/virtualcamera/tests/Android.bp
index c51b4a3..543cc10 100644
--- a/services/camera/virtualcamera/tests/Android.bp
+++ b/services/camera/virtualcamera/tests/Android.bp
@@ -17,6 +17,7 @@
],
srcs: [
"EglUtilTest.cc",
+ "JpegUtilTest.cc",
"VirtualCameraDeviceTest.cc",
"VirtualCameraProviderTest.cc",
"VirtualCameraRenderThreadTest.cc",
diff --git a/services/camera/virtualcamera/tests/JpegUtilTest.cc b/services/camera/virtualcamera/tests/JpegUtilTest.cc
new file mode 100644
index 0000000..e6481f0
--- /dev/null
+++ b/services/camera/virtualcamera/tests/JpegUtilTest.cc
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2023 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.
+ */
+
+#include <sys/types.h>
+
+#include "system/graphics.h"
+#define LOG_TAG "JpegUtilTest"
+
+#include <array>
+#include <cstdint>
+#include <cstring>
+
+#include "android/hardware_buffer.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "jpeglib.h"
+#include "util/JpegUtil.h"
+#include "util/Util.h"
+#include "utils/Errors.h"
+
+namespace android {
+namespace companion {
+namespace virtualcamera {
+namespace {
+
+using testing::Eq;
+using testing::Gt;
+using testing::Optional;
+using testing::VariantWith;
+
+constexpr int kOutputBufferSize = 1024 * 1024; // 1 MiB.
+constexpr int kJpegQuality = 80;
+
+// Create black YUV420 buffer for testing purposes.
+std::shared_ptr<AHardwareBuffer> createHardwareBufferForTest(const int width,
+ const int height) {
+ const AHardwareBuffer_Desc desc{.width = static_cast<uint32_t>(width),
+ .height = static_cast<uint32_t>(height),
+ .layers = 1,
+ .format = AHARDWAREBUFFER_FORMAT_Y8Cb8Cr8_420,
+ .usage = AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN,
+ .stride = 0,
+ .rfu0 = 0,
+ .rfu1 = 0};
+
+ AHardwareBuffer* hwBufferPtr;
+ int status = AHardwareBuffer_allocate(&desc, &hwBufferPtr);
+ if (status != NO_ERROR) {
+ ALOGE(
+ "%s: Failed to allocate hardware buffer for temporary framebuffer: %d",
+ __func__, status);
+ return nullptr;
+ }
+
+ std::shared_ptr<AHardwareBuffer> hwBuffer(hwBufferPtr,
+ AHardwareBuffer_release);
+
+ YCbCrLockGuard yCbCrLock(hwBuffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN);
+ const android_ycbcr& ycbr = (*yCbCrLock);
+
+ uint8_t* y = reinterpret_cast<uint8_t*>(ycbr.y);
+ for (int r = 0; r < height; r++) {
+ memset(y + r * ycbr.ystride, 0x00, width);
+ }
+
+ uint8_t* cb = reinterpret_cast<uint8_t*>(ycbr.cb);
+ uint8_t* cr = reinterpret_cast<uint8_t*>(ycbr.cr);
+ for (int r = 0; r < height / 2; r++) {
+ for (int c = 0; c < width / 2; c++) {
+ cb[r * ycbr.cstride + c * ycbr.chroma_step] = 0xff / 2;
+ cr[r * ycbr.cstride + c * ycbr.chroma_step] = 0xff / 2;
+ }
+ }
+
+ return hwBuffer;
+}
+
+// Decode JPEG header, return image resolution on success or error message on error.
+std::variant<std::string, Resolution> verifyHeaderAndGetResolution(
+ const uint8_t* data, int size) {
+ struct jpeg_decompress_struct ctx;
+ struct jpeg_error_mgr jerr;
+
+ struct DecompressionError {
+ bool success = true;
+ std::string error;
+ } result;
+
+ ctx.client_data = &result;
+
+ ctx.err = jpeg_std_error(&jerr);
+ ctx.err->error_exit = [](j_common_ptr cinfo) {
+ reinterpret_cast<DecompressionError*>(cinfo->client_data)->success = false;
+ };
+ ctx.err->output_message = [](j_common_ptr cinfo) {
+ char buffer[JMSG_LENGTH_MAX];
+ (*cinfo->err->format_message)(cinfo, buffer);
+ reinterpret_cast<DecompressionError*>(cinfo->client_data)->error = buffer;
+ ALOGE("libjpeg error: %s", buffer);
+ };
+
+ jpeg_create_decompress(&ctx);
+ jpeg_mem_src(&ctx, data, size);
+ jpeg_read_header(&ctx, /*require_image=*/true);
+
+ if (!result.success) {
+ jpeg_destroy_decompress(&ctx);
+ return result.error;
+ }
+
+ Resolution resolution(ctx.image_width, ctx.image_height);
+ jpeg_destroy_decompress(&ctx);
+ return resolution;
+}
+
+TEST(JpegUtil, roundToDctSize) {
+ EXPECT_THAT(roundTo2DctSize(Resolution(640, 480)), Eq(Resolution(640, 480)));
+ EXPECT_THAT(roundTo2DctSize(Resolution(5, 5)), Eq(Resolution(16, 16)));
+ EXPECT_THAT(roundTo2DctSize(Resolution(32, 32)), Eq(Resolution(32, 32)));
+ EXPECT_THAT(roundTo2DctSize(Resolution(33, 32)), Eq(Resolution(48, 32)));
+ EXPECT_THAT(roundTo2DctSize(Resolution(32, 33)), Eq(Resolution(32, 48)));
+}
+
+class JpegUtilTest : public ::testing::Test {
+ public:
+ void SetUp() override {
+ std::fill(mOutputBuffer.begin(), mOutputBuffer.end(), 0);
+ }
+
+ protected:
+ std::optional<size_t> compress(int imageWidth, int imageHeight,
+ std::shared_ptr<AHardwareBuffer> inBuffer) {
+ return compressJpeg(imageWidth, imageHeight, kJpegQuality, inBuffer,
+ /*app1ExifData=*/{}, mOutputBuffer.size(),
+ mOutputBuffer.data());
+ }
+
+ std::array<uint8_t, kOutputBufferSize> mOutputBuffer;
+};
+
+TEST_F(JpegUtilTest, compressImageSizeAlignedWithDctSucceeds) {
+ std::shared_ptr<AHardwareBuffer> inBuffer =
+ createHardwareBufferForTest(640, 480);
+
+ std::optional<size_t> compressedSize = compress(640, 480, inBuffer);
+
+ EXPECT_THAT(compressedSize, Optional(Gt(0)));
+ EXPECT_THAT(verifyHeaderAndGetResolution(mOutputBuffer.data(),
+ compressedSize.value()),
+ VariantWith<Resolution>(Resolution(640, 480)));
+}
+
+TEST_F(JpegUtilTest, compressImageSizeNotAlignedWidthDctSucceeds) {
+ std::shared_ptr<AHardwareBuffer> inBuffer =
+ createHardwareBufferForTest(640, 480);
+
+ std::optional<size_t> compressedSize = compress(630, 470, inBuffer);
+
+ EXPECT_THAT(compressedSize, Optional(Gt(0)));
+ EXPECT_THAT(verifyHeaderAndGetResolution(mOutputBuffer.data(),
+ compressedSize.value()),
+ VariantWith<Resolution>(Resolution(630, 470)));
+}
+
+TEST_F(JpegUtilTest, compressImageWithBufferNotAlignedWithDctFails) {
+ std::shared_ptr<AHardwareBuffer> inBuffer =
+ createHardwareBufferForTest(641, 480);
+
+ std::optional<size_t> compressedSize = compress(640, 480, inBuffer);
+
+ EXPECT_THAT(compressedSize, Eq(std::nullopt));
+}
+
+TEST_F(JpegUtilTest, compressImageWithBufferTooSmallFails) {
+ std::shared_ptr<AHardwareBuffer> inBuffer =
+ createHardwareBufferForTest(634, 464);
+
+ std::optional<size_t> compressedSize = compress(640, 480, inBuffer);
+
+ EXPECT_THAT(compressedSize, Eq(std::nullopt));
+}
+
+} // namespace
+} // namespace virtualcamera
+} // namespace companion
+} // namespace android
diff --git a/services/camera/virtualcamera/tests/VirtualCameraServiceTest.cc b/services/camera/virtualcamera/tests/VirtualCameraServiceTest.cc
index d4d00a2..74c97cc 100644
--- a/services/camera/virtualcamera/tests/VirtualCameraServiceTest.cc
+++ b/services/camera/virtualcamera/tests/VirtualCameraServiceTest.cc
@@ -238,17 +238,6 @@
EXPECT_THAT(getCameraIds(), IsEmpty());
}
-TEST_F(VirtualCameraServiceTest, ConfigurationWithUnalignedResolutionFails) {
- bool aidlRet;
- VirtualCameraConfiguration config =
- createConfiguration(641, 481, Format::YUV_420_888, kMaxFps);
-
- ASSERT_FALSE(
- mCameraService->registerCamera(mNdkOwnerToken, config, &aidlRet).isOk());
- EXPECT_FALSE(aidlRet);
- EXPECT_THAT(getCameraIds(), IsEmpty());
-}
-
TEST_F(VirtualCameraServiceTest, ConfigurationWithNegativeResolutionFails) {
bool aidlRet;
VirtualCameraConfiguration config =
diff --git a/services/camera/virtualcamera/util/JpegUtil.cc b/services/camera/virtualcamera/util/JpegUtil.cc
index 8569eff..b034584 100644
--- a/services/camera/virtualcamera/util/JpegUtil.cc
+++ b/services/camera/virtualcamera/util/JpegUtil.cc
@@ -14,19 +14,20 @@
* limitations under the License.
*/
// #define LOG_NDEBUG 0
+#include "system/graphics.h"
#define LOG_TAG "JpegUtil"
-#include "JpegUtil.h"
-
#include <cstddef>
#include <cstdint>
#include <optional>
#include <vector>
+#include "JpegUtil.h"
#include "android/hardware_buffer.h"
#include "jpeglib.h"
#include "log/log.h"
#include "ui/GraphicBuffer.h"
#include "ui/GraphicBufferMapper.h"
+#include "util/Util.h"
#include "utils/Errors.h"
namespace android {
@@ -34,6 +35,8 @@
namespace virtualcamera {
namespace {
+constexpr int k2DCTSIZE = 2 * DCTSIZE;
+
class LibJpegContext {
public:
LibJpegContext(int width, int height, int quality, const size_t outBufferSize,
@@ -98,23 +101,55 @@
return *this;
}
- std::optional<size_t> compress(const android_ycbcr& ycbr) {
- // TODO(b/301023410) - Add support for compressing image sizes not aligned
- // with DCT size.
- if (mWidth % (2 * DCTSIZE) || (mHeight % (2 * DCTSIZE))) {
+ std::optional<size_t> compress(std::shared_ptr<AHardwareBuffer> inBuffer) {
+ GraphicBuffer* gBuffer = GraphicBuffer::fromAHardwareBuffer(inBuffer.get());
+
+ if (gBuffer == nullptr) {
+ ALOGE("%s: Input graphic buffer is nullptr", __func__);
+ return std::nullopt;
+ }
+
+ if (gBuffer->getPixelFormat() != HAL_PIXEL_FORMAT_YCbCr_420_888) {
+ // This should never happen since we're allocating the temporary buffer
+ // with YUV420 layout above.
+ ALOGE("%s: Cannot compress non-YUV buffer (pixelFormat %d)", __func__,
+ gBuffer->getPixelFormat());
+ return std::nullopt;
+ }
+
+ YCbCrLockGuard yCbCrLock(inBuffer, AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN);
+ if (yCbCrLock.getStatus() != OK) {
+ ALOGE("%s: Failed to lock the input buffer: %s", __func__,
+ statusToString(yCbCrLock.getStatus()).c_str());
+ return std::nullopt;
+ }
+ const android_ycbcr& ycbr = *yCbCrLock;
+
+ const int inBufferWidth = gBuffer->getWidth();
+ const int inBufferHeight = gBuffer->getHeight();
+
+ if (inBufferWidth % k2DCTSIZE || (inBufferHeight % k2DCTSIZE)) {
ALOGE(
- "%s: Compressing YUV420 image with size %dx%d not aligned with 2 * "
+ "%s: Compressing YUV420 buffer with size %dx%d not aligned with 2 * "
"DCTSIZE (%d) is not currently supported.",
- __func__, mWidth, mHeight, 2 * DCTSIZE);
+ __func__, inBufferWidth, inBufferHeight, DCTSIZE);
+ return std::nullopt;
+ }
+
+ if (inBufferWidth < mWidth || inBufferHeight < mHeight) {
+ ALOGE(
+ "%s: Input buffer has smaller size (%dx%d) than image to be "
+ "compressed (%dx%d)",
+ __func__, inBufferWidth, inBufferHeight, mWidth, mHeight);
return std::nullopt;
}
// Chroma planes have 1/2 resolution of the original image.
- const int cHeight = mHeight / 2;
- const int cWidth = mWidth / 2;
+ const int cHeight = inBufferHeight / 2;
+ const int cWidth = inBufferWidth / 2;
// Prepare arrays of pointers to scanlines of each plane.
- std::vector<JSAMPROW> yLines(mHeight);
+ std::vector<JSAMPROW> yLines(inBufferHeight);
std::vector<JSAMPROW> cbLines(cHeight);
std::vector<JSAMPROW> crLines(cHeight);
@@ -142,12 +177,12 @@
}
// Collect pointers to individual scanline of each plane.
- for (int i = 0; i < mHeight; ++i) {
+ for (int i = 0; i < inBufferHeight; ++i) {
yLines[i] = y + i * ycbr.ystride;
}
for (int i = 0; i < cHeight; ++i) {
- cbLines[i] = cb_plane.data() + i * (mWidth / 2);
- crLines[i] = cr_plane.data() + i * (mWidth / 2);
+ cbLines[i] = cb_plane.data() + i * cWidth;
+ crLines[i] = cr_plane.data() + i * cWidth;
}
return compress(yLines, cbLines, crLines);
@@ -254,17 +289,28 @@
boolean mSuccess = true;
};
+int roundTo2DCTMultiple(const int n) {
+ const int mod = n % k2DCTSIZE;
+ return mod == 0 ? n : n + (k2DCTSIZE - mod);
+}
+
} // namespace
std::optional<size_t> compressJpeg(const int width, const int height,
- const int quality, const android_ycbcr& ycbcr,
+ const int quality,
+ std::shared_ptr<AHardwareBuffer> inBuffer,
const std::vector<uint8_t>& app1ExifData,
size_t outBufferSize, void* outBuffer) {
LibJpegContext context(width, height, quality, outBufferSize, outBuffer);
if (!app1ExifData.empty()) {
context.setApp1Data(app1ExifData.data(), app1ExifData.size());
}
- return context.compress(ycbcr);
+ return context.compress(inBuffer);
+}
+
+Resolution roundTo2DctSize(const Resolution resolution) {
+ return Resolution(roundTo2DCTMultiple(resolution.width),
+ roundTo2DCTMultiple(resolution.height));
}
} // namespace virtualcamera
diff --git a/services/camera/virtualcamera/util/JpegUtil.h b/services/camera/virtualcamera/util/JpegUtil.h
index 83ed74b..184dd56 100644
--- a/services/camera/virtualcamera/util/JpegUtil.h
+++ b/services/camera/virtualcamera/util/JpegUtil.h
@@ -19,17 +19,20 @@
#include <optional>
-#include "system/graphics.h"
+#include "android/hardware_buffer.h"
+#include "util/Util.h"
namespace android {
namespace companion {
namespace virtualcamera {
// Jpeg-compress image into the output buffer.
-// * width - width of the image
-// * heigh - height of the image
+// * width - width of the image, can be less than width of inBuffer.
+// * heigh - height of the image, can be less than height of inBuffer.
// * quality - 0-100, higher number corresponds to higher quality.
-// * ycbr - android_ycbr structure describing layout of input YUV420 image.
+// * inBuffer - Input buffer, the dimensions of the buffer must be aligned to
+// 2*DCT_SIZE (16) to include necessary padding in case width and height of
+// image is not aligned with 2*DCT_SIZE.
// * app1ExifData - vector containing data to be included in APP1
// segment. Can be empty.
// * outBufferSize - capacity of the output buffer.
@@ -37,10 +40,14 @@
// Returns size of compressed data if the compression was successful,
// empty optional otherwise.
std::optional<size_t> compressJpeg(int width, int height, int quality,
- const android_ycbcr& ycbcr,
+ std::shared_ptr<AHardwareBuffer> inBuffer,
const std::vector<uint8_t>& app1ExifData,
size_t outBufferSize, void* outBuffer);
+// Round the resolution to the closest higher resolution where width and height
+// are divisible by 2*DCT_SIZE ().
+Resolution roundTo2DctSize(Resolution resolution);
+
} // namespace virtualcamera
} // namespace companion
} // namespace android
diff --git a/services/camera/virtualcamera/util/Util.cc b/services/camera/virtualcamera/util/Util.cc
index b2048bc..0c607d7 100644
--- a/services/camera/virtualcamera/util/Util.cc
+++ b/services/camera/virtualcamera/util/Util.cc
@@ -146,13 +146,6 @@
return false;
}
- if (width % kLibJpegDctSize != 0 || height % kLibJpegDctSize != 0) {
- // Input dimension needs to be multiple of libjpeg DCT size.
- // TODO(b/301023410) This restriction can be removed once we add support for
- // unaligned jpeg compression.
- return false;
- }
-
if (maxFps <= 0 || maxFps > kMaxFpsUpperLimit) {
return false;
}