fix(android): consolidate native media decoders

This commit is contained in:
edde746
2026-07-24 03:56:40 +02:00
parent fb45ff44f3
commit 54a002f9b7
26 changed files with 2101 additions and 65 deletions
+15
View File
@@ -0,0 +1,15 @@
cmake_minimum_required(VERSION 3.22.1)
project(dovi_bridge_test LANGUAGES CXX)
enable_testing()
add_executable(dovi_bridge_test dovi_bridge_test.cpp)
target_compile_features(dovi_bridge_test PRIVATE cxx_std_17)
target_include_directories(dovi_bridge_test PRIVATE fakes)
add_test(NAME dovi_bridge_test COMMAND dovi_bridge_test)
add_executable(ffmpeg_audio_buffer_test ffmpeg_audio_buffer_test.cpp)
target_compile_features(ffmpeg_audio_buffer_test PRIVATE cxx_std_17)
add_test(NAME ffmpeg_audio_buffer_test COMMAND ffmpeg_audio_buffer_test)
@@ -0,0 +1,259 @@
#include <cstdarg>
#include <cstdio>
#include <string>
#include <vector>
#define DOVI_REAL_LINKED 1
#include "../../main/cpp/dovi_bridge.cpp"
struct DoviRpuOpaque {
std::string error;
};
enum class ParseResult { kValid, kError, kNull };
enum class WriteResult { kValid, kNull, kEmpty, kOversized };
static ParseResult parse_result;
static WriteResult write_result;
static int conversion_result;
static std::string parser_error;
static std::string last_log;
static std::vector<uint8_t> last_unspec_input;
static std::vector<uint8_t> encoded_output;
static int unspec_parse_calls;
static int raw_parse_calls;
static int conversion_calls;
static int write_calls;
static int rpu_free_calls;
static int data_free_calls;
extern "C" int __android_log_print(int, const char*, const char* format, ...) {
char buffer[2048];
va_list args;
va_start(args, format);
std::vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
last_log = buffer;
return static_cast<int>(last_log.size());
}
extern "C" DoviRpuOpaque* dovi_parse_unspec62_nalu(const uint8_t* data, size_t len) {
++unspec_parse_calls;
last_unspec_input.assign(data, data + len);
if (parse_result == ParseResult::kNull) return nullptr;
return new DoviRpuOpaque{parse_result == ParseResult::kError ? parser_error : ""};
}
// A raw parse deliberately succeeds so the malformed-NAL test detects any
// regression that retries framed input through dovi_parse_rpu.
extern "C" DoviRpuOpaque* dovi_parse_rpu(const uint8_t*, size_t) {
++raw_parse_calls;
return new DoviRpuOpaque{};
}
extern "C" const char* dovi_rpu_get_error(const DoviRpuOpaque* rpu) {
return rpu->error.empty() ? nullptr : rpu->error.c_str();
}
extern "C" void dovi_rpu_free(DoviRpuOpaque* rpu) {
++rpu_free_calls;
delete rpu;
}
extern "C" int32_t dovi_convert_rpu_with_mode(DoviRpuOpaque* rpu, uint8_t) {
++conversion_calls;
if (conversion_result != 0) rpu->error = "conversion error";
return conversion_result;
}
extern "C" const DoviData* dovi_write_unspec62_nalu(DoviRpuOpaque*) {
++write_calls;
if (write_result == WriteResult::kNull) return nullptr;
if (write_result == WriteResult::kEmpty) return new DoviData{nullptr, 0};
const size_t len = write_result == WriteResult::kOversized ? MAX_RPU_OUTPUT_SIZE + 1 : encoded_output.size();
auto* bytes = new uint8_t[len];
for (size_t i = 0; i < len; ++i) {
bytes[i] = write_result == WriteResult::kOversized ? 0 : encoded_output[i];
}
return new DoviData{bytes, len};
}
extern "C" void dovi_data_free(const DoviData* data) {
++data_free_calls;
delete[] data->data;
delete data;
}
static void resetFakes() {
parse_result = ParseResult::kValid;
write_result = WriteResult::kValid;
conversion_result = 0;
parser_error.clear();
last_log.clear();
last_unspec_input.clear();
encoded_output = {0x7c, 0x01, 0x19, 0x08, 0x55};
unspec_parse_calls = 0;
raw_parse_calls = 0;
conversion_calls = 0;
write_calls = 0;
rpu_free_calls = 0;
data_free_calls = 0;
}
static _jbyteArray byteArray(const std::vector<uint8_t>& bytes) {
_jbyteArray array;
array.bytes.reserve(bytes.size());
for (uint8_t byte : bytes) array.bytes.push_back(static_cast<jbyte>(byte));
return array;
}
static jint convert(
JNIEnv& env, _jbyteArray& payload, _jbyteArray& output, jint output_offset = 0, jint output_capacity = -1) {
if (output_capacity < 0) output_capacity = static_cast<jint>(output.bytes.size());
return Java_com_edde746_plezy_exoplayer_DoviBridge_nativeConvertDv7RpuToDv81(
&env, nullptr, &payload, 0, static_cast<jint>(payload.bytes.size()), &output, output_offset, output_capacity, 2);
}
#define CHECK(condition) \
do { \
if (!(condition)) { \
std::fprintf(stderr, "%s:%d: check failed: %s\n", __FILE__, __LINE__, #condition); \
return false; \
} \
} while (false)
static bool validUnspec62NalConvertsAndFreesAllocations() {
resetFakes();
const std::vector<uint8_t> framed_nal = {0x7c, 0x01, 0x19, 0x08, 0x22};
auto payload = byteArray(framed_nal);
auto output = byteArray(std::vector<uint8_t>(16, 0));
JNIEnv env;
const jint result = convert(env, payload, output, 2);
CHECK(result == static_cast<jint>(encoded_output.size()));
CHECK(last_unspec_input == framed_nal);
CHECK(raw_parse_calls == 0);
CHECK(unspec_parse_calls == 1);
CHECK(conversion_calls == 1);
CHECK(write_calls == 1);
CHECK(rpu_free_calls == 1);
CHECK(data_free_calls == 1);
for (size_t i = 0; i < encoded_output.size(); ++i) {
CHECK(static_cast<uint8_t>(output.bytes[i + 2]) == encoded_output[i]);
}
return true;
}
static bool malformedTruncatedNalFailsWithoutRawFallback() {
resetFakes();
parse_result = ParseResult::kError;
parser_error.assign(2048, 'x');
auto payload = byteArray({0x7c, 0x01, 0x19});
auto output = byteArray(std::vector<uint8_t>(16, 0));
JNIEnv env;
const jint result = convert(env, payload, output);
CHECK(result == CONVERT_FAILED);
CHECK(unspec_parse_calls == 1);
CHECK(raw_parse_calls == 0);
CHECK(conversion_calls == 0);
CHECK(write_calls == 0);
CHECK(rpu_free_calls == 1);
CHECK(data_free_calls == 0);
const std::string prefix = "RPU NAL parse failed: ";
CHECK(last_log == prefix + std::string(MAX_ERROR_LOG_LENGTH, 'x'));
return true;
}
static bool nullParserResultFailsWithoutFreeingNonexistentAllocation() {
resetFakes();
parse_result = ParseResult::kNull;
auto payload = byteArray({0x7c, 0x01});
auto output = byteArray(std::vector<uint8_t>(16, 0));
JNIEnv env;
CHECK(convert(env, payload, output) == CONVERT_FAILED);
CHECK(raw_parse_calls == 0);
CHECK(rpu_free_calls == 0);
CHECK(data_free_calls == 0);
return true;
}
static bool conversionFailureFreesRpu() {
resetFakes();
conversion_result = -1;
auto payload = byteArray({0x7c, 0x01, 0x19, 0x08});
auto output = byteArray(std::vector<uint8_t>(16, 0));
JNIEnv env;
CHECK(convert(env, payload, output) == CONVERT_FAILED);
CHECK(rpu_free_calls == 1);
CHECK(data_free_calls == 0);
return true;
}
static bool unusableWriterResultFreesBothAllocations() {
resetFakes();
write_result = WriteResult::kEmpty;
auto payload = byteArray({0x7c, 0x01, 0x19, 0x08});
auto output = byteArray(std::vector<uint8_t>(16, 0));
JNIEnv env;
CHECK(convert(env, payload, output) == CONVERT_FAILED);
CHECK(rpu_free_calls == 1);
CHECK(data_free_calls == 1);
return true;
}
static bool destinationTooSmallFreesBothAllocations() {
resetFakes();
auto payload = byteArray({0x7c, 0x01, 0x19, 0x08});
auto output = byteArray(std::vector<uint8_t>(4, 0));
JNIEnv env;
CHECK(convert(env, payload, output) == DESTINATION_TOO_SMALL);
CHECK(rpu_free_calls == 1);
CHECK(data_free_calls == 1);
return true;
}
static bool jniWriteFailureFreesBothAllocations() {
resetFakes();
auto payload = byteArray({0x7c, 0x01, 0x19, 0x08});
auto output = byteArray(std::vector<uint8_t>(16, 0));
JNIEnv env;
env.fail_next_write = true;
CHECK(convert(env, payload, output) == CONVERT_FAILED);
CHECK(rpu_free_calls == 1);
CHECK(data_free_calls == 1);
return true;
}
int main() {
struct TestCase {
const char* name;
bool (*run)();
};
const TestCase tests[] = {
{"valid conversion", validUnspec62NalConvertsAndFreesAllocations},
{"malformed truncated NAL", malformedTruncatedNalFailsWithoutRawFallback},
{"null parser result", nullParserResultFailsWithoutFreeingNonexistentAllocation},
{"conversion failure", conversionFailureFreesRpu},
{"unusable writer result", unusableWriterResultFreesBothAllocations},
{"destination too small", destinationTooSmallFreesBothAllocations},
{"JNI write failure", jniWriteFailureFreesBothAllocations},
};
for (const TestCase& test : tests) {
if (!test.run()) {
std::fprintf(stderr, "FAILED: %s\n", test.name);
return 1;
}
}
std::printf("Passed %zu dovi_bridge tests\n", sizeof(tests) / sizeof(tests[0]));
return 0;
}
@@ -0,0 +1,14 @@
#pragma once
#define ANDROID_LOG_INFO 4
#define ANDROID_LOG_WARN 5
#ifdef __cplusplus
extern "C" {
#endif
int __android_log_print(int priority, const char* tag, const char* format, ...);
#ifdef __cplusplus
}
#endif
+62
View File
@@ -0,0 +1,62 @@
#pragma once
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
#define JNIEXPORT
#define JNICALL
#define JNI_TRUE 1
#define JNI_FALSE 0
using jboolean = uint8_t;
using jbyte = int8_t;
using jint = int32_t;
using jsize = jint;
struct _jclass {};
using jclass = _jclass*;
struct _jbyteArray {
std::vector<jbyte> bytes;
};
using jbyteArray = _jbyteArray*;
struct _jstring {
std::string value;
};
using jstring = _jstring*;
class JNIEnv {
public:
bool exception_pending = false;
bool fail_next_write = false;
jsize GetArrayLength(jbyteArray array) { return static_cast<jsize>(array->bytes.size()); }
void GetByteArrayRegion(jbyteArray array, jsize offset, jsize length, jbyte* destination) {
if (offset < 0 || length < 0 || offset > GetArrayLength(array) || length > GetArrayLength(array) - offset) {
exception_pending = true;
return;
}
std::memcpy(destination, array->bytes.data() + offset, static_cast<size_t>(length));
}
void SetByteArrayRegion(jbyteArray array, jsize offset, jsize length, const jbyte* source) {
if (fail_next_write) {
fail_next_write = false;
exception_pending = true;
return;
}
if (offset < 0 || length < 0 || offset > GetArrayLength(array) || length > GetArrayLength(array) - offset) {
exception_pending = true;
return;
}
std::memcpy(array->bytes.data() + offset, source, static_cast<size_t>(length));
}
jboolean ExceptionCheck() const { return exception_pending ? JNI_TRUE : JNI_FALSE; }
jstring NewStringUTF(const char* value) { return new _jstring{value}; }
};
@@ -0,0 +1,76 @@
#include "../../main/cpp/media3_ffmpeg_decoder/ffmpeg_audio_buffer.h"
#include <climits>
#include <cstdio>
namespace {
bool check(bool condition, const char* message) {
if (!condition) std::fprintf(stderr, "%s\n", message);
return condition;
}
bool computesPackedPcmSizes() {
int bytes = -1;
return check(plezy::ffmpeg::CheckedAudioByteCount(1024, 2, 2, &bytes), "stereo PCM size rejected") &&
check(bytes == 4096, "wrong stereo PCM size") &&
check(plezy::ffmpeg::CheckedAudioByteCount(1024, 6, 4, &bytes), "5.1 float PCM size rejected") &&
check(bytes == 24576, "wrong 5.1 float PCM size") &&
check(plezy::ffmpeg::CheckedAudioByteCount(1024, 8, 2, &bytes), "7.1 PCM size rejected") &&
check(bytes == 16384, "wrong 7.1 PCM size");
}
bool usesConvertedSampleCount() {
int capacity = -1;
int written = -1;
return check(plezy::ffmpeg::CheckedAudioByteCount(2048, 6, 2, &capacity), "output capacity rejected") &&
check(plezy::ffmpeg::CheckedAudioByteCount(1536, 6, 2, &written), "converted sample count rejected") &&
check(capacity == 24576, "wrong output capacity") && check(written == 18432, "wrong converted byte count") &&
check(written < capacity, "converted bytes must not equal an unused upper bound");
}
bool rejectsInvalidAndOverflowingSizes() {
int bytes = 7;
const int largestSafeStereoSampleCount = INT_MAX / 4;
return check(!plezy::ffmpeg::CheckedAudioByteCount(-1, 2, 2, &bytes), "negative samples accepted") &&
check(!plezy::ffmpeg::CheckedAudioByteCount(1, 0, 2, &bytes), "zero channels accepted") &&
check(!plezy::ffmpeg::CheckedAudioByteCount(1, 2, 0, &bytes), "zero sample size accepted") &&
check(
!plezy::ffmpeg::CheckedAudioByteCount(largestSafeStereoSampleCount + 1, 2, 2, &bytes),
"multiplication overflow accepted") &&
check(!plezy::ffmpeg::CheckedAddByteCount(INT_MAX, 1, &bytes), "addition overflow accepted") &&
check(!plezy::ffmpeg::CheckedAddByteCount(-1, 1, &bytes), "negative accumulated size accepted");
}
bool acceptsEmptyOutputAndIntBoundary() {
int bytes = -1;
int total = -1;
return check(plezy::ffmpeg::CheckedAudioByteCount(0, 8, 4, &bytes), "empty output rejected") &&
check(bytes == 0, "empty output is not zero bytes") &&
check(plezy::ffmpeg::CheckedAddByteCount(INT_MAX - 4, 4, &total), "INT_MAX boundary rejected") &&
check(total == INT_MAX, "wrong INT_MAX boundary sum");
}
} // namespace
int main() {
struct TestCase {
const char* name;
bool (*run)();
};
const TestCase tests[] = {
{"packed PCM sizes", computesPackedPcmSizes},
{"converted sample count", usesConvertedSampleCount},
{"invalid and overflowing sizes", rejectsInvalidAndOverflowingSizes},
{"empty output and boundary", acceptsEmptyOutputAndIntBoundary},
};
for (const TestCase& test : tests) {
if (!test.run()) {
std::fprintf(stderr, "FAILED: %s\n", test.name);
return 1;
}
}
std::printf("Passed %zu ffmpeg_audio_buffer tests\n", sizeof(tests) / sizeof(tests[0]));
return 0;
}