| // Copyright 2020 The Chromium 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 <memory> |
| #include <vector> |
| |
| #include "base/files/file_util.h" |
| #include "base/run_loop.h" |
| #include "build/build_config.h" |
| #include "media/base/audio_codecs.h" |
| #include "media/base/channel_layout.h" |
| #include "media/base/decode_status.h" |
| #include "media/base/decoder_buffer.h" |
| #include "media/base/media_util.h" |
| #include "media/base/mock_filters.h" |
| #include "media/base/sample_format.h" |
| #include "media/base/test_data_util.h" |
| #include "media/base/test_helpers.h" |
| #include "media/mojo/buildflags.h" |
| #include "media/mojo/mojom/audio_decoder.mojom.h" |
| #include "media/mojo/mojom/interface_factory.mojom.h" |
| #include "media/mojo/services/interface_factory_impl.h" |
| #include "media/mojo/services/mojo_audio_decoder_service.h" |
| #include "media/mojo/services/mojo_cdm_service_context.h" |
| #include "mojo/public/cpp/bindings/unique_receiver_set.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/thread_safe_browser_interface_broker_proxy.h" |
| #include "third_party/blink/public/platform/platform.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h" |
| #include "third_party/blink/renderer/platform/wtf/functional.h" |
| |
| #include "third_party/blink/renderer/modules/webcodecs/audio_decoder_broker.h" |
| |
| using ::testing::_; |
| using ::testing::Return; |
| |
| namespace blink { |
| |
| namespace { |
| |
| // Constants to specify the type of audio data used. |
| constexpr media::AudioCodec kCodec = media::kCodecVorbis; |
| constexpr media::SampleFormat kSampleFormat = media::kSampleFormatPlanarF32; |
| constexpr media::ChannelLayout kChannelLayout = media::CHANNEL_LAYOUT_STEREO; |
| constexpr int kChannels = 2; |
| constexpr int kSamplesPerSecond = 44100; |
| constexpr int kInputFramesChunk = 256; |
| |
| // FakeAudioDecoder is very agreeable. |
| // - any configuration is supported |
| // - all decodes immediately succeed |
| // - non EOS decodes produce an output |
| // - reset immediately succeeds. |
| class FakeAudioDecoder : public media::MockAudioDecoder { |
| public: |
| FakeAudioDecoder() : MockAudioDecoder("FakeAudioDecoder") {} |
| ~FakeAudioDecoder() override = default; |
| |
| void Initialize(const media::AudioDecoderConfig& config, |
| media::CdmContext* cdm_context, |
| InitCB init_cb, |
| const OutputCB& output_cb, |
| const media::WaitingCB& waiting_cb) override { |
| output_cb_ = output_cb; |
| std::move(init_cb).Run(media::OkStatus()); |
| } |
| |
| void Decode(scoped_refptr<media::DecoderBuffer> buffer, |
| DecodeCB done_cb) override { |
| DCHECK(output_cb_); |
| |
| std::move(done_cb).Run(media::DecodeStatus::OK); |
| |
| if (!buffer->end_of_stream()) { |
| output_cb_.Run(MakeAudioBuffer(kSampleFormat, kChannelLayout, kChannels, |
| kSamplesPerSecond, 1.0f, 0.0f, |
| kInputFramesChunk, buffer->timestamp())); |
| } |
| } |
| |
| void Reset(base::OnceClosure closure) override { std::move(closure).Run(); } |
| |
| private: |
| OutputCB output_cb_; |
| }; |
| |
| // Other end of remote InterfaceFactory requested by AudioDecoderBroker. Used |
| // to create our (fake) media::mojom::AudioDecoder. |
| class FakeInterfaceFactory : public media::mojom::InterfaceFactory { |
| public: |
| FakeInterfaceFactory() = default; |
| ~FakeInterfaceFactory() override = default; |
| |
| void BindRequest(mojo::ScopedMessagePipeHandle handle) { |
| receiver_.Bind(mojo::PendingReceiver<media::mojom::InterfaceFactory>( |
| std::move(handle))); |
| receiver_.set_disconnect_handler(WTF::Bind( |
| &FakeInterfaceFactory::OnConnectionError, base::Unretained(this))); |
| } |
| |
| void OnConnectionError() { receiver_.reset(); } |
| |
| // Implement this one interface from mojom::InterfaceFactory. Using the real |
| // MojoAudioDecoderService allows us to reuse buffer conversion code. The |
| // FakeMojoMediaClient will create a FakeGpuAudioDecoder. |
| void CreateAudioDecoder( |
| mojo::PendingReceiver<media::mojom::AudioDecoder> receiver) override { |
| audio_decoder_receivers_.Add( |
| std::make_unique<media::MojoAudioDecoderService>( |
| &cdm_service_context_, std::make_unique<FakeAudioDecoder>()), |
| std::move(receiver)); |
| } |
| |
| // Stub out other mojom::InterfaceFactory interfaces. |
| void CreateVideoDecoder( |
| mojo::PendingReceiver<media::mojom::VideoDecoder> receiver) override {} |
| void CreateDefaultRenderer( |
| const std::string& audio_device_id, |
| mojo::PendingReceiver<media::mojom::Renderer> receiver) override {} |
| #if BUILDFLAG(ENABLE_CAST_RENDERER) |
| void CreateCastRenderer( |
| const base::UnguessableToken& overlay_plane_id, |
| mojo::PendingReceiver<media::mojom::Renderer> receiver) override {} |
| #endif |
| #if defined(OS_ANDROID) |
| void CreateMediaPlayerRenderer( |
| mojo::PendingRemote<media::mojom::MediaPlayerRendererClientExtension> |
| client_extension_remote, |
| mojo::PendingReceiver<media::mojom::Renderer> receiver, |
| mojo::PendingReceiver<media::mojom::MediaPlayerRendererExtension> |
| renderer_extension_receiver) override {} |
| void CreateFlingingRenderer( |
| const std::string& presentation_id, |
| mojo::PendingRemote<media::mojom::FlingingRendererClientExtension> |
| client_extension, |
| mojo::PendingReceiver<media::mojom::Renderer> receiver) override {} |
| #endif // defined(OS_ANDROID |
| void CreateCdm(const std::string& key_system, |
| const media::CdmConfig& cdm_config, |
| CreateCdmCallback callback) override { |
| std::move(callback).Run(mojo::NullRemote(), base::nullopt, |
| mojo::NullRemote(), "CDM creation not supported"); |
| } |
| #if defined(OS_WIN) |
| void CreateMediaFoundationRenderer( |
| mojo::PendingReceiver<media::mojom::Renderer> receiver, |
| mojo::PendingReceiver<media::mojom::MediaFoundationRendererExtension> |
| renderer_extension_receiver) override {} |
| #endif // defined(OS_WIN) |
| |
| private: |
| media::MojoCdmServiceContext cdm_service_context_; |
| mojo::Receiver<media::mojom::InterfaceFactory> receiver_{this}; |
| mojo::UniqueReceiverSet<media::mojom::AudioDecoder> audio_decoder_receivers_; |
| }; |
| |
| } // namespace |
| |
| class AudioDecoderBrokerTest : public testing::Test { |
| public: |
| AudioDecoderBrokerTest() = default; |
| ~AudioDecoderBrokerTest() override = default; |
| |
| void OnInitWithClosure(base::RepeatingClosure done_cb, media::Status status) { |
| OnInit(status); |
| done_cb.Run(); |
| } |
| void OnDecodeDoneWithClosure(base::RepeatingClosure done_cb, |
| media::Status status) { |
| OnDecodeDone(std::move(status)); |
| done_cb.Run(); |
| } |
| |
| void OnResetDoneWithClosure(base::RepeatingClosure done_cb) { |
| OnResetDone(); |
| done_cb.Run(); |
| } |
| |
| MOCK_METHOD1(OnInit, void(media::Status status)); |
| MOCK_METHOD1(OnDecodeDone, void(media::Status)); |
| MOCK_METHOD0(OnResetDone, void()); |
| |
| void OnOutput(scoped_refptr<media::AudioBuffer> buffer) { |
| output_buffers_.push_back(std::move(buffer)); |
| } |
| |
| void SetupMojo(ExecutionContext& execution_context) { |
| // Register FakeInterfaceFactory as impl for media::mojom::InterfaceFactory |
| // required by MojoAudioDecoder. The factory will vend FakeGpuAudioDecoders |
| // that simulate gpu-accelerated decode. |
| interface_factory_ = std::make_unique<FakeInterfaceFactory>(); |
| EXPECT_TRUE( |
| Platform::Current()->GetBrowserInterfaceBroker()->SetBinderForTesting( |
| media::mojom::InterfaceFactory::Name_, |
| WTF::BindRepeating(&FakeInterfaceFactory::BindRequest, |
| base::Unretained(interface_factory_.get())))); |
| } |
| |
| void ConstructDecoder(ExecutionContext& execution_context) { |
| decoder_broker_ = std::make_unique<AudioDecoderBroker>(&null_media_log_, |
| execution_context); |
| } |
| |
| void InitializeDecoder(media::AudioDecoderConfig config) { |
| base::RunLoop run_loop; |
| EXPECT_CALL(*this, OnInit(media::SameStatusCode(media::OkStatus()))); |
| decoder_broker_->Initialize( |
| config, nullptr /* cdm_context */, |
| WTF::Bind(&AudioDecoderBrokerTest::OnInitWithClosure, |
| WTF::Unretained(this), run_loop.QuitClosure()), |
| WTF::BindRepeating(&AudioDecoderBrokerTest::OnOutput, |
| WTF::Unretained(this)), |
| media::WaitingCB()); |
| run_loop.Run(); |
| testing::Mock::VerifyAndClearExpectations(this); |
| } |
| |
| void DecodeBuffer( |
| scoped_refptr<media::DecoderBuffer> buffer, |
| media::StatusCode expected_status = media::StatusCode::kOk) { |
| base::RunLoop run_loop; |
| EXPECT_CALL(*this, OnDecodeDone(HasStatusCode(expected_status))); |
| decoder_broker_->Decode( |
| buffer, WTF::Bind(&AudioDecoderBrokerTest::OnDecodeDoneWithClosure, |
| WTF::Unretained(this), run_loop.QuitClosure())); |
| run_loop.Run(); |
| testing::Mock::VerifyAndClearExpectations(this); |
| } |
| |
| void ResetDecoder() { |
| base::RunLoop run_loop; |
| EXPECT_CALL(*this, OnResetDone()); |
| decoder_broker_->Reset( |
| WTF::Bind(&AudioDecoderBrokerTest::OnResetDoneWithClosure, |
| WTF::Unretained(this), run_loop.QuitClosure())); |
| run_loop.Run(); |
| testing::Mock::VerifyAndClearExpectations(this); |
| } |
| |
| std::string GetDisplayName() { return decoder_broker_->GetDisplayName(); } |
| |
| bool IsPlatformDecoder() { return decoder_broker_->IsPlatformDecoder(); } |
| bool SupportsDecryption() { return decoder_broker_->SupportsDecryption(); } |
| |
| protected: |
| media::NullMediaLog null_media_log_; |
| std::unique_ptr<AudioDecoderBroker> decoder_broker_; |
| std::vector<scoped_refptr<media::AudioBuffer>> output_buffers_; |
| std::unique_ptr<FakeInterfaceFactory> interface_factory_; |
| }; |
| |
| TEST_F(AudioDecoderBrokerTest, Decode_Uninitialized) { |
| V8TestingScope v8_scope; |
| |
| ConstructDecoder(*v8_scope.GetExecutionContext()); |
| EXPECT_EQ(GetDisplayName(), "EmptyWebCodecsAudioDecoder"); |
| |
| // No call to Initialize. Other APIs should fail gracefully. |
| |
| DecodeBuffer(media::ReadTestDataFile("vorbis-packet-0"), |
| media::DecodeStatus::DECODE_ERROR); |
| DecodeBuffer(media::DecoderBuffer::CreateEOSBuffer(), |
| media::DecodeStatus::DECODE_ERROR); |
| ASSERT_EQ(0U, output_buffers_.size()); |
| |
| ResetDecoder(); |
| } |
| |
| media::AudioDecoderConfig MakeVorbisConfig() { |
| std::string extradata_name = "vorbis-extradata"; |
| base::FilePath extradata_path = media::GetTestDataFilePath(extradata_name); |
| int64_t tmp = 0; |
| CHECK(base::GetFileSize(extradata_path, &tmp)) |
| << "Failed to get file size for '" << extradata_name << "'"; |
| int file_size = base::checked_cast<int>(tmp); |
| std::vector<uint8_t> extradata(file_size); |
| CHECK_EQ(file_size, |
| base::ReadFile(extradata_path, |
| reinterpret_cast<char*>(&extradata[0]), file_size)) |
| << "Failed to read '" << extradata_name << "'"; |
| |
| return media::AudioDecoderConfig(kCodec, kSampleFormat, kChannelLayout, |
| kSamplesPerSecond, std::move(extradata), |
| media::EncryptionScheme::kUnencrypted); |
| } |
| |
| TEST_F(AudioDecoderBrokerTest, Decode_NoMojoDecoder) { |
| V8TestingScope v8_scope; |
| |
| ConstructDecoder(*v8_scope.GetExecutionContext()); |
| EXPECT_EQ(GetDisplayName(), "EmptyWebCodecsAudioDecoder"); |
| |
| InitializeDecoder(MakeVorbisConfig()); |
| EXPECT_NE(GetDisplayName(), "EmptyWebCodecsAudioDecoder"); |
| |
| DecodeBuffer(media::ReadTestDataFile("vorbis-packet-0")); |
| DecodeBuffer(media::ReadTestDataFile("vorbis-packet-1")); |
| DecodeBuffer(media::ReadTestDataFile("vorbis-packet-2")); |
| DecodeBuffer(media::DecoderBuffer::CreateEOSBuffer()); |
| // 2, not 3, because the first frame doesn't generate an output. |
| ASSERT_EQ(2U, output_buffers_.size()); |
| |
| ResetDecoder(); |
| |
| DecodeBuffer(media::ReadTestDataFile("vorbis-packet-0")); |
| DecodeBuffer(media::ReadTestDataFile("vorbis-packet-1")); |
| DecodeBuffer(media::ReadTestDataFile("vorbis-packet-2")); |
| DecodeBuffer(media::DecoderBuffer::CreateEOSBuffer()); |
| // 2 more than last time. |
| ASSERT_EQ(4U, output_buffers_.size()); |
| |
| ResetDecoder(); |
| } |
| |
| #if BUILDFLAG(ENABLE_MOJO_AUDIO_DECODER) |
| TEST_F(AudioDecoderBrokerTest, Decode_WithMojoDecoder) { |
| V8TestingScope v8_scope; |
| ExecutionContext* execution_context = v8_scope.GetExecutionContext(); |
| |
| SetupMojo(*execution_context); |
| ConstructDecoder(*execution_context); |
| EXPECT_EQ(GetDisplayName(), "EmptyWebCodecsAudioDecoder"); |
| EXPECT_FALSE(IsPlatformDecoder()); |
| EXPECT_FALSE(SupportsDecryption()); |
| |
| // Use an MpegH config to prevent FFmpeg from being selected. |
| InitializeDecoder(media::AudioDecoderConfig( |
| media::kCodecMpegHAudio, kSampleFormat, kChannelLayout, kSamplesPerSecond, |
| media::EmptyExtraData(), media::EncryptionScheme::kUnencrypted)); |
| EXPECT_EQ(GetDisplayName(), "MojoAudioDecoder"); |
| |
| // Using vorbis buffer here because its easy and the fake decoder generates |
| // output regardless of the input details. |
| DecodeBuffer(media::ReadTestDataFile("vorbis-packet-0")); |
| DecodeBuffer(media::DecoderBuffer::CreateEOSBuffer()); |
| // Our fake decoder immediately generates output for any input. |
| ASSERT_EQ(1U, output_buffers_.size()); |
| |
| // True for MojoAudioDecoder. |
| EXPECT_TRUE(IsPlatformDecoder()); |
| // True for for MojoVideoDecoder on Android, but WebCodecs doesn't do |
| // decryption, so this is hard-coded to false. |
| EXPECT_FALSE(SupportsDecryption()); |
| |
| ResetDecoder(); |
| } |
| #endif // BUILDFLAG(ENABLE_MOJO_AUDIO_DECODER) |
| } // namespace blink |