| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/mirroring/service/openscreen_session_host.h" |
| |
| #include <memory> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| #include <variant> |
| #include <vector> |
| |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/run_loop.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/task_environment.h" |
| #include "base/time/time.h" |
| #include "base/values.h" |
| #include "components/mirroring/service/fake_network_service.h" |
| #include "components/mirroring/service/fake_video_capture_host.h" |
| #include "components/mirroring/service/mirror_settings.h" |
| #include "components/mirroring/service/mirroring_features.h" |
| #include "media/base/audio_codecs.h" |
| #include "media/base/media_switches.h" |
| #include "media/base/video_codecs.h" |
| #include "media/cast/cast_config.h" |
| #include "media/cast/common/openscreen_conversion_helpers.h" |
| #include "media/cast/encoding/encoding_support.h" |
| #include "media/cast/test/utility/default_config.h" |
| #include "media/video/video_decode_accelerator.h" |
| #include "mojo/public/cpp/bindings/pending_receiver.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "mojo/public/cpp/bindings/receiver.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "net/base/ip_address.h" |
| #include "services/network/public/mojom/clear_data_filter.mojom.h" |
| #include "services/viz/public/cpp/gpu/gpu.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/jsoncpp/source/include/json/reader.h" |
| #include "third_party/jsoncpp/source/include/json/writer.h" |
| #include "third_party/openscreen/src/cast/streaming/message_fields.h" |
| #include "third_party/openscreen/src/cast/streaming/public/offer_messages.h" |
| #include "third_party/openscreen/src/cast/streaming/remoting_capabilities.h" |
| #include "third_party/openscreen/src/cast/streaming/sender_message.h" |
| #include "third_party/openscreen/src/cast/streaming/ssrc.h" |
| |
| using media::cast::FrameSenderConfig; |
| using media::mojom::RemotingSinkMetadata; |
| using media::mojom::RemotingSinkMetadataPtr; |
| using media::mojom::RemotingStartFailReason; |
| using media::mojom::RemotingStopReason; |
| using mirroring::mojom::SessionError; |
| using mirroring::mojom::SessionType; |
| using openscreen::ErrorOr; |
| using openscreen::cast::Offer; |
| using openscreen::cast::SenderMessage; |
| using openscreen::cast::VideoStream; |
| using ::testing::_; |
| using ::testing::AtLeast; |
| using ::testing::InvokeWithoutArgs; |
| using ::testing::Mock; |
| using ::testing::NiceMock; |
| |
| namespace mirroring { |
| |
| namespace { |
| |
| const openscreen::cast::Answer kAnswerWithConstraints{ |
| 1234, |
| // Send indexes and SSRCs are set later. |
| {}, |
| {}, |
| openscreen::cast::Constraints{ |
| openscreen::cast::AudioConstraints{44100, 2, 32000, 960000, |
| std::chrono::milliseconds(4000)}, |
| openscreen::cast::VideoConstraints{ |
| 40000.0, openscreen::cast::Dimensions{320, 480, {30, 1}}, |
| openscreen::cast::Dimensions{1920, 1080, {60, 1}}, 300000, |
| 144000000, std::chrono::milliseconds(4000)}}, |
| openscreen::cast::DisplayDescription{ |
| openscreen::cast::Dimensions{1280, 720, {60, 1}}, |
| openscreen::cast::AspectRatio{16, 9}, |
| openscreen::cast::AspectRatioConstraint::kFixed, |
| }, |
| }; |
| |
| class MockRemotingSource : public media::mojom::RemotingSource { |
| public: |
| MockRemotingSource() = default; |
| ~MockRemotingSource() override = default; |
| |
| void Bind(mojo::PendingReceiver<media::mojom::RemotingSource> receiver) { |
| receiver_.Bind(std::move(receiver)); |
| } |
| |
| void reset_on_disconnect() { |
| receiver_.set_disconnect_handler( |
| base::BindOnce(&MockRemotingSource::reset, weak_factory_.GetWeakPtr())); |
| } |
| |
| void reset() { receiver_.reset(); } |
| |
| MOCK_METHOD0(OnSinkGone, void()); |
| MOCK_METHOD0(OnStarted, void()); |
| MOCK_METHOD1(OnStartFailed, void(RemotingStartFailReason)); |
| MOCK_METHOD1(OnMessageFromSink, void(const std::vector<uint8_t>&)); |
| MOCK_METHOD1(OnStopped, void(RemotingStopReason)); |
| MOCK_METHOD1(OnSinkAvailable, void(const RemotingSinkMetadata&)); |
| void OnSinkAvailable(RemotingSinkMetadataPtr metadata) override { |
| OnSinkAvailable(*metadata); |
| } |
| |
| private: |
| mojo::Receiver<media::mojom::RemotingSource> receiver_{this}; |
| base::WeakPtrFactory<MockRemotingSource> weak_factory_{this}; |
| }; |
| |
| Json::Value ParseAsJsoncppValue(std::string_view document) { |
| Json::CharReaderBuilder builder; |
| Json::CharReaderBuilder::strictMode(&builder.settings_); |
| EXPECT_FALSE(document.empty()); |
| |
| Json::Value root_node; |
| std::string error_msg; |
| std::unique_ptr<Json::CharReader> reader(builder.newCharReader()); |
| EXPECT_TRUE(reader->parse(&*document.begin(), &*document.end(), &root_node, |
| &error_msg)); |
| |
| return root_node; |
| } |
| |
| std::string Stringify(const Json::Value& value) { |
| EXPECT_FALSE(value.empty()); |
| Json::StreamWriterBuilder factory; |
| factory["indentation"] = ""; |
| |
| std::unique_ptr<Json::StreamWriter> const writer(factory.newStreamWriter()); |
| std::ostringstream stream; |
| writer->write(value, &stream); |
| |
| EXPECT_TRUE(stream); |
| return stream.str(); |
| } |
| |
| openscreen::cast::SenderStats ConstructDefaultSenderStats() { |
| return openscreen::cast::SenderStats{ |
| .audio_statistics = openscreen::cast::SenderStats::StatisticsList(), |
| .audio_histograms = openscreen::cast::SenderStats::HistogramsList(), |
| .video_statistics = openscreen::cast::SenderStats::StatisticsList(), |
| .video_histograms = openscreen::cast::SenderStats::HistogramsList()}; |
| } |
| |
| } // namespace |
| |
| class OpenscreenSessionHostTest : public mojom::ResourceProvider, |
| public mojom::SessionObserver, |
| public mojom::CastMessageChannel, |
| public ::testing::Test { |
| public: |
| OpenscreenSessionHostTest() = default; |
| |
| OpenscreenSessionHostTest(const OpenscreenSessionHostTest&) = delete; |
| OpenscreenSessionHostTest& operator=(const OpenscreenSessionHostTest&) = |
| delete; |
| |
| void TearDown() override { |
| media::cast::encoding_support::ClearHardwareCodecDenyListForTesting(); |
| } |
| |
| void OnSessionHostDeletion() { |
| ASSERT_TRUE(session_host_deletion_cb_); |
| if (session_host_deletion_cb_) { |
| std::move(session_host_deletion_cb_).Run(); |
| } |
| } |
| |
| ~OpenscreenSessionHostTest() override { |
| // We may have already deleted the session host if the session was stopped. |
| if (session_host_) { |
| DeleteSessionHost(); |
| } |
| } |
| |
| protected: |
| void PauseCapturingVideo() { session_host().PauseCapturingVideo(); } |
| |
| bool TryResumeCapturingVideo() { |
| return session_host().TryResumeCapturingVideo(); |
| } |
| |
| MirrorSettings* mirror_settings() { return &session_host_->mirror_settings_; } |
| |
| // mojom::SessionObserver implementation. |
| MOCK_METHOD(void, OnError, (SessionError)); |
| MOCK_METHOD(void, DidStart, ()); |
| MOCK_METHOD(void, DidStop, ()); |
| void LogInfoMessage(const std::string&) override {} |
| MOCK_METHOD(void, LogErrorMessage, (const std::string&)); |
| MOCK_METHOD(void, OnSourceChanged, ()); |
| MOCK_METHOD(void, OnRemotingStateChanged, (bool is_remoting)); |
| |
| MOCK_METHOD(void, OnGetVideoCaptureHost, ()); |
| MOCK_METHOD(void, OnGetNetworkContext, ()); |
| MOCK_METHOD(void, OnCreateAudioStream, ()); |
| MOCK_METHOD(void, OnConnectToRemotingSource, ()); |
| |
| // Called when an outbound message is sent. |
| MOCK_METHOD(void, OnOutboundMessage, (SenderMessage::Type type)); |
| |
| MOCK_METHOD(void, OnInitialized, ()); |
| |
| // mojom::CastMessageChannel implementation (outbound messages). |
| void OnMessage(mojom::CastMessagePtr message) override { |
| EXPECT_TRUE(message->message_namespace == mojom::kWebRtcNamespace || |
| message->message_namespace == mojom::kRemotingNamespace); |
| |
| const Json::Value json_value = |
| ParseAsJsoncppValue(message->json_format_data); |
| |
| ErrorOr<SenderMessage> parsed_message = SenderMessage::Parse(json_value); |
| EXPECT_TRUE(parsed_message); |
| last_sent_offer_ = parsed_message.value(); |
| if (parsed_message.value().type == SenderMessage::Type::kOffer) { |
| EXPECT_GT(parsed_message.value().sequence_number, 0); |
| const auto offer = std::get<Offer>(parsed_message.value().body); |
| |
| for (const openscreen::cast::AudioStream& stream : offer.audio_streams) { |
| EXPECT_EQ( |
| base::Milliseconds( |
| std::chrono::milliseconds(stream.stream.target_delay).count()), |
| target_playout_delay_); |
| } |
| for (const VideoStream& stream : offer.video_streams) { |
| EXPECT_EQ( |
| base::Milliseconds( |
| std::chrono::milliseconds(stream.stream.target_delay).count()), |
| target_playout_delay_); |
| } |
| } else if (parsed_message.value().type == |
| SenderMessage::Type::kGetCapabilities) { |
| EXPECT_GT(parsed_message.value().sequence_number, 0); |
| } |
| |
| OnOutboundMessage(parsed_message.value().type); |
| } |
| |
| // mojom::ResourceProvider overrides. |
| void BindGpu(mojo::PendingReceiver<viz::mojom::Gpu> receiver) override {} |
| void GetVideoCaptureHost( |
| mojo::PendingReceiver<media::mojom::VideoCaptureHost> receiver) override { |
| video_host_ = |
| std::make_unique<NiceMock<FakeVideoCaptureHost>>(std::move(receiver)); |
| OnGetVideoCaptureHost(); |
| } |
| |
| void GetVideoEncoderMetricsProvider( |
| mojo::PendingReceiver<media::mojom::VideoEncoderMetricsProvider> receiver) |
| override {} |
| |
| void GetNetworkContext( |
| mojo::PendingReceiver<network::mojom::NetworkContext> receiver) override { |
| network_context_ = |
| std::make_unique<NiceMock<MockNetworkContext>>(std::move(receiver)); |
| OnGetNetworkContext(); |
| } |
| |
| void CreateAudioStream( |
| mojo::PendingRemote<mojom::AudioStreamCreatorClient> client, |
| const media::AudioParameters& params, |
| uint32_t total_segments) override { |
| OnCreateAudioStream(); |
| } |
| |
| void ConnectToRemotingSource( |
| mojo::PendingRemote<media::mojom::Remoter> remoter, |
| mojo::PendingReceiver<media::mojom::RemotingSource> receiver) override { |
| remoter_.Bind(std::move(remoter)); |
| remoter_.reset_on_disconnect(); |
| remoting_source_.Bind(std::move(receiver)); |
| remoting_source_.reset_on_disconnect(); |
| OnConnectToRemotingSource(); |
| } |
| |
| void GenerateAndReplyWithAnswer() { |
| ASSERT_TRUE(session_host_); |
| ASSERT_TRUE(last_sent_offer_); |
| |
| const Offer& offer = std::get<Offer>(last_sent_offer_->body); |
| openscreen::cast::Answer answer{.udp_port = 1234}; |
| |
| if (!offer.audio_streams.empty()) { |
| answer.send_indexes.push_back(offer.audio_streams[0].stream.index); |
| answer.ssrcs.push_back(next_receiver_ssrc_++); |
| } |
| |
| if (!offer.video_streams.empty()) { |
| answer.send_indexes.push_back(offer.video_streams[0].stream.index); |
| answer.ssrcs.push_back(next_receiver_ssrc_++); |
| } |
| |
| openscreen::cast::ReceiverMessage receiver_message{ |
| .type = openscreen::cast::ReceiverMessage::Type::kAnswer, |
| .sequence_number = last_sent_offer_->sequence_number, |
| .valid = true, |
| .body = std::move(answer)}; |
| Json::Value message_json = receiver_message.ToJson().value(); |
| ErrorOr<std::string> message_string = Stringify(message_json); |
| ASSERT_TRUE(message_string); |
| |
| mojom::CastMessagePtr message = mojom::CastMessage::New( |
| openscreen::cast::kCastWebrtcNamespace, message_string.value()); |
| inbound_channel_->OnMessage(std::move(message)); |
| } |
| |
| OpenscreenSessionHost::AsyncInitializedCallback MakeOnInitializedCallback() { |
| return base::BindOnce(&OpenscreenSessionHostTest::OnInitialized, |
| base::Unretained(this)); |
| } |
| |
| // Create a mirroring session. Expect to send OFFER message. |
| void CreateSession(SessionType session_type, |
| bool is_remote_playback = false, |
| bool enable_rtcp_reporting = false) { |
| session_type_ = session_type; |
| is_remote_playback_ = is_remote_playback; |
| mojom::SessionParametersPtr session_params = |
| mojom::SessionParameters::New(); |
| session_params->type = session_type_; |
| session_params->receiver_address = receiver_endpoint_.address(); |
| session_params->receiver_friendly_name = "Chromecast Ultra"; |
| session_params->source_id = "sender-123"; |
| session_params->destination_id = "receiver-456"; |
| if (target_playout_delay_ != kDefaultPlayoutDelay) { |
| session_params->target_playout_delay = target_playout_delay_; |
| } |
| if (force_letterboxing_) { |
| session_params->force_letterboxing = true; |
| } |
| if (enable_rtcp_reporting) { |
| session_params->enable_rtcp_reporting = true; |
| } |
| session_params->is_remote_playback = is_remote_playback_; |
| cast_mode_ = "mirroring"; |
| mojo::PendingRemote<mojom::ResourceProvider> resource_provider_remote; |
| mojo::PendingRemote<mojom::SessionObserver> session_observer_remote; |
| mojo::PendingRemote<mojom::CastMessageChannel> outbound_channel_remote; |
| resource_provider_receiver_.Bind( |
| resource_provider_remote.InitWithNewPipeAndPassReceiver()); |
| session_observer_receiver_.Bind( |
| session_observer_remote.InitWithNewPipeAndPassReceiver()); |
| outbound_channel_receiver_.Bind( |
| outbound_channel_remote.InitWithNewPipeAndPassReceiver()); |
| // Expect to send OFFER message when session is created. |
| EXPECT_CALL(*this, OnGetNetworkContext()); |
| EXPECT_CALL(*this, OnError(_)).Times(0); |
| EXPECT_CALL(*this, OnOutboundMessage(SenderMessage::Type::kOffer)); |
| EXPECT_CALL(*this, OnInitialized()); |
| EXPECT_CALL(*this, OnRemotingStateChanged(false)); |
| session_host_ = std::make_unique<OpenscreenSessionHost>( |
| std::move(session_params), gfx::Size(1920, 1080), |
| std::move(session_observer_remote), std::move(resource_provider_remote), |
| std::move(outbound_channel_remote), |
| inbound_channel_.BindNewPipeAndPassReceiver(), nullptr, |
| // NOTE: unretained used is safe since we wait for this task to complete |
| // before deleting `this`. |
| base::BindOnce(&OpenscreenSessionHostTest::OnSessionHostDeletion, |
| base::Unretained(this))); |
| session_host_->AsyncInitialize(MakeOnInitializedCallback()); |
| task_environment_.RunUntilIdle(); |
| Mock::VerifyAndClear(this); |
| } |
| |
| void ExpectStreamsToStart() { |
| const int get_video_host_call_count = |
| session_type_ == SessionType::AUDIO_ONLY ? 0 : 1; |
| EXPECT_CALL(*this, OnGetVideoCaptureHost()) |
| .Times(get_video_host_call_count); |
| |
| const int create_audio_stream_call_count = |
| session_type_ == SessionType::VIDEO_ONLY ? 0 : 1; |
| EXPECT_CALL(*this, OnCreateAudioStream()) |
| .Times(create_audio_stream_call_count); |
| } |
| |
| // Negotiates a mirroring session. |
| void StartSession() { |
| ASSERT_EQ(cast_mode_, "mirroring"); |
| ExpectStreamsToStart(); |
| EXPECT_CALL(*this, OnError(_)).Times(0); |
| if (!is_remote_playback_) { |
| EXPECT_CALL(*this, |
| OnOutboundMessage(SenderMessage::Type::kGetCapabilities)); |
| } |
| EXPECT_CALL(*this, DidStart()); |
| GenerateAndReplyWithAnswer(); |
| task_environment_.RunUntilIdle(); |
| Mock::VerifyAndClear(this); |
| } |
| |
| // Negotiate mirroring. |
| void NegotiateMirroring() { session_host_->NegotiateMirroring(); } |
| |
| void DeleteSessionHost() { |
| ASSERT_TRUE(session_host_); |
| session_host_deletion_cb_ = task_environment_.QuitClosure(); |
| session_host_.reset(); |
| task_environment_.RunUntilQuit(); |
| } |
| |
| void StopSession() { |
| if (video_host_) { |
| EXPECT_CALL(*video_host_, OnStopped()); |
| } |
| EXPECT_CALL(*this, DidStop()); |
| DeleteSessionHost(); |
| task_environment_.RunUntilIdle(); |
| Mock::VerifyAndClear(this); |
| } |
| |
| void RemotePlaybackSessionTimeOut() { |
| EXPECT_TRUE(video_host_); |
| EXPECT_CALL(*video_host_, OnStopped()); |
| EXPECT_CALL(*this, DidStop()); |
| task_environment_.AdvanceClock(base::Seconds(5)); |
| task_environment_.RunUntilIdle(); |
| Mock::VerifyAndClear(this); |
| } |
| |
| void CaptureOneVideoFrame() { |
| ASSERT_EQ(cast_mode_, "mirroring"); |
| ASSERT_TRUE(video_host_); |
| // Expect to send out some UDP packets. |
| EXPECT_CALL(*network_context_->udp_socket(), OnSendTo()).Times(AtLeast(1)); |
| EXPECT_CALL(*video_host_, ReleaseBuffer(_, _, _)); |
| // Send one video frame to the consumer. |
| video_host_->SendOneFrame(gfx::Size(64, 32), base::TimeTicks::Now()); |
| task_environment_.RunUntilIdle(); |
| Mock::VerifyAndClear(network_context_.get()); |
| Mock::VerifyAndClear(video_host_.get()); |
| } |
| |
| void SignalAnswerTimeout() { |
| EXPECT_CALL(*this, LogErrorMessage(_)); |
| if (cast_mode_ == "mirroring") { |
| EXPECT_CALL(*this, DidStop()); |
| EXPECT_CALL(*this, OnError(SessionError::ANSWER_TIME_OUT)); |
| } else { |
| EXPECT_CALL(*this, DidStop()).Times(0); |
| EXPECT_CALL(*this, OnError(SessionError::ANSWER_TIME_OUT)).Times(0); |
| // Expect to send OFFER message to fallback on mirroring. |
| EXPECT_CALL(*this, OnOutboundMessage(SenderMessage::Type::kOffer)); |
| EXPECT_CALL(*this, OnRemotingStateChanged(false)); |
| // The start of remoting is expected to fail. |
| EXPECT_CALL( |
| remoting_source_, |
| OnStartFailed(RemotingStartFailReason::INVALID_ANSWER_MESSAGE)); |
| EXPECT_CALL(remoting_source_, OnSinkGone()).Times(AtLeast(1)); |
| } |
| |
| session_host_->OnError(session_host_->session_.get(), |
| openscreen::Error::Code::kAnswerTimeout); |
| task_environment_.RunUntilIdle(); |
| cast_mode_ = "mirroring"; |
| Mock::VerifyAndClear(this); |
| Mock::VerifyAndClear(&remoting_source_); |
| } |
| |
| void SendRemotingCapabilities() { |
| static const openscreen::cast::RemotingCapabilities capabilities{ |
| {openscreen::cast::AudioCapability::kBaselineSet, |
| openscreen::cast::AudioCapability::kAac, |
| openscreen::cast::AudioCapability::kOpus}, |
| {openscreen::cast::VideoCapability::kSupports4k, |
| openscreen::cast::VideoCapability::kVp8, |
| openscreen::cast::VideoCapability::kVp9, |
| openscreen::cast::VideoCapability::kH264, |
| openscreen::cast::VideoCapability::kHevc}}; |
| |
| EXPECT_CALL(*this, OnConnectToRemotingSource()); |
| EXPECT_CALL(remoting_source_, OnSinkAvailable(_)); |
| |
| session_host_->OnCapabilitiesDetermined(session_host_->session_.get(), |
| capabilities); |
| task_environment_.RunUntilIdle(); |
| Mock::VerifyAndClear(this); |
| Mock::VerifyAndClear(&remoting_source_); |
| } |
| |
| void SendRemotingNotSupported() { |
| // Remoting not being supported should NOT surface an error. |
| EXPECT_CALL(*this, OnError(_)).Times(0); |
| |
| session_host_->OnError( |
| session_host_->session_.get(), |
| openscreen::Error(openscreen::Error::Code::kRemotingNotSupported, |
| "this receiver does not support remoting")); |
| |
| task_environment_.RunUntilIdle(); |
| Mock::VerifyAndClear(this); |
| } |
| |
| void StartRemoting() { |
| base::RunLoop run_loop; |
| ASSERT_TRUE(remoter_.is_bound()); |
| // GET_CAPABILITIES is only sent once at the start of mirroring. |
| EXPECT_CALL(*this, OnOutboundMessage(SenderMessage::Type::kGetCapabilities)) |
| .Times(0); |
| EXPECT_CALL(*this, OnOutboundMessage(SenderMessage::Type::kOffer)) |
| .WillOnce(InvokeWithoutArgs(&run_loop, &base::RunLoop::Quit)); |
| EXPECT_CALL(*this, OnRemotingStateChanged(true)); |
| if (is_remote_playback_) { |
| EXPECT_TRUE(video_host_); |
| if (video_host_) { |
| EXPECT_TRUE(video_host_->paused()); |
| } |
| } |
| remoter_->Start(); |
| run_loop.Run(); |
| task_environment_.RunUntilIdle(); |
| cast_mode_ = "remoting"; |
| Mock::VerifyAndClear(this); |
| } |
| |
| void RemotingStarted() { |
| ASSERT_EQ(cast_mode_, "remoting"); |
| EXPECT_CALL(remoting_source_, OnStarted()); |
| GenerateAndReplyWithAnswer(); |
| task_environment_.RunUntilIdle(); |
| Mock::VerifyAndClear(this); |
| Mock::VerifyAndClear(&remoting_source_); |
| } |
| |
| void StopRemotingAndRestartMirroring() { |
| ASSERT_EQ(cast_mode_, "remoting"); |
| const RemotingStopReason reason = RemotingStopReason::LOCAL_PLAYBACK; |
| // Expect to send OFFER message to fallback on mirroring. |
| EXPECT_CALL(*this, OnOutboundMessage(SenderMessage::Type::kOffer)); |
| EXPECT_CALL(*this, OnRemotingStateChanged(false)); |
| EXPECT_CALL(remoting_source_, OnStopped(reason)); |
| remoter_->Stop(reason); |
| task_environment_.RunUntilIdle(); |
| cast_mode_ = "mirroring"; |
| Mock::VerifyAndClear(this); |
| Mock::VerifyAndClear(&remoting_source_); |
| } |
| |
| void StopRemotingAndStopSession() { |
| ASSERT_EQ(cast_mode_, "remoting"); |
| const RemotingStopReason reason = RemotingStopReason::LOCAL_PLAYBACK; |
| EXPECT_CALL(remoting_source_, OnStopped(reason)); |
| if (video_host_) { |
| EXPECT_CALL(*video_host_, OnStopped()); |
| } |
| EXPECT_CALL(*this, DidStop()); |
| remoter_->Stop(reason); |
| task_environment_.RunUntilIdle(); |
| Mock::VerifyAndClear(this); |
| Mock::VerifyAndClear(&remoting_source_); |
| } |
| |
| void SwitchSourceTab() { |
| ExpectStreamsToStart(); |
| EXPECT_CALL(*video_host_, OnStopped()); |
| EXPECT_CALL(*this, OnConnectToRemotingSource()); |
| EXPECT_CALL(*this, OnSourceChanged()); |
| |
| if (cast_mode_ == "remoting") { |
| EXPECT_CALL(*this, OnOutboundMessage(SenderMessage::Type::kOffer)); |
| EXPECT_CALL(*this, OnError(_)).Times(0); |
| // GET_CAPABILITIES is only sent once at the start of mirroring. |
| EXPECT_CALL(*this, |
| OnOutboundMessage(SenderMessage::Type::kGetCapabilities)) |
| .Times(0); |
| const RemotingStopReason reason = RemotingStopReason::LOCAL_PLAYBACK; |
| EXPECT_CALL(remoting_source_, OnStopped(reason)); |
| } |
| |
| ASSERT_TRUE(session_host_); |
| session_host_->SwitchSourceTab(); |
| task_environment_.RunUntilIdle(); |
| |
| // Offer/Answer calls are unnecessary when switching from mirroring to |
| // mirroring. |
| if (cast_mode_ != "mirroring") { |
| cast_mode_ = "mirroring"; |
| GenerateAndReplyWithAnswer(); |
| task_environment_.RunUntilIdle(); |
| } |
| |
| Mock::VerifyAndClear(this); |
| Mock::VerifyAndClear(&remoting_source_); |
| } |
| |
| void PushNonFatalAudioCaptureError() { |
| ASSERT_TRUE(session_host_->state_ != |
| OpenscreenSessionHost::State::kStopped); |
| |
| FrameSenderConfig config; |
| config.audio_codec_params = |
| media::cast::AudioCodecParams{media::AudioCodec::kOpus}; |
| |
| PushAudioEncoderStatus( |
| config, media::cast::OperationalStatus::STATUS_CODEC_REINIT_PENDING); |
| |
| // An ignored status should not stop the session or pause capture. |
| ASSERT_NE(session_host_->state_, OpenscreenSessionHost::State::kStopped); |
| } |
| |
| void PushFatalAudioCaptureError() { |
| ASSERT_TRUE(session_host_->state_ != |
| OpenscreenSessionHost::State::kStopped); |
| |
| FrameSenderConfig config; |
| config.audio_codec_params = |
| media::cast::AudioCodecParams{media::AudioCodec::kOpus}; |
| |
| PushAudioEncoderStatus( |
| config, media::cast::OperationalStatus::STATUS_UNSUPPORTED_CODEC); |
| |
| // A fatal encoder status update should stop the session. |
| ASSERT_EQ(session_host_->state_, OpenscreenSessionHost::State::kStopped); |
| } |
| |
| void RestartVideoEncoderDueToError() { |
| ASSERT_TRUE(session_host_->state_ != |
| OpenscreenSessionHost::State::kStopped); |
| |
| // Uh oh! The encoder has a pending reinitialization. |
| FrameSenderConfig config; |
| config.use_hardware_encoder = false; |
| config.video_codec_params = |
| media::cast::VideoCodecParams{media::VideoCodec::kVP8}; |
| |
| PushVideoEncoderStatus( |
| config, media::cast::OperationalStatus::STATUS_CODEC_REINIT_PENDING); |
| |
| // A pending reinitialization should not stop the session entirely. |
| ASSERT_NE(session_host_->state_, OpenscreenSessionHost::State::kStopped); |
| |
| // Then restart the encoder. |
| PushVideoEncoderStatus(config, |
| media::cast::OperationalStatus::STATUS_INITIALIZED); |
| |
| // The session should be still going and the video encoder should be |
| // started. |
| ASSERT_NE(session_host_->state_, OpenscreenSessionHost::State::kStopped); |
| } |
| |
| void SetTargetPlayoutDelay(int target_playout_delay_ms) { |
| target_playout_delay_ = base::Milliseconds(target_playout_delay_ms); |
| } |
| |
| void ForceLetterboxing() { force_letterboxing_ = true; } |
| |
| void SetAnswer(std::unique_ptr<openscreen::cast::Answer> answer) { |
| answer_ = std::move(answer); |
| } |
| |
| OpenscreenSessionHost& session_host() { return *session_host_; } |
| |
| const openscreen::cast::SenderMessage& last_sent_offer() const { |
| EXPECT_TRUE(last_sent_offer_); |
| return *last_sent_offer_; |
| } |
| |
| base::test::TaskEnvironment& task_environment() { return task_environment_; } |
| |
| void PushAudioEncoderStatus(const media::cast::FrameSenderConfig& config, |
| media::cast::OperationalStatus status) { |
| session_host_->OnAudioEncoderStatus(config, status); |
| } |
| |
| void PushVideoEncoderStatus(const media::cast::FrameSenderConfig& config, |
| media::cast::OperationalStatus status) { |
| session_host_->OnVideoEncoderStatus(config, status); |
| } |
| |
| const std::vector<media::cast::FrameSenderConfig>& LastOfferedVideoConfigs() { |
| return session_host_->last_offered_video_configs_; |
| } |
| |
| void SetSupportedProfiles( |
| media::VideoEncodeAccelerator::SupportedProfiles profiles) { |
| session_host_->supported_profiles_ = std::move(profiles); |
| } |
| |
| void AssertCodecWasOffered(media::VideoCodec codec, |
| bool use_hardware_encoder = false) { |
| // First check that is was actually included in the offer. |
| const auto& offer = std::get<Offer>(last_sent_offer().body); |
| ASSERT_TRUE(std::any_of( |
| offer.video_streams.begin(), offer.video_streams.end(), |
| [codec](const VideoStream& stream) { |
| return stream.codec == media::cast::ToOpenscreenVideoCodec(codec); |
| })); |
| |
| // Ensure that we recorded it as a last offered video config, with the right |
| // selection of hardware or software encoding. |
| ASSERT_TRUE(std::any_of( |
| LastOfferedVideoConfigs().begin(), LastOfferedVideoConfigs().end(), |
| [codec, |
| use_hardware_encoder](const media::cast::FrameSenderConfig& config) { |
| return config.video_codec() == codec && |
| config.use_hardware_encoder == use_hardware_encoder; |
| })); |
| } |
| |
| protected: |
| std::unique_ptr<FakeVideoCaptureHost> video_host_; |
| |
| private: |
| base::test::TaskEnvironment task_environment_{ |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME}; |
| const net::IPEndPoint receiver_endpoint_ = GetFreeLocalPort(); |
| mojo::Receiver<mojom::ResourceProvider> resource_provider_receiver_{this}; |
| mojo::Receiver<mojom::SessionObserver> session_observer_receiver_{this}; |
| mojo::Receiver<mojom::CastMessageChannel> outbound_channel_receiver_{this}; |
| mojo::Remote<mojom::CastMessageChannel> inbound_channel_; |
| SessionType session_type_ = SessionType::AUDIO_AND_VIDEO; |
| bool is_remote_playback_ = false; |
| mojo::Remote<media::mojom::Remoter> remoter_; |
| NiceMock<MockRemotingSource> remoting_source_; |
| std::string cast_mode_; |
| base::TimeDelta target_playout_delay_{kDefaultPlayoutDelay}; |
| bool force_letterboxing_{false}; |
| |
| std::unique_ptr<OpenscreenSessionHost> session_host_; |
| base::OnceClosure session_host_deletion_cb_; |
| std::unique_ptr<MockNetworkContext> network_context_; |
| std::unique_ptr<openscreen::cast::Answer> answer_; |
| |
| int next_receiver_ssrc_{35336}; |
| std::optional<openscreen::cast::SenderMessage> last_sent_offer_; |
| }; |
| |
| TEST_F(OpenscreenSessionHostTest, AudioOnlyMirroring) { |
| CreateSession(SessionType::AUDIO_ONLY); |
| StartSession(); |
| StopSession(); |
| } |
| |
| TEST_F(OpenscreenSessionHostTest, VideoOnlyMirroring) { |
| SetTargetPlayoutDelay(1000); |
| CreateSession(SessionType::VIDEO_ONLY); |
| StartSession(); |
| CaptureOneVideoFrame(); |
| StopSession(); |
| } |
| |
| TEST_F(OpenscreenSessionHostTest, AudioAndVideoMirroring) { |
| SetTargetPlayoutDelay(150); |
| CreateSession(SessionType::AUDIO_AND_VIDEO); |
| StartSession(); |
| StopSession(); |
| } |
| |
| // TODO(crbug.com/1363512): Remove support for sender side letterboxing. |
| TEST_F(OpenscreenSessionHostTest, AnswerWithConstraints) { |
| SetAnswer(std::make_unique<openscreen::cast::Answer>(kAnswerWithConstraints)); |
| media::VideoCaptureParams::SuggestedConstraints expected_constraints = { |
| .min_frame_size = gfx::Size(320, 180), |
| .max_frame_size = gfx::Size(1920, 1080), |
| .fixed_aspect_ratio = true}; |
| CreateSession(SessionType::AUDIO_AND_VIDEO); |
| StartSession(); |
| StopSession(); |
| EXPECT_EQ(video_host_->GetVideoCaptureParams().SuggestConstraints(), |
| expected_constraints); |
| } |
| |
| // TODO(crbug.com/1363512): Remove support for sender side letterboxing. |
| TEST_F(OpenscreenSessionHostTest, AnswerWithConstraintsLetterboxForced) { |
| ForceLetterboxing(); |
| SetAnswer(std::make_unique<openscreen::cast::Answer>(kAnswerWithConstraints)); |
| media::VideoCaptureParams::SuggestedConstraints expected_constraints = { |
| .min_frame_size = gfx::Size(320, 180), |
| .max_frame_size = gfx::Size(1920, 1080), |
| .fixed_aspect_ratio = true}; |
| CreateSession(SessionType::AUDIO_AND_VIDEO); |
| StartSession(); |
| StopSession(); |
| EXPECT_EQ(video_host_->GetVideoCaptureParams().SuggestConstraints(), |
| expected_constraints); |
| } |
| |
| TEST_F(OpenscreenSessionHostTest, AnswerTimeout) { |
| CreateSession(SessionType::AUDIO_AND_VIDEO); |
| SignalAnswerTimeout(); |
| } |
| |
| TEST_F(OpenscreenSessionHostTest, IgnoresNonFatalAudioCaptureErrors) { |
| CreateSession(SessionType::AUDIO_AND_VIDEO); |
| StartSession(); |
| PushNonFatalAudioCaptureError(); |
| } |
| |
| TEST_F(OpenscreenSessionHostTest, StopsSessionOnAudioCaptureError) { |
| CreateSession(SessionType::AUDIO_AND_VIDEO); |
| StartSession(); |
| PushFatalAudioCaptureError(); |
| } |
| |
| TEST_F(OpenscreenSessionHostTest, |
| PauseAndUnpauseDueToVideoEncoderStatusChange) { |
| CreateSession(SessionType::AUDIO_AND_VIDEO); |
| StartSession(); |
| RestartVideoEncoderDueToError(); |
| StopSession(); |
| } |
| |
| TEST_F(OpenscreenSessionHostTest, ResumeVideoCapture) { |
| CreateSession(SessionType::VIDEO_ONLY); |
| StartSession(); |
| |
| // WE should pause the session twice, and only resume once. |
| EXPECT_CALL(*video_host_, OnPaused()).Times(2); |
| EXPECT_CALL(*video_host_, OnResumed()); |
| |
| // Pause video capture. |
| PauseCapturingVideo(); |
| |
| // Resume with the same parameters. |
| EXPECT_TRUE(TryResumeCapturingVideo()); |
| |
| // Pause again. |
| PauseCapturingVideo(); |
| |
| // Change the video capture parameters. |
| mirror_settings()->SetResolutionConstraints(1280, 720); |
| |
| // Resume with different parameters. |
| EXPECT_FALSE(TryResumeCapturingVideo()); |
| |
| StopSession(); |
| } |
| |
| TEST_F(OpenscreenSessionHostTest, SwitchToAndFromRemoting) { |
| CreateSession(SessionType::AUDIO_AND_VIDEO); |
| StartSession(); |
| SendRemotingCapabilities(); |
| StartRemoting(); |
| RemotingStarted(); |
| StopRemotingAndRestartMirroring(); |
| StopSession(); |
| } |
| |
| TEST_F(OpenscreenSessionHostTest, SwitchFromRemotingForRemotePlayback) { |
| CreateSession(SessionType::AUDIO_AND_VIDEO, true); |
| StartSession(); |
| StartRemoting(); |
| RemotingStarted(); |
| StopRemotingAndStopSession(); |
| } |
| |
| TEST_F(OpenscreenSessionHostTest, RemotingNotSupported) { |
| CreateSession(SessionType::AUDIO_AND_VIDEO); |
| StartSession(); |
| SendRemotingNotSupported(); |
| StopSession(); |
| } |
| |
| TEST_F(OpenscreenSessionHostTest, StopSessionWhileRemoting) { |
| CreateSession(SessionType::AUDIO_AND_VIDEO); |
| StartSession(); |
| SendRemotingCapabilities(); |
| StartRemoting(); |
| RemotingStarted(); |
| StopSession(); |
| } |
| |
| TEST_F(OpenscreenSessionHostTest, StartRemotingFailed) { |
| CreateSession(SessionType::AUDIO_AND_VIDEO); |
| StartSession(); |
| SendRemotingCapabilities(); |
| StartRemoting(); |
| SignalAnswerTimeout(); |
| GenerateAndReplyWithAnswer(); |
| CaptureOneVideoFrame(); |
| StopSession(); |
| } |
| |
| TEST_F(OpenscreenSessionHostTest, SwitchSourceTabFromMirroring) { |
| CreateSession(SessionType::AUDIO_AND_VIDEO); |
| StartSession(); |
| SendRemotingCapabilities(); |
| SwitchSourceTab(); |
| StartRemoting(); |
| RemotingStarted(); |
| StopSession(); |
| } |
| |
| TEST_F(OpenscreenSessionHostTest, SwitchSourceTabFromRemoting) { |
| CreateSession(SessionType::AUDIO_AND_VIDEO); |
| StartSession(); |
| SendRemotingCapabilities(); |
| StartRemoting(); |
| RemotingStarted(); |
| SwitchSourceTab(); |
| StopSession(); |
| } |
| |
| TEST_F(OpenscreenSessionHostTest, StartRemotePlaybackTimeOut) { |
| CreateSession(SessionType::AUDIO_AND_VIDEO, true); |
| StartSession(); |
| RemotePlaybackSessionTimeOut(); |
| } |
| |
| // TODO(crbug.com/40238532): reenable adaptive playout delay. |
| TEST_F(OpenscreenSessionHostTest, ChangeTargetPlayoutDelay) { |
| CreateSession(SessionType::AUDIO_AND_VIDEO); |
| StartSession(); |
| |
| // Currently new delays are ignored due to the playout delay being bounded by |
| // the minimum and maximum both being set to the default value. |
| session_host().SetTargetPlayoutDelay(base::Milliseconds(300)); |
| EXPECT_EQ(session_host().audio_stream_->GetTargetPlayoutDelay(), |
| kDefaultPlayoutDelay); |
| EXPECT_EQ(session_host().audio_stream_->GetTargetPlayoutDelay(), |
| kDefaultPlayoutDelay); |
| |
| StopSession(); |
| } |
| |
| TEST_F(OpenscreenSessionHostTest, UpdateBandwidthEstimate) { |
| CreateSession(SessionType::VIDEO_ONLY); |
| StartSession(); |
| |
| constexpr int kMinVideoBitrate = 393216; |
| constexpr int kMaxVideoBitrate = 1250000; |
| // Default bitrate should be twice the minimum. |
| EXPECT_EQ(786432, session_host().GetVideoNetworkBandwidth()); |
| |
| // If the estimate is below the minimum, it should stay at the minimum. |
| session_host().forced_bandwidth_estimate_for_testing_ = 1000; |
| session_host().UpdateBandwidthEstimate(); |
| EXPECT_EQ(kMinVideoBitrate, session_host().GetVideoNetworkBandwidth()); |
| |
| // It should gradually reach the max bandwidth estimate when raised. |
| session_host().forced_bandwidth_estimate_for_testing_ = 1000000; |
| session_host().UpdateBandwidthEstimate(); |
| EXPECT_EQ(432537, session_host().GetVideoNetworkBandwidth()); |
| |
| session_host().UpdateBandwidthEstimate(); |
| EXPECT_EQ(475790, session_host().GetVideoNetworkBandwidth()); |
| for (int i = 0; i < 20; ++i) { |
| session_host().UpdateBandwidthEstimate(); |
| } |
| // The max should be 80% of `forced_bandwidth_estimate_for_testing_`. |
| EXPECT_EQ(800000, session_host().GetVideoNetworkBandwidth()); |
| |
| // The video bitrate should stay saturated at the cap when reached. |
| session_host().forced_bandwidth_estimate_for_testing_ = kMaxVideoBitrate + 1; |
| for (int i = 0; i < 20; ++i) { |
| session_host().UpdateBandwidthEstimate(); |
| } |
| // The max should be 80% of `kMaxVideoBitrate`. |
| EXPECT_EQ(1000000, session_host().GetVideoNetworkBandwidth()); |
| |
| StopSession(); |
| } |
| |
| TEST_F(OpenscreenSessionHostTest, CanRequestRefresh) { |
| CreateSession(SessionType::VIDEO_ONLY); |
| |
| // We just want to make sure this doesn't result in an error or crash. |
| session_host().RequestRefreshFrame(); |
| } |
| |
| TEST_F(OpenscreenSessionHostTest, Vp9CodecEnabledInOffer) { |
| base::test::ScopedFeatureList feature_list(media::kCastStreamingVp9); |
| CreateSession(SessionType::VIDEO_ONLY); |
| AssertCodecWasOffered(media::VideoCodec::kVP9); |
| } |
| |
| TEST_F(OpenscreenSessionHostTest, Av1CodecEnabledInOffer) { |
| // Cast streaming of AV1 is desktop only. |
| #if !BUILDFLAG(IS_ANDROID) && defined(ENABLE_LIBAOM) |
| base::test::ScopedFeatureList feature_list(media::kCastStreamingAv1); |
| CreateSession(SessionType::VIDEO_ONLY); |
| AssertCodecWasOffered(media::VideoCodec::kAV1); |
| #endif |
| } |
| |
| TEST_F(OpenscreenSessionHostTest, ShouldEnableHardwareVp8EncodingIfSupported) { |
| CreateSession(SessionType::VIDEO_ONLY); |
| |
| // Mock the profiles to enable VP8 hardware encode. |
| SetSupportedProfiles( |
| std::vector<media::VideoEncodeAccelerator::SupportedProfile>{ |
| media::VideoEncodeAccelerator::SupportedProfile( |
| media::VideoCodecProfile::VP8PROFILE_ANY, |
| gfx::Size{1920, 1080})}); |
| NegotiateMirroring(); |
| task_environment().RunUntilIdle(); |
| |
| AssertCodecWasOffered(media::VideoCodec::kVP8, true); |
| } |
| |
| TEST_F(OpenscreenSessionHostTest, |
| ShouldDisableHardwareEncodingIfEncoderReportsAnIssue) { |
| CreateSession(SessionType::VIDEO_ONLY); |
| |
| // Mock the profiles to enable VP8 hardware encode. |
| SetSupportedProfiles( |
| std::vector<media::VideoEncodeAccelerator::SupportedProfile>{ |
| media::VideoEncodeAccelerator::SupportedProfile( |
| media::VideoCodecProfile::VP8PROFILE_ANY, |
| gfx::Size{1920, 1080})}); |
| NegotiateMirroring(); |
| task_environment().RunUntilIdle(); |
| |
| // We should have offered VP8 with hardware ENABLED. |
| AssertCodecWasOffered(media::VideoCodec::kVP8, true); |
| |
| // Oh no! The encoder had a problem. |
| FrameSenderConfig config; |
| config.use_hardware_encoder = true; |
| config.video_codec_params = |
| media::cast::VideoCodecParams{media::VideoCodec::kVP8}; |
| PushVideoEncoderStatus( |
| config, media::cast::OperationalStatus::STATUS_CODEC_INIT_FAILED); |
| |
| // This should have forced a renegotiation with hardware DISABLED. |
| AssertCodecWasOffered(media::VideoCodec::kVP8, false); |
| } |
| |
| TEST_F(OpenscreenSessionHostTest, ShouldEnableHardwareH264EncodingIfSupported) { |
| #if !BUILDFLAG(IS_WIN) |
| CreateSession(SessionType::VIDEO_ONLY); |
| |
| SetSupportedProfiles( |
| std::vector<media::VideoEncodeAccelerator::SupportedProfile>{ |
| media::VideoEncodeAccelerator::SupportedProfile( |
| media::VideoCodecProfile::H264PROFILE_MIN, |
| gfx::Size{1920, 1080})}); |
| NegotiateMirroring(); |
| task_environment().RunUntilIdle(); |
| |
| AssertCodecWasOffered(media::VideoCodec::kH264, true); |
| #endif |
| } |
| |
| TEST_F(OpenscreenSessionHostTest, GetStatsDefault) { |
| CreateSession(SessionType::AUDIO_AND_VIDEO); |
| EXPECT_TRUE(session_host().GetMirroringStats().empty()); |
| } |
| |
| TEST_F(OpenscreenSessionHostTest, GetStatsEnabled) { |
| CreateSession(SessionType::AUDIO_AND_VIDEO, /* remote_playback */ false, |
| /* rtcp_reporting */ true); |
| session_host().SetSenderStatsForTest(ConstructDefaultSenderStats()); |
| EXPECT_FALSE(session_host().GetMirroringStats().empty()); |
| } |
| |
| } // namespace mirroring |