blob: a8df886c1e239277f1a7b9eab919e1ec001160de [file]
// Copyright 2026 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <fuchsia/media/cpp/fidl.h>
#include <cstddef>
#include <cstdint>
#include <mutex>
#include <string>
#include <utility>
#include <vector>
#include <gtest/gtest.h>
#include "src/media/codec/codecs/sw/lc3/codec_adapter_lc3_decoder.h"
#include "src/media/codec/codecs/sw/lc3/codec_adapter_lc3_encoder.h"
#include "src/media/codec/codecs/test/test_codec_packets.h"
#include "src/media/codec/codecs/test/test_fake_codec_adapter_events.h"
namespace {
class TestCodecAdapterLc3Decoder : public CodecAdapterLc3Decoder {
public:
TestCodecAdapterLc3Decoder(std::mutex& lock, CodecAdapterEvents* events)
: CodecAdapterLc3Decoder(lock, events) {}
using CodecAdapterLc3Decoder::CleanUpAfterStream;
using CodecAdapterLc3Decoder::InputChunkSize;
using CodecAdapterLc3Decoder::InputLoopStatus;
using CodecAdapterLc3Decoder::MinOutputBufferSize;
using CodecAdapterLc3Decoder::OutputFormatDetails;
using CodecAdapterLc3Decoder::ProcessFormatDetails;
using CodecAdapterLc3Decoder::ProcessInputChunkData;
};
class TestCodecAdapterLc3Encoder : public CodecAdapterLc3Encoder {
public:
TestCodecAdapterLc3Encoder(std::mutex& lock, CodecAdapterEvents* events)
: CodecAdapterLc3Encoder(lock, events) {}
using CodecAdapterLc3Encoder::CleanUpAfterStream;
using CodecAdapterLc3Encoder::CreateTimestampExtrapolator;
using CodecAdapterLc3Encoder::InputChunkSize;
using CodecAdapterLc3Encoder::InputLoopStatus;
using CodecAdapterLc3Encoder::MinOutputBufferSize;
using CodecAdapterLc3Encoder::ProcessFormatDetails;
using CodecAdapterLc3Encoder::ProcessInputChunkData;
};
fuchsia::media::FormatDetails MakeLc3FormatDetails(std::vector<uint8_t> oob_bytes) {
fuchsia::media::FormatDetails format_details;
format_details.set_mime_type(kLc3MimeType);
format_details.set_oob_bytes(std::move(oob_bytes));
return format_details;
}
fuchsia::media::FormatDetails MakeValidLc3DecoderFormatDetails() {
// Valid 16-byte LTV configuration:
// Sampling_Frequency: 48 kHz (0x08)
// Frame_Duration: 10 ms (0x01)
// Audio_Channel_Allocation: LF (0x00000001)
// Octets_Per_Codec_Frame: 40 (0x0028)
std::vector<uint8_t> oob_bytes = {
0x02, 0x01, 0x08, // Sampling_Frequency: 48 kHz
0x02, 0x02, 0x01, // Frame_Duration: 10 ms
0x05, 0x03, 0x00, 0x00, 0x00, 0x01, // Audio_Channel_Allocation: LF
0x03, 0x04, 0x00, 0x28 // Octets_Per_Codec_Frame: 40
};
return MakeLc3FormatDetails(std::move(oob_bytes));
}
fuchsia::media::FormatDetails MakeValidLc3EncoderFormatDetails() {
fuchsia::media::PcmFormat pcm;
pcm.pcm_mode = fuchsia::media::AudioPcmMode::LINEAR;
pcm.bits_per_sample = 16;
pcm.frames_per_second = 48000;
pcm.channel_map = {fuchsia::media::AudioChannelId::LF};
fuchsia::media::AudioUncompressedFormat uncompressed;
uncompressed.set_pcm(std::move(pcm));
fuchsia::media::AudioFormat audio;
audio.set_uncompressed(std::move(uncompressed));
fuchsia::media::DomainFormat domain;
domain.set_audio(std::move(audio));
fuchsia::media::Lc3EncoderSettings lc3_settings;
lc3_settings.set_nbytes(40);
lc3_settings.set_frame_duration(fuchsia::media::Lc3FrameDuration::D10_MS);
fuchsia::media::EncoderSettings encoder_settings;
encoder_settings.set_lc3(std::move(lc3_settings));
fuchsia::media::FormatDetails format_details;
format_details.set_domain(std::move(domain));
format_details.set_encoder_settings(std::move(encoder_settings));
return format_details;
}
TEST(CodecAdapterLc3DecoderTest, ValidOobBytesSucceeds) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Decoder decoder(lock, &events);
auto status = decoder.ProcessFormatDetails(MakeValidLc3DecoderFormatDetails());
EXPECT_EQ(status, TestCodecAdapterLc3Decoder::kOk);
EXPECT_EQ(events.fail_codec_count(), 0u);
EXPECT_EQ(decoder.InputChunkSize(), 40u);
EXPECT_EQ(decoder.MinOutputBufferSize(), 960u);
}
TEST(CodecAdapterLc3DecoderTest, MissingMimeTypeOrOobBytesRejected) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Decoder decoder(lock, &events);
fuchsia::media::FormatDetails missing_mime;
missing_mime.set_oob_bytes(std::vector<uint8_t>(16, 0));
EXPECT_EQ(decoder.ProcessFormatDetails(missing_mime),
TestCodecAdapterLc3Decoder::kShouldTerminate);
EXPECT_EQ(events.fail_codec_count(), 1u);
fuchsia::media::FormatDetails short_oob;
short_oob.set_mime_type(kLc3MimeType);
short_oob.set_oob_bytes(std::vector<uint8_t>(15, 0));
EXPECT_EQ(decoder.ProcessFormatDetails(short_oob), TestCodecAdapterLc3Decoder::kShouldTerminate);
EXPECT_EQ(events.fail_codec_count(), 2u);
}
TEST(CodecAdapterLc3DecoderTest, OutOfRangeOctetsPerCodecFrameRejected) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Decoder decoder(lock, &events);
// Octets_Per_Codec_Frame = 19 (< kMinExternalByteCount = 20).
std::vector<uint8_t> oob_bytes = {
0x02, 0x01, 0x08, // Sampling_Frequency
0x02, 0x02, 0x01, // Frame_Duration
0x05, 0x03, 0x00, 0x00, 0x00, 0x01, // Audio_Channel_Allocation
0x03, 0x04, 0x00, 0x13 // Octets_Per_Codec_Frame = 19
};
auto status = decoder.ProcessFormatDetails(MakeLc3FormatDetails(std::move(oob_bytes)));
EXPECT_EQ(status, TestCodecAdapterLc3Decoder::kShouldTerminate);
EXPECT_EQ(events.fail_codec_count(), 1u);
EXPECT_NE(events.last_fail_message().find("Octets_Per_Codec_Frame"), std::string::npos)
<< "Actual message: " << events.last_fail_message();
}
TEST(CodecAdapterLc3DecoderTest, ValidOobBytesWithUnknownLtvParamSucceeds) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Decoder decoder(lock, &events);
// Valid 16-byte LTV configuration plus optional/unknown LTV parameter (e.g.
// Codec_Frame_Blocks_Per_SDU = 0x05).
std::vector<uint8_t> oob_bytes = {
0x02, 0x01, 0x08, // Sampling_Frequency
0x02, 0x02, 0x01, // Frame_Duration
0x05, 0x03, 0x00, 0x00, 0x00, 0x01, // Audio_Channel_Allocation
0x03, 0x04, 0x00, 0x28, // Octets_Per_Codec_Frame
0x02, 0x05, 0x01 // Optional Codec_Frame_Blocks_Per_SDU
};
auto status = decoder.ProcessFormatDetails(MakeLc3FormatDetails(std::move(oob_bytes)));
EXPECT_EQ(status, TestCodecAdapterLc3Decoder::kOk);
EXPECT_EQ(events.fail_codec_count(), 0u);
}
TEST(CodecAdapterLc3DecoderTest, TruncatedAudioChannelAllocationRejected) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Decoder decoder(lock, &events);
// Audio_Channel_Allocation with LTV length 2 (1 byte value instead of 4).
std::vector<uint8_t> oob_bytes = {
0x02, 0x01, 0x08, // Sampling_Frequency
0x02, 0x02, 0x01, // Frame_Duration
0x02, 0x03, 0x00, // Truncated Audio_Channel_Allocation (len=2)
0x03, 0x04, 0x00, 0x28, // Valid Octets_Per_Codec_Frame
0x04, 0x99, 0x00, 0x00, 0x00 // Unknown LTV param to pad to >= 16B
};
auto status = decoder.ProcessFormatDetails(MakeLc3FormatDetails(std::move(oob_bytes)));
EXPECT_EQ(status, TestCodecAdapterLc3Decoder::kShouldTerminate);
EXPECT_EQ(events.fail_codec_count(), 1u);
EXPECT_NE(events.last_fail_message().find("Audio_Channel_Allocation"), std::string::npos)
<< "Actual message: " << events.last_fail_message();
}
TEST(CodecAdapterLc3DecoderTest, TruncatedOctetsPerCodecFrameRejected) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Decoder decoder(lock, &events);
// Octets_Per_Codec_Frame with LTV length 2 (1 byte value instead of 2).
std::vector<uint8_t> oob_bytes = {
0x02, 0x01, 0x08, // Sampling_Frequency
0x02, 0x02, 0x01, // Frame_Duration
0x05, 0x03, 0x00, 0x00, 0x00, 0x01, // Audio_Channel_Allocation
0x02, 0x04, 0x28, // Truncated Octets_Per_Codec_Frame (len=2)
0x01, 0x99 // Unknown param to pad to 16B
};
auto status = decoder.ProcessFormatDetails(MakeLc3FormatDetails(std::move(oob_bytes)));
EXPECT_EQ(status, TestCodecAdapterLc3Decoder::kShouldTerminate);
EXPECT_EQ(events.fail_codec_count(), 1u);
EXPECT_NE(events.last_fail_message().find("Octets_Per_Codec_Frame"), std::string::npos)
<< "Actual message: " << events.last_fail_message();
}
TEST(CodecAdapterLc3DecoderTest, TruncatedSamplingFrequencyRejected) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Decoder decoder(lock, &events);
// Sampling_Frequency with LTV length 1 (0 value bytes).
std::vector<uint8_t> oob_bytes = {
0x01, 0x01, // Truncated Sampling_Frequency (len=1)
0x02, 0x02, 0x01, // Frame_Duration
0x05, 0x03, 0x00, 0x00, 0x00, 0x01, // Audio_Channel_Allocation
0x03, 0x04, 0x00, 0x28, // Octets_Per_Codec_Frame
0x01, 0x99 // Padding to >= 16B
};
auto status = decoder.ProcessFormatDetails(MakeLc3FormatDetails(std::move(oob_bytes)));
EXPECT_EQ(status, TestCodecAdapterLc3Decoder::kShouldTerminate);
EXPECT_EQ(events.fail_codec_count(), 1u);
EXPECT_NE(events.last_fail_message().find("Sampling_Frequency"), std::string::npos)
<< "Actual message: " << events.last_fail_message();
}
TEST(CodecAdapterLc3DecoderTest, TruncatedFrameDurationRejected) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Decoder decoder(lock, &events);
// Frame_Duration with LTV length 1 (0 value bytes).
std::vector<uint8_t> oob_bytes = {
0x02, 0x01, 0x08, // Sampling_Frequency
0x01, 0x02, // Truncated Frame_Duration (len=1)
0x05, 0x03, 0x00, 0x00, 0x00, 0x01, // Audio_Channel_Allocation
0x03, 0x04, 0x00, 0x28, // Octets_Per_Codec_Frame
0x01, 0x99 // Padding to >= 16B
};
auto status = decoder.ProcessFormatDetails(MakeLc3FormatDetails(std::move(oob_bytes)));
EXPECT_EQ(status, TestCodecAdapterLc3Decoder::kShouldTerminate);
EXPECT_EQ(events.fail_codec_count(), 1u);
EXPECT_NE(events.last_fail_message().find("Frame_Duration"), std::string::npos)
<< "Actual message: " << events.last_fail_message();
}
TEST(CodecAdapterLc3DecoderTest, ZeroLengthLtvEntryAtEndRejected) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Decoder decoder(lock, &events);
// Valid LTVs followed by a trailing 0x00 length byte at the very end of oob_bytes.
std::vector<uint8_t> oob_bytes = {
0x02, 0x01, 0x08, // Sampling_Frequency
0x02, 0x02, 0x01, // Frame_Duration
0x05, 0x03, 0x00, 0x00, 0x00, 0x01, // Audio_Channel_Allocation
0x03, 0x04, 0x00, 0x28, // Octets_Per_Codec_Frame
0x00 // len == 0 at final byte
};
auto status = decoder.ProcessFormatDetails(MakeLc3FormatDetails(std::move(oob_bytes)));
EXPECT_EQ(status, TestCodecAdapterLc3Decoder::kShouldTerminate);
EXPECT_EQ(events.fail_codec_count(), 1u);
EXPECT_NE(events.last_fail_message().find("invalid LTV length"), std::string::npos)
<< "Actual message: " << events.last_fail_message();
}
TEST(CodecAdapterLc3DecoderTest, LtvLengthExceedsRemainingBufferRejected) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Decoder decoder(lock, &events);
// Final LTV claims length 10 when only 3 bytes remain.
std::vector<uint8_t> oob_bytes = {
0x02, 0x01, 0x08, // Sampling_Frequency
0x02, 0x02, 0x01, // Frame_Duration
0x05, 0x03, 0x00, 0x00, 0x00, 0x01, // Audio_Channel_Allocation
0x0a, 0x04, 0x00, 0x28 // len=10 exceeds buffer
};
auto status = decoder.ProcessFormatDetails(MakeLc3FormatDetails(std::move(oob_bytes)));
EXPECT_EQ(status, TestCodecAdapterLc3Decoder::kShouldTerminate);
EXPECT_EQ(events.fail_codec_count(), 1u);
EXPECT_NE(events.last_fail_message().find("invalid LTV length"), std::string::npos)
<< "Actual message: " << events.last_fail_message();
}
TEST(CodecAdapterLc3DecoderTest, DuplicateLtvParameterKeyRejected) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Decoder decoder(lock, &events);
// Duplicate Sampling_Frequency (key 0x01) entries.
std::vector<uint8_t> oob_bytes = {
0x02, 0x01, 0x08, // Sampling_Frequency #1
0x02, 0x01, 0x08, // Sampling_Frequency #2 (duplicate)
0x02, 0x02, 0x01, // Frame_Duration
0x05, 0x03, 0x00, 0x00, 0x00, 0x01, // Audio_Channel_Allocation
0x03, 0x04, 0x00, 0x28 // Octets_Per_Codec_Frame
};
auto status = decoder.ProcessFormatDetails(MakeLc3FormatDetails(std::move(oob_bytes)));
EXPECT_EQ(status, TestCodecAdapterLc3Decoder::kShouldTerminate);
EXPECT_EQ(events.fail_codec_count(), 1u);
EXPECT_NE(events.last_fail_message().find("duplicate LTV parameter key"), std::string::npos)
<< "Actual message: " << events.last_fail_message();
}
TEST(CodecAdapterLc3DecoderTest, ZeroAudioChannelAllocationRejected) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Decoder decoder(lock, &events);
// Audio_Channel_Allocation = 0x00000000 (0 channels).
std::vector<uint8_t> oob_bytes = {
0x02, 0x01, 0x08, // Sampling_Frequency
0x02, 0x02, 0x01, // Frame_Duration
0x05, 0x03, 0x00, 0x00, 0x00, 0x00, // Audio_Channel_Allocation = 0
0x03, 0x04, 0x00, 0x28 // Octets_Per_Codec_Frame
};
auto status = decoder.ProcessFormatDetails(MakeLc3FormatDetails(std::move(oob_bytes)));
EXPECT_EQ(status, TestCodecAdapterLc3Decoder::kShouldTerminate);
EXPECT_EQ(events.fail_codec_count(), 1u);
EXPECT_NE(events.last_fail_message().find("Audio_Channel_Allocation"), std::string::npos)
<< "Actual message: " << events.last_fail_message();
}
TEST(CodecAdapterLc3DecoderTest, IncompleteRequiredLtvParamsRejected) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Decoder decoder(lock, &events);
// oob_bytes >= 16 bytes (padded with unknown LTV key 0x05), missing Octets_Per_Codec_Frame
// (0x04).
std::vector<uint8_t> oob_bytes = {
0x02, 0x01, 0x08, // Sampling_Frequency
0x02, 0x02, 0x01, // Frame_Duration
0x05, 0x03, 0x00, 0x00, 0x00, 0x01, // Audio_Channel_Allocation
0x04, 0x05, 0x01, 0x00, 0x00 // Unknown param to pad to >= 16B
};
auto status = decoder.ProcessFormatDetails(MakeLc3FormatDetails(std::move(oob_bytes)));
EXPECT_EQ(status, TestCodecAdapterLc3Decoder::kShouldTerminate);
EXPECT_EQ(events.fail_codec_count(), 1u);
EXPECT_NE(events.last_fail_message().find("incomplete"), std::string::npos)
<< "Actual message: " << events.last_fail_message();
}
TEST(CodecAdapterLc3DecoderTest, MidstreamFormatChangeRejected) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Decoder decoder(lock, &events);
ASSERT_EQ(decoder.ProcessFormatDetails(MakeValidLc3DecoderFormatDetails()),
TestCodecAdapterLc3Decoder::kOk);
EXPECT_EQ(events.fail_codec_count(), 0u);
// A second ProcessFormatDetails call on an active stream should fail the codec.
EXPECT_EQ(decoder.ProcessFormatDetails(MakeValidLc3DecoderFormatDetails()),
TestCodecAdapterLc3Decoder::kShouldTerminate);
EXPECT_EQ(events.fail_codec_count(), 1u);
EXPECT_NE(events.last_fail_message().find("Midstream input format change"), std::string::npos)
<< "Actual message: " << events.last_fail_message();
}
TEST(CodecAdapterLc3DecoderTest, ProcessInputChunkDataBufferSizeValidation) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Decoder decoder(lock, &events);
ASSERT_EQ(decoder.ProcessFormatDetails(MakeValidLc3DecoderFormatDetails()),
TestCodecAdapterLc3Decoder::kOk);
const size_t input_size = decoder.InputChunkSize();
const size_t min_output_size = decoder.MinOutputBufferSize();
std::vector<uint8_t> input(input_size, 0);
std::vector<uint8_t> output(min_output_size, 0);
// Mismatched input_data_size should return -1.
EXPECT_EQ(
decoder.ProcessInputChunkData(input.data(), input_size - 1, output.data(), output.size()),
-1);
// Undersized output_buffer_size should return -1.
EXPECT_EQ(
decoder.ProcessInputChunkData(input.data(), input_size, output.data(), min_output_size - 1),
-1);
// Zeroed input triggers liblc3 Packet Loss Concealment (return code 1) and succeeds.
EXPECT_EQ(decoder.ProcessInputChunkData(input.data(), input_size, output.data(), output.size()),
static_cast<int>(min_output_size));
}
TEST(CodecAdapterLc3EncoderTest, ValidFormatDetailsSucceeds) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Encoder encoder(lock, &events);
auto status = encoder.ProcessFormatDetails(MakeValidLc3EncoderFormatDetails());
EXPECT_EQ(status, TestCodecAdapterLc3Encoder::kOk);
EXPECT_EQ(events.fail_codec_count(), 0u);
EXPECT_EQ(encoder.InputChunkSize(), 960u);
EXPECT_EQ(encoder.MinOutputBufferSize(), 40u);
}
TEST(CodecAdapterLc3EncoderTest, MissingDomainOrEncoderSettingsRejected) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Encoder encoder(lock, &events);
fuchsia::media::FormatDetails missing_domain = MakeValidLc3EncoderFormatDetails();
missing_domain.clear_domain();
EXPECT_EQ(encoder.ProcessFormatDetails(missing_domain),
TestCodecAdapterLc3Encoder::kShouldTerminate);
EXPECT_EQ(events.fail_codec_count(), 1u);
fuchsia::media::FormatDetails missing_settings = MakeValidLc3EncoderFormatDetails();
missing_settings.clear_encoder_settings();
EXPECT_EQ(encoder.ProcessFormatDetails(missing_settings),
TestCodecAdapterLc3Encoder::kShouldTerminate);
EXPECT_EQ(events.fail_codec_count(), 2u);
}
TEST(CodecAdapterLc3EncoderTest, OutOfRangeEncoderNbytesRejected) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Encoder encoder(lock, &events);
auto format_details = MakeValidLc3EncoderFormatDetails();
format_details.mutable_encoder_settings()->lc3().set_nbytes(401);
auto status = encoder.ProcessFormatDetails(format_details);
EXPECT_EQ(status, TestCodecAdapterLc3Encoder::kShouldTerminate);
EXPECT_EQ(events.fail_codec_count(), 1u);
EXPECT_NE(events.last_fail_message().find("Byte count"), std::string::npos)
<< "Actual message: " << events.last_fail_message();
}
TEST(CodecAdapterLc3EncoderTest, EmptyChannelMapRejected) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Encoder encoder(lock, &events);
auto format_details = MakeValidLc3EncoderFormatDetails();
format_details.mutable_domain()->audio().uncompressed().pcm().channel_map.clear();
auto status = encoder.ProcessFormatDetails(format_details);
EXPECT_EQ(status, TestCodecAdapterLc3Encoder::kShouldTerminate);
EXPECT_EQ(events.fail_codec_count(), 1u);
EXPECT_NE(events.last_fail_message().find("Unsupported channel count"), std::string::npos)
<< "Actual message: " << events.last_fail_message();
}
TEST(CodecAdapterLc3EncoderTest, ExcessiveChannelMapRejected) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Encoder encoder(lock, &events);
auto format_details = MakeValidLc3EncoderFormatDetails();
format_details.mutable_domain()->audio().uncompressed().pcm().channel_map.assign(
kMaxChannelCount + 1, fuchsia::media::AudioChannelId::LF);
auto status = encoder.ProcessFormatDetails(format_details);
EXPECT_EQ(status, TestCodecAdapterLc3Encoder::kShouldTerminate);
EXPECT_EQ(events.fail_codec_count(), 1u);
EXPECT_NE(events.last_fail_message().find("Unsupported channel count"), std::string::npos)
<< "Actual message: " << events.last_fail_message();
}
TEST(CodecAdapterLc3EncoderTest, MissingNbytesRejected) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Encoder encoder(lock, &events);
auto format_details = MakeValidLc3EncoderFormatDetails();
format_details.mutable_encoder_settings()->lc3().clear_nbytes();
auto status = encoder.ProcessFormatDetails(format_details);
EXPECT_EQ(status, TestCodecAdapterLc3Encoder::kShouldTerminate);
EXPECT_EQ(events.fail_codec_count(), 1u);
EXPECT_NE(events.last_fail_message().find("Byte count"), std::string::npos)
<< "Actual message: " << events.last_fail_message();
}
TEST(CodecAdapterLc3EncoderTest, MissingFrameDurationRejected) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Encoder encoder(lock, &events);
auto format_details = MakeValidLc3EncoderFormatDetails();
format_details.mutable_encoder_settings()->lc3().clear_frame_duration();
auto status = encoder.ProcessFormatDetails(format_details);
EXPECT_EQ(status, TestCodecAdapterLc3Encoder::kShouldTerminate);
EXPECT_EQ(events.fail_codec_count(), 1u);
EXPECT_NE(events.last_fail_message().find("frame duration"), std::string::npos)
<< "Actual message: " << events.last_fail_message();
}
TEST(CodecAdapterLc3EncoderTest, InvalidFrameDurationRejected) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Encoder encoder(lock, &events);
auto format_details = MakeValidLc3EncoderFormatDetails();
format_details.mutable_encoder_settings()->lc3().set_frame_duration(
static_cast<fuchsia::media::Lc3FrameDuration>(99));
auto status = encoder.ProcessFormatDetails(format_details);
EXPECT_EQ(status, TestCodecAdapterLc3Encoder::kShouldTerminate);
EXPECT_EQ(events.fail_codec_count(), 1u);
EXPECT_NE(events.last_fail_message().find("frame duration"), std::string::npos)
<< "Actual message: " << events.last_fail_message();
}
TEST(CodecAdapterLc3EncoderTest, MidstreamFormatChangeRejected) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Encoder encoder(lock, &events);
ASSERT_EQ(encoder.ProcessFormatDetails(MakeValidLc3EncoderFormatDetails()),
TestCodecAdapterLc3Encoder::kOk);
EXPECT_EQ(events.fail_codec_count(), 0u);
EXPECT_EQ(encoder.ProcessFormatDetails(MakeValidLc3EncoderFormatDetails()),
TestCodecAdapterLc3Encoder::kShouldTerminate);
EXPECT_EQ(events.fail_codec_count(), 1u);
EXPECT_NE(events.last_fail_message().find("Midstream input format change"), std::string::npos)
<< "Actual message: " << events.last_fail_message();
}
TEST(CodecAdapterLc3EncoderTest, ProcessInputChunkDataValidationAndUnalignedInput) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Encoder encoder(lock, &events);
auto format_details = MakeValidLc3EncoderFormatDetails();
format_details.mutable_domain()->audio().uncompressed().pcm().bits_per_sample = 24;
ASSERT_EQ(encoder.ProcessFormatDetails(format_details), TestCodecAdapterLc3Encoder::kOk);
const size_t input_size = encoder.InputChunkSize();
const size_t min_output_size = encoder.MinOutputBufferSize();
// Allocate extra padding so input_storage.data() + 1 is guaranteed misaligned for int32_t.
std::vector<uint8_t> input_storage(input_size + 4, 0);
const uint8_t* unaligned_input = input_storage.data() + 1;
std::vector<uint8_t> output(min_output_size, 0);
// Mismatched input_data_size should return -1.
EXPECT_EQ(
encoder.ProcessInputChunkData(unaligned_input, input_size - 1, output.data(), output.size()),
-1);
// Undersized output_buffer_size should return -1.
EXPECT_EQ(encoder.ProcessInputChunkData(unaligned_input, input_size, output.data(),
min_output_size - 1),
-1);
// Valid unaligned input chunk should succeed via aligned scratch buffer copy.
EXPECT_EQ(
encoder.ProcessInputChunkData(unaligned_input, input_size, output.data(), output.size()),
static_cast<int>(min_output_size));
}
TEST(CodecAdapterLc3DecoderTest, SamplingFrequency44100HzPreservedInOutputFormat) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Decoder decoder(lock, &events);
// Sampling_Frequency 0x07 (44.1 kHz), Frame_Duration 0x01 (10 ms), 1 ch, 40 bytes.
std::vector<uint8_t> oob_bytes = {
0x02, 0x01, 0x07, // Sampling_Frequency: 44.1 kHz
0x02, 0x02, 0x01, // Frame_Duration: 10 ms
0x05, 0x03, 0x00, 0x00, 0x00, 0x01, // Audio_Channel_Allocation: LF
0x03, 0x04, 0x00, 0x28 // Octets_Per_Codec_Frame: 40
};
ASSERT_EQ(decoder.ProcessFormatDetails(MakeLc3FormatDetails(std::move(oob_bytes))),
TestCodecAdapterLc3Decoder::kOk);
EXPECT_EQ(events.fail_codec_count(), 0u);
// liblc3 uses 480 samples per 10ms frame for 44.1 kHz, so MinOutputBufferSize is 480 * 2 = 960 B.
EXPECT_EQ(decoder.MinOutputBufferSize(), 960u);
// Output format details must report the actual PCM sample rate (44100 Hz), not coerced 48000 Hz.
auto [out_format, min_size] = decoder.OutputFormatDetails();
EXPECT_EQ(out_format.domain().audio().uncompressed().pcm().frames_per_second, 44100u);
}
TEST(CodecAdapterLc3EncoderTest, TimestampExtrapolatorUsesTrueSampleRateFor44100Hz) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Encoder encoder(lock, &events);
auto format_details = MakeValidLc3EncoderFormatDetails();
format_details.mutable_domain()->audio().uncompressed().pcm().frames_per_second = 44100;
format_details.set_timebase(1'000'000'000ull); // 1 second in ns
ASSERT_EQ(encoder.ProcessFormatDetails(format_details), TestCodecAdapterLc3Encoder::kOk);
// For 44.1 kHz 10ms nominal frame, liblc3 encodes 480 samples per frame (InputChunkSize = 960 B).
// At 44,100 samples/sec, 480 samples lasts 480 / 44100 s = 10,884,353 ns (~10.884 ms).
const size_t chunk_size = encoder.InputChunkSize();
EXPECT_EQ(chunk_size, 960u);
auto extrapolator = encoder.CreateTimestampExtrapolator(format_details);
extrapolator.Inform(0, 0);
auto extrapolated = extrapolator.Extrapolate(chunk_size);
ASSERT_TRUE(extrapolated.has_value());
EXPECT_EQ(*extrapolated, 480ull * 1'000'000'000ull / 44100ull);
}
TEST(CodecAdapterLc3EncoderTest, TimestampExtrapolatorUsesFourBytesPerSampleFor24BitAudio) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Encoder encoder(lock, &events);
auto format_details = MakeValidLc3EncoderFormatDetails();
format_details.mutable_domain()->audio().uncompressed().pcm().bits_per_sample = 24;
format_details.set_timebase(1'000'000'000ull); // 1 second in ns
ASSERT_EQ(encoder.ProcessFormatDetails(format_details), TestCodecAdapterLc3Encoder::kOk);
// For 48 kHz 10ms frame, liblc3 encodes 480 samples per frame. Since 24-bit PCM
// (LC3_PCM_FORMAT_S24) uses 4-byte (int32_t) words in memory, InputChunkSize is 480 * 4 = 1920 B.
const size_t chunk_size = encoder.InputChunkSize();
EXPECT_EQ(chunk_size, 1920u);
// One 1920-byte chunk at 48 kHz (480 samples) must advance the timestamp by 10 ms (10,000,000
// ns), not 13.333 ms (which would happen if bits_per_sample / 8 == 3 bytes/sample were used).
auto extrapolator = encoder.CreateTimestampExtrapolator(format_details);
extrapolator.Inform(0, 0);
auto extrapolated = extrapolator.Extrapolate(chunk_size);
ASSERT_TRUE(extrapolated.has_value());
EXPECT_EQ(*extrapolated, 10'000'000ull);
}
TEST(CodecAdapterLc3DecoderTest, OutputBufferConstraintsAvailableAfterCleanUpAfterStream) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Decoder decoder(lock, &events);
ASSERT_EQ(decoder.ProcessFormatDetails(MakeValidLc3DecoderFormatDetails()),
TestCodecAdapterLc3Decoder::kOk);
// Stop/clean up the stream (which resets codec_params_ and chunk_input_stream_) and verify
// CoreCodecGetBufferCollectionConstraints2(kOutputPort) still succeeds using cached size.
decoder.CleanUpAfterStream();
fuchsia::media::StreamBufferConstraints stream_constraints;
fuchsia::media::StreamBufferPartialSettings partial_settings;
auto constraints = decoder.CoreCodecGetBufferCollectionConstraints2(
kOutputPort, stream_constraints, partial_settings);
ASSERT_TRUE(constraints.buffer_memory_constraints().has_value());
EXPECT_EQ(constraints.buffer_memory_constraints()->min_size_bytes(), 960u);
}
TEST(CodecAdapterLc3EncoderTest, OutputBufferConstraintsAvailableAfterCleanUpAfterStream) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Encoder encoder(lock, &events);
ASSERT_EQ(encoder.ProcessFormatDetails(MakeValidLc3EncoderFormatDetails()),
TestCodecAdapterLc3Encoder::kOk);
encoder.CleanUpAfterStream();
fuchsia::media::StreamBufferConstraints stream_constraints;
fuchsia::media::StreamBufferPartialSettings partial_settings;
auto constraints = encoder.CoreCodecGetBufferCollectionConstraints2(
kOutputPort, stream_constraints, partial_settings);
ASSERT_TRUE(constraints.buffer_memory_constraints().has_value());
EXPECT_EQ(constraints.buffer_memory_constraints()->min_size_bytes(), 40u);
}
TEST(CodecAdapterLc3DecoderTest, EmptyStreamEndOfStreamAndStaleChunkInputStreamCleanedUp) {
std::mutex lock;
FakeCodecAdapterEvents events;
TestCodecAdapterLc3Decoder decoder(lock, &events);
auto format_details = MakeValidLc3DecoderFormatDetails();
format_details.set_format_details_version_ordinal(0);
decoder.CoreCodecInit(format_details);
ASSERT_EQ(events.fail_codec_count(), 0u);
// Stream 1: Queue EndOfStream immediately without any FormatDetails or Packet.
decoder.CoreCodecStartStream();
decoder.CoreCodecQueueInputEndOfStream();
events.WaitForOutputEosCount(1);
EXPECT_EQ(events.fail_codec_count(), 0u);
EXPECT_EQ(events.output_eos_count(), 1u);
decoder.CoreCodecStopStream();
// Stream 2: Queue FormatDetails and a partial input packet (10 bytes < 40-byte frame size)
// so chunk_input_stream_ buffers partial bytes in scratch_block_, then stop stream without EOS.
decoder.CoreCodecStartStream();
decoder.CoreCodecQueueInputFormatDetails(format_details);
events.WaitForOutputConstraintsChangeCount(1);
decoder.CoreCodecMidStreamOutputBufferReConfigFinish();
auto input_buffers = Buffers({4096});
auto input_packets = Packets(1);
CodecPacket* input_packet = input_packets.ptr(0);
input_packet->SetBuffer(input_buffers.ptr(0));
input_packet->SetStartOffset(0);
input_packet->SetValidLengthBytes(10); // Partial frame (< 40 bytes)
decoder.CoreCodecQueueInputPacket(input_packet);
events.WaitForInputPacketDoneCount(1);
EXPECT_EQ(events.fail_codec_count(), 0u);
decoder.CoreCodecStopStream();
// Between streams: CoreCodecGetBufferCollectionConstraints2(kOutputPort) must succeed.
fuchsia::media::StreamBufferConstraints stream_constraints;
fuchsia::media::StreamBufferPartialSettings partial_settings;
auto constraints = decoder.CoreCodecGetBufferCollectionConstraints2(
kOutputPort, stream_constraints, partial_settings);
ASSERT_TRUE(constraints.buffer_memory_constraints().has_value());
EXPECT_EQ(constraints.buffer_memory_constraints()->min_size_bytes(), 960u);
// Stream 3: Queue EndOfStream without FormatDetails or Packet; must not flush stale partial
// bytes from Stream 2 or dereference null codec_params_.
decoder.CoreCodecStartStream();
decoder.CoreCodecQueueInputEndOfStream();
events.WaitForOutputEosCount(2);
EXPECT_EQ(events.fail_codec_count(), 0u);
EXPECT_EQ(events.output_eos_count(), 2u);
EXPECT_EQ(events.output_packet_count(), 0u);
decoder.CoreCodecStopStream();
}
} // namespace