media/webrtc/signaling/test/mediapipeline_unittest.cpp

Wed, 31 Dec 2014 06:55:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:50 +0100
changeset 2
7e26c7da4463
permissions
-rw-r--r--

Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 // Original author: ekr@rtfm.com
michael@0 6
michael@0 7 #include <iostream>
michael@0 8
michael@0 9 #include "sigslot.h"
michael@0 10
michael@0 11 #include "logging.h"
michael@0 12 #include "nsThreadUtils.h"
michael@0 13 #include "nsXPCOM.h"
michael@0 14 #include "nss.h"
michael@0 15 #include "ssl.h"
michael@0 16 #include "sslproto.h"
michael@0 17
michael@0 18 #include "dtlsidentity.h"
michael@0 19 #include "mozilla/RefPtr.h"
michael@0 20 #include "FakeMediaStreams.h"
michael@0 21 #include "FakeMediaStreamsImpl.h"
michael@0 22 #include "MediaConduitErrors.h"
michael@0 23 #include "MediaConduitInterface.h"
michael@0 24 #include "MediaPipeline.h"
michael@0 25 #include "MediaPipelineFilter.h"
michael@0 26 #include "runnable_utils.h"
michael@0 27 #include "transportflow.h"
michael@0 28 #include "transportlayerloopback.h"
michael@0 29 #include "transportlayerdtls.h"
michael@0 30 #include "mozilla/SyncRunnable.h"
michael@0 31
michael@0 32
michael@0 33 #include "mtransport_test_utils.h"
michael@0 34 #include "runnable_utils.h"
michael@0 35
michael@0 36 #include "webrtc/modules/interface/module_common_types.h"
michael@0 37
michael@0 38 #define GTEST_HAS_RTTI 0
michael@0 39 #include "gtest/gtest.h"
michael@0 40 #include "gtest_utils.h"
michael@0 41
michael@0 42 using namespace mozilla;
michael@0 43 MOZ_MTLOG_MODULE("mediapipeline")
michael@0 44
michael@0 45 MtransportTestUtils *test_utils;
michael@0 46
michael@0 47 namespace {
michael@0 48
michael@0 49 class TransportInfo {
michael@0 50 public:
michael@0 51 TransportInfo() :
michael@0 52 flow_(nullptr),
michael@0 53 loopback_(nullptr),
michael@0 54 dtls_(nullptr) {}
michael@0 55
michael@0 56 static void InitAndConnect(TransportInfo &client, TransportInfo &server) {
michael@0 57 client.Init(true);
michael@0 58 server.Init(false);
michael@0 59 client.PushLayers();
michael@0 60 server.PushLayers();
michael@0 61 client.Connect(&server);
michael@0 62 server.Connect(&client);
michael@0 63 }
michael@0 64
michael@0 65 void Init(bool client) {
michael@0 66 nsresult res;
michael@0 67
michael@0 68 flow_ = new TransportFlow();
michael@0 69 loopback_ = new TransportLayerLoopback();
michael@0 70 dtls_ = new TransportLayerDtls();
michael@0 71
michael@0 72 res = loopback_->Init();
michael@0 73 if (res != NS_OK) {
michael@0 74 FreeLayers();
michael@0 75 }
michael@0 76 ASSERT_EQ((nsresult)NS_OK, res);
michael@0 77
michael@0 78 std::vector<uint16_t> ciphers;
michael@0 79 ciphers.push_back(SRTP_AES128_CM_HMAC_SHA1_80);
michael@0 80 dtls_->SetSrtpCiphers(ciphers);
michael@0 81 dtls_->SetIdentity(DtlsIdentity::Generate());
michael@0 82 dtls_->SetRole(client ? TransportLayerDtls::CLIENT :
michael@0 83 TransportLayerDtls::SERVER);
michael@0 84 dtls_->SetVerificationAllowAll();
michael@0 85 }
michael@0 86
michael@0 87 void PushLayers() {
michael@0 88 nsresult res;
michael@0 89
michael@0 90 nsAutoPtr<std::queue<TransportLayer *> > layers(
michael@0 91 new std::queue<TransportLayer *>);
michael@0 92 layers->push(loopback_);
michael@0 93 layers->push(dtls_);
michael@0 94 res = flow_->PushLayers(layers);
michael@0 95 if (res != NS_OK) {
michael@0 96 FreeLayers();
michael@0 97 }
michael@0 98 ASSERT_EQ((nsresult)NS_OK, res);
michael@0 99 }
michael@0 100
michael@0 101 void Connect(TransportInfo* peer) {
michael@0 102 MOZ_ASSERT(loopback_);
michael@0 103 MOZ_ASSERT(peer->loopback_);
michael@0 104
michael@0 105 loopback_->Connect(peer->loopback_);
michael@0 106 }
michael@0 107
michael@0 108 // Free the memory allocated at the beginning of Init
michael@0 109 // if failure occurs before layers setup.
michael@0 110 void FreeLayers() {
michael@0 111 delete loopback_;
michael@0 112 loopback_ = nullptr;
michael@0 113 delete dtls_;
michael@0 114 dtls_ = nullptr;
michael@0 115 }
michael@0 116
michael@0 117 void Shutdown() {
michael@0 118 if (loopback_) {
michael@0 119 loopback_->Disconnect();
michael@0 120 }
michael@0 121 loopback_ = nullptr;
michael@0 122 dtls_ = nullptr;
michael@0 123 flow_ = nullptr;
michael@0 124 }
michael@0 125
michael@0 126 mozilla::RefPtr<TransportFlow> flow_;
michael@0 127 TransportLayerLoopback *loopback_;
michael@0 128 TransportLayerDtls *dtls_;
michael@0 129 };
michael@0 130
michael@0 131 class TestAgent {
michael@0 132 public:
michael@0 133 TestAgent() :
michael@0 134 audio_config_(109, "opus", 48000, 960, 2, 64000),
michael@0 135 audio_conduit_(mozilla::AudioSessionConduit::Create(nullptr)),
michael@0 136 audio_(),
michael@0 137 audio_pipeline_() {
michael@0 138 }
michael@0 139
michael@0 140 static void ConnectRtp(TestAgent *client, TestAgent *server) {
michael@0 141 TransportInfo::InitAndConnect(client->audio_rtp_transport_,
michael@0 142 server->audio_rtp_transport_);
michael@0 143 }
michael@0 144
michael@0 145 static void ConnectRtcp(TestAgent *client, TestAgent *server) {
michael@0 146 TransportInfo::InitAndConnect(client->audio_rtcp_transport_,
michael@0 147 server->audio_rtcp_transport_);
michael@0 148 }
michael@0 149
michael@0 150 static void ConnectBundle(TestAgent *client, TestAgent *server) {
michael@0 151 TransportInfo::InitAndConnect(client->bundle_transport_,
michael@0 152 server->bundle_transport_);
michael@0 153 }
michael@0 154
michael@0 155 virtual void CreatePipelines_s(bool aIsRtcpMux) = 0;
michael@0 156
michael@0 157 void Start() {
michael@0 158 nsresult ret;
michael@0 159
michael@0 160 MOZ_MTLOG(ML_DEBUG, "Starting");
michael@0 161
michael@0 162 mozilla::SyncRunnable::DispatchToThread(
michael@0 163 test_utils->sts_target(),
michael@0 164 WrapRunnableRet(audio_->GetStream(), &Fake_MediaStream::Start, &ret));
michael@0 165
michael@0 166 ASSERT_TRUE(NS_SUCCEEDED(ret));
michael@0 167 }
michael@0 168
michael@0 169 void StopInt() {
michael@0 170 audio_->GetStream()->Stop();
michael@0 171 }
michael@0 172
michael@0 173 void Stop() {
michael@0 174 MOZ_MTLOG(ML_DEBUG, "Stopping");
michael@0 175
michael@0 176 if (audio_pipeline_)
michael@0 177 audio_pipeline_->ShutdownMedia_m();
michael@0 178
michael@0 179 mozilla::SyncRunnable::DispatchToThread(
michael@0 180 test_utils->sts_target(),
michael@0 181 WrapRunnable(this, &TestAgent::StopInt));
michael@0 182 }
michael@0 183
michael@0 184 void Shutdown_s() {
michael@0 185 audio_rtp_transport_.Shutdown();
michael@0 186 audio_rtcp_transport_.Shutdown();
michael@0 187 bundle_transport_.Shutdown();
michael@0 188 if (audio_pipeline_)
michael@0 189 audio_pipeline_->ShutdownTransport_s();
michael@0 190 }
michael@0 191
michael@0 192 void Shutdown() {
michael@0 193 if (audio_pipeline_)
michael@0 194 audio_pipeline_->ShutdownMedia_m();
michael@0 195
michael@0 196 mozilla::SyncRunnable::DispatchToThread(
michael@0 197 test_utils->sts_target(),
michael@0 198 WrapRunnable(this, &TestAgent::Shutdown_s));
michael@0 199 }
michael@0 200
michael@0 201 uint32_t GetRemoteSSRC() {
michael@0 202 uint32_t res = 0;
michael@0 203 audio_conduit_->GetRemoteSSRC(&res);
michael@0 204 return res;
michael@0 205 }
michael@0 206
michael@0 207 uint32_t GetLocalSSRC() {
michael@0 208 uint32_t res = 0;
michael@0 209 audio_conduit_->GetLocalSSRC(&res);
michael@0 210 return res;
michael@0 211 }
michael@0 212
michael@0 213 int GetAudioRtpCountSent() {
michael@0 214 return audio_pipeline_->rtp_packets_sent();
michael@0 215 }
michael@0 216
michael@0 217 int GetAudioRtpCountReceived() {
michael@0 218 return audio_pipeline_->rtp_packets_received();
michael@0 219 }
michael@0 220
michael@0 221 int GetAudioRtcpCountSent() {
michael@0 222 return audio_pipeline_->rtcp_packets_sent();
michael@0 223 }
michael@0 224
michael@0 225 int GetAudioRtcpCountReceived() {
michael@0 226 return audio_pipeline_->rtcp_packets_received();
michael@0 227 }
michael@0 228
michael@0 229 protected:
michael@0 230 mozilla::AudioCodecConfig audio_config_;
michael@0 231 mozilla::RefPtr<mozilla::MediaSessionConduit> audio_conduit_;
michael@0 232 nsRefPtr<DOMMediaStream> audio_;
michael@0 233 // TODO(bcampen@mozilla.com): Right now this does not let us test RTCP in
michael@0 234 // both directions; only the sender's RTCP is sent, but the receiver should
michael@0 235 // be sending it too.
michael@0 236 mozilla::RefPtr<mozilla::MediaPipeline> audio_pipeline_;
michael@0 237 TransportInfo audio_rtp_transport_;
michael@0 238 TransportInfo audio_rtcp_transport_;
michael@0 239 TransportInfo bundle_transport_;
michael@0 240 };
michael@0 241
michael@0 242 class TestAgentSend : public TestAgent {
michael@0 243 public:
michael@0 244 TestAgentSend() : use_bundle_(false) {}
michael@0 245
michael@0 246 virtual void CreatePipelines_s(bool aIsRtcpMux) {
michael@0 247 audio_ = new Fake_DOMMediaStream(new Fake_AudioStreamSource());
michael@0 248
michael@0 249 mozilla::MediaConduitErrorCode err =
michael@0 250 static_cast<mozilla::AudioSessionConduit *>(audio_conduit_.get())->
michael@0 251 ConfigureSendMediaCodec(&audio_config_);
michael@0 252 EXPECT_EQ(mozilla::kMediaConduitNoError, err);
michael@0 253
michael@0 254 std::string test_pc("PC");
michael@0 255
michael@0 256 if (aIsRtcpMux) {
michael@0 257 ASSERT_FALSE(audio_rtcp_transport_.flow_);
michael@0 258 }
michael@0 259
michael@0 260 RefPtr<TransportFlow> rtp(audio_rtp_transport_.flow_);
michael@0 261 RefPtr<TransportFlow> rtcp(audio_rtcp_transport_.flow_);
michael@0 262
michael@0 263 if (use_bundle_) {
michael@0 264 rtp = bundle_transport_.flow_;
michael@0 265 rtcp = nullptr;
michael@0 266 }
michael@0 267
michael@0 268 audio_pipeline_ = new mozilla::MediaPipelineTransmit(
michael@0 269 test_pc,
michael@0 270 nullptr,
michael@0 271 test_utils->sts_target(),
michael@0 272 audio_,
michael@0 273 1,
michael@0 274 1,
michael@0 275 audio_conduit_,
michael@0 276 rtp,
michael@0 277 rtcp);
michael@0 278
michael@0 279 audio_pipeline_->Init();
michael@0 280 }
michael@0 281
michael@0 282 void SetUsingBundle(bool use_bundle) {
michael@0 283 use_bundle_ = use_bundle;
michael@0 284 }
michael@0 285
michael@0 286 private:
michael@0 287 bool use_bundle_;
michael@0 288 };
michael@0 289
michael@0 290
michael@0 291 class TestAgentReceive : public TestAgent {
michael@0 292 public:
michael@0 293 virtual void CreatePipelines_s(bool aIsRtcpMux) {
michael@0 294 mozilla::SourceMediaStream *audio = new Fake_SourceMediaStream();
michael@0 295 audio->SetPullEnabled(true);
michael@0 296
michael@0 297 mozilla::AudioSegment* segment= new mozilla::AudioSegment();
michael@0 298 audio->AddTrack(0, 100, 0, segment);
michael@0 299 audio->AdvanceKnownTracksTime(mozilla::STREAM_TIME_MAX);
michael@0 300
michael@0 301 audio_ = new Fake_DOMMediaStream(audio);
michael@0 302
michael@0 303 std::vector<mozilla::AudioCodecConfig *> codecs;
michael@0 304 codecs.push_back(&audio_config_);
michael@0 305
michael@0 306 mozilla::MediaConduitErrorCode err =
michael@0 307 static_cast<mozilla::AudioSessionConduit *>(audio_conduit_.get())->
michael@0 308 ConfigureRecvMediaCodecs(codecs);
michael@0 309 EXPECT_EQ(mozilla::kMediaConduitNoError, err);
michael@0 310
michael@0 311 std::string test_pc("PC");
michael@0 312
michael@0 313 if (aIsRtcpMux) {
michael@0 314 ASSERT_FALSE(audio_rtcp_transport_.flow_);
michael@0 315 }
michael@0 316
michael@0 317 // For now, assume bundle always uses rtcp mux
michael@0 318 RefPtr<TransportFlow> dummy;
michael@0 319 RefPtr<TransportFlow> bundle_transport;
michael@0 320 if (bundle_filter_) {
michael@0 321 bundle_transport = bundle_transport_.flow_;
michael@0 322 bundle_filter_->AddLocalSSRC(GetLocalSSRC());
michael@0 323 }
michael@0 324
michael@0 325 audio_pipeline_ = new mozilla::MediaPipelineReceiveAudio(
michael@0 326 test_pc,
michael@0 327 nullptr,
michael@0 328 test_utils->sts_target(),
michael@0 329 audio_->GetStream(), 1, 1,
michael@0 330 static_cast<mozilla::AudioSessionConduit *>(audio_conduit_.get()),
michael@0 331 audio_rtp_transport_.flow_,
michael@0 332 audio_rtcp_transport_.flow_,
michael@0 333 bundle_transport,
michael@0 334 dummy,
michael@0 335 bundle_filter_);
michael@0 336
michael@0 337 audio_pipeline_->Init();
michael@0 338 }
michael@0 339
michael@0 340 void SetBundleFilter(nsAutoPtr<MediaPipelineFilter> filter) {
michael@0 341 bundle_filter_ = filter;
michael@0 342 }
michael@0 343
michael@0 344 void SetUsingBundle_s(bool decision) {
michael@0 345 audio_pipeline_->SetUsingBundle_s(decision);
michael@0 346 }
michael@0 347
michael@0 348 void UpdateFilterFromRemoteDescription_s(
michael@0 349 nsAutoPtr<MediaPipelineFilter> filter) {
michael@0 350 audio_pipeline_->UpdateFilterFromRemoteDescription_s(filter);
michael@0 351 }
michael@0 352
michael@0 353 private:
michael@0 354 nsAutoPtr<MediaPipelineFilter> bundle_filter_;
michael@0 355 };
michael@0 356
michael@0 357
michael@0 358 class MediaPipelineTest : public ::testing::Test {
michael@0 359 public:
michael@0 360 ~MediaPipelineTest() {
michael@0 361 p1_.Stop();
michael@0 362 p2_.Stop();
michael@0 363 p1_.Shutdown();
michael@0 364 p2_.Shutdown();
michael@0 365 }
michael@0 366
michael@0 367 // Setup transport.
michael@0 368 void InitTransports(bool aIsRtcpMux) {
michael@0 369 // RTP, p1_ is server, p2_ is client
michael@0 370 mozilla::SyncRunnable::DispatchToThread(
michael@0 371 test_utils->sts_target(),
michael@0 372 WrapRunnableNM(&TestAgent::ConnectRtp, &p2_, &p1_));
michael@0 373
michael@0 374 // Create RTCP flows separately if we are not muxing them.
michael@0 375 if(!aIsRtcpMux) {
michael@0 376 // RTCP, p1_ is server, p2_ is client
michael@0 377 mozilla::SyncRunnable::DispatchToThread(
michael@0 378 test_utils->sts_target(),
michael@0 379 WrapRunnableNM(&TestAgent::ConnectRtcp, &p2_, &p1_));
michael@0 380 }
michael@0 381
michael@0 382 // BUNDLE, p1_ is server, p2_ is client
michael@0 383 mozilla::SyncRunnable::DispatchToThread(
michael@0 384 test_utils->sts_target(),
michael@0 385 WrapRunnableNM(&TestAgent::ConnectBundle, &p2_, &p1_));
michael@0 386 }
michael@0 387
michael@0 388 // Verify RTP and RTCP
michael@0 389 void TestAudioSend(bool aIsRtcpMux,
michael@0 390 bool bundle = false,
michael@0 391 nsAutoPtr<MediaPipelineFilter> localFilter =
michael@0 392 nsAutoPtr<MediaPipelineFilter>(nullptr),
michael@0 393 nsAutoPtr<MediaPipelineFilter> remoteFilter =
michael@0 394 nsAutoPtr<MediaPipelineFilter>(nullptr),
michael@0 395 unsigned int ms_until_answer = 500,
michael@0 396 unsigned int ms_of_traffic_after_answer = 10000) {
michael@0 397
michael@0 398 // We do not support testing bundle without rtcp mux, since that doesn't
michael@0 399 // make any sense.
michael@0 400 ASSERT_FALSE(!aIsRtcpMux && bundle);
michael@0 401
michael@0 402 p1_.SetUsingBundle(bundle);
michael@0 403 p2_.SetBundleFilter(localFilter);
michael@0 404
michael@0 405 // Setup transport flows
michael@0 406 InitTransports(aIsRtcpMux);
michael@0 407
michael@0 408 mozilla::SyncRunnable::DispatchToThread(
michael@0 409 test_utils->sts_target(),
michael@0 410 WrapRunnable(&p1_, &TestAgent::CreatePipelines_s, aIsRtcpMux));
michael@0 411
michael@0 412 mozilla::SyncRunnable::DispatchToThread(
michael@0 413 test_utils->sts_target(),
michael@0 414 WrapRunnable(&p2_, &TestAgent::CreatePipelines_s, aIsRtcpMux));
michael@0 415
michael@0 416 p2_.Start();
michael@0 417 p1_.Start();
michael@0 418
michael@0 419 // Simulate pre-answer traffic
michael@0 420 PR_Sleep(ms_until_answer);
michael@0 421
michael@0 422 mozilla::SyncRunnable::DispatchToThread(
michael@0 423 test_utils->sts_target(),
michael@0 424 WrapRunnable(&p2_, &TestAgentReceive::SetUsingBundle_s, bundle));
michael@0 425
michael@0 426 if (bundle) {
michael@0 427 // Leaving remoteFilter not set implies we want to test sunny-day
michael@0 428 if (!remoteFilter) {
michael@0 429 remoteFilter = new MediaPipelineFilter;
michael@0 430 // Might not be safe, strictly speaking.
michael@0 431 remoteFilter->AddRemoteSSRC(p1_.GetLocalSSRC());
michael@0 432 }
michael@0 433
michael@0 434 mozilla::SyncRunnable::DispatchToThread(
michael@0 435 test_utils->sts_target(),
michael@0 436 WrapRunnable(&p2_,
michael@0 437 &TestAgentReceive::UpdateFilterFromRemoteDescription_s,
michael@0 438 remoteFilter));
michael@0 439 }
michael@0 440
michael@0 441
michael@0 442 // wait for some RTP/RTCP tx and rx to happen
michael@0 443 PR_Sleep(ms_of_traffic_after_answer);
michael@0 444
michael@0 445 p1_.Stop();
michael@0 446 p2_.Stop();
michael@0 447
michael@0 448 // wait for any packets in flight to arrive
michael@0 449 PR_Sleep(100);
michael@0 450
michael@0 451 p1_.Shutdown();
michael@0 452 p2_.Shutdown();
michael@0 453
michael@0 454 if (!bundle) {
michael@0 455 // If we are doing bundle, allow the test-case to do this checking.
michael@0 456 ASSERT_GE(p1_.GetAudioRtpCountSent(), 40);
michael@0 457 ASSERT_EQ(p1_.GetAudioRtpCountReceived(), p2_.GetAudioRtpCountSent());
michael@0 458 ASSERT_EQ(p1_.GetAudioRtpCountSent(), p2_.GetAudioRtpCountReceived());
michael@0 459
michael@0 460 // Calling ShutdownMedia_m on both pipelines does not stop the flow of
michael@0 461 // RTCP. So, we might be off by one here.
michael@0 462 ASSERT_LE(p2_.GetAudioRtcpCountReceived(), p1_.GetAudioRtcpCountSent());
michael@0 463 ASSERT_GE(p2_.GetAudioRtcpCountReceived() + 1, p1_.GetAudioRtcpCountSent());
michael@0 464 }
michael@0 465
michael@0 466 }
michael@0 467
michael@0 468 void TestAudioReceiverOffersBundle(bool bundle_accepted,
michael@0 469 nsAutoPtr<MediaPipelineFilter> localFilter,
michael@0 470 nsAutoPtr<MediaPipelineFilter> remoteFilter =
michael@0 471 nsAutoPtr<MediaPipelineFilter>(nullptr),
michael@0 472 unsigned int ms_until_answer = 500,
michael@0 473 unsigned int ms_of_traffic_after_answer = 10000) {
michael@0 474 TestAudioSend(true,
michael@0 475 bundle_accepted,
michael@0 476 localFilter,
michael@0 477 remoteFilter,
michael@0 478 ms_until_answer,
michael@0 479 ms_of_traffic_after_answer);
michael@0 480 }
michael@0 481 protected:
michael@0 482 TestAgentSend p1_;
michael@0 483 TestAgentReceive p2_;
michael@0 484 };
michael@0 485
michael@0 486 class MediaPipelineFilterTest : public ::testing::Test {
michael@0 487 public:
michael@0 488 bool Filter(MediaPipelineFilter& filter,
michael@0 489 int32_t correlator,
michael@0 490 uint32_t ssrc,
michael@0 491 uint8_t payload_type) {
michael@0 492
michael@0 493 webrtc::RTPHeader header;
michael@0 494 header.ssrc = ssrc;
michael@0 495 header.payloadType = payload_type;
michael@0 496 return filter.Filter(header, correlator);
michael@0 497 }
michael@0 498 };
michael@0 499
michael@0 500 TEST_F(MediaPipelineFilterTest, TestConstruct) {
michael@0 501 MediaPipelineFilter filter;
michael@0 502 }
michael@0 503
michael@0 504 TEST_F(MediaPipelineFilterTest, TestDefault) {
michael@0 505 MediaPipelineFilter filter;
michael@0 506 ASSERT_FALSE(Filter(filter, 0, 233, 110));
michael@0 507 }
michael@0 508
michael@0 509 TEST_F(MediaPipelineFilterTest, TestSSRCFilter) {
michael@0 510 MediaPipelineFilter filter;
michael@0 511 filter.AddRemoteSSRC(555);
michael@0 512 ASSERT_TRUE(Filter(filter, 0, 555, 110));
michael@0 513 ASSERT_FALSE(Filter(filter, 0, 556, 110));
michael@0 514 }
michael@0 515
michael@0 516 #define SSRC(ssrc) \
michael@0 517 ((ssrc >> 24) & 0xFF), \
michael@0 518 ((ssrc >> 16) & 0xFF), \
michael@0 519 ((ssrc >> 8 ) & 0xFF), \
michael@0 520 (ssrc & 0xFF)
michael@0 521
michael@0 522 #define REPORT_FRAGMENT(ssrc) \
michael@0 523 SSRC(ssrc), \
michael@0 524 0,0,0,0, \
michael@0 525 0,0,0,0, \
michael@0 526 0,0,0,0, \
michael@0 527 0,0,0,0, \
michael@0 528 0,0,0,0
michael@0 529
michael@0 530 #define RTCP_TYPEINFO(num_rrs, type, size) \
michael@0 531 0x80 + num_rrs, type, 0, size
michael@0 532
michael@0 533 const unsigned char rtcp_rr_s16[] = {
michael@0 534 // zero rrs, size 1 words
michael@0 535 RTCP_TYPEINFO(0, MediaPipelineFilter::RECEIVER_REPORT_T, 1),
michael@0 536 SSRC(16)
michael@0 537 };
michael@0 538
michael@0 539 const unsigned char rtcp_rr_s16_r17[] = {
michael@0 540 // one rr, 7 words
michael@0 541 RTCP_TYPEINFO(1, MediaPipelineFilter::RECEIVER_REPORT_T, 7),
michael@0 542 SSRC(16),
michael@0 543 REPORT_FRAGMENT(17)
michael@0 544 };
michael@0 545
michael@0 546 const unsigned char rtcp_rr_s16_r17_18[] = {
michael@0 547 // two rrs, size 13 words
michael@0 548 RTCP_TYPEINFO(2, MediaPipelineFilter::RECEIVER_REPORT_T, 13),
michael@0 549 SSRC(16),
michael@0 550 REPORT_FRAGMENT(17),
michael@0 551 REPORT_FRAGMENT(18)
michael@0 552 };
michael@0 553
michael@0 554 const unsigned char rtcp_sr_s16[] = {
michael@0 555 // zero rrs, size 6 words
michael@0 556 RTCP_TYPEINFO(0, MediaPipelineFilter::SENDER_REPORT_T, 6),
michael@0 557 REPORT_FRAGMENT(16)
michael@0 558 };
michael@0 559
michael@0 560 const unsigned char rtcp_sr_s16_r17[] = {
michael@0 561 // one rr, size 12 words
michael@0 562 RTCP_TYPEINFO(1, MediaPipelineFilter::SENDER_REPORT_T, 12),
michael@0 563 REPORT_FRAGMENT(16),
michael@0 564 REPORT_FRAGMENT(17)
michael@0 565 };
michael@0 566
michael@0 567 const unsigned char rtcp_sr_s16_r17_18[] = {
michael@0 568 // two rrs, size 18 words
michael@0 569 RTCP_TYPEINFO(2, MediaPipelineFilter::SENDER_REPORT_T, 18),
michael@0 570 REPORT_FRAGMENT(16),
michael@0 571 REPORT_FRAGMENT(17),
michael@0 572 REPORT_FRAGMENT(18)
michael@0 573 };
michael@0 574
michael@0 575 const unsigned char unknown_type[] = {
michael@0 576 RTCP_TYPEINFO(1, 222, 0)
michael@0 577 };
michael@0 578
michael@0 579 TEST_F(MediaPipelineFilterTest, TestEmptyFilterReport0) {
michael@0 580 MediaPipelineFilter filter;
michael@0 581 ASSERT_EQ(MediaPipelineFilter::FAIL,
michael@0 582 filter.FilterRTCP(rtcp_sr_s16, sizeof(rtcp_sr_s16)));
michael@0 583 ASSERT_EQ(MediaPipelineFilter::FAIL,
michael@0 584 filter.FilterRTCP(rtcp_rr_s16, sizeof(rtcp_rr_s16)));
michael@0 585 }
michael@0 586
michael@0 587 TEST_F(MediaPipelineFilterTest, TestFilterReport0) {
michael@0 588 MediaPipelineFilter filter;
michael@0 589 filter.AddRemoteSSRC(16);
michael@0 590 ASSERT_EQ(MediaPipelineFilter::PASS,
michael@0 591 filter.FilterRTCP(rtcp_sr_s16, sizeof(rtcp_sr_s16)));
michael@0 592 ASSERT_EQ(MediaPipelineFilter::PASS,
michael@0 593 filter.FilterRTCP(rtcp_rr_s16, sizeof(rtcp_rr_s16)));
michael@0 594 }
michael@0 595
michael@0 596 TEST_F(MediaPipelineFilterTest, TestFilterReport0SSRCTruncated) {
michael@0 597 MediaPipelineFilter filter;
michael@0 598 filter.AddRemoteSSRC(16);
michael@0 599 const unsigned char data[] = {
michael@0 600 RTCP_TYPEINFO(0, MediaPipelineFilter::RECEIVER_REPORT_T, 1),
michael@0 601 0,0,0
michael@0 602 };
michael@0 603 ASSERT_EQ(MediaPipelineFilter::FAIL,
michael@0 604 filter.FilterRTCP(data, sizeof(data)));
michael@0 605 }
michael@0 606
michael@0 607 TEST_F(MediaPipelineFilterTest, TestFilterReport0PTTruncated) {
michael@0 608 MediaPipelineFilter filter;
michael@0 609 filter.AddRemoteSSRC(16);
michael@0 610 const unsigned char data[] = {0x80};
michael@0 611 ASSERT_EQ(MediaPipelineFilter::FAIL,
michael@0 612 filter.FilterRTCP(data, sizeof(data)));
michael@0 613 }
michael@0 614
michael@0 615 TEST_F(MediaPipelineFilterTest, TestFilterReport0CountTruncated) {
michael@0 616 MediaPipelineFilter filter;
michael@0 617 filter.AddRemoteSSRC(16);
michael@0 618 const unsigned char data[] = {};
michael@0 619 ASSERT_EQ(MediaPipelineFilter::FAIL,
michael@0 620 filter.FilterRTCP(data, sizeof(data)));
michael@0 621 }
michael@0 622
michael@0 623 TEST_F(MediaPipelineFilterTest, TestFilterReport1BothMatch) {
michael@0 624 MediaPipelineFilter filter;
michael@0 625 filter.AddRemoteSSRC(16);
michael@0 626 filter.AddLocalSSRC(17);
michael@0 627 ASSERT_EQ(MediaPipelineFilter::PASS,
michael@0 628 filter.FilterRTCP(rtcp_sr_s16_r17, sizeof(rtcp_sr_s16_r17)));
michael@0 629 ASSERT_EQ(MediaPipelineFilter::PASS,
michael@0 630 filter.FilterRTCP(rtcp_rr_s16_r17, sizeof(rtcp_rr_s16_r17)));
michael@0 631 }
michael@0 632
michael@0 633 TEST_F(MediaPipelineFilterTest, TestFilterReport1SSRCTruncated) {
michael@0 634 MediaPipelineFilter filter;
michael@0 635 filter.AddRemoteSSRC(16);
michael@0 636 filter.AddLocalSSRC(17);
michael@0 637 const unsigned char rr[] = {
michael@0 638 RTCP_TYPEINFO(1, MediaPipelineFilter::RECEIVER_REPORT_T, 7),
michael@0 639 SSRC(16),
michael@0 640 0,0,0
michael@0 641 };
michael@0 642 ASSERT_EQ(MediaPipelineFilter::FAIL,
michael@0 643 filter.FilterRTCP(rr, sizeof(rr)));
michael@0 644 const unsigned char sr[] = {
michael@0 645 RTCP_TYPEINFO(1, MediaPipelineFilter::RECEIVER_REPORT_T, 12),
michael@0 646 REPORT_FRAGMENT(16),
michael@0 647 0,0,0
michael@0 648 };
michael@0 649 ASSERT_EQ(MediaPipelineFilter::FAIL,
michael@0 650 filter.FilterRTCP(sr, sizeof(rr)));
michael@0 651 }
michael@0 652
michael@0 653 TEST_F(MediaPipelineFilterTest, TestFilterReport1BigSSRC) {
michael@0 654 MediaPipelineFilter filter;
michael@0 655 filter.AddRemoteSSRC(0x01020304);
michael@0 656 filter.AddLocalSSRC(0x11121314);
michael@0 657 const unsigned char rr[] = {
michael@0 658 RTCP_TYPEINFO(1, MediaPipelineFilter::RECEIVER_REPORT_T, 7),
michael@0 659 SSRC(0x01020304),
michael@0 660 REPORT_FRAGMENT(0x11121314)
michael@0 661 };
michael@0 662 ASSERT_EQ(MediaPipelineFilter::PASS,
michael@0 663 filter.FilterRTCP(rr, sizeof(rr)));
michael@0 664 const unsigned char sr[] = {
michael@0 665 RTCP_TYPEINFO(1, MediaPipelineFilter::RECEIVER_REPORT_T, 12),
michael@0 666 SSRC(0x01020304),
michael@0 667 REPORT_FRAGMENT(0x11121314)
michael@0 668 };
michael@0 669 ASSERT_EQ(MediaPipelineFilter::PASS,
michael@0 670 filter.FilterRTCP(sr, sizeof(rr)));
michael@0 671 }
michael@0 672
michael@0 673 TEST_F(MediaPipelineFilterTest, TestFilterReport1LocalMatch) {
michael@0 674 MediaPipelineFilter filter;
michael@0 675 filter.AddLocalSSRC(17);
michael@0 676 ASSERT_EQ(MediaPipelineFilter::PASS,
michael@0 677 filter.FilterRTCP(rtcp_sr_s16_r17, sizeof(rtcp_sr_s16_r17)));
michael@0 678 ASSERT_EQ(MediaPipelineFilter::PASS,
michael@0 679 filter.FilterRTCP(rtcp_rr_s16_r17, sizeof(rtcp_rr_s16_r17)));
michael@0 680 }
michael@0 681
michael@0 682 TEST_F(MediaPipelineFilterTest, TestFilterReport1Inconsistent) {
michael@0 683 MediaPipelineFilter filter;
michael@0 684 filter.AddRemoteSSRC(16);
michael@0 685 // We assume that the filter is exactly correct in terms of local ssrcs.
michael@0 686 // So, when RTCP shows up with a remote SSRC that matches, and a local
michael@0 687 // ssrc that doesn't, we assume the other end has messed up and put ssrcs
michael@0 688 // from more than one m-line in the packet.
michael@0 689 ASSERT_EQ(MediaPipelineFilter::FAIL,
michael@0 690 filter.FilterRTCP(rtcp_sr_s16_r17, sizeof(rtcp_sr_s16_r17)));
michael@0 691 ASSERT_EQ(MediaPipelineFilter::FAIL,
michael@0 692 filter.FilterRTCP(rtcp_rr_s16_r17, sizeof(rtcp_rr_s16_r17)));
michael@0 693 }
michael@0 694
michael@0 695 TEST_F(MediaPipelineFilterTest, TestFilterReport1NeitherMatch) {
michael@0 696 MediaPipelineFilter filter;
michael@0 697 filter.AddRemoteSSRC(17);
michael@0 698 filter.AddLocalSSRC(18);
michael@0 699 ASSERT_EQ(MediaPipelineFilter::PASS,
michael@0 700 filter.FilterRTCP(rtcp_sr_s16_r17, sizeof(rtcp_sr_s16_r17)));
michael@0 701 ASSERT_EQ(MediaPipelineFilter::PASS,
michael@0 702 filter.FilterRTCP(rtcp_rr_s16_r17, sizeof(rtcp_rr_s16_r17)));
michael@0 703 }
michael@0 704
michael@0 705 TEST_F(MediaPipelineFilterTest, TestFilterReport2AllMatch) {
michael@0 706 MediaPipelineFilter filter;
michael@0 707 filter.AddRemoteSSRC(16);
michael@0 708 filter.AddLocalSSRC(17);
michael@0 709 filter.AddLocalSSRC(18);
michael@0 710 ASSERT_EQ(MediaPipelineFilter::PASS,
michael@0 711 filter.FilterRTCP(rtcp_sr_s16_r17_18,
michael@0 712 sizeof(rtcp_sr_s16_r17_18)));
michael@0 713 }
michael@0 714
michael@0 715 TEST_F(MediaPipelineFilterTest, TestFilterReport2LocalMatch) {
michael@0 716 MediaPipelineFilter filter;
michael@0 717 filter.AddLocalSSRC(17);
michael@0 718 filter.AddLocalSSRC(18);
michael@0 719 ASSERT_EQ(MediaPipelineFilter::PASS,
michael@0 720 filter.FilterRTCP(rtcp_sr_s16_r17_18,
michael@0 721 sizeof(rtcp_sr_s16_r17_18)));
michael@0 722 ASSERT_EQ(MediaPipelineFilter::PASS,
michael@0 723 filter.FilterRTCP(rtcp_rr_s16_r17_18,
michael@0 724 sizeof(rtcp_rr_s16_r17_18)));
michael@0 725 }
michael@0 726
michael@0 727 TEST_F(MediaPipelineFilterTest, TestFilterReport2Inconsistent101) {
michael@0 728 MediaPipelineFilter filter;
michael@0 729 filter.AddRemoteSSRC(16);
michael@0 730 filter.AddLocalSSRC(18);
michael@0 731 ASSERT_EQ(MediaPipelineFilter::FAIL,
michael@0 732 filter.FilterRTCP(rtcp_sr_s16_r17_18,
michael@0 733 sizeof(rtcp_sr_s16_r17_18)));
michael@0 734 ASSERT_EQ(MediaPipelineFilter::FAIL,
michael@0 735 filter.FilterRTCP(rtcp_rr_s16_r17_18,
michael@0 736 sizeof(rtcp_rr_s16_r17_18)));
michael@0 737 }
michael@0 738
michael@0 739 TEST_F(MediaPipelineFilterTest, TestFilterReport2Inconsistent001) {
michael@0 740 MediaPipelineFilter filter;
michael@0 741 filter.AddLocalSSRC(18);
michael@0 742 ASSERT_EQ(MediaPipelineFilter::FAIL,
michael@0 743 filter.FilterRTCP(rtcp_sr_s16_r17_18,
michael@0 744 sizeof(rtcp_sr_s16_r17_18)));
michael@0 745 ASSERT_EQ(MediaPipelineFilter::FAIL,
michael@0 746 filter.FilterRTCP(rtcp_rr_s16_r17_18,
michael@0 747 sizeof(rtcp_rr_s16_r17_18)));
michael@0 748 }
michael@0 749
michael@0 750 TEST_F(MediaPipelineFilterTest, TestFilterUnknownRTCPType) {
michael@0 751 MediaPipelineFilter filter;
michael@0 752 filter.AddLocalSSRC(18);
michael@0 753 ASSERT_EQ(MediaPipelineFilter::UNSUPPORTED,
michael@0 754 filter.FilterRTCP(unknown_type, sizeof(unknown_type)));
michael@0 755 }
michael@0 756
michael@0 757 TEST_F(MediaPipelineFilterTest, TestCorrelatorFilter) {
michael@0 758 MediaPipelineFilter filter;
michael@0 759 filter.SetCorrelator(7777);
michael@0 760 ASSERT_TRUE(Filter(filter, 7777, 16, 110));
michael@0 761 ASSERT_FALSE(Filter(filter, 7778, 17, 110));
michael@0 762 // This should also have resulted in the SSRC 16 being added to the filter
michael@0 763 ASSERT_TRUE(Filter(filter, 0, 16, 110));
michael@0 764 ASSERT_FALSE(Filter(filter, 0, 17, 110));
michael@0 765
michael@0 766 // rtcp_sr_s16 has 16 as an SSRC
michael@0 767 ASSERT_EQ(MediaPipelineFilter::PASS,
michael@0 768 filter.FilterRTCP(rtcp_sr_s16, sizeof(rtcp_sr_s16)));
michael@0 769 ASSERT_EQ(MediaPipelineFilter::PASS,
michael@0 770 filter.FilterRTCP(rtcp_rr_s16, sizeof(rtcp_rr_s16)));
michael@0 771 }
michael@0 772
michael@0 773 TEST_F(MediaPipelineFilterTest, TestPayloadTypeFilter) {
michael@0 774 MediaPipelineFilter filter;
michael@0 775 filter.AddUniquePT(110);
michael@0 776 ASSERT_TRUE(Filter(filter, 0, 555, 110));
michael@0 777 ASSERT_FALSE(Filter(filter, 0, 556, 111));
michael@0 778 }
michael@0 779
michael@0 780 TEST_F(MediaPipelineFilterTest, TestPayloadTypeFilterSSRCUpdate) {
michael@0 781 MediaPipelineFilter filter;
michael@0 782 filter.AddUniquePT(110);
michael@0 783 ASSERT_TRUE(Filter(filter, 0, 16, 110));
michael@0 784
michael@0 785 // rtcp_sr_s16 has 16 as an SSRC
michael@0 786 ASSERT_EQ(MediaPipelineFilter::PASS,
michael@0 787 filter.FilterRTCP(rtcp_sr_s16, sizeof(rtcp_sr_s16)));
michael@0 788 }
michael@0 789
michael@0 790 TEST_F(MediaPipelineFilterTest, TestAnswerAddsSSRCs) {
michael@0 791 MediaPipelineFilter filter;
michael@0 792 filter.SetCorrelator(7777);
michael@0 793 ASSERT_TRUE(Filter(filter, 7777, 555, 110));
michael@0 794 ASSERT_FALSE(Filter(filter, 7778, 556, 110));
michael@0 795 // This should also have resulted in the SSRC 555 being added to the filter
michael@0 796 ASSERT_TRUE(Filter(filter, 0, 555, 110));
michael@0 797 ASSERT_FALSE(Filter(filter, 0, 556, 110));
michael@0 798
michael@0 799 // This sort of thing can happen when getting an answer with SSRC attrs
michael@0 800 // The answer will not contain the correlator.
michael@0 801 MediaPipelineFilter filter2;
michael@0 802 filter2.AddRemoteSSRC(555);
michael@0 803 filter2.AddRemoteSSRC(556);
michael@0 804 filter2.AddRemoteSSRC(557);
michael@0 805
michael@0 806 filter.IncorporateRemoteDescription(filter2);
michael@0 807
michael@0 808 // Ensure that the old SSRC still works.
michael@0 809 ASSERT_TRUE(Filter(filter, 0, 555, 110));
michael@0 810
michael@0 811 // Ensure that the new SSRCs work.
michael@0 812 ASSERT_TRUE(Filter(filter, 0, 556, 110));
michael@0 813 ASSERT_TRUE(Filter(filter, 0, 557, 110));
michael@0 814
michael@0 815 // Ensure that the correlator continues to work
michael@0 816 ASSERT_TRUE(Filter(filter, 7777, 558, 110));
michael@0 817 }
michael@0 818
michael@0 819 TEST_F(MediaPipelineFilterTest, TestSSRCMovedWithSDP) {
michael@0 820 MediaPipelineFilter filter;
michael@0 821 filter.SetCorrelator(7777);
michael@0 822 filter.AddUniquePT(111);
michael@0 823 ASSERT_TRUE(Filter(filter, 7777, 555, 110));
michael@0 824
michael@0 825 MediaPipelineFilter filter2;
michael@0 826 filter2.AddRemoteSSRC(556);
michael@0 827
michael@0 828 filter.IncorporateRemoteDescription(filter2);
michael@0 829
michael@0 830 // Ensure that the old SSRC has been removed.
michael@0 831 ASSERT_FALSE(Filter(filter, 0, 555, 110));
michael@0 832
michael@0 833 // Ensure that the new SSRC works.
michael@0 834 ASSERT_TRUE(Filter(filter, 0, 556, 110));
michael@0 835
michael@0 836 // Ensure that the correlator continues to work
michael@0 837 ASSERT_TRUE(Filter(filter, 7777, 558, 110));
michael@0 838
michael@0 839 // Ensure that the payload type mapping continues to work
michael@0 840 ASSERT_TRUE(Filter(filter, 0, 559, 111));
michael@0 841 }
michael@0 842
michael@0 843 TEST_F(MediaPipelineFilterTest, TestSSRCMovedWithCorrelator) {
michael@0 844 MediaPipelineFilter filter;
michael@0 845 filter.SetCorrelator(7777);
michael@0 846 ASSERT_TRUE(Filter(filter, 7777, 555, 110));
michael@0 847 ASSERT_TRUE(Filter(filter, 0, 555, 110));
michael@0 848 ASSERT_FALSE(Filter(filter, 7778, 555, 110));
michael@0 849 ASSERT_FALSE(Filter(filter, 0, 555, 110));
michael@0 850 }
michael@0 851
michael@0 852 TEST_F(MediaPipelineFilterTest, TestRemoteSDPNoSSRCs) {
michael@0 853 // If the remote SDP doesn't have SSRCs, right now this is a no-op and
michael@0 854 // there is no point of even incorporating a filter, but we make the
michael@0 855 // behavior consistent to avoid confusion.
michael@0 856 MediaPipelineFilter filter;
michael@0 857 filter.SetCorrelator(7777);
michael@0 858 filter.AddUniquePT(111);
michael@0 859 ASSERT_TRUE(Filter(filter, 7777, 555, 110));
michael@0 860
michael@0 861 MediaPipelineFilter filter2;
michael@0 862
michael@0 863 filter.IncorporateRemoteDescription(filter2);
michael@0 864
michael@0 865 // Ensure that the old SSRC still works.
michael@0 866 ASSERT_TRUE(Filter(filter, 7777, 555, 110));
michael@0 867 }
michael@0 868
michael@0 869 TEST_F(MediaPipelineTest, TestAudioSendNoMux) {
michael@0 870 TestAudioSend(false);
michael@0 871 }
michael@0 872
michael@0 873 TEST_F(MediaPipelineTest, TestAudioSendMux) {
michael@0 874 TestAudioSend(true);
michael@0 875 }
michael@0 876
michael@0 877 TEST_F(MediaPipelineTest, TestAudioSendBundleOfferedAndDeclined) {
michael@0 878 nsAutoPtr<MediaPipelineFilter> filter(new MediaPipelineFilter);
michael@0 879 TestAudioReceiverOffersBundle(false, filter);
michael@0 880 }
michael@0 881
michael@0 882 TEST_F(MediaPipelineTest, TestAudioSendBundleOfferedAndAccepted) {
michael@0 883 nsAutoPtr<MediaPipelineFilter> filter(new MediaPipelineFilter);
michael@0 884 // These durations have to be _extremely_ long to have any assurance that
michael@0 885 // some RTCP will be sent at all. This is because the first RTCP packet
michael@0 886 // is sometimes sent before the transports are ready, which causes it to
michael@0 887 // be dropped.
michael@0 888 TestAudioReceiverOffersBundle(true,
michael@0 889 filter,
michael@0 890 // We do not specify the filter for the remote description, so it will be
michael@0 891 // set to something sane after a short time.
michael@0 892 nsAutoPtr<MediaPipelineFilter>(),
michael@0 893 10000,
michael@0 894 10000);
michael@0 895
michael@0 896 // Some packets should have been dropped, but not all
michael@0 897 ASSERT_GT(p1_.GetAudioRtpCountSent(), p2_.GetAudioRtpCountReceived());
michael@0 898 ASSERT_GT(p2_.GetAudioRtpCountReceived(), 40);
michael@0 899 ASSERT_GT(p1_.GetAudioRtcpCountSent(), 1);
michael@0 900 ASSERT_GT(p1_.GetAudioRtcpCountSent(), p2_.GetAudioRtcpCountReceived());
michael@0 901 ASSERT_GT(p2_.GetAudioRtcpCountReceived(), 0);
michael@0 902 }
michael@0 903
michael@0 904 TEST_F(MediaPipelineTest, TestAudioSendBundleOfferedAndAcceptedEmptyFilter) {
michael@0 905 nsAutoPtr<MediaPipelineFilter> filter(new MediaPipelineFilter);
michael@0 906 nsAutoPtr<MediaPipelineFilter> bad_answer_filter(new MediaPipelineFilter);
michael@0 907 TestAudioReceiverOffersBundle(true, filter, bad_answer_filter);
michael@0 908 // Filter is empty, so should drop everything.
michael@0 909 ASSERT_EQ(0, p2_.GetAudioRtpCountReceived());
michael@0 910 ASSERT_EQ(0, p2_.GetAudioRtcpCountReceived());
michael@0 911 }
michael@0 912
michael@0 913 } // end namespace
michael@0 914
michael@0 915
michael@0 916 int main(int argc, char **argv) {
michael@0 917 test_utils = new MtransportTestUtils();
michael@0 918 // Start the tests
michael@0 919 NSS_NoDB_Init(nullptr);
michael@0 920 NSS_SetDomesticPolicy();
michael@0 921 ::testing::InitGoogleTest(&argc, argv);
michael@0 922
michael@0 923 int rv = RUN_ALL_TESTS();
michael@0 924 delete test_utils;
michael@0 925 return rv;
michael@0 926 }
michael@0 927
michael@0 928
michael@0 929

mercurial