| // 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 <mutex> |
| #include <utility> |
| |
| #include <gtest/gtest.h> |
| |
| #include "src/media/codec/codecs/sw/aac/codec_adapter_aac_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 { |
| |
| fuchsia::media::FormatDetails MakeValidAacEncoderFormatDetails() { |
| 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::AacEncoderSettings aac_settings; |
| aac_settings.aot = fuchsia::media::AacAudioObjectType::MPEG4_AAC_LC; |
| aac_settings.channel_mode = fuchsia::media::AacChannelMode::MONO; |
| aac_settings.bit_rate = fuchsia::media::AacBitRate::WithConstant( |
| fuchsia::media::AacConstantBitRate{.bit_rate = 64000}); |
| aac_settings.transport = fuchsia::media::AacTransport::WithRaw(fuchsia::media::AacTransportRaw{}); |
| |
| fuchsia::media::EncoderSettings encoder_settings; |
| encoder_settings.set_aac(std::move(aac_settings)); |
| |
| fuchsia::media::FormatDetails format_details; |
| format_details.set_format_details_version_ordinal(0); |
| format_details.set_domain(std::move(domain)); |
| format_details.set_encoder_settings(std::move(encoder_settings)); |
| return format_details; |
| } |
| |
| TEST(CodecAdapterAacEncoderTest, EmptyStreamEndOfStreamAndPostStopOutputConstraints) { |
| std::mutex lock; |
| FakeCodecAdapterEvents events; |
| CodecAdapterAacEncoder encoder(lock, &events); |
| |
| auto format_details = MakeValidAacEncoderFormatDetails(); |
| encoder.CoreCodecInit(format_details); |
| ASSERT_EQ(events.fail_codec_count(), 0u); |
| |
| // Stream 1: Queue EndOfStream immediately without any FormatDetails or Packet. |
| encoder.CoreCodecStartStream(); |
| encoder.CoreCodecQueueInputEndOfStream(); |
| events.WaitForOutputEosCount(1); |
| EXPECT_EQ(events.fail_codec_count(), 0u); |
| EXPECT_EQ(events.output_eos_count(), 1u); |
| encoder.CoreCodecStopStream(); |
| |
| // Stream 2: Queue FormatDetails and a partial input packet (16 bytes < 2048-byte AAC chunk size) |
| // so stream_->chunk_input_stream buffers partial bytes, then stop stream without EOS. |
| encoder.CoreCodecStartStream(); |
| encoder.CoreCodecQueueInputFormatDetails(format_details); |
| events.WaitForOutputConstraintsChangeCount(1); |
| encoder.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(16); // Partial AAC frame (< 2048 bytes) |
| encoder.CoreCodecQueueInputPacket(input_packet); |
| events.WaitForInputPacketDoneCount(1); |
| EXPECT_EQ(events.fail_codec_count(), 0u); |
| encoder.CoreCodecStopStream(); |
| |
| // Between streams: CoreCodecGetBufferCollectionConstraints2(kOutputPort) must succeed using |
| // format_configuration_->recommended_output_buffer_size even after stream_ was reset. |
| 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()); |
| // kFdkMaxOutBytesPerChannel (6144 / 8 = 768) * 1 channel = 768 bytes. |
| EXPECT_EQ(constraints.buffer_memory_constraints()->min_size_bytes(), 768u); |
| |
| // Stream 3: Queue EndOfStream without FormatDetails or Packet; must not flush stale partial |
| // bytes from Stream 2 or dereference null stream_. |
| encoder.CoreCodecStartStream(); |
| encoder.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); |
| encoder.CoreCodecStopStream(); |
| } |
| |
| TEST(CodecAdapterAacEncoderTest, QueuedPacketAndEosIgnoredAfterFormatDetailsFailure) { |
| std::mutex lock; |
| FakeCodecAdapterEvents events; |
| CodecAdapterAacEncoder encoder(lock, &events); |
| |
| auto valid_format_details = MakeValidAacEncoderFormatDetails(); |
| encoder.CoreCodecInit(valid_format_details); |
| ASSERT_EQ(events.fail_codec_count(), 0u); |
| |
| encoder.CoreCodecStartStream(); |
| |
| // Queue invalid FormatDetails (missing encoder_settings) followed immediately by an input packet, |
| // EndOfStream, and a trailing input packet on input_processing_loop_ before CoreCodecStopStream() |
| // is called. |
| fuchsia::media::FormatDetails invalid_format_details = MakeValidAacEncoderFormatDetails(); |
| invalid_format_details.clear_encoder_settings(); |
| encoder.CoreCodecQueueInputFormatDetails(invalid_format_details); |
| |
| auto input_buffers = Buffers({4096, 4096}); |
| auto input_packets = Packets(2); |
| CodecPacket* input_packet_0 = input_packets.ptr(0); |
| input_packet_0->SetBuffer(input_buffers.ptr(0)); |
| input_packet_0->SetStartOffset(0); |
| input_packet_0->SetValidLengthBytes(16); |
| CodecPacket* input_packet_1 = input_packets.ptr(1); |
| input_packet_1->SetBuffer(input_buffers.ptr(1)); |
| input_packet_1->SetStartOffset(0); |
| input_packet_1->SetValidLengthBytes(16); |
| |
| encoder.CoreCodecQueueInputPacket(input_packet_0); |
| encoder.CoreCodecQueueInputEndOfStream(); |
| encoder.CoreCodecQueueInputPacket(input_packet_1); |
| |
| // Wait for both input packets (including the one queued after EndOfStream) to be returned on |
| // input_processing_loop_ before calling CoreCodecStopStream(), verifying that ProcessInput itself |
| // deactivated the stream upon FormatDetails failure. |
| events.WaitForInputPacketDoneCount(2, /*stop_on_fail_codec=*/false); |
| |
| EXPECT_EQ(events.fail_codec_count(), 1u); |
| EXPECT_EQ(events.input_packet_done_count(), 2u); |
| EXPECT_EQ(events.output_eos_count(), 0u); |
| |
| encoder.CoreCodecStopStream(); |
| } |
| |
| TEST(CodecAdapterAacEncoderTest, StopAndMidStreamFormatFailureWhileOutputReconfigPending) { |
| std::mutex lock; |
| FakeCodecAdapterEvents events; |
| CodecAdapterAacEncoder encoder(lock, &events); |
| |
| auto format_details_v0 = MakeValidAacEncoderFormatDetails(); |
| encoder.CoreCodecInit(format_details_v0); |
| ASSERT_EQ(events.fail_codec_count(), 0u); |
| |
| auto input_buffers = Buffers({4096, 4096}); |
| auto input_packets = Packets(2); |
| CodecPacket* input_packet_0 = input_packets.ptr(0); |
| input_packet_0->SetBuffer(input_buffers.ptr(0)); |
| input_packet_0->SetStartOffset(0); |
| input_packet_0->SetValidLengthBytes(16); |
| CodecPacket* input_packet_1 = input_packets.ptr(1); |
| input_packet_1->SetBuffer(input_buffers.ptr(1)); |
| input_packet_1->SetStartOffset(0); |
| input_packet_1->SetValidLengthBytes(16); |
| |
| // Stream 1: Queue FormatDetails (setting output_reconfig_pending_ = true) and a Packet + EOS |
| // without calling CoreCodecMidStreamOutputBufferReConfigFinish(), then stop the stream. |
| // CoreCodecStopStream() must unblock the waiting ProcessInput() call without deadlocking. |
| encoder.CoreCodecStartStream(); |
| encoder.CoreCodecQueueInputFormatDetails(format_details_v0); |
| events.WaitForOutputConstraintsChangeCount(1); |
| encoder.CoreCodecQueueInputPacket(input_packet_0); |
| encoder.CoreCodecQueueInputEndOfStream(); |
| encoder.CoreCodecStopStream(); |
| EXPECT_EQ(events.fail_codec_count(), 0u); |
| EXPECT_EQ(events.input_packet_done_count(), 1u); |
| EXPECT_EQ(events.output_eos_count(), 0u); |
| |
| // Stream 2: Queue FormatDetails(v0) (setting output_reconfig_pending_ = true), followed by a |
| // midstream FormatDetails(v1) (which fails before ReConfigFinish) and a Packet + EOS. |
| // ProcessInput() must reset output_reconfig_pending_ on failure so the trailing Packet + EOS |
| // do not hang waiting on reconfig_cond_. |
| encoder.CoreCodecStartStream(); |
| encoder.CoreCodecQueueInputFormatDetails(format_details_v0); |
| events.WaitForOutputConstraintsChangeCount(2); |
| |
| auto format_details_v1 = MakeValidAacEncoderFormatDetails(); |
| format_details_v1.set_format_details_version_ordinal(1); |
| encoder.CoreCodecQueueInputFormatDetails(format_details_v1); |
| encoder.CoreCodecQueueInputPacket(input_packet_1); |
| encoder.CoreCodecQueueInputEndOfStream(); |
| |
| events.WaitForInputPacketDoneCount(2, /*stop_on_fail_codec=*/false); |
| EXPECT_EQ(events.fail_codec_count(), 1u); |
| EXPECT_EQ(events.input_packet_done_count(), 2u); |
| EXPECT_EQ(events.output_eos_count(), 0u); |
| |
| encoder.CoreCodecStopStream(); |
| } |
| |
| } // namespace |