Skip to content

Commit 99fe2f5

Browse files
adambengismeta-codesync[bot]
authored andcommitted
Make ImageFormat::CUSTOM_CODEC decodable via DecoderFactory (public)
Summary: Lets VRS natively decode ImageFormat::CUSTOM_CODEC image streams when a decoder is registered for the stream's codec name, and ships a public example showing how. This is the generic, codec-agnostic half: no proprietary codec is added here. Previously the CUSTOM_CODEC read/decode dispatch lived in the internal-only PixelFrame_fb.cpp behind #if IS_VRS_FB_INTERNAL(), so open-source VRS could not decode any custom codec. This moves the two generic methods (readCustomCodecFrame / decodeCustomCodecFrame) into PixelFrame.cpp, always compiled, and removes the gating on the two switch arms. The methods only dispatch through DecoderFactory keyed on the codec name -- exactly like the video codecs -- so there is nothing codec-specific in core. A binary that has registered no decoder for a given codec name gets makeDecoder() == nullptr and the frame is left undecoded, so stock VRS is unchanged. areCompatible(CUSTOM_CODEC) stays false (decodability is decided by the registered decoder, not the static table). sample_code/SampleCustomCodec.cpp is a minimal, Apache-licensed example: a DecoderI plus a registerSampleCustomCodecDecoder() that registers it with DecoderFactory. It documents the extension point for open-source users (the public docs previously said custom codecs must be decoded "with your own implementation, presumably unknown to VRS", with no mention of DecoderFactory). The example "codec" is an identity codec -- the wiring is the point, not the algorithm. Stage-then-commit decode (decode into a local buffer, re-spec the frame only on success) matches png/jpg/jxl so a failed decode can't leave a reused frame presenting the previous image as fresh. Block size is bounded against the record before allocating so a malformed .vrs can't escape the bool API as a bad_alloc. Both CUSTOM_CODEC entry points (the immediate readCustomCodecFrame and the deferred decompressImage path) now read the codec spec from an authoritative source and self-guard it with XR_VERIFY, so a reused frame can't silently mis-dispatch. Reviewed By: georges-berenger Differential Revision: D108314140 fbshipit-source-id: 4afda4b7611240901c7e41ed783cc4afbde1f7ae
1 parent 6e94fd3 commit 99fe2f5

6 files changed

Lines changed: 207 additions & 4 deletions

File tree

sample_code/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ target_link_libraries(sample_code_app
2121
vrslib
2222
vrs_logging
2323
vrs_os
24+
vrs_utils
2425
)

sample_code/SampleCustomCodec.cpp

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include <sample_code/SampleCustomCodec.h>
18+
19+
#include <algorithm>
20+
#include <cstdint>
21+
22+
#include <vrs/ErrorCode.h>
23+
#include <vrs/RecordFormat.h>
24+
#include <vrs/utils/DecoderFactory.h>
25+
26+
using namespace vrs;
27+
using namespace vrs::utils;
28+
29+
// Sample: make ImageFormat::CUSTOM_CODEC streams decodable by VRS tools. VRS bundles no custom
30+
// codec; you register a DecoderI with DecoderFactory keyed on your codec name, and PixelFrame
31+
// dispatches to it. This example "codec" is an identity codec whose blob is the raw pixels, to keep
32+
// the focus on the wiring. Call registerSampleCustomCodecDecoder() once at startup.
33+
34+
namespace vrs_sample_code {
35+
36+
namespace {
37+
38+
constexpr const char* kSampleCodecName = "sample_identity_codec";
39+
40+
class SampleCustomCodecDecoder : public DecoderI {
41+
public:
42+
int decode(
43+
const vector<uint8_t>& encodedFrame,
44+
void* outDecodedFrame,
45+
const ImageContentBlockSpec& outputImageSpec) override {
46+
const size_t rawSize = ImageContentBlockSpec(
47+
outputImageSpec.getPixelFormat(),
48+
outputImageSpec.getWidth(),
49+
outputImageSpec.getHeight())
50+
.getRawImageSize();
51+
if (rawSize == ContentBlock::kSizeUnknown || rawSize == 0) {
52+
return domainError(DecodeStatus::UnexpectedImageDimensions);
53+
}
54+
// Identity codec: input is exactly the raw pixels. A real codec decompresses into
55+
// outDecodedFrame here; the invariant that carries over is to write at most rawSize bytes.
56+
if (encodedFrame.size() != rawSize) {
57+
return domainError(DecodeStatus::DecoderError);
58+
}
59+
std::copy_n(encodedFrame.data(), rawSize, static_cast<uint8_t*>(outDecodedFrame));
60+
return 0;
61+
}
62+
};
63+
64+
} // namespace
65+
66+
void registerSampleCustomCodecDecoder() {
67+
DecoderFactory::get().registerDecoderMaker(
68+
[](const vector<uint8_t>& /*encodedFrame*/,
69+
void* /*outDecodedFrame*/,
70+
const ImageContentBlockSpec& outputImageSpec,
71+
const DecoderOptions& /*options*/) -> std::unique_ptr<DecoderI> {
72+
if (outputImageSpec.getCodecName() == kSampleCodecName) {
73+
return std::make_unique<SampleCustomCodecDecoder>();
74+
}
75+
return nullptr;
76+
});
77+
}
78+
79+
} // namespace vrs_sample_code

sample_code/SampleCustomCodec.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#pragma once
18+
19+
namespace vrs_sample_code {
20+
21+
/// Register the sample "sample_identity_codec" decoder with DecoderFactory. Call once at startup.
22+
void registerSampleCustomCodecDecoder();
23+
24+
} // namespace vrs_sample_code

vrs/utils/DecoderFactory.h

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,22 @@ class DecoderI {
4848
DecoderI& operator=(const DecoderI&) = delete;
4949
DecoderI(DecoderI&&) = delete;
5050
DecoderI& operator=(DecoderI&&) = delete;
51-
/// Decode compressed image to a frame
51+
/// Decode compressed image to a frame. outDecodedFrame is sized to
52+
/// outputImageSpec.getRawImageSize(); never write past it. Returns 0 on success, else a
53+
/// domainError(DecodeStatus::X) so errorCodeToMessage() can render it.
5254
virtual int decode(
5355
const vector<uint8_t>& encodedFrame,
5456
void* outDecodedFrame,
5557
const ImageContentBlockSpec& outputImageSpec) = 0;
5658
/// Flush the decoder's internal state (e.g., decoded picture buffer).
5759
/// Call this when seeking backward to avoid duplicate POC errors.
58-
/// Default implementation does nothing.
60+
/// Default implementation does nothing: custom image codecs are stateless and never need it,
61+
/// unlike video decoders that carry inter-frame state across seeks.
5962
virtual void flush() {}
6063
};
6164

65+
/// Selects a DecoderI for a stream; it must only select, never decode, else the frame decodes
66+
/// twice.
6267
using DecoderMaker = std::function<std::unique_ptr<DecoderI>(
6368
const vector<uint8_t>& encodedFrame,
6469
void* outDecodedFrame,
@@ -69,6 +74,8 @@ class DecoderFactory {
6974
public:
7075
static DecoderFactory& get();
7176

77+
/// Not thread-safe: register all makers at startup before any file is read. makeDecoder()
78+
/// iterates the maker list without a lock.
7279
void registerDecoderMaker(const DecoderMaker& decoderMaker);
7380

7481
void registerDecoderMaker(

vrs/utils/PixelFrame.cpp

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include <vrs/helpers/FileMacros.h>
3333
#include <vrs/helpers/Throttler.h>
3434
#include <vrs/utils/BufferRecordReader.hpp>
35+
#include <vrs/utils/DecoderFactory.h>
3536
#include <vrs/utils/converters/Grey10PackedConverters.h>
3637
#include <vrs/utils/converters/Raw10ToGrey10Converter.h>
3738
#include <utility>
@@ -170,6 +171,8 @@ bool PixelFrame::readFrame(RecordReader* reader, const ContentBlock& cb) {
170171
return readJpegFrame(reader, cb.getBlockSize());
171172
case ImageFormat::JXL:
172173
return readJxlFrame(reader, cb.getBlockSize());
174+
case ImageFormat::CUSTOM_CODEC:
175+
return readCustomCodecFrame(reader, cb);
173176
default:
174177
return false;
175178
}
@@ -213,12 +216,91 @@ bool PixelFrame::decompressImage(VideoFrameHandler* videoFrameHandler) {
213216
vector<uint8_t> compressedData(std::move(frameBytes_));
214217
return readJxlFrame(compressedData);
215218
}
219+
case ImageFormat::CUSTOM_CODEC: {
220+
// No ContentBlock here, so imageSpec_ is the only source of the codec name. It is
221+
// authoritative only when the frame came from readDiskImageData, which preserves the spec
222+
// verbatim.
223+
if (!XR_VERIFY(imageSpec_.getImageFormat() == ImageFormat::CUSTOM_CODEC) ||
224+
!XR_VERIFY(!imageSpec_.getCodecName().empty())) {
225+
return false;
226+
}
227+
vector<uint8_t> compressedData(std::move(frameBytes_));
228+
return decodeCustomCodecFrame(compressedData, imageSpec_);
229+
}
216230
default:
217231
return false;
218232
}
219233
return false;
220234
}
221235

236+
bool PixelFrame::readCustomCodecFrame(RecordReader* reader, const ContentBlock& cb) {
237+
// The codec spec, which carries the codec name, comes from the content block; imageSpec_ is not
238+
// authoritative here.
239+
const ImageContentBlockSpec& codecSpec = cb.image();
240+
if (!XR_VERIFY(codecSpec.getImageFormat() == ImageFormat::CUSTOM_CODEC) ||
241+
!XR_VERIFY(!codecSpec.getCodecName().empty())) {
242+
return false;
243+
}
244+
size_t sizeBytes = cb.getBlockSize();
245+
if (sizeBytes == 0 || sizeBytes == ContentBlock::kSizeUnknown) {
246+
return false;
247+
}
248+
// Bound against the record before allocating, like readRawFrame, so a bad size can't throw
249+
// bad_alloc out of this bool API.
250+
if (sizeBytes > reader->getUnreadBytes()) {
251+
THROTTLED_LOGE(
252+
reader->getRef(),
253+
"Custom codec image {} needs {} bytes, only {} available. Recording bug?",
254+
codecSpec.asString(),
255+
sizeBytes,
256+
reader->getUnreadBytes());
257+
return false;
258+
}
259+
vector<uint8_t> compressedData(sizeBytes);
260+
if (!VERIFY_SUCCESS(reader->read(compressedData.data(), sizeBytes))) {
261+
return false;
262+
}
263+
return decodeCustomCodecFrame(compressedData, codecSpec);
264+
}
265+
266+
bool PixelFrame::decodeCustomCodecFrame(
267+
const vector<uint8_t>& compressedData,
268+
const ImageContentBlockSpec& codecSpec) {
269+
if (codecSpec.getImageFormat() != ImageFormat::CUSTOM_CODEC || codecSpec.getCodecName().empty()) {
270+
return false;
271+
}
272+
PixelFormat pixelFormat = codecSpec.getPixelFormat();
273+
uint32_t width = codecSpec.getWidth();
274+
uint32_t height = codecSpec.getHeight();
275+
if (pixelFormat == PixelFormat::UNDEFINED || width == 0 || height == 0) {
276+
return false;
277+
}
278+
// Decode into a local buffer and commit only on success, as png/jpg/jxl do, so a failed decode
279+
// can't leave a reused frame presenting the prior image as fresh.
280+
ImageContentBlockSpec rawSpec(pixelFormat, width, height);
281+
size_t rawSize = rawSpec.getRawImageSize();
282+
if (rawSize == ContentBlock::kSizeUnknown || rawSize == 0) {
283+
return false;
284+
}
285+
vector<uint8_t> decoded(rawSize);
286+
// Hand the decoder a spec with default strides so getRawImageSize() matches decoded.size();
287+
// codecSpec may carry an explicit stride a stride-honoring decoder would overrun.
288+
ImageContentBlockSpec decodeSpec(
289+
ImageFormat::CUSTOM_CODEC,
290+
codecSpec.getCodecName(),
291+
ImageContentBlockSpec::kQualityUndefined,
292+
pixelFormat,
293+
width,
294+
height);
295+
unique_ptr<DecoderI> decoder =
296+
DecoderFactory::get().makeDecoder(compressedData, decoded.data(), decodeSpec);
297+
if (!decoder || decoder->decode(compressedData, decoded.data(), decodeSpec) != 0) {
298+
return false;
299+
}
300+
init(rawSpec, std::move(decoded));
301+
return true;
302+
}
303+
222304
bool PixelFrame::readRawFrame(
223305
RecordReader* reader,
224306
const ImageContentBlockSpec& inputImageSpec,

vrs/utils/PixelFrame.h

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,10 @@ class PixelFrame {
183183
/// Decode compressed image data, except for video codec compression.
184184
bool readCompressedFrame(const vector<uint8_t>& pixels, ImageFormat imageFormat);
185185

186+
/// Read a CUSTOM_CODEC frame and decode it via a DecoderFactory-registered decoder. Returns
187+
/// false if no decoder is registered for the codec name (frame left undecoded).
188+
bool readCustomCodecFrame(RecordReader* reader, const ContentBlock& cb);
189+
186190
/// Read a JPEG encoded frame into the internal buffer.
187191
/// @return True if the frame type is supported & the frame was read.
188192
bool readJpegFrame(RecordReader* reader, uint32_t sizeBytes);
@@ -336,7 +340,8 @@ class PixelFrame {
336340
/// Tell if an image format supports a specific pixel format.
337341
/// Only meaningful for png, jpg, jxl.
338342
/// Always true for ImageFormat::VIDEO (needs decoders to be sure).
339-
/// Always false for ImageFormat::CUSTOM_CODEC (never available).
343+
/// Always false for ImageFormat::CUSTOM_CODEC (decodability comes from a registered decoder,
344+
/// not this table).
340345
static bool areCompatible(ImageFormat imageFormat, PixelFormat pixelFormat);
341346

342347
/// Conversion in place from RGBA to RGB (no memory allocation)
@@ -414,7 +419,12 @@ class PixelFrame {
414419
PixelFormat targetPixelFormat,
415420
const NormalizeOptions& options) const;
416421

417-
private:
422+
/// Decode an in-memory CUSTOM_CODEC payload to RAW pixels via a DecoderFactory-registered
423+
/// decoder. codecSpec carries the codec name and decoded format/dimensions.
424+
bool decodeCustomCodecFrame(
425+
const vector<uint8_t>& compressedData,
426+
const ImageContentBlockSpec& codecSpec);
427+
418428
ImageContentBlockSpec imageSpec_;
419429
vector<uint8_t> frameBytes_;
420430
};

0 commit comments

Comments
 (0)