| // 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 "cast/streaming/sender_session.h" |
| |
| #include <cstdio> |
| #include <utility> |
| |
| #include "cast/streaming/capture_configs.h" |
| #include "cast/streaming/capture_recommendations.h" |
| #include "cast/streaming/mock_environment.h" |
| #include "cast/streaming/testing/simple_message_port.h" |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| #include "platform/base/ip_address.h" |
| #include "platform/test/fake_clock.h" |
| #include "platform/test/fake_task_runner.h" |
| #include "util/chrono_helpers.h" |
| #include "util/stringprintf.h" |
| |
| using ::testing::_; |
| using ::testing::InSequence; |
| using ::testing::Invoke; |
| using ::testing::NiceMock; |
| using ::testing::Return; |
| using ::testing::StrictMock; |
| |
| namespace openscreen { |
| namespace cast { |
| |
| namespace { |
| constexpr char kMalformedAnswerMessage[] = R"({ |
| "type": "ANSWER", |
| "seqNum": 1, |
| "answer": { |
| "castMode": "mirroring", |
| "udpPort": 1234, |
| "sendIndexes": [1, 3], |
| "ssrcs": [1, 2] |
| })"; |
| |
| constexpr char kValidJsonInvalidFormatAnswerMessage[] = R"({ |
| "type": "ANSWER", |
| "seqNum": 1, |
| "answer-2": { |
| "castMode": "mirroring", |
| "udpPort": 1234, |
| "sendIndexes": [1, 3], |
| "ssrcs": [1, 2] |
| } |
| })"; |
| |
| constexpr char kValidJsonInvalidAnswerMessage[] = R"({ |
| "type": "ANSWER", |
| "seqNum": 1, |
| "answer": { |
| "castMode": "mirroring", |
| "udpPort": -1234, |
| "sendIndexes": [1, 3], |
| "ssrcs": [1, 2] |
| } |
| })"; |
| |
| constexpr char kMissingAnswerMessage[] = R"({ |
| "type": "ANSWER", |
| "seqNum": 1 |
| })"; |
| |
| constexpr char kInvalidSequenceNumberMessage[] = R"({ |
| "type": "ANSWER", |
| "seqNum": "not actually a number" |
| })"; |
| |
| constexpr char kUnknownTypeMessage[] = R"({ |
| "type": "ANSWER_VERSION_2", |
| "seqNum": 1 |
| })"; |
| |
| constexpr char kInvalidTypeMessage[] = R"({ |
| "type": 39, |
| "seqNum": 1 |
| })"; |
| |
| constexpr char kInvalidTypeMessageWithNoSeqNum[] = R"({ |
| "type": 39 |
| })"; |
| |
| constexpr char kErrorAnswerMessage[] = R"({ |
| "seqNum": 1, |
| "type": "ANSWER", |
| "result": "error", |
| "error": { |
| "code": 123, |
| "description": "something bad happened" |
| } |
| })"; |
| |
| const AudioCaptureConfig kAudioCaptureConfigInvalidChannels{ |
| AudioCodec::kAac, -1 /* channels */, 44000 /* bit_rate */, |
| 96000 /* sample_rate */ |
| }; |
| |
| const AudioCaptureConfig kAudioCaptureConfigValid{ |
| AudioCodec::kOpus, 5 /* channels */, 32000 /* bit_rate */, |
| 44000 /* sample_rate */ |
| }; |
| |
| const VideoCaptureConfig kVideoCaptureConfigMissingResolutions{ |
| VideoCodec::kHevc, FrameRate{60, 1}, 300000 /* max_bit_rate */, |
| std::vector<DisplayResolution>{}}; |
| |
| const VideoCaptureConfig kVideoCaptureConfigInvalid{ |
| VideoCodec::kHevc, FrameRate{60, 1}, -300000 /* max_bit_rate */, |
| std::vector<DisplayResolution>{DisplayResolution{1920, 1080}, |
| DisplayResolution{1280, 720}}}; |
| |
| const VideoCaptureConfig kVideoCaptureConfigValid{ |
| VideoCodec::kHevc, FrameRate{60, 1}, 300000 /* max_bit_rate */, |
| std::vector<DisplayResolution>{DisplayResolution{1280, 720}, |
| DisplayResolution{1920, 1080}}}; |
| |
| const VideoCaptureConfig kVideoCaptureConfigValidSimplest{ |
| VideoCodec::kHevc, FrameRate{60, 1}, 300000 /* max_bit_rate */, |
| std::vector<DisplayResolution>{DisplayResolution{1920, 1080}}}; |
| |
| class FakeClient : public SenderSession::Client { |
| public: |
| MOCK_METHOD(void, |
| OnNegotiated, |
| (const SenderSession*, |
| SenderSession::ConfiguredSenders, |
| capture_recommendations::Recommendations), |
| (override)); |
| MOCK_METHOD(void, OnError, (const SenderSession*, Error error), (override)); |
| }; |
| |
| } // namespace |
| |
| class SenderSessionTest : public ::testing::Test { |
| public: |
| SenderSessionTest() : clock_(Clock::time_point{}), task_runner_(&clock_) {} |
| |
| std::unique_ptr<MockEnvironment> MakeEnvironment() { |
| auto environment_ = std::make_unique<NiceMock<MockEnvironment>>( |
| &FakeClock::now, &task_runner_); |
| ON_CALL(*environment_, GetBoundLocalEndpoint()) |
| .WillByDefault( |
| Return(IPEndpoint{IPAddress::Parse("127.0.0.1").value(), 12345})); |
| return environment_; |
| } |
| |
| void SetUp() { |
| message_port_ = std::make_unique<SimpleMessagePort>("receiver-12345"); |
| environment_ = MakeEnvironment(); |
| session_ = std::make_unique<SenderSession>( |
| IPAddress::kV4LoopbackAddress(), &client_, environment_.get(), |
| message_port_.get(), "sender-12345", "receiver-12345"); |
| } |
| |
| std::string NegotiateOfferAndConstructAnswer() { |
| const Error error = session_->Negotiate( |
| std::vector<AudioCaptureConfig>{kAudioCaptureConfigValid}, |
| std::vector<VideoCaptureConfig>{kVideoCaptureConfigValid}); |
| if (!error.ok()) { |
| return {}; |
| } |
| |
| const auto& messages = message_port_->posted_messages(); |
| if (messages.size() != 1) { |
| return {}; |
| } |
| auto message_body = json::Parse(messages[0]); |
| if (message_body.is_error()) { |
| return {}; |
| } |
| const Json::Value offer = std::move(message_body.value()); |
| EXPECT_EQ("OFFER", offer["type"].asString()); |
| EXPECT_LT(0, offer["seqNum"].asInt()); |
| |
| const Json::Value& offer_body = offer["offer"]; |
| if (!offer_body.isObject()) { |
| return {}; |
| } |
| |
| const Json::Value& streams = offer_body["supportedStreams"]; |
| EXPECT_TRUE(streams.isArray()); |
| EXPECT_EQ(2u, streams.size()); |
| |
| const Json::Value& audio_stream = streams[0]; |
| const int audio_index = audio_stream["index"].asInt(); |
| const int audio_ssrc = audio_stream["ssrc"].asUInt(); |
| |
| const Json::Value& video_stream = streams[1]; |
| const int video_index = video_stream["index"].asInt(); |
| const int video_ssrc = video_stream["ssrc"].asUInt(); |
| |
| constexpr char kAnswerTemplate[] = R"({ |
| "type": "ANSWER", |
| "seqNum": %d, |
| "result": "ok", |
| "answer": { |
| "castMode": "mirroring", |
| "udpPort": 1234, |
| "sendIndexes": [%d, %d], |
| "ssrcs": [%d, %d] |
| } |
| })"; |
| return StringPrintf(kAnswerTemplate, offer["seqNum"].asInt(), audio_index, |
| video_index, audio_ssrc + 1, video_ssrc + 1); |
| } |
| |
| protected: |
| StrictMock<FakeClient> client_; |
| FakeClock clock_; |
| std::unique_ptr<MockEnvironment> environment_; |
| std::unique_ptr<SimpleMessagePort> message_port_; |
| std::unique_ptr<SenderSession> session_; |
| FakeTaskRunner task_runner_; |
| }; |
| |
| TEST_F(SenderSessionTest, ComplainsIfNoConfigsToOffer) { |
| const Error error = session_->Negotiate(std::vector<AudioCaptureConfig>{}, |
| std::vector<VideoCaptureConfig>{}); |
| |
| EXPECT_EQ(error, |
| Error(Error::Code::kParameterInvalid, |
| "Need at least one audio or video config to negotiate.")); |
| } |
| |
| TEST_F(SenderSessionTest, ComplainsIfInvalidAudioCaptureConfig) { |
| const Error error = session_->Negotiate( |
| std::vector<AudioCaptureConfig>{kAudioCaptureConfigInvalidChannels}, |
| std::vector<VideoCaptureConfig>{}); |
| |
| EXPECT_EQ(error, |
| Error(Error::Code::kParameterInvalid, "Invalid configs provided.")); |
| } |
| |
| TEST_F(SenderSessionTest, ComplainsIfInvalidVideoCaptureConfig) { |
| const Error error = session_->Negotiate( |
| std::vector<AudioCaptureConfig>{}, |
| std::vector<VideoCaptureConfig>{kVideoCaptureConfigInvalid}); |
| EXPECT_EQ(error, |
| Error(Error::Code::kParameterInvalid, "Invalid configs provided.")); |
| } |
| |
| TEST_F(SenderSessionTest, ComplainsIfMissingResolutions) { |
| const Error error = session_->Negotiate( |
| std::vector<AudioCaptureConfig>{}, |
| std::vector<VideoCaptureConfig>{kVideoCaptureConfigMissingResolutions}); |
| EXPECT_EQ(error, |
| Error(Error::Code::kParameterInvalid, "Invalid configs provided.")); |
| } |
| |
| TEST_F(SenderSessionTest, SendsOfferWithZeroBitrateOptions) { |
| VideoCaptureConfig video_config = kVideoCaptureConfigValid; |
| video_config.max_bit_rate = 0; |
| AudioCaptureConfig audio_config = kAudioCaptureConfigValid; |
| audio_config.bit_rate = 0; |
| |
| const Error error = |
| session_->Negotiate(std::vector<AudioCaptureConfig>{audio_config}, |
| std::vector<VideoCaptureConfig>{video_config}); |
| EXPECT_TRUE(error.ok()); |
| |
| const auto& messages = message_port_->posted_messages(); |
| ASSERT_EQ(1u, messages.size()); |
| auto message_body = json::Parse(messages[0]); |
| ASSERT_TRUE(message_body.is_value()); |
| const Json::Value offer = std::move(message_body.value()); |
| EXPECT_EQ("OFFER", offer["type"].asString()); |
| } |
| |
| TEST_F(SenderSessionTest, SendsOfferWithSimpleVideoOnly) { |
| const Error error = session_->Negotiate( |
| std::vector<AudioCaptureConfig>{}, |
| std::vector<VideoCaptureConfig>{kVideoCaptureConfigValid}); |
| EXPECT_TRUE(error.ok()); |
| |
| const auto& messages = message_port_->posted_messages(); |
| ASSERT_EQ(1u, messages.size()); |
| auto message_body = json::Parse(messages[0]); |
| ASSERT_TRUE(message_body.is_value()); |
| const Json::Value offer = std::move(message_body.value()); |
| EXPECT_EQ("OFFER", offer["type"].asString()); |
| } |
| |
| TEST_F(SenderSessionTest, SendsOfferAudioOnly) { |
| const Error error = session_->Negotiate( |
| std::vector<AudioCaptureConfig>{kAudioCaptureConfigValid}, |
| std::vector<VideoCaptureConfig>{}); |
| EXPECT_TRUE(error.ok()); |
| |
| const auto& messages = message_port_->posted_messages(); |
| ASSERT_EQ(1u, messages.size()); |
| auto message_body = json::Parse(messages[0]); |
| ASSERT_TRUE(message_body.is_value()); |
| const Json::Value offer = std::move(message_body.value()); |
| EXPECT_EQ("OFFER", offer["type"].asString()); |
| } |
| |
| TEST_F(SenderSessionTest, SendsOfferMessage) { |
| session_->Negotiate( |
| std::vector<AudioCaptureConfig>{kAudioCaptureConfigValid}, |
| std::vector<VideoCaptureConfig>{kVideoCaptureConfigValid}); |
| |
| const auto& messages = message_port_->posted_messages(); |
| ASSERT_EQ(1u, messages.size()); |
| |
| auto message_body = json::Parse(messages[0]); |
| ASSERT_TRUE(message_body.is_value()); |
| const Json::Value offer = std::move(message_body.value()); |
| EXPECT_EQ("OFFER", offer["type"].asString()); |
| EXPECT_LT(0, offer["seqNum"].asInt()); |
| |
| const Json::Value& offer_body = offer["offer"]; |
| ASSERT_FALSE(offer_body.isNull()); |
| ASSERT_TRUE(offer_body.isObject()); |
| EXPECT_EQ("mirroring", offer_body["castMode"].asString()); |
| EXPECT_EQ(false, offer_body["receiverGetStatus"].asBool()); |
| |
| const Json::Value& streams = offer_body["supportedStreams"]; |
| EXPECT_TRUE(streams.isArray()); |
| EXPECT_EQ(2u, streams.size()); |
| |
| const Json::Value& audio_stream = streams[0]; |
| EXPECT_EQ("opus", audio_stream["codecName"].asString()); |
| EXPECT_EQ(0, audio_stream["index"].asInt()); |
| EXPECT_EQ(32u, audio_stream["aesKey"].asString().length()); |
| EXPECT_EQ(32u, audio_stream["aesIvMask"].asString().length()); |
| EXPECT_EQ(5, audio_stream["channels"].asInt()); |
| EXPECT_LT(0u, audio_stream["ssrc"].asUInt()); |
| EXPECT_EQ(127, audio_stream["rtpPayloadType"].asInt()); |
| |
| const Json::Value& video_stream = streams[1]; |
| EXPECT_EQ("hevc", video_stream["codecName"].asString()); |
| EXPECT_EQ(1, video_stream["index"].asInt()); |
| EXPECT_EQ(32u, video_stream["aesKey"].asString().length()); |
| EXPECT_EQ(32u, video_stream["aesIvMask"].asString().length()); |
| EXPECT_EQ(1, video_stream["channels"].asInt()); |
| EXPECT_LT(0u, video_stream["ssrc"].asUInt()); |
| EXPECT_EQ(96, video_stream["rtpPayloadType"].asInt()); |
| } |
| |
| TEST_F(SenderSessionTest, HandlesValidAnswer) { |
| std::string answer = NegotiateOfferAndConstructAnswer(); |
| |
| EXPECT_CALL(client_, OnNegotiated(session_.get(), _, _)); |
| message_port_->ReceiveMessage(answer); |
| } |
| |
| TEST_F(SenderSessionTest, HandlesInvalidNamespace) { |
| std::string answer = NegotiateOfferAndConstructAnswer(); |
| message_port_->ReceiveMessage("random-namespace", answer); |
| } |
| |
| TEST_F(SenderSessionTest, HandlesMalformedAnswer) { |
| session_->Negotiate( |
| std::vector<AudioCaptureConfig>{kAudioCaptureConfigValid}, |
| std::vector<VideoCaptureConfig>{kVideoCaptureConfigValid}); |
| |
| // Note that unlike when we simply don't select any streams, when the answer |
| // is actually malformed we have no way of knowing it was an answer at all, |
| // so we report an Error and drop the message. |
| EXPECT_CALL(client_, OnError(session_.get(), _)); |
| message_port_->ReceiveMessage(kMalformedAnswerMessage); |
| } |
| |
| TEST_F(SenderSessionTest, HandlesImproperlyFormattedAnswer) { |
| session_->Negotiate( |
| std::vector<AudioCaptureConfig>{kAudioCaptureConfigValid}, |
| std::vector<VideoCaptureConfig>{kVideoCaptureConfigValid}); |
| |
| EXPECT_CALL(client_, OnError(session_.get(), _)); |
| message_port_->ReceiveMessage(kValidJsonInvalidFormatAnswerMessage); |
| } |
| |
| TEST_F(SenderSessionTest, HandlesInvalidAnswer) { |
| const Error error = session_->Negotiate( |
| std::vector<AudioCaptureConfig>{kAudioCaptureConfigValid}, |
| std::vector<VideoCaptureConfig>{kVideoCaptureConfigValid}); |
| |
| EXPECT_CALL(client_, OnError(session_.get(), _)); |
| message_port_->ReceiveMessage(kValidJsonInvalidAnswerMessage); |
| } |
| |
| TEST_F(SenderSessionTest, HandlesNullAnswer) { |
| const Error error = session_->Negotiate( |
| std::vector<AudioCaptureConfig>{kAudioCaptureConfigValid}, |
| std::vector<VideoCaptureConfig>{kVideoCaptureConfigValid}); |
| |
| EXPECT_TRUE(error.ok()); |
| EXPECT_CALL(client_, OnError(session_.get(), _)); |
| message_port_->ReceiveMessage(kMissingAnswerMessage); |
| } |
| |
| TEST_F(SenderSessionTest, HandlesInvalidSequenceNumber) { |
| const Error error = session_->Negotiate( |
| std::vector<AudioCaptureConfig>{kAudioCaptureConfigValid}, |
| std::vector<VideoCaptureConfig>{kVideoCaptureConfigValid}); |
| |
| // We should just discard messages with an invalid sequence number. |
| message_port_->ReceiveMessage(kInvalidSequenceNumberMessage); |
| } |
| |
| TEST_F(SenderSessionTest, HandlesUnknownTypeMessageWithValidSeqNum) { |
| session_->Negotiate( |
| std::vector<AudioCaptureConfig>{kAudioCaptureConfigValid}, |
| std::vector<VideoCaptureConfig>{kVideoCaptureConfigValid}); |
| |
| // If a message is of unknown type but has an expected seqnum, it's |
| // probably a malformed response. |
| EXPECT_CALL(client_, OnError(session_.get(), _)); |
| message_port_->ReceiveMessage(kUnknownTypeMessage); |
| } |
| |
| TEST_F(SenderSessionTest, HandlesInvalidTypeMessageWithValidSeqNum) { |
| session_->Negotiate( |
| std::vector<AudioCaptureConfig>{kAudioCaptureConfigValid}, |
| std::vector<VideoCaptureConfig>{kVideoCaptureConfigValid}); |
| |
| // If a message is of unknown type but has an expected seqnum, it's |
| // probably a malformed response. |
| EXPECT_CALL(client_, OnError(session_.get(), _)); |
| message_port_->ReceiveMessage(kInvalidTypeMessage); |
| } |
| |
| TEST_F(SenderSessionTest, HandlesInvalidTypeMessage) { |
| session_->Negotiate( |
| std::vector<AudioCaptureConfig>{kAudioCaptureConfigValid}, |
| std::vector<VideoCaptureConfig>{kVideoCaptureConfigValid}); |
| |
| // We should just discard messages with an invalid message type and |
| // no sequence number. |
| message_port_->ReceiveMessage(kInvalidTypeMessageWithNoSeqNum); |
| } |
| |
| TEST_F(SenderSessionTest, HandlesErrorMessage) { |
| session_->Negotiate( |
| std::vector<AudioCaptureConfig>{kAudioCaptureConfigValid}, |
| std::vector<VideoCaptureConfig>{kVideoCaptureConfigValid}); |
| |
| // We should report error answers. |
| EXPECT_CALL(client_, OnError(session_.get(), _)); |
| message_port_->ReceiveMessage(kErrorAnswerMessage); |
| } |
| |
| TEST_F(SenderSessionTest, DoesNotCrashOnMessagePortError) { |
| session_->Negotiate( |
| std::vector<AudioCaptureConfig>{kAudioCaptureConfigValid}, |
| std::vector<VideoCaptureConfig>{kVideoCaptureConfigValid}); |
| |
| message_port_->ReceiveError(Error(Error::Code::kUnknownError)); |
| } |
| |
| } // namespace cast |
| } // namespace openscreen |