blob: 4716a6a317d5a89be8bd4935253c5acf3be7d794 [file] [log] [blame]
// Copyright 2019 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/receiver_session.h"
#include <utility>
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "platform/test/fake_clock.h"
#include "platform/test/fake_task_runner.h"
using openscreen::Clock;
using openscreen::FakeClock;
using openscreen::FakeTaskRunner;
using ::testing::_;
using ::testing::Invoke;
using ::testing::StrictMock;
namespace cast {
namespace streaming {
namespace {
constexpr char kValidOfferMessage[] = R"({
"type": "OFFER",
"seqNum": 1337,
"offer": {
"castMode": "mirroring",
"receiverGetStatus": true,
"supportedStreams": [
{
"index": 31337,
"type": "video_source",
"codecName": "vp9",
"rtpProfile": "cast",
"rtpPayloadType": 127,
"ssrc": 19088743,
"maxFrameRate": "60000/1000",
"timeBase": "1/90000",
"maxBitRate": 5000000,
"profile": "main",
"level": "4",
"aesKey": "bbf109bf84513b456b13a184453b66ce",
"aesIvMask": "edaf9e4536e2b66191f560d9c04b2a69",
"resolutions": [
{
"width": 1280,
"height": 720
}
]
},
{
"index": 31338,
"type": "video_source",
"codecName": "vp8",
"rtpProfile": "cast",
"rtpPayloadType": 127,
"ssrc": 19088745,
"maxFrameRate": "60000/1000",
"timeBase": "1/90000",
"maxBitRate": 5000000,
"profile": "main",
"level": "4",
"aesKey": "040d756791711fd3adb939066e6d8690",
"aesIvMask": "9ff0f022a959150e70a2d05a6c184aed",
"resolutions": [
{
"width": 1280,
"height": 720
}
]
},
{
"index": 1337,
"type": "audio_source",
"codecName": "opus",
"rtpProfile": "cast",
"rtpPayloadType": 97,
"ssrc": 19088747,
"bitRate": 124000,
"timeBase": "1/48000",
"channels": 2,
"aesKey": "51027e4e2347cbcb49d57ef10177aebc",
"aesIvMask": "7f12a19be62a36c04ae4116caaeff6d1"
}
]
}
})";
constexpr char kNoAudioOfferMessage[] = R"({
"type": "OFFER",
"seqNum": 1337,
"offer": {
"castMode": "mirroring",
"receiverGetStatus": true,
"supportedStreams": [
{
"index": 31338,
"type": "video_source",
"codecName": "vp8",
"rtpProfile": "cast",
"rtpPayloadType": 127,
"ssrc": 19088745,
"maxFrameRate": "60000/1000",
"timeBase": "1/90000",
"maxBitRate": 5000000,
"profile": "main",
"level": "4",
"aesKey": "040d756791711fd3adb939066e6d8690",
"aesIvMask": "9ff0f022a959150e70a2d05a6c184aed",
"resolutions": [
{
"width": 1280,
"height": 720
}
]
}
]
}
})";
constexpr char kNoVideoOfferMessage[] = R"({
"type": "OFFER",
"seqNum": 1337,
"offer": {
"castMode": "mirroring",
"receiverGetStatus": true,
"supportedStreams": [
{
"index": 1337,
"type": "audio_source",
"codecName": "opus",
"rtpProfile": "cast",
"rtpPayloadType": 97,
"ssrc": 19088747,
"bitRate": 124000,
"timeBase": "1/48000",
"channels": 2,
"aesKey": "51027e4e2347cbcb49d57ef10177aebc",
"aesIvMask": "7f12a19be62a36c04ae4116caaeff6d1"
}
]
}
})";
constexpr char kNoAudioOrVideoOfferMessage[] = R"({
"type": "OFFER",
"seqNum": 1337,
"offer": {
"castMode": "mirroring",
"receiverGetStatus": true,
"supportedStreams": []
}
})";
constexpr char kInvalidJsonOfferMessage[] = R"({
"type": "OFFER",
"seqNum": 1337,,,
"offer":
"castMode": "mirroring",
"receiverGetStatus": true,
"supportedStreams": [
}
})";
class SimpleMessagePort : public MessagePort {
public:
~SimpleMessagePort() override{};
void SetClient(MessagePort::Client* client) override { client_ = client; }
void ReceiveMessage(absl::string_view message) {
ASSERT_NE(client_, nullptr);
client_->OnMessage("sender-id", "urn:x-cast:com.google.cast.webrtc",
message);
}
void ReceiveError(openscreen::Error error) {
ASSERT_NE(client_, nullptr);
client_->OnError(error);
}
void PostMessage(absl::string_view sender_id,
absl::string_view message_namespace,
absl::string_view message) override {
posted_messages_.emplace_back(std::move(message));
}
MessagePort::Client* client() const { return client_; }
const std::vector<std::string> posted_messages() const {
return posted_messages_;
}
private:
MessagePort::Client* client_ = nullptr;
std::vector<std::string> posted_messages_;
};
class FakeClient : public ReceiverSession::Client {
public:
MOCK_METHOD(void,
OnNegotiated,
(ReceiverSession*, ReceiverSession::ConfiguredReceivers),
(override));
MOCK_METHOD(void, OnReceiversDestroyed, (ReceiverSession*), (override));
MOCK_METHOD(void,
OnError,
(ReceiverSession*, openscreen::Error error),
(override));
};
void ExpectIsErrorAnswerMessage(
const openscreen::ErrorOr<Json::Value>& message_or_error) {
EXPECT_TRUE(message_or_error.is_value());
const Json::Value message = std::move(message_or_error.value());
EXPECT_TRUE(message["answer"].isNull());
EXPECT_EQ("error", message["result"].asString());
EXPECT_EQ(1337, message["seqNum"].asInt());
EXPECT_EQ("ANSWER", message["type"].asString());
const Json::Value& error = message["error"];
EXPECT_TRUE(error.isObject());
EXPECT_EQ(83, error["code"].asInt());
EXPECT_EQ("", error["description"].asString());
}
} // namespace
class ReceiverSessionTest : public ::testing::Test {
public:
ReceiverSessionTest()
: clock_(Clock::time_point{}),
task_runner_(&clock_),
env_(std::make_unique<Environment>(&FakeClock::now,
&task_runner_,
openscreen::IPEndpoint{})) {}
FakeClock clock_;
FakeTaskRunner task_runner_;
std::unique_ptr<Environment> env_;
};
TEST_F(ReceiverSessionTest, RegistersSelfOnMessagePump) {
auto message_port = std::make_unique<SimpleMessagePort>();
// This should be safe, since the message_port location should not move
// just because of being moved into the ReceiverSession.
SimpleMessagePort* raw_port = message_port.get();
StrictMock<FakeClient> client;
auto session = std::make_unique<ReceiverSession>(
&client, std::move(env_), std::move(message_port),
ReceiverSession::Preferences{});
EXPECT_EQ(raw_port->client(), session.get());
}
TEST_F(ReceiverSessionTest, CanNegotiateWithDefaultPreferences) {
auto message_port = std::make_unique<SimpleMessagePort>();
SimpleMessagePort* raw_port = message_port.get();
StrictMock<FakeClient> client;
ReceiverSession session(&client, std::move(env_), std::move(message_port),
ReceiverSession::Preferences{});
EXPECT_CALL(client, OnNegotiated(&session, _))
.WillOnce([](ReceiverSession* session,
ReceiverSession::ConfiguredReceivers cr) {
EXPECT_TRUE(cr.audio_receiver());
EXPECT_TRUE(cr.audio_session_config());
EXPECT_EQ(cr.audio_session_config().value().sender_ssrc, 19088747u);
EXPECT_EQ(cr.audio_session_config().value().receiver_ssrc, 19088748u);
EXPECT_EQ(cr.audio_session_config().value().channels, 2);
EXPECT_EQ(cr.audio_session_config().value().rtp_timebase, 48000);
EXPECT_TRUE(cr.video_receiver());
EXPECT_TRUE(cr.video_session_config());
// We should have chosen vp8
EXPECT_EQ(cr.video_session_config().value().sender_ssrc, 19088745u);
EXPECT_EQ(cr.video_session_config().value().receiver_ssrc, 19088746u);
EXPECT_EQ(cr.video_session_config().value().channels, 1);
EXPECT_EQ(cr.video_session_config().value().rtp_timebase, 90000);
});
EXPECT_CALL(client, OnReceiversDestroyed(&session)).Times(1);
raw_port->ReceiveMessage(kValidOfferMessage);
const auto& messages = raw_port->posted_messages();
ASSERT_EQ(1u, messages.size());
auto message_body = openscreen::json::Parse(messages[0]);
EXPECT_TRUE(message_body.is_value());
const Json::Value answer = std::move(message_body.value());
EXPECT_EQ("ANSWER", answer["type"].asString());
EXPECT_EQ(1337, answer["seqNum"].asInt());
EXPECT_EQ("ok", answer["result"].asString());
const Json::Value& answer_body = answer["answer"];
EXPECT_TRUE(answer_body.isObject());
// Spot check the answer body fields. We have more in depth testing
// of answer behavior in answer_messages_unittest, but here we can
// ensure that the ReceiverSession properly configured the answer.
EXPECT_EQ("mirroring", answer_body["castMode"].asString());
EXPECT_EQ(1337, answer_body["sendIndexes"][0].asInt());
EXPECT_EQ(31338, answer_body["sendIndexes"][1].asInt());
EXPECT_LT(0, answer_body["udpPort"].asInt());
EXPECT_GT(65535, answer_body["udpPort"].asInt());
// Get status should always be false, as we have no plans to implement it.
EXPECT_EQ(false, answer_body["receiverGetStatus"].asBool());
// Constraints and display should not be present with no preferences.
EXPECT_TRUE(answer_body["constraints"].isNull());
EXPECT_TRUE(answer_body["display"].isNull());
}
TEST_F(ReceiverSessionTest, CanNegotiateWithCustomCodecPreferences) {
auto message_port = std::make_unique<SimpleMessagePort>();
SimpleMessagePort* raw_port = message_port.get();
StrictMock<FakeClient> client;
ReceiverSession session(
&client, std::move(env_), std::move(message_port),
ReceiverSession::Preferences{{ReceiverSession::VideoCodec::kVp9},
{ReceiverSession::AudioCodec::kOpus}});
EXPECT_CALL(client, OnNegotiated(&session, _))
.WillOnce([](ReceiverSession* session,
ReceiverSession::ConfiguredReceivers cr) {
EXPECT_TRUE(cr.audio_receiver());
EXPECT_TRUE(cr.audio_session_config());
EXPECT_EQ(cr.audio_session_config().value().sender_ssrc, 19088747u);
EXPECT_EQ(cr.audio_session_config().value().receiver_ssrc, 19088748u);
EXPECT_EQ(cr.audio_session_config().value().channels, 2);
EXPECT_EQ(cr.audio_session_config().value().rtp_timebase, 48000);
EXPECT_TRUE(cr.video_receiver());
EXPECT_TRUE(cr.video_session_config());
// We should have chosen vp9
EXPECT_EQ(cr.video_session_config().value().sender_ssrc, 19088743u);
EXPECT_EQ(cr.video_session_config().value().receiver_ssrc, 19088744u);
EXPECT_EQ(cr.video_session_config().value().channels, 1);
EXPECT_EQ(cr.video_session_config().value().rtp_timebase, 90000);
});
EXPECT_CALL(client, OnReceiversDestroyed(&session)).Times(1);
raw_port->ReceiveMessage(kValidOfferMessage);
}
TEST_F(ReceiverSessionTest, CanNegotiateWithCustomConstraints) {
auto message_port = std::make_unique<SimpleMessagePort>();
SimpleMessagePort* raw_port = message_port.get();
StrictMock<FakeClient> client;
auto constraints = std::unique_ptr<Constraints>{new Constraints{
AudioConstraints{1, 2, 3, 4},
VideoConstraints{3.14159, Dimensions{320, 240, 24, 1},
Dimensions{1920, 1080, 144, 1}, 3000, 90000000,
std::chrono::milliseconds{1000}}}};
auto display = std::unique_ptr<DisplayDescription>{
new DisplayDescription{Dimensions{640, 480, 60, 1}, AspectRatio{16, 9},
AspectRatioConstraint::kFixed}};
ReceiverSession session(
&client, std::move(env_), std::move(message_port),
ReceiverSession::Preferences{{ReceiverSession::VideoCodec::kVp9},
{ReceiverSession::AudioCodec::kOpus},
std::move(constraints),
std::move(display)});
EXPECT_CALL(client, OnNegotiated(&session, _)).Times(1);
EXPECT_CALL(client, OnReceiversDestroyed(&session)).Times(1);
raw_port->ReceiveMessage(kValidOfferMessage);
const auto& messages = raw_port->posted_messages();
EXPECT_EQ(1u, messages.size());
auto message_body = openscreen::json::Parse(messages[0]);
ASSERT_TRUE(message_body.is_value());
const Json::Value answer = std::move(message_body.value());
const Json::Value& answer_body = answer["answer"];
ASSERT_TRUE(answer_body.isObject());
// Constraints and display should be valid with valid preferences.
ASSERT_FALSE(answer_body["constraints"].isNull());
ASSERT_FALSE(answer_body["display"].isNull());
const Json::Value& display_json = answer_body["display"];
EXPECT_EQ("16:9", display_json["aspectRatio"].asString());
EXPECT_EQ("60", display_json["dimensions"]["frameRate"].asString());
EXPECT_EQ(640, display_json["dimensions"]["width"].asInt());
EXPECT_EQ(480, display_json["dimensions"]["height"].asInt());
EXPECT_EQ("sender", display_json["scaling"].asString());
const Json::Value& constraints_json = answer_body["constraints"];
ASSERT_TRUE(constraints_json.isObject());
const Json::Value& audio = constraints_json["audio"];
ASSERT_TRUE(audio.isObject());
EXPECT_EQ(4, audio["maxBitRate"].asInt());
EXPECT_EQ(2, audio["maxChannels"].asInt());
EXPECT_EQ(0, audio["maxDelay"].asInt());
EXPECT_EQ(1, audio["maxSampleRate"].asInt());
EXPECT_EQ(3, audio["minBitRate"].asInt());
const Json::Value& video = constraints_json["video"];
ASSERT_TRUE(video.isObject());
EXPECT_EQ(90000000, video["maxBitRate"].asInt());
EXPECT_EQ(1000, video["maxDelay"].asInt());
EXPECT_EQ("144", video["maxDimensions"]["frameRate"].asString());
EXPECT_EQ(1920, video["maxDimensions"]["width"].asInt());
EXPECT_EQ(1080, video["maxDimensions"]["height"].asInt());
EXPECT_DOUBLE_EQ(3.14159, video["maxPixelsPerSecond"].asDouble());
EXPECT_EQ(3000, video["minBitRate"].asInt());
EXPECT_EQ("24", video["minDimensions"]["frameRate"].asString());
EXPECT_EQ(320, video["minDimensions"]["width"].asInt());
EXPECT_EQ(240, video["minDimensions"]["height"].asInt());
}
TEST_F(ReceiverSessionTest, HandlesNoValidAudioStream) {
auto message_port = std::make_unique<SimpleMessagePort>();
SimpleMessagePort* raw_port = message_port.get();
StrictMock<FakeClient> client;
ReceiverSession session(&client, std::move(env_), std::move(message_port),
ReceiverSession::Preferences{});
EXPECT_CALL(client, OnNegotiated(&session, _)).Times(1);
EXPECT_CALL(client, OnReceiversDestroyed(&session)).Times(1);
raw_port->ReceiveMessage(kNoAudioOfferMessage);
const auto& messages = raw_port->posted_messages();
EXPECT_EQ(1u, messages.size());
auto message_body = openscreen::json::Parse(messages[0]);
EXPECT_TRUE(message_body.is_value());
const Json::Value& answer_body = message_body.value()["answer"];
EXPECT_TRUE(answer_body.isObject());
// Should still select video stream.
EXPECT_EQ(1u, answer_body["sendIndexes"].size());
EXPECT_EQ(31338, answer_body["sendIndexes"][0].asInt());
EXPECT_EQ(1u, answer_body["ssrcs"].size());
EXPECT_EQ(19088746, answer_body["ssrcs"][0].asInt());
}
TEST_F(ReceiverSessionTest, HandlesNoValidVideoStream) {
auto message_port = std::make_unique<SimpleMessagePort>();
SimpleMessagePort* raw_port = message_port.get();
StrictMock<FakeClient> client;
ReceiverSession session(&client, std::move(env_), std::move(message_port),
ReceiverSession::Preferences{});
EXPECT_CALL(client, OnNegotiated(&session, _)).Times(1);
EXPECT_CALL(client, OnReceiversDestroyed(&session)).Times(1);
raw_port->ReceiveMessage(kNoVideoOfferMessage);
const auto& messages = raw_port->posted_messages();
EXPECT_EQ(1u, messages.size());
auto message_body = openscreen::json::Parse(messages[0]);
EXPECT_TRUE(message_body.is_value());
const Json::Value& answer_body = message_body.value()["answer"];
EXPECT_TRUE(answer_body.isObject());
// Should still select audio stream.
EXPECT_EQ(1u, answer_body["sendIndexes"].size());
EXPECT_EQ(1337, answer_body["sendIndexes"][0].asInt());
EXPECT_EQ(1u, answer_body["ssrcs"].size());
EXPECT_EQ(19088748, answer_body["ssrcs"][0].asInt());
}
TEST_F(ReceiverSessionTest, HandlesNoValidStreams) {
auto message_port = std::make_unique<SimpleMessagePort>();
SimpleMessagePort* raw_port = message_port.get();
StrictMock<FakeClient> client;
ReceiverSession session(&client, std::move(env_), std::move(message_port),
ReceiverSession::Preferences{});
// We shouldn't call OnNegotiated if we failed to negotiate any streams.
EXPECT_CALL(client, OnNegotiated(&session, _)).Times(0);
EXPECT_CALL(client, OnReceiversDestroyed(&session)).Times(0);
raw_port->ReceiveMessage(kNoAudioOrVideoOfferMessage);
const auto& messages = raw_port->posted_messages();
EXPECT_EQ(1u, messages.size());
auto message_body = openscreen::json::Parse(messages[0]);
ExpectIsErrorAnswerMessage(message_body);
}
TEST_F(ReceiverSessionTest, HandlesMalformedOffer) {
auto message_port = std::make_unique<SimpleMessagePort>();
SimpleMessagePort* raw_port = message_port.get();
StrictMock<FakeClient> client;
ReceiverSession session(&client, std::move(env_), std::move(message_port),
ReceiverSession::Preferences{});
// We shouldn't call OnNegotiated if we failed to negotiate any streams.
// Note that unlike when we simply don't select any streams, when the offer
// is actually completely invalid we call OnError.
EXPECT_CALL(client, OnNegotiated(&session, _)).Times(0);
EXPECT_CALL(client, OnReceiversDestroyed(&session)).Times(0);
EXPECT_CALL(client,
OnError(&session, openscreen::Error(
openscreen::Error::Code::kJsonParseError)))
.Times(1);
raw_port->ReceiveMessage(kInvalidJsonOfferMessage);
}
TEST_F(ReceiverSessionTest, NotifiesReceiverDestruction) {
auto message_port = std::make_unique<SimpleMessagePort>();
SimpleMessagePort* raw_port = message_port.get();
StrictMock<FakeClient> client;
ReceiverSession session(&client, std::move(env_), std::move(message_port),
ReceiverSession::Preferences{});
EXPECT_CALL(client, OnNegotiated(&session, _)).Times(2);
EXPECT_CALL(client, OnReceiversDestroyed(&session)).Times(2);
raw_port->ReceiveMessage(kNoAudioOfferMessage);
raw_port->ReceiveMessage(kValidOfferMessage);
}
} // namespace streaming
} // namespace cast