diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /media/webrtc | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'media/webrtc')
-rw-r--r-- | media/webrtc/.gclient | 10 | ||||
-rw-r--r-- | media/webrtc/.gclient_entries | 31 | ||||
-rw-r--r-- | media/webrtc/moz.build | 16 | ||||
-rw-r--r-- | media/webrtc/signaling/gtest/Canonicals.h | 153 | ||||
-rw-r--r-- | media/webrtc/signaling/gtest/MockCall.cpp | 53 | ||||
-rw-r--r-- | media/webrtc/signaling/gtest/MockCall.h | 370 | ||||
-rw-r--r-- | media/webrtc/signaling/gtest/MockConduit.h | 65 | ||||
-rw-r--r-- | media/webrtc/signaling/gtest/audioconduit_unittests.cpp | 781 | ||||
-rw-r--r-- | media/webrtc/signaling/gtest/jsep_session_unittest.cpp | 7609 | ||||
-rw-r--r-- | media/webrtc/signaling/gtest/jsep_track_unittest.cpp | 1748 | ||||
-rw-r--r-- | media/webrtc/signaling/gtest/mediapipeline_unittest.cpp | 697 | ||||
-rw-r--r-- | media/webrtc/signaling/gtest/moz.build | 54 | ||||
-rw-r--r-- | media/webrtc/signaling/gtest/sdp_unittests.cpp | 5915 | ||||
-rw-r--r-- | media/webrtc/signaling/gtest/videoconduit_unittests.cpp | 2336 |
14 files changed, 19838 insertions, 0 deletions
diff --git a/media/webrtc/.gclient b/media/webrtc/.gclient new file mode 100644 index 0000000000..14532a1f3c --- /dev/null +++ b/media/webrtc/.gclient @@ -0,0 +1,10 @@ +solutions = [ + { "name" : "trunk", + "url" : "http://webrtc.googlecode.com/svn/trunk/peerconnection", + "deps_file" : "DEPS", + "managed" : True, + "custom_deps" : { + }, + "safesync_url": "", + }, +] diff --git a/media/webrtc/.gclient_entries b/media/webrtc/.gclient_entries new file mode 100644 index 0000000000..e00be99175 --- /dev/null +++ b/media/webrtc/.gclient_entries @@ -0,0 +1,31 @@ +entries = { + 'trunk': 'http://webrtc.googlecode.com/svn/trunk/peerconnection', + 'trunk/build': 'http://src.chromium.org/svn/trunk/src/build@120526', + 'trunk/src': 'http://webrtc.googlecode.com/svn/trunk/src@1538', + 'trunk/test': 'http://webrtc.googlecode.com/svn/trunk/test@1538', + 'trunk/testing': 'http://src.chromium.org/svn/trunk/src/testing@120526', + 'trunk/testing/gmock': 'http://googlemock.googlecode.com/svn/trunk@386', + 'trunk/testing/gtest': 'http://googletest.googlecode.com/svn/trunk@573', + 'trunk/third_party/expat/': 'http://src.chromium.org/svn/trunk/src/third_party/expat@120526', + 'trunk/third_party/google-gflags': 'http://webrtc.googlecode.com/svn/trunk/third_party/google-gflags@1538', + 'trunk/third_party/google-gflags/src': 'http://google-gflags.googlecode.com/svn/trunk/src@45', + 'trunk/third_party/jsoncpp/': 'http://src.chromium.org/svn/trunk/src/third_party/jsoncpp@120526', + 'trunk/third_party/jsoncpp/source': 'http://jsoncpp.svn.sourceforge.net/svnroot/jsoncpp/trunk/jsoncpp@248', + 'trunk/third_party/libjingle/source': 'http://libjingle.googlecode.com/svn/trunk@115', + 'trunk/third_party/libjpeg/': 'http://src.chromium.org/svn/trunk/src/third_party/libjpeg@120526', + 'trunk/third_party/libjpeg_turbo/': 'http://src.chromium.org/svn/trunk/deps/third_party/libjpeg_turbo@119959', + 'trunk/third_party/libsrtp/': 'http://src.chromium.org/svn/trunk/deps/third_party/libsrtp@119742', + 'trunk/third_party/libvpx': 'http://webrtc.googlecode.com/svn/trunk/third_party/libvpx@1538', + 'trunk/third_party/libvpx/source/libvpx': 'http://git.chromium.org/webm/libvpx.git@e479379a', + 'trunk/third_party/libyuv': 'http://libyuv.googlecode.com/svn/trunk@121', + 'trunk/third_party/protobuf/': 'http://src.chromium.org/svn/trunk/src/third_party/protobuf@120526', + 'trunk/third_party/yasm/': 'http://src.chromium.org/svn/trunk/src/third_party/yasm@120526', + 'trunk/third_party/yasm/binaries': 'http://src.chromium.org/svn/trunk/deps/third_party/yasm/binaries@74228', + 'trunk/third_party/yasm/source/patched-yasm': 'http://src.chromium.org/svn/trunk/deps/third_party/yasm/patched-yasm@73761', + 'trunk/tools': 'http://webrtc.googlecode.com/svn/trunk/tools@1538', + 'trunk/tools/clang/scripts': 'http://src.chromium.org/svn/trunk/src/tools/clang/scripts@120526', + 'trunk/tools/gyp': 'http://gyp.googlecode.com/svn/trunk@1187', + 'trunk/tools/python': 'http://src.chromium.org/svn/trunk/src/tools/python@120526', + 'trunk/tools/valgrind': 'http://src.chromium.org/svn/trunk/src/tools/valgrind@120526', + 'trunk/tools/win/supalink': 'http://src.chromium.org/svn/trunk/src/tools/win/supalink@120526', +} diff --git a/media/webrtc/moz.build b/media/webrtc/moz.build new file mode 100644 index 0000000000..4c828f0b4e --- /dev/null +++ b/media/webrtc/moz.build @@ -0,0 +1,16 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("signaling/**"): + BUG_COMPONENT = ("Core", "WebRTC: Signaling") + +if CONFIG["MOZ_WEBRTC_SIGNALING"]: + # Avoid warnings from third-party code that we can not modify. + if CONFIG["CC_TYPE"] == "clang-cl": + CXXFLAGS += ["-Wno-invalid-source-encoding"] + + if CONFIG["ENABLE_TESTS"]: + TEST_DIRS += [ + "signaling/gtest", + ] diff --git a/media/webrtc/signaling/gtest/Canonicals.h b/media/webrtc/signaling/gtest/Canonicals.h new file mode 100644 index 0000000000..2d865a5ef2 --- /dev/null +++ b/media/webrtc/signaling/gtest/Canonicals.h @@ -0,0 +1,153 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef MEDIA_WEBRTC_SIGNALING_GTEST_CANONICALS_H_ +#define MEDIA_WEBRTC_SIGNALING_GTEST_CANONICALS_H_ + +#include "MediaConduitControl.h" +#include "MediaPipeline.h" +#include "WaitFor.h" + +namespace mozilla { + +#define INIT_CANONICAL(name, val) \ + name(AbstractThread::MainThread(), val, "ConcreteCanonicals::" #name) +class ConcreteCanonicals { + public: + ConcreteCanonicals() + : INIT_CANONICAL(mReceiving, false), + INIT_CANONICAL(mTransmitting, false), + INIT_CANONICAL(mLocalSsrcs, Ssrcs()), + INIT_CANONICAL(mLocalVideoRtxSsrcs, Ssrcs()), + INIT_CANONICAL(mLocalCname, std::string()), + INIT_CANONICAL(mMid, std::string()), + INIT_CANONICAL(mSyncGroup, std::string()), + INIT_CANONICAL(mLocalSendRtpExtensions, RtpExtList()), + INIT_CANONICAL(mLocalRecvRtpExtensions, RtpExtList()), + INIT_CANONICAL(mRemoteSsrc, 0), + INIT_CANONICAL(mRemoteVideoRtxSsrc, 0), + INIT_CANONICAL(mAudioRecvCodecs, std::vector<AudioCodecConfig>()), + INIT_CANONICAL(mAudioSendCodec, Nothing()), + INIT_CANONICAL(mVideoRecvCodecs, std::vector<VideoCodecConfig>()), + INIT_CANONICAL(mVideoSendCodec, Nothing()), + INIT_CANONICAL(mVideoRecvRtpRtcpConfig, Nothing()), + INIT_CANONICAL(mVideoSendRtpRtcpConfig, Nothing()), + INIT_CANONICAL(mVideoCodecMode, + webrtc::VideoCodecMode::kRealtimeVideo) {} + + Canonical<bool> mReceiving; + Canonical<bool> mTransmitting; + Canonical<Ssrcs> mLocalSsrcs; + Canonical<Ssrcs> mLocalVideoRtxSsrcs; + Canonical<std::string> mLocalCname; + Canonical<std::string> mMid; + Canonical<std::string> mSyncGroup; + Canonical<RtpExtList> mLocalSendRtpExtensions; + Canonical<RtpExtList> mLocalRecvRtpExtensions; + Canonical<Ssrc> mRemoteSsrc; + Canonical<Ssrc> mRemoteVideoRtxSsrc; + + Canonical<std::vector<AudioCodecConfig>> mAudioRecvCodecs; + Canonical<Maybe<AudioCodecConfig>> mAudioSendCodec; + MediaEventProducer<DtmfEvent> mDtmfEvent; + + Canonical<std::vector<VideoCodecConfig>> mVideoRecvCodecs; + Canonical<Maybe<VideoCodecConfig>> mVideoSendCodec; + Canonical<Maybe<RtpRtcpConfig>> mVideoRecvRtpRtcpConfig; + Canonical<Maybe<RtpRtcpConfig>> mVideoSendRtpRtcpConfig; + Canonical<webrtc::VideoCodecMode> mVideoCodecMode; +}; +#undef INIT_CANONICAL + +class ConcreteControl : public AudioConduitControlInterface, + public VideoConduitControlInterface, + public MediaPipelineReceiveControlInterface, + public MediaPipelineTransmitControlInterface, + private ConcreteCanonicals { + private: + RefPtr<nsISerialEventTarget> mTarget; + + public: + explicit ConcreteControl(RefPtr<nsISerialEventTarget> aTarget) + : mTarget(std::move(aTarget)) {} + + template <typename Function> + void Update(Function aFunction) { + mTarget->Dispatch(NS_NewRunnableFunction( + __func__, [&] { aFunction(*static_cast<ConcreteCanonicals*>(this)); })); + WaitForMirrors(mTarget); + } + + // MediaConduitControlInterface + // -- MediaPipelineReceiveControlInterface + AbstractCanonical<bool>* CanonicalReceiving() override { return &mReceiving; } + // -- MediaPipelineTransmitControlInterface + AbstractCanonical<bool>* CanonicalTransmitting() override { + return &mTransmitting; + } + AbstractCanonical<Ssrcs>* CanonicalLocalSsrcs() override { + return &mLocalSsrcs; + } + AbstractCanonical<Ssrcs>* CanonicalLocalVideoRtxSsrcs() override { + return &mLocalVideoRtxSsrcs; + } + AbstractCanonical<std::string>* CanonicalLocalCname() override { + return &mLocalCname; + } + AbstractCanonical<std::string>* CanonicalMid() override { return &mMid; } + AbstractCanonical<Ssrc>* CanonicalRemoteSsrc() override { + return &mRemoteSsrc; + } + AbstractCanonical<Ssrc>* CanonicalRemoteVideoRtxSsrc() override { + return &mRemoteVideoRtxSsrc; + } + AbstractCanonical<std::string>* CanonicalSyncGroup() override { + return &mSyncGroup; + } + AbstractCanonical<RtpExtList>* CanonicalLocalRecvRtpExtensions() override { + return &mLocalRecvRtpExtensions; + } + AbstractCanonical<RtpExtList>* CanonicalLocalSendRtpExtensions() override { + return &mLocalSendRtpExtensions; + } + + // AudioConduitControlInterface + AbstractCanonical<Maybe<AudioCodecConfig>>* CanonicalAudioSendCodec() + override { + return &mAudioSendCodec; + } + AbstractCanonical<std::vector<AudioCodecConfig>>* CanonicalAudioRecvCodecs() + override { + return &mAudioRecvCodecs; + } + MediaEventSource<DtmfEvent>& OnDtmfEvent() override { return mDtmfEvent; } + + // VideoConduitControlInterface + AbstractCanonical<Maybe<VideoCodecConfig>>* CanonicalVideoSendCodec() + override { + return &mVideoSendCodec; + } + AbstractCanonical<Maybe<RtpRtcpConfig>>* CanonicalVideoSendRtpRtcpConfig() + override { + return &mVideoSendRtpRtcpConfig; + } + AbstractCanonical<std::vector<VideoCodecConfig>>* CanonicalVideoRecvCodecs() + override { + return &mVideoRecvCodecs; + } + AbstractCanonical<Maybe<RtpRtcpConfig>>* CanonicalVideoRecvRtpRtcpConfig() + override { + return &mVideoRecvRtpRtcpConfig; + } + AbstractCanonical<webrtc::VideoCodecMode>* CanonicalVideoCodecMode() + override { + return &mVideoCodecMode; + } +}; + +} // namespace mozilla + +#endif diff --git a/media/webrtc/signaling/gtest/MockCall.cpp b/media/webrtc/signaling/gtest/MockCall.cpp new file mode 100644 index 0000000000..8f70565a8f --- /dev/null +++ b/media/webrtc/signaling/gtest/MockCall.cpp @@ -0,0 +1,53 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "MockCall.h" + +namespace test { + +const webrtc::AudioSendStream::Config& MockAudioSendStream::GetConfig() const { + return *mCallWrapper->GetMockCall()->mAudioSendConfig; +} + +void MockAudioSendStream::Reconfigure(const Config& config, + webrtc::SetParametersCallback callback) { + mCallWrapper->GetMockCall()->mAudioSendConfig = mozilla::Some(config); +} + +void MockAudioReceiveStream::SetDecoderMap( + std::map<int, webrtc::SdpAudioFormat> decoder_map) { + MOZ_ASSERT(mCallWrapper->GetMockCall()->mAudioReceiveConfig.isSome()); + mCallWrapper->GetMockCall()->mAudioReceiveConfig->decoder_map = + std::move(decoder_map); +} + +void MockAudioReceiveStream::SetRtpExtensions( + std::vector<webrtc::RtpExtension> extensions) { + MOZ_ASSERT(mCallWrapper->GetMockCall()->mAudioReceiveConfig.isSome()); + mCallWrapper->GetMockCall()->mAudioReceiveConfig->rtp.extensions = + std::move(extensions); +} + +webrtc::RtpHeaderExtensionMap MockAudioReceiveStream::GetRtpExtensionMap() + const { + return webrtc::RtpHeaderExtensionMap(); +} + +void MockVideoSendStream::ReconfigureVideoEncoder( + webrtc::VideoEncoderConfig config) { + mCallWrapper->GetMockCall()->mVideoSendEncoderConfig = + mozilla::Some(config.Copy()); +} + +void MockVideoSendStream::ReconfigureVideoEncoder( + webrtc::VideoEncoderConfig config, webrtc::SetParametersCallback callback) { + ReconfigureVideoEncoder(std::move(config)); +} + +webrtc::RtpHeaderExtensionMap MockVideoReceiveStream::GetRtpExtensionMap() + const { + return webrtc::RtpHeaderExtensionMap(); +} + +} // namespace test diff --git a/media/webrtc/signaling/gtest/MockCall.h b/media/webrtc/signaling/gtest/MockCall.h new file mode 100644 index 0000000000..09b7779150 --- /dev/null +++ b/media/webrtc/signaling/gtest/MockCall.h @@ -0,0 +1,370 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef MOCK_CALL_H_ +#define MOCK_CALL_H_ + +#include "gmock/gmock.h" +#include "mozilla/Assertions.h" +#include "mozilla/Maybe.h" +#include "mozilla/media/MediaUtils.h" +#include "WebrtcCallWrapper.h" +#include "PeerConnectionCtx.h" + +// libwebrtc +#include "api/call/audio_sink.h" +#include "call/call.h" + +namespace test { +class MockCallWrapper; + +class MockAudioSendStream : public webrtc::AudioSendStream { + public: + explicit MockAudioSendStream(RefPtr<MockCallWrapper> aCallWrapper) + : mCallWrapper(std::move(aCallWrapper)) {} + + const webrtc::AudioSendStream::Config& GetConfig() const override; + + void Reconfigure(const Config& config, + webrtc::SetParametersCallback callback) override; + + void Start() override {} + + void Stop() override {} + + void SendAudioData(std::unique_ptr<webrtc::AudioFrame> audio_frame) override { + } + + bool SendTelephoneEvent(int payload_type, int payload_frequency, int event, + int duration_ms) override { + return true; + } + + void SetMuted(bool muted) override {} + + Stats GetStats() const override { return mStats; } + + Stats GetStats(bool has_remote_tracks) const override { return mStats; } + + virtual ~MockAudioSendStream() {} + + const RefPtr<MockCallWrapper> mCallWrapper; + webrtc::AudioSendStream::Stats mStats; +}; + +class MockAudioReceiveStream : public webrtc::AudioReceiveStreamInterface { + public: + explicit MockAudioReceiveStream(RefPtr<MockCallWrapper> aCallWrapper) + : mCallWrapper(std::move(aCallWrapper)) {} + + void Start() override {} + + void Stop() override {} + + bool IsRunning() const override { return true; } + + Stats GetStats(bool get_and_clear_legacy_stats) const override { + return mStats; + } + + void SetSink(webrtc::AudioSinkInterface* sink) override {} + + void SetGain(float gain) override {} + + std::vector<webrtc::RtpSource> GetSources() const override { + return mRtpSources; + } + + virtual void SetDepacketizerToDecoderFrameTransformer( + rtc::scoped_refptr<webrtc::FrameTransformerInterface> frame_transformer) + override { + // Unimplemented after webrtc.org e2561e17e2 removed the Reconfigure + // method. + MOZ_ASSERT(false); + } + virtual void SetDecoderMap( + std::map<int, webrtc::SdpAudioFormat> decoder_map) override; + virtual void SetNackHistory(int history_ms) override { + // Unimplemented after webrtc.org e2561e17e2 removed the Reconfigure + // method. + MOZ_ASSERT(false); + } + virtual void SetNonSenderRttMeasurement(bool enabled) override {} + void SetFrameDecryptor(rtc::scoped_refptr<webrtc::FrameDecryptorInterface> + frame_decryptor) override {} + void SetRtpExtensions(std::vector<webrtc::RtpExtension> extensions) override; + webrtc::RtpHeaderExtensionMap GetRtpExtensionMap() const override; + bool SetBaseMinimumPlayoutDelayMs(int delay_ms) override { return false; } + int GetBaseMinimumPlayoutDelayMs() const override { return 0; } + uint32_t remote_ssrc() const override { return 0; } + + virtual ~MockAudioReceiveStream() {} + + const RefPtr<MockCallWrapper> mCallWrapper; + webrtc::AudioReceiveStreamInterface::Stats mStats; + std::vector<webrtc::RtpSource> mRtpSources; +}; + +class MockVideoSendStream : public webrtc::VideoSendStream { + public: + explicit MockVideoSendStream(RefPtr<MockCallWrapper> aCallWrapper) + : mCallWrapper(std::move(aCallWrapper)) {} + + void Start() override {} + + void Stop() override {} + + bool started() override { return false; } + + void SetSource( + rtc::VideoSourceInterface<webrtc::VideoFrame>* source, + const webrtc::DegradationPreference& degradation_preference) override {} + + void ReconfigureVideoEncoder(webrtc::VideoEncoderConfig config) override; + + void ReconfigureVideoEncoder(webrtc::VideoEncoderConfig config, + webrtc::SetParametersCallback callback) override; + + Stats GetStats() override { return mStats; } + + void StartPerRtpStream(const std::vector<bool> active_layers) override {} + + void AddAdaptationResource( + rtc::scoped_refptr<webrtc::Resource> resource) override {} + + std::vector<rtc::scoped_refptr<webrtc::Resource>> GetAdaptationResources() + override { + return std::vector<rtc::scoped_refptr<webrtc::Resource>>(); + } + + void GenerateKeyFrame(const std::vector<std::string>& rids) override {} + + virtual ~MockVideoSendStream() {} + + const RefPtr<MockCallWrapper> mCallWrapper; + webrtc::VideoSendStream::Stats mStats; +}; + +class MockVideoReceiveStream : public webrtc::VideoReceiveStreamInterface { + public: + explicit MockVideoReceiveStream(RefPtr<MockCallWrapper> aCallWrapper) + : mCallWrapper(std::move(aCallWrapper)) {} + + void Start() override {} + + void Stop() override {} + + Stats GetStats() const override { return mStats; } + + std::vector<webrtc::RtpSource> GetSources() const override { + return std::vector<webrtc::RtpSource>(); + } + + bool SetBaseMinimumPlayoutDelayMs(int delay_ms) override { return false; } + + int GetBaseMinimumPlayoutDelayMs() const override { return 0; } + + void SetFrameDecryptor(rtc::scoped_refptr<webrtc::FrameDecryptorInterface> + frame_decryptor) override {} + + void SetDepacketizerToDecoderFrameTransformer( + rtc::scoped_refptr<webrtc::FrameTransformerInterface> frame_transformer) + override {} + + RecordingState SetAndGetRecordingState(RecordingState state, + bool generate_key_frame) override { + return {}; + } + + void GenerateKeyFrame() override {} + + void SetRtcpMode(webrtc::RtcpMode mode) override {} + + void SetFlexFecProtection( + webrtc::RtpPacketSinkInterface* flexfec_sink) override {} + + void SetLossNotificationEnabled(bool enabled) override {} + + void SetNackHistory(webrtc::TimeDelta history) override {} + + void SetProtectionPayloadTypes(int red_payload_type, + int ulpfec_payload_type) override {} + + void SetRtcpXr(Config::Rtp::RtcpXr rtcp_xr) override {} + + virtual void SetAssociatedPayloadTypes( + std::map<int, int> associated_payload_types) override {} + + void SetRtpExtensions(std::vector<webrtc::RtpExtension> extensions) override { + } + webrtc::RtpHeaderExtensionMap GetRtpExtensionMap() const override; + + virtual ~MockVideoReceiveStream() {} + + const RefPtr<MockCallWrapper> mCallWrapper; + webrtc::VideoReceiveStreamInterface::Stats mStats; +}; + +class MockCall : public webrtc::Call { + public: + explicit MockCall(RefPtr<MockCallWrapper> aCallWrapper) + : mCallWrapper(std::move(aCallWrapper)) {} + + webrtc::AudioSendStream* CreateAudioSendStream( + const webrtc::AudioSendStream::Config& config) override { + MOZ_RELEASE_ASSERT(!mAudioSendConfig); + mAudioSendConfig = mozilla::Some(config); + return new MockAudioSendStream(mCallWrapper); + } + + void DestroyAudioSendStream(webrtc::AudioSendStream* send_stream) override { + mAudioSendConfig = mozilla::Nothing(); + delete static_cast<MockAudioSendStream*>(send_stream); + } + + webrtc::AudioReceiveStreamInterface* CreateAudioReceiveStream( + const webrtc::AudioReceiveStreamInterface::Config& config) override { + MOZ_RELEASE_ASSERT(!mAudioReceiveConfig); + mAudioReceiveConfig = mozilla::Some(config); + return new MockAudioReceiveStream(mCallWrapper); + } + void DestroyAudioReceiveStream( + webrtc::AudioReceiveStreamInterface* receive_stream) override { + mAudioReceiveConfig = mozilla::Nothing(); + delete static_cast<MockAudioReceiveStream*>(receive_stream); + } + + webrtc::VideoSendStream* CreateVideoSendStream( + webrtc::VideoSendStream::Config config, + webrtc::VideoEncoderConfig encoder_config) override { + MOZ_RELEASE_ASSERT(!mVideoSendConfig); + MOZ_RELEASE_ASSERT(!mVideoSendEncoderConfig); + mVideoSendConfig = mozilla::Some(std::move(config)); + mVideoSendEncoderConfig = mozilla::Some(std::move(encoder_config)); + return new MockVideoSendStream(mCallWrapper); + } + + void DestroyVideoSendStream(webrtc::VideoSendStream* send_stream) override { + mVideoSendConfig = mozilla::Nothing(); + mVideoSendEncoderConfig = mozilla::Nothing(); + delete static_cast<MockVideoSendStream*>(send_stream); + } + + webrtc::VideoReceiveStreamInterface* CreateVideoReceiveStream( + webrtc::VideoReceiveStreamInterface::Config configuration) override { + MOZ_RELEASE_ASSERT(!mVideoReceiveConfig); + mVideoReceiveConfig = mozilla::Some(std::move(configuration)); + return new MockVideoReceiveStream(mCallWrapper); + } + + void DestroyVideoReceiveStream( + webrtc::VideoReceiveStreamInterface* receive_stream) override { + mVideoReceiveConfig = mozilla::Nothing(); + delete static_cast<MockVideoReceiveStream*>(receive_stream); + } + + webrtc::FlexfecReceiveStream* CreateFlexfecReceiveStream( + const webrtc::FlexfecReceiveStream::Config config) override { + return nullptr; + } + + void DestroyFlexfecReceiveStream( + webrtc::FlexfecReceiveStream* receive_stream) override {} + + void AddAdaptationResource( + rtc::scoped_refptr<webrtc::Resource> resource) override {} + + webrtc::PacketReceiver* Receiver() override { return nullptr; } + + webrtc::RtpTransportControllerSendInterface* GetTransportControllerSend() + override { + return nullptr; + } + + Stats GetStats() const override { return mStats; } + + void SignalChannelNetworkState(webrtc::MediaType media, + webrtc::NetworkState state) override {} + + void OnAudioTransportOverheadChanged( + int transport_overhead_per_packet) override {} + + void OnLocalSsrcUpdated(webrtc::AudioReceiveStreamInterface& stream, + uint32_t local_ssrc) override {} + void OnLocalSsrcUpdated(webrtc::VideoReceiveStreamInterface& stream, + uint32_t local_ssrc) override {} + void OnLocalSsrcUpdated(webrtc::FlexfecReceiveStream& stream, + uint32_t local_ssrc) override {} + + void OnUpdateSyncGroup(webrtc::AudioReceiveStreamInterface& stream, + absl::string_view sync_group) override {} + + void OnSentPacket(const rtc::SentPacket& sent_packet) override {} + + void SetClientBitratePreferences( + const webrtc::BitrateSettings& preferences) override {} + + std::vector<webrtc::VideoStream> CreateEncoderStreams(int width, int height) { + return mVideoSendEncoderConfig->video_stream_factory->CreateEncoderStreams( + width, height, *mVideoSendEncoderConfig); + } + + virtual const webrtc::WebRtcKeyValueConfig& trials() const override { + return mUnusedConfig; + } + + virtual webrtc::TaskQueueBase* network_thread() const override { + return nullptr; + } + + virtual webrtc::TaskQueueBase* worker_thread() const override { + return nullptr; + } + + virtual ~MockCall(){}; + + const RefPtr<MockCallWrapper> mCallWrapper; + mozilla::Maybe<webrtc::AudioReceiveStreamInterface::Config> + mAudioReceiveConfig; + mozilla::Maybe<webrtc::AudioSendStream::Config> mAudioSendConfig; + mozilla::Maybe<webrtc::VideoReceiveStreamInterface::Config> + mVideoReceiveConfig; + mozilla::Maybe<webrtc::VideoSendStream::Config> mVideoSendConfig; + mozilla::Maybe<webrtc::VideoEncoderConfig> mVideoSendEncoderConfig; + webrtc::Call::Stats mStats; + webrtc::NoTrialsConfig mUnusedConfig; +}; + +class MockCallWrapper : public mozilla::WebrtcCallWrapper { + public: + MockCallWrapper( + RefPtr<mozilla::SharedWebrtcState> aSharedState, + mozilla::UniquePtr<webrtc::VideoBitrateAllocatorFactory> + aVideoBitrateAllocatorFactory, + mozilla::UniquePtr<webrtc::RtcEventLog> aEventLog, + mozilla::UniquePtr<webrtc::TaskQueueFactory> aTaskQueueFactory, + const mozilla::dom::RTCStatsTimestampMaker& aTimestampMaker, + mozilla::UniquePtr<mozilla::media::ShutdownBlockingTicket> + aShutdownTicket) + : mozilla::WebrtcCallWrapper( + std::move(aSharedState), std::move(aVideoBitrateAllocatorFactory), + std::move(aEventLog), std::move(aTaskQueueFactory), aTimestampMaker, + std::move(aShutdownTicket)) {} + + static RefPtr<MockCallWrapper> Create() { + auto state = mozilla::MakeRefPtr<mozilla::SharedWebrtcState>( + mozilla::AbstractThread::GetCurrent(), webrtc::AudioState::Config(), + nullptr, nullptr); + auto wrapper = mozilla::MakeRefPtr<MockCallWrapper>( + state, nullptr, nullptr, nullptr, + mozilla::dom::RTCStatsTimestampMaker::Create(), nullptr); + wrapper->SetCall(mozilla::WrapUnique(new MockCall(wrapper))); + return wrapper; + } + + MockCall* GetMockCall() { return static_cast<MockCall*>(Call()); } +}; + +} // namespace test +#endif diff --git a/media/webrtc/signaling/gtest/MockConduit.h b/media/webrtc/signaling/gtest/MockConduit.h new file mode 100644 index 0000000000..d0c43f7c45 --- /dev/null +++ b/media/webrtc/signaling/gtest/MockConduit.h @@ -0,0 +1,65 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef MEDIA_WEBRTC_SIGNALING_GTEST_MOCKCONDUIT_H_ +#define MEDIA_WEBRTC_SIGNALING_GTEST_MOCKCONDUIT_H_ + +#include "gmock/gmock.h" +#include "MediaConduitInterface.h" + +namespace webrtc { +std::ostream& operator<<(std::ostream& aStream, + const webrtc::Call::Stats& aObj) { + aStream << aObj.ToString(0); + return aStream; +} +} // namespace webrtc + +namespace mozilla { +class MockConduit : public MediaSessionConduit { + public: + MockConduit() = default; + + MOCK_CONST_METHOD0(type, Type()); + MOCK_CONST_METHOD0(ActiveSendPayloadType, Maybe<int>()); + MOCK_CONST_METHOD0(ActiveRecvPayloadType, Maybe<int>()); + MOCK_METHOD1(SetTransportActive, void(bool)); + MOCK_METHOD0(SenderRtpSendEvent, MediaEventSourceExc<MediaPacket>&()); + MOCK_METHOD0(SenderRtcpSendEvent, MediaEventSourceExc<MediaPacket>&()); + MOCK_METHOD0(ReceiverRtcpSendEvent, MediaEventSourceExc<MediaPacket>&()); + MOCK_METHOD1( + ConnectReceiverRtpEvent, + void(MediaEventSourceExc<webrtc::RtpPacketReceived, webrtc::RTPHeader>&)); + MOCK_METHOD1(ConnectReceiverRtcpEvent, + void(MediaEventSourceExc<MediaPacket>&)); + MOCK_METHOD1(ConnectSenderRtcpEvent, void(MediaEventSourceExc<MediaPacket>&)); + MOCK_CONST_METHOD0(LastRtcpReceived, Maybe<DOMHighResTimeStamp>()); + MOCK_CONST_METHOD1(RtpSendBaseSeqFor, Maybe<uint16_t>(uint32_t)); + MOCK_CONST_METHOD0(GetNow, DOMHighResTimeStamp()); + MOCK_CONST_METHOD0(GetTimestampMaker, dom::RTCStatsTimestampMaker&()); + MOCK_CONST_METHOD0(GetLocalSSRCs, Ssrcs()); + MOCK_CONST_METHOD0(GetRemoteSSRC, Maybe<Ssrc>()); + MOCK_METHOD1(UnsetRemoteSSRC, void(Ssrc)); + MOCK_METHOD0(DisableSsrcChanges, void()); + MOCK_CONST_METHOD1(HasCodecPluginID, bool(uint64_t)); + MOCK_METHOD0(RtcpByeEvent, MediaEventSource<void>&()); + MOCK_METHOD0(RtcpTimeoutEvent, MediaEventSource<void>&()); + MOCK_METHOD0(RtpPacketEvent, MediaEventSource<void>&()); + MOCK_METHOD3(SendRtp, + bool(const uint8_t*, size_t, const webrtc::PacketOptions&)); + MOCK_METHOD2(SendSenderRtcp, bool(const uint8_t*, size_t)); + MOCK_METHOD2(SendReceiverRtcp, bool(const uint8_t*, size_t)); + MOCK_METHOD2(DeliverPacket, void(rtc::CopyOnWriteBuffer, PacketType)); + MOCK_METHOD0(Shutdown, RefPtr<GenericPromise>()); + MOCK_METHOD0(AsAudioSessionConduit, Maybe<RefPtr<AudioSessionConduit>>()); + MOCK_METHOD0(AsVideoSessionConduit, Maybe<RefPtr<VideoSessionConduit>>()); + MOCK_CONST_METHOD0(GetCallStats, Maybe<webrtc::Call::Stats>()); + MOCK_METHOD1(SetJitterBufferTarget, void(DOMHighResTimeStamp)); + MOCK_CONST_METHOD0(GetUpstreamRtpSources, std::vector<webrtc::RtpSource>()); +}; +} // namespace mozilla + +#endif diff --git a/media/webrtc/signaling/gtest/audioconduit_unittests.cpp b/media/webrtc/signaling/gtest/audioconduit_unittests.cpp new file mode 100644 index 0000000000..12f35f344f --- /dev/null +++ b/media/webrtc/signaling/gtest/audioconduit_unittests.cpp @@ -0,0 +1,781 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#define GTEST_HAS_RTTI 0 +#include "gtest/gtest.h" + +#include "AudioConduit.h" +#include "Canonicals.h" +#include "WaitFor.h" + +#include "MockCall.h" + +using namespace mozilla; +using namespace testing; +using namespace webrtc; + +namespace test { + +class AudioConduitTest : public ::testing::Test { + public: + AudioConduitTest() + : mCallWrapper(MockCallWrapper::Create()), + mAudioConduit(MakeRefPtr<WebrtcAudioConduit>( + mCallWrapper, GetCurrentSerialEventTarget())), + mControl(GetCurrentSerialEventTarget()) { + mAudioConduit->InitControl(&mControl); + } + + ~AudioConduitTest() override { + mozilla::Unused << WaitFor(mAudioConduit->Shutdown()); + mCallWrapper->Destroy(); + } + + MockCall* Call() { return mCallWrapper->GetMockCall(); } + + const RefPtr<MockCallWrapper> mCallWrapper; + const RefPtr<WebrtcAudioConduit> mAudioConduit; + ConcreteControl mControl; +}; + +TEST_F(AudioConduitTest, TestConfigureSendMediaCodec) { + mControl.Update([&](auto& aControl) { + // defaults + aControl.mAudioSendCodec = + Some(AudioCodecConfig(114, "opus", 48000, 2, false)); + aControl.mTransmitting = true; + }); + + ASSERT_TRUE(Call()->mAudioSendConfig); + { + const webrtc::SdpAudioFormat& f = + Call()->mAudioSendConfig->send_codec_spec->format; + ASSERT_EQ(f.name, "opus"); + ASSERT_EQ(f.clockrate_hz, 48000); + ASSERT_EQ(f.num_channels, 2UL); + ASSERT_NE(f.parameters.find("stereo"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("stereo"), "1"); + ASSERT_EQ(f.parameters.find("maxplaybackrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("useinbandfec"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxaveragebitrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("usedtx"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("cbr"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("ptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("minptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxptime"), f.parameters.end()); + } + + mControl.Update([&](auto& aControl) { + // empty codec name + aControl.mAudioSendCodec = Some(AudioCodecConfig(114, "", 48000, 2, false)); + }); + + ASSERT_TRUE(Call()->mAudioSendConfig); + { + // Invalid codec was ignored. + const webrtc::SdpAudioFormat& f = + Call()->mAudioSendConfig->send_codec_spec->format; + ASSERT_EQ(f.name, "opus"); + } +} + +TEST_F(AudioConduitTest, TestConfigureSendOpusMono) { + mControl.Update([&](auto& aControl) { + // opus mono + aControl.mAudioSendCodec = + Some(AudioCodecConfig(114, "opus", 48000, 1, false)); + aControl.mTransmitting = true; + }); + + ASSERT_TRUE(Call()->mAudioSendConfig); + { + const webrtc::SdpAudioFormat& f = + Call()->mAudioSendConfig->send_codec_spec->format; + ASSERT_EQ(f.name, "opus"); + ASSERT_EQ(f.clockrate_hz, 48000); + ASSERT_EQ(f.num_channels, 1UL); + ASSERT_EQ(f.parameters.find("stereo"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxplaybackrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("useinbandfec"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxaveragebitrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("usedtx"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("cbr"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("ptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("minptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxptime"), f.parameters.end()); + } +} + +TEST_F(AudioConduitTest, TestConfigureSendOpusFEC) { + mControl.Update([&](auto& aControl) { + // opus with inband Forward Error Correction + AudioCodecConfig codecConfig = + AudioCodecConfig(114, "opus", 48000, 2, true); + aControl.mAudioSendCodec = Some(codecConfig); + aControl.mTransmitting = true; + }); + + ASSERT_TRUE(Call()->mAudioSendConfig); + { + const webrtc::SdpAudioFormat& f = + Call()->mAudioSendConfig->send_codec_spec->format; + ASSERT_EQ(f.name, "opus"); + ASSERT_EQ(f.clockrate_hz, 48000); + ASSERT_EQ(f.num_channels, 2UL); + ASSERT_NE(f.parameters.find("stereo"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("stereo"), "1"); + ASSERT_NE(f.parameters.find("useinbandfec"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("useinbandfec"), "1"); + ASSERT_EQ(f.parameters.find("maxplaybackrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxaveragebitrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("usedtx"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("cbr"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("ptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("minptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxptime"), f.parameters.end()); + } +} + +TEST_F(AudioConduitTest, TestConfigureSendOpusMaxPlaybackRate) { + mControl.Update([&](auto& aControl) { + AudioCodecConfig codecConfig = + AudioCodecConfig(114, "opus", 48000, 2, false); + codecConfig.mMaxPlaybackRate = 1234; + aControl.mAudioSendCodec = Some(codecConfig); + aControl.mTransmitting = true; + }); + + ASSERT_TRUE(Call()->mAudioSendConfig); + { + const webrtc::SdpAudioFormat& f = + Call()->mAudioSendConfig->send_codec_spec->format; + ASSERT_EQ(f.name, "opus"); + ASSERT_EQ(f.clockrate_hz, 48000); + ASSERT_EQ(f.num_channels, 2UL); + ASSERT_NE(f.parameters.find("stereo"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("stereo"), "1"); + ASSERT_EQ(f.parameters.find("useinbandfec"), f.parameters.end()); + ASSERT_NE(f.parameters.find("maxplaybackrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("maxplaybackrate"), "1234"); + ASSERT_EQ(f.parameters.find("maxaveragebitrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("usedtx"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("cbr"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("ptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("minptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxptime"), f.parameters.end()); + } +} + +TEST_F(AudioConduitTest, TestConfigureSendOpusMaxAverageBitrate) { + mControl.Update([&](auto& aControl) { + AudioCodecConfig codecConfig = + AudioCodecConfig(114, "opus", 48000, 2, false); + codecConfig.mMaxAverageBitrate = 12345; + aControl.mAudioSendCodec = Some(codecConfig); + aControl.mTransmitting = true; + }); + + ASSERT_TRUE(Call()->mAudioSendConfig); + { + const webrtc::SdpAudioFormat& f = + Call()->mAudioSendConfig->send_codec_spec->format; + ASSERT_EQ(f.name, "opus"); + ASSERT_EQ(f.clockrate_hz, 48000); + ASSERT_EQ(f.num_channels, 2UL); + ASSERT_NE(f.parameters.find("stereo"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("stereo"), "1"); + ASSERT_EQ(f.parameters.find("useinbandfec"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxplaybackrate"), f.parameters.end()); + ASSERT_NE(f.parameters.find("maxaveragebitrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("maxaveragebitrate"), "12345"); + ASSERT_EQ(f.parameters.find("usedtx"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("cbr"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("ptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("minptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxptime"), f.parameters.end()); + } +} + +TEST_F(AudioConduitTest, TestConfigureSendOpusDtx) { + mControl.Update([&](auto& aControl) { + AudioCodecConfig codecConfig = + AudioCodecConfig(114, "opus", 48000, 2, false); + codecConfig.mDTXEnabled = true; + aControl.mAudioSendCodec = Some(codecConfig); + aControl.mTransmitting = true; + }); + + ASSERT_TRUE(Call()->mAudioSendConfig); + { + const webrtc::SdpAudioFormat& f = + Call()->mAudioSendConfig->send_codec_spec->format; + ASSERT_EQ(f.name, "opus"); + ASSERT_EQ(f.clockrate_hz, 48000); + ASSERT_EQ(f.num_channels, 2UL); + ASSERT_NE(f.parameters.find("stereo"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("stereo"), "1"); + ASSERT_EQ(f.parameters.find("useinbandfec"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxplaybackrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxaveragebitrate"), f.parameters.end()); + ASSERT_NE(f.parameters.find("usedtx"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("usedtx"), "1"); + ASSERT_EQ(f.parameters.find("cbr"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("ptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("minptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxptime"), f.parameters.end()); + } +} + +TEST_F(AudioConduitTest, TestConfigureSendOpusCbr) { + mControl.Update([&](auto& aControl) { + AudioCodecConfig codecConfig = + AudioCodecConfig(114, "opus", 48000, 2, false); + codecConfig.mCbrEnabled = true; + aControl.mAudioSendCodec = Some(codecConfig); + aControl.mTransmitting = true; + }); + + ASSERT_TRUE(Call()->mAudioSendConfig); + { + const webrtc::SdpAudioFormat& f = + Call()->mAudioSendConfig->send_codec_spec->format; + ASSERT_EQ(f.name, "opus"); + ASSERT_EQ(f.clockrate_hz, 48000); + ASSERT_EQ(f.num_channels, 2UL); + ASSERT_NE(f.parameters.find("stereo"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("stereo"), "1"); + ASSERT_EQ(f.parameters.find("useinbandfec"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxplaybackrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxaveragebitrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("usedtx"), f.parameters.end()); + ASSERT_NE(f.parameters.find("cbr"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("cbr"), "1"); + ASSERT_EQ(f.parameters.find("ptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("minptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxptime"), f.parameters.end()); + } +} + +TEST_F(AudioConduitTest, TestConfigureSendOpusPtime) { + mControl.Update([&](auto& aControl) { + AudioCodecConfig codecConfig = + AudioCodecConfig(114, "opus", 48000, 2, false); + codecConfig.mFrameSizeMs = 100; + aControl.mAudioSendCodec = Some(codecConfig); + aControl.mTransmitting = true; + }); + + ASSERT_TRUE(Call()->mAudioSendConfig); + { + const webrtc::SdpAudioFormat& f = + Call()->mAudioSendConfig->send_codec_spec->format; + ASSERT_EQ(f.name, "opus"); + ASSERT_EQ(f.clockrate_hz, 48000); + ASSERT_EQ(f.num_channels, 2UL); + ASSERT_NE(f.parameters.find("stereo"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("stereo"), "1"); + ASSERT_EQ(f.parameters.find("useinbandfec"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxplaybackrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxaveragebitrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("usedtx"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("cbr"), f.parameters.end()); + ASSERT_NE(f.parameters.find("ptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("ptime"), "100"); + ASSERT_EQ(f.parameters.find("minptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxptime"), f.parameters.end()); + } +} + +TEST_F(AudioConduitTest, TestConfigureSendOpusMinPtime) { + mControl.Update([&](auto& aControl) { + AudioCodecConfig codecConfig = + AudioCodecConfig(114, "opus", 48000, 2, false); + codecConfig.mMinFrameSizeMs = 201; + aControl.mAudioSendCodec = Some(codecConfig); + aControl.mTransmitting = true; + }); + + ASSERT_TRUE(Call()->mAudioSendConfig); + { + const webrtc::SdpAudioFormat& f = + Call()->mAudioSendConfig->send_codec_spec->format; + ASSERT_EQ(f.name, "opus"); + ASSERT_EQ(f.clockrate_hz, 48000); + ASSERT_EQ(f.num_channels, 2UL); + ASSERT_NE(f.parameters.find("stereo"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("stereo"), "1"); + ASSERT_EQ(f.parameters.find("useinbandfec"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxplaybackrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxaveragebitrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("usedtx"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("cbr"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("ptime"), f.parameters.end()); + ASSERT_NE(f.parameters.find("minptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("minptime"), "201"); + ASSERT_EQ(f.parameters.find("maxptime"), f.parameters.end()); + } +} + +TEST_F(AudioConduitTest, TestConfigureSendOpusMaxPtime) { + mControl.Update([&](auto& aControl) { + AudioCodecConfig codecConfig = + AudioCodecConfig(114, "opus", 48000, 2, false); + codecConfig.mMaxFrameSizeMs = 321; + aControl.mAudioSendCodec = Some(codecConfig); + aControl.mTransmitting = true; + }); + + ASSERT_TRUE(Call()->mAudioSendConfig); + { + const webrtc::SdpAudioFormat& f = + Call()->mAudioSendConfig->send_codec_spec->format; + ASSERT_EQ(f.name, "opus"); + ASSERT_EQ(f.clockrate_hz, 48000); + ASSERT_EQ(f.num_channels, 2UL); + ASSERT_NE(f.parameters.find("stereo"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("stereo"), "1"); + ASSERT_EQ(f.parameters.find("useinbandfec"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxplaybackrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxaveragebitrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("usedtx"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("cbr"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("ptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("minptime"), f.parameters.end()); + ASSERT_NE(f.parameters.find("maxptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("maxptime"), "321"); + } +} + +TEST_F(AudioConduitTest, TestConfigureSendOpusAllParams) { + mControl.Update([&](auto& aControl) { + AudioCodecConfig codecConfig = + AudioCodecConfig(114, "opus", 48000, 2, true); + codecConfig.mMaxPlaybackRate = 5432; + codecConfig.mMaxAverageBitrate = 54321; + codecConfig.mDTXEnabled = true; + codecConfig.mCbrEnabled = true; + codecConfig.mFrameSizeMs = 999; + codecConfig.mMinFrameSizeMs = 123; + codecConfig.mMaxFrameSizeMs = 789; + aControl.mAudioSendCodec = Some(codecConfig); + aControl.mTransmitting = true; + }); + + ASSERT_TRUE(Call()->mAudioSendConfig); + { + const webrtc::SdpAudioFormat& f = + Call()->mAudioSendConfig->send_codec_spec->format; + ASSERT_EQ(f.name, "opus"); + ASSERT_EQ(f.clockrate_hz, 48000); + ASSERT_EQ(f.num_channels, 2UL); + ASSERT_NE(f.parameters.find("stereo"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("stereo"), "1"); + ASSERT_NE(f.parameters.find("useinbandfec"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("useinbandfec"), "1"); + ASSERT_NE(f.parameters.find("maxplaybackrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("maxplaybackrate"), "5432"); + ASSERT_NE(f.parameters.find("maxaveragebitrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("maxaveragebitrate"), "54321"); + ASSERT_NE(f.parameters.find("usedtx"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("usedtx"), "1"); + ASSERT_NE(f.parameters.find("cbr"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("cbr"), "1"); + ASSERT_NE(f.parameters.find("ptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("ptime"), "999"); + ASSERT_NE(f.parameters.find("minptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("minptime"), "123"); + ASSERT_NE(f.parameters.find("maxptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("maxptime"), "789"); + } +} + +TEST_F(AudioConduitTest, TestConfigureReceiveMediaCodecs) { + mControl.Update([&](auto& aControl) { + // just default opus stereo + std::vector<mozilla::AudioCodecConfig> codecs; + codecs.emplace_back(AudioCodecConfig(114, "opus", 48000, 2, false)); + aControl.mAudioRecvCodecs = codecs; + aControl.mReceiving = true; + }); + ASSERT_TRUE(Call()->mAudioReceiveConfig); + ASSERT_EQ(Call()->mAudioReceiveConfig->sync_group, ""); + ASSERT_EQ(Call()->mAudioReceiveConfig->decoder_map.size(), 1U); + { + const webrtc::SdpAudioFormat& f = + Call()->mAudioReceiveConfig->decoder_map.at(114); + ASSERT_EQ(f.name, "opus"); + ASSERT_EQ(f.clockrate_hz, 48000); + ASSERT_EQ(f.num_channels, 2UL); + ASSERT_EQ(f.parameters.at("stereo"), "1"); + ASSERT_EQ(f.parameters.find("maxplaybackrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("useinbandfec"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxaveragebitrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("usedtx"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("cbr"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("ptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("minptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxptime"), f.parameters.end()); + } + + mControl.Update([&](auto& aControl) { + // multiple codecs + std::vector<mozilla::AudioCodecConfig> codecs; + codecs.emplace_back(AudioCodecConfig(9, "g722", 16000, 2, false)); + codecs.emplace_back(AudioCodecConfig(114, "opus", 48000, 2, false)); + aControl.mAudioRecvCodecs = codecs; + aControl.mReceiving = true; + }); + ASSERT_TRUE(Call()->mAudioReceiveConfig); + ASSERT_EQ(Call()->mAudioReceiveConfig->sync_group, ""); + ASSERT_EQ(Call()->mAudioReceiveConfig->decoder_map.size(), 2U); + { + const webrtc::SdpAudioFormat& f = + Call()->mAudioReceiveConfig->decoder_map.at(9); + ASSERT_EQ(f.name, "g722"); + ASSERT_EQ(f.clockrate_hz, 16000); + ASSERT_EQ(f.num_channels, 2U); + ASSERT_EQ(f.parameters.size(), 0U); + } + { + const webrtc::SdpAudioFormat& f = + Call()->mAudioReceiveConfig->decoder_map.at(114); + ASSERT_EQ(f.name, "opus"); + ASSERT_EQ(f.clockrate_hz, 48000); + ASSERT_EQ(f.num_channels, 2U); + ASSERT_EQ(f.parameters.at("stereo"), "1"); + } + + mControl.Update([&](auto& aControl) { + // no codecs + std::vector<mozilla::AudioCodecConfig> codecs; + aControl.mAudioRecvCodecs = codecs; + }); + ASSERT_TRUE(Call()->mAudioReceiveConfig); + ASSERT_EQ(Call()->mAudioReceiveConfig->decoder_map.size(), 0U); + + mControl.Update([&](auto& aControl) { + // invalid codec name + std::vector<mozilla::AudioCodecConfig> codecs; + codecs.emplace_back(AudioCodecConfig(114, "", 48000, 2, false)); + aControl.mAudioRecvCodecs = codecs; + }); + ASSERT_TRUE(Call()->mAudioReceiveConfig); + ASSERT_EQ(Call()->mAudioReceiveConfig->decoder_map.size(), 0U); + + mControl.Update([&](auto& aControl) { + // invalid number of channels + std::vector<mozilla::AudioCodecConfig> codecs; + codecs.emplace_back(AudioCodecConfig(114, "opus", 48000, 42, false)); + aControl.mAudioRecvCodecs = codecs; + }); + ASSERT_TRUE(Call()->mAudioReceiveConfig); + ASSERT_EQ(Call()->mAudioReceiveConfig->decoder_map.size(), 0U); +} + +TEST_F(AudioConduitTest, TestConfigureReceiveOpusMono) { + mControl.Update([&](auto& aControl) { + // opus mono + std::vector<mozilla::AudioCodecConfig> codecs; + codecs.emplace_back(AudioCodecConfig(114, "opus", 48000, 1, false)); + aControl.mAudioRecvCodecs = codecs; + aControl.mReceiving = true; + }); + ASSERT_TRUE(Call()->mAudioReceiveConfig); + ASSERT_EQ(Call()->mAudioReceiveConfig->sync_group, ""); + ASSERT_EQ(Call()->mAudioReceiveConfig->decoder_map.size(), 1U); + { + const webrtc::SdpAudioFormat& f = + Call()->mAudioReceiveConfig->decoder_map.at(114); + ASSERT_EQ(f.name, "opus"); + ASSERT_EQ(f.clockrate_hz, 48000); + ASSERT_EQ(f.num_channels, 1UL); + ASSERT_EQ(f.parameters.find("stereo"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxplaybackrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("useinbandfec"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxaveragebitrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("usedtx"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("cbr"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("ptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("minptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxptime"), f.parameters.end()); + } +} + +TEST_F(AudioConduitTest, TestConfigureReceiveOpusDtx) { + mControl.Update([&](auto& aControl) { + // opus mono + std::vector<mozilla::AudioCodecConfig> codecs; + codecs.emplace_back(AudioCodecConfig(114, "opus", 48000, 2, false)); + codecs[0].mDTXEnabled = true; + aControl.mAudioRecvCodecs = codecs; + aControl.mReceiving = true; + }); + ASSERT_TRUE(Call()->mAudioReceiveConfig); + ASSERT_EQ(Call()->mAudioReceiveConfig->sync_group, ""); + ASSERT_EQ(Call()->mAudioReceiveConfig->decoder_map.size(), 1U); + { + const webrtc::SdpAudioFormat& f = + Call()->mAudioReceiveConfig->decoder_map.at(114); + ASSERT_EQ(f.name, "opus"); + ASSERT_EQ(f.clockrate_hz, 48000); + ASSERT_EQ(f.num_channels, 2UL); + ASSERT_NE(f.parameters.find("stereo"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("stereo"), "1"); + ASSERT_EQ(f.parameters.find("maxplaybackrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("useinbandfec"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxaveragebitrate"), f.parameters.end()); + ASSERT_NE(f.parameters.find("usedtx"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("usedtx"), "1"); + ASSERT_EQ(f.parameters.find("cbr"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("ptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("minptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxptime"), f.parameters.end()); + } +} + +TEST_F(AudioConduitTest, TestConfigureReceiveOpusFEC) { + mControl.Update([&](auto& aControl) { + // opus with inband Forward Error Correction + std::vector<mozilla::AudioCodecConfig> codecs; + codecs.emplace_back(AudioCodecConfig(114, "opus", 48000, 2, true)); + aControl.mAudioRecvCodecs = codecs; + aControl.mReceiving = true; + }); + ASSERT_TRUE(Call()->mAudioReceiveConfig); + ASSERT_EQ(Call()->mAudioReceiveConfig->sync_group, ""); + ASSERT_EQ(Call()->mAudioReceiveConfig->decoder_map.size(), 1U); + { + const webrtc::SdpAudioFormat& f = + Call()->mAudioReceiveConfig->decoder_map.at(114); + ASSERT_EQ(f.name, "opus"); + ASSERT_EQ(f.clockrate_hz, 48000); + ASSERT_EQ(f.num_channels, 2UL); + ASSERT_NE(f.parameters.find("stereo"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("stereo"), "1"); + ASSERT_NE(f.parameters.find("useinbandfec"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("useinbandfec"), "1"); + ASSERT_EQ(f.parameters.find("maxplaybackrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxaveragebitrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("usedtx"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("cbr"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("ptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("minptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxptime"), f.parameters.end()); + } +} + +TEST_F(AudioConduitTest, TestConfigureReceiveOpusMaxPlaybackRate) { + std::vector<mozilla::AudioCodecConfig> codecs; + codecs.emplace_back(AudioCodecConfig(114, "opus", 48000, 2, false)); + + mControl.Update([&](auto& aControl) { + codecs[0].mMaxPlaybackRate = 0; + aControl.mAudioRecvCodecs = codecs; + aControl.mReceiving = true; + }); + ASSERT_TRUE(Call()->mAudioReceiveConfig); + ASSERT_EQ(Call()->mAudioReceiveConfig->decoder_map.size(), 1U); + { + const webrtc::SdpAudioFormat& f = + Call()->mAudioReceiveConfig->decoder_map.at(114); + ASSERT_EQ(f.name, "opus"); + ASSERT_EQ(f.clockrate_hz, 48000); + ASSERT_EQ(f.num_channels, 2UL); + ASSERT_EQ(f.parameters.at("stereo"), "1"); + ASSERT_EQ(f.parameters.find("useinbandfec"), f.parameters.end()); + ASSERT_EQ(f.parameters.count("maxplaybackrate"), 0U); + ASSERT_EQ(f.parameters.find("maxaveragebitrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("usedtx"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("cbr"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("ptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("minptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxptime"), f.parameters.end()); + } + + mControl.Update([&](auto& aControl) { + codecs[0].mMaxPlaybackRate = 8000; + aControl.mAudioRecvCodecs = codecs; + }); + ASSERT_TRUE(Call()->mAudioReceiveConfig); + ASSERT_EQ(Call()->mAudioReceiveConfig->decoder_map.size(), 1U); + { + const webrtc::SdpAudioFormat& f = + Call()->mAudioReceiveConfig->decoder_map.at(114); + ASSERT_EQ(f.name, "opus"); + ASSERT_EQ(f.clockrate_hz, 48000); + ASSERT_EQ(f.num_channels, 2UL); + ASSERT_EQ(f.parameters.at("stereo"), "1"); + ASSERT_EQ(f.parameters.at("maxplaybackrate"), "8000"); + ASSERT_EQ(f.parameters.find("useinbandfec"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxaveragebitrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("usedtx"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("cbr"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("ptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("minptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxptime"), f.parameters.end()); + } +} + +TEST_F(AudioConduitTest, TestConfigureReceiveOpusMaxAverageBitrate) { + std::vector<mozilla::AudioCodecConfig> codecs; + codecs.emplace_back(AudioCodecConfig(114, "opus", 48000, 2, false)); + mControl.Update([&](auto& aControl) { + codecs[0].mMaxAverageBitrate = 0; + aControl.mAudioRecvCodecs = codecs; + aControl.mReceiving = true; + }); + ASSERT_TRUE(Call()->mAudioReceiveConfig); + ASSERT_EQ(Call()->mAudioReceiveConfig->decoder_map.size(), 1U); + { + const webrtc::SdpAudioFormat& f = + Call()->mAudioReceiveConfig->decoder_map.at(114); + ASSERT_EQ(f.name, "opus"); + ASSERT_EQ(f.clockrate_hz, 48000); + ASSERT_EQ(f.num_channels, 2UL); + ASSERT_EQ(f.parameters.at("stereo"), "1"); + ASSERT_EQ(f.parameters.find("useinbandfec"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxplaybackrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.count("maxaveragebitrate"), 0U); + ASSERT_EQ(f.parameters.find("usedtx"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("cbr"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("ptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("minptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxptime"), f.parameters.end()); + } + + mControl.Update([&](auto& aControl) { + codecs[0].mMaxAverageBitrate = 8000; + aControl.mAudioRecvCodecs = codecs; + }); + ASSERT_TRUE(Call()->mAudioReceiveConfig); + ASSERT_EQ(Call()->mAudioReceiveConfig->decoder_map.size(), 1U); + { + const webrtc::SdpAudioFormat& f = + Call()->mAudioReceiveConfig->decoder_map.at(114); + ASSERT_EQ(f.name, "opus"); + ASSERT_EQ(f.clockrate_hz, 48000); + ASSERT_EQ(f.num_channels, 2UL); + ASSERT_EQ(f.parameters.at("stereo"), "1"); + ASSERT_EQ(f.parameters.find("useinbandfec"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxplaybackrate"), f.parameters.end()); + ASSERT_EQ(f.parameters.at("maxaveragebitrate"), "8000"); + ASSERT_EQ(f.parameters.find("usedtx"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("cbr"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("ptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("minptime"), f.parameters.end()); + ASSERT_EQ(f.parameters.find("maxptime"), f.parameters.end()); + } +} + +TEST_F(AudioConduitTest, TestConfigureReceiveOpusAllParameters) { + std::vector<mozilla::AudioCodecConfig> codecs; + codecs.emplace_back(AudioCodecConfig(114, "opus", 48000, 2, true)); + + mControl.Update([&](auto& aControl) { + codecs[0].mMaxPlaybackRate = 8000; + codecs[0].mMaxAverageBitrate = 9000; + codecs[0].mDTXEnabled = true; + codecs[0].mCbrEnabled = true; + codecs[0].mFrameSizeMs = 10; + codecs[0].mMinFrameSizeMs = 20; + codecs[0].mMaxFrameSizeMs = 30; + + aControl.mAudioRecvCodecs = codecs; + aControl.mReceiving = true; + }); + ASSERT_TRUE(Call()->mAudioReceiveConfig); + ASSERT_EQ(Call()->mAudioReceiveConfig->decoder_map.size(), 1U); + { + const webrtc::SdpAudioFormat& f = + Call()->mAudioReceiveConfig->decoder_map.at(114); + ASSERT_EQ(f.name, "opus"); + ASSERT_EQ(f.clockrate_hz, 48000); + ASSERT_EQ(f.num_channels, 2UL); + ASSERT_EQ(f.parameters.at("stereo"), "1"); + ASSERT_EQ(f.parameters.at("useinbandfec"), "1"); + ASSERT_EQ(f.parameters.at("maxplaybackrate"), "8000"); + ASSERT_EQ(f.parameters.at("maxaveragebitrate"), "9000"); + ASSERT_EQ(f.parameters.at("usedtx"), "1"); + ASSERT_EQ(f.parameters.at("cbr"), "1"); + ASSERT_EQ(f.parameters.at("ptime"), "10"); + ASSERT_EQ(f.parameters.at("minptime"), "20"); + ASSERT_EQ(f.parameters.at("maxptime"), "30"); + } +} + +TEST_F(AudioConduitTest, TestSetLocalRTPExtensions) { + // Empty extensions + mControl.Update([&](auto& aControl) { + RtpExtList extensions; + aControl.mLocalRecvRtpExtensions = extensions; + aControl.mReceiving = true; + aControl.mLocalSendRtpExtensions = extensions; + aControl.mTransmitting = true; + }); + ASSERT_TRUE(Call()->mAudioReceiveConfig); + ASSERT_TRUE(Call()->mAudioReceiveConfig->rtp.extensions.empty()); + ASSERT_TRUE(Call()->mAudioSendConfig); + ASSERT_TRUE(Call()->mAudioSendConfig->rtp.extensions.empty()); + + // Audio level + mControl.Update([&](auto& aControl) { + RtpExtList extensions; + webrtc::RtpExtension extension; + extension.uri = webrtc::RtpExtension::kAudioLevelUri; + extensions.emplace_back(extension); + aControl.mLocalRecvRtpExtensions = extensions; + aControl.mLocalSendRtpExtensions = extensions; + }); + ASSERT_TRUE(Call()->mAudioReceiveConfig); + ASSERT_EQ(Call()->mAudioReceiveConfig->rtp.extensions.back().uri, + webrtc::RtpExtension::kAudioLevelUri); + ASSERT_TRUE(Call()->mAudioSendConfig); + ASSERT_EQ(Call()->mAudioSendConfig->rtp.extensions.back().uri, + webrtc::RtpExtension::kAudioLevelUri); + + // Contributing sources audio level + mControl.Update([&](auto& aControl) { + // We do not support configuring sending csrc-audio-level. It will be + // ignored. + RtpExtList extensions; + webrtc::RtpExtension extension; + extension.uri = webrtc::RtpExtension::kCsrcAudioLevelsUri; + extensions.emplace_back(extension); + aControl.mLocalRecvRtpExtensions = extensions; + aControl.mLocalSendRtpExtensions = extensions; + }); + ASSERT_TRUE(Call()->mAudioReceiveConfig); + ASSERT_EQ(Call()->mAudioReceiveConfig->rtp.extensions.back().uri, + webrtc::RtpExtension::kCsrcAudioLevelsUri); + ASSERT_TRUE(Call()->mAudioSendConfig); + ASSERT_TRUE(Call()->mAudioSendConfig->rtp.extensions.empty()); + + // Mid + mControl.Update([&](auto& aControl) { + // We do not support configuring receiving MId. It will be ignored. + RtpExtList extensions; + webrtc::RtpExtension extension; + extension.uri = webrtc::RtpExtension::kMidUri; + extensions.emplace_back(extension); + aControl.mLocalRecvRtpExtensions = extensions; + aControl.mLocalSendRtpExtensions = extensions; + }); + ASSERT_TRUE(Call()->mAudioReceiveConfig); + ASSERT_TRUE(Call()->mAudioReceiveConfig->rtp.extensions.empty()); + ASSERT_EQ(Call()->mAudioSendConfig->rtp.extensions.back().uri, + webrtc::RtpExtension::kMidUri); +} + +TEST_F(AudioConduitTest, TestSyncGroup) { + mControl.Update([&](auto& aControl) { + aControl.mSyncGroup = "test"; + aControl.mReceiving = true; + }); + ASSERT_TRUE(Call()->mAudioReceiveConfig); + ASSERT_EQ(Call()->mAudioReceiveConfig->sync_group, "test"); +} + +} // End namespace test. diff --git a/media/webrtc/signaling/gtest/jsep_session_unittest.cpp b/media/webrtc/signaling/gtest/jsep_session_unittest.cpp new file mode 100644 index 0000000000..e9062ad971 --- /dev/null +++ b/media/webrtc/signaling/gtest/jsep_session_unittest.cpp @@ -0,0 +1,7609 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <iostream> +#include <map> + +#include "nss.h" +#include "ssl.h" + +#include "mozilla/Preferences.h" +#include "mozilla/RefPtr.h" + +#define GTEST_HAS_RTTI 0 +#include "gtest/gtest.h" + +#include "PeerConnectionImpl.h" +#include "sdp/SdpMediaSection.h" +#include "sdp/SipccSdpParser.h" +#include "jsep/JsepCodecDescription.h" +#include "jsep/JsepTrack.h" +#include "jsep/JsepSession.h" +#include "jsep/JsepSessionImpl.h" + +namespace mozilla { +static std::string kAEqualsCandidate("a=candidate:"); +const static size_t kNumCandidatesPerComponent = 3; + +class JsepSessionTestBase : public ::testing::Test { + public: + static void SetUpTestCase() { + NSS_NoDB_Init(nullptr); + NSS_SetDomesticPolicy(); + } +}; + +class FakeUuidGenerator : public mozilla::JsepUuidGenerator { + public: + bool Generate(std::string* str) { + std::ostringstream os; + os << "FAKE_UUID_" << ++ctr; + *str = os.str(); + + return true; + } + + mozilla::JsepUuidGenerator* Clone() const { + return new FakeUuidGenerator(*this); + } + + private: + static uint64_t ctr; +}; + +uint64_t FakeUuidGenerator::ctr = 1000; + +class JsepSessionTest : public JsepSessionTestBase, + public ::testing::WithParamInterface<std::string> { + public: + JsepSessionTest() : mSdpHelper(&mLastError) { + Preferences::SetCString("media.peerconnection.sdp.parser", "legacy"); + Preferences::SetCString("media.peerconnection.sdp.alternate_parse_mode", + "never"); + Preferences::SetBool("media.peerconnection.video.use_rtx", true); + Preferences::SetBool("media.navigator.video.use_transport_cc", true); + + mSessionOff = + MakeUnique<JsepSessionImpl>("Offerer", MakeUnique<FakeUuidGenerator>()); + mSessionAns = MakeUnique<JsepSessionImpl>("Answerer", + MakeUnique<FakeUuidGenerator>()); + + EXPECT_EQ(NS_OK, mSessionOff->Init()); + EXPECT_EQ(NS_OK, mSessionAns->Init()); + + std::vector<UniquePtr<JsepCodecDescription>> preferredCodecs; + PeerConnectionImpl::SetupPreferredCodecs(preferredCodecs); + mSessionOff->SetDefaultCodecs(preferredCodecs); + mSessionAns->SetDefaultCodecs(preferredCodecs); + + std::vector<PeerConnectionImpl::RtpExtensionHeader> preferredHeaders; + PeerConnectionImpl::SetupPreferredRtpExtensions(preferredHeaders); + + for (const auto& header : preferredHeaders) { + mSessionOff->AddRtpExtension(header.mMediaType, header.extensionname, + header.direction); + mSessionAns->AddRtpExtension(header.mMediaType, header.extensionname, + header.direction); + } + + mOffererTransport = MakeUnique<TransportData>(); + mAnswererTransport = MakeUnique<TransportData>(); + + AddTransportData(*mSessionOff, *mOffererTransport); + AddTransportData(*mSessionAns, *mAnswererTransport); + + mOffCandidates = MakeUnique<CandidateSet>(); + mAnsCandidates = MakeUnique<CandidateSet>(); + } + + static std::vector<JsepTransceiver>& GetTransceivers(JsepSession& aSession) { + return aSession.GetTransceivers(); + } + + static const std::vector<JsepTransceiver>& GetTransceivers( + const JsepSession& aSession) { + return aSession.GetTransceivers(); + } + + protected: + struct TransportData { + std::map<std::string, std::vector<uint8_t>> mFingerprints; + }; + + void AddDtlsFingerprint(const std::string& alg, JsepSessionImpl& session, + TransportData& tdata) { + std::vector<uint8_t> fp; + fp.assign((alg == "sha-1") ? 20 : 32, + (session.GetName() == "Offerer") ? 0x4f : 0x41); + session.AddDtlsFingerprint(alg, fp); + tdata.mFingerprints[alg] = fp; + } + + void AddTransportData(JsepSessionImpl& session, TransportData& tdata) { + AddDtlsFingerprint("sha-1", session, tdata); + AddDtlsFingerprint("sha-256", session, tdata); + } + + void CheckTransceiverInvariants( + const std::vector<JsepTransceiver>& oldTransceivers, + const std::vector<JsepTransceiver>& newTransceivers) { + ASSERT_LE(oldTransceivers.size(), newTransceivers.size()); + std::set<size_t> levels; + + for (const auto& newTransceiver : newTransceivers) { + if (newTransceiver.HasLevel()) { + ASSERT_FALSE(levels.count(newTransceiver.GetLevel())) + << "Two new transceivers are mapped to level " + << newTransceiver.GetLevel(); + levels.insert(newTransceiver.GetLevel()); + } + } + + auto last = levels.rbegin(); + if (last != levels.rend()) { + ASSERT_LE(*last, levels.size()) + << "Max level observed in transceivers was " << *last + << ", but there are only " << levels.size() + << " levels in the " + "transceivers."; + } + + for (const auto& oldTransceiver : oldTransceivers) { + if (oldTransceiver.HasLevel()) { + ASSERT_TRUE(levels.count(oldTransceiver.GetLevel())) + << "Level " << oldTransceiver.GetLevel() + << " had a transceiver in the old, but not the new (or, " + "perhaps this level had more than one transceiver in the " + "old)"; + levels.erase(oldTransceiver.GetLevel()); + } + } + } + + std::string CreateOffer(const Maybe<JsepOfferOptions>& options = Nothing()) { + std::vector<JsepTransceiver> transceiversBefore = + GetTransceivers(*mSessionOff); + JsepOfferOptions defaultOptions; + const JsepOfferOptions& optionsRef = options ? *options : defaultOptions; + std::string offer; + JsepSession::Result result = mSessionOff->CreateOffer(optionsRef, &offer); + EXPECT_FALSE(result.mError.isSome()) << mSessionOff->GetLastError(); + + std::cerr << "OFFER: " << offer << std::endl; + + ValidateTransport(*mOffererTransport, offer, sdp::kOffer); + + if (transceiversBefore.size() != GetTransceivers(*mSessionOff).size()) { + EXPECT_TRUE(false) << "CreateOffer changed number of transceivers!"; + return offer; + } + + CheckTransceiverInvariants(transceiversBefore, + GetTransceivers(*mSessionOff)); + + for (size_t i = 0; i < transceiversBefore.size(); ++i) { + JsepTransceiver oldTransceiver = transceiversBefore[i]; + JsepTransceiver newTransceiver = GetTransceivers(*mSessionOff)[i]; + EXPECT_EQ(oldTransceiver.IsStopped(), newTransceiver.IsStopped()); + + if (oldTransceiver.IsStopped()) { + if (!newTransceiver.HasLevel()) { + // Tolerate unmapping of stopped transceivers by removing this + // difference. + oldTransceiver.ClearLevel(); + } + } else if (!oldTransceiver.HasLevel()) { + EXPECT_TRUE(newTransceiver.HasLevel()); + // Tolerate new mappings. + oldTransceiver.SetLevel(newTransceiver.GetLevel()); + } + + EXPECT_TRUE(Equals(oldTransceiver, newTransceiver)); + } + + return offer; + } + + typedef enum { NO_ADDTRACK_MAGIC, ADDTRACK_MAGIC } AddTrackMagic; + + void AddTracks(JsepSessionImpl& side, AddTrackMagic magic = ADDTRACK_MAGIC) { + // Add tracks. + if (types.empty()) { + types = BuildTypes(GetParam()); + } + AddTracks(side, types, magic); + + // Now, we move datachannel to the end + auto it = + std::find(types.begin(), types.end(), SdpMediaSection::kApplication); + if (it != types.end()) { + types.erase(it); + types.push_back(SdpMediaSection::kApplication); + } + } + + void AddTracks(JsepSessionImpl& side, const std::string& mediatypes, + AddTrackMagic magic = ADDTRACK_MAGIC) { + AddTracks(side, BuildTypes(mediatypes), magic); + } + + JsepTrack RemoveTrack(JsepSession& side, size_t index) { + if (GetTransceivers(side).size() <= index) { + EXPECT_TRUE(false) << "Index " << index << " out of bounds!"; + return JsepTrack(SdpMediaSection::kAudio, sdp::kSend); + } + + JsepTransceiver transceiver(GetTransceivers(side)[index]); + JsepTrack& track = transceiver.mSendTrack; + EXPECT_FALSE(track.GetStreamIds().empty()) << "No track at index " << index; + + JsepTrack original(track); + track.ClearStreamIds(); + transceiver.mJsDirection &= SdpDirectionAttribute::Direction::kRecvonly; + side.SetTransceiver(transceiver); + return original; + } + + void SetDirection(JsepSession& side, size_t index, + SdpDirectionAttribute::Direction direction) { + ASSERT_LT(index, GetTransceivers(side).size()) + << "Index " << index << " out of bounds!"; + + auto transceiver = GetTransceivers(side)[index]; + transceiver.mJsDirection = direction; + side.SetTransceiver(transceiver); + } + + std::vector<SdpMediaSection::MediaType> BuildTypes( + const std::string& mediatypes) { + std::vector<SdpMediaSection::MediaType> result; + size_t ptr = 0; + + for (;;) { + size_t comma = mediatypes.find(',', ptr); + std::string chunk = mediatypes.substr(ptr, comma - ptr); + + if (chunk == "audio") { + result.push_back(SdpMediaSection::kAudio); + } else if (chunk == "video") { + result.push_back(SdpMediaSection::kVideo); + } else if (chunk == "datachannel") { + result.push_back(SdpMediaSection::kApplication); + } else { + MOZ_CRASH(); + } + + if (comma == std::string::npos) break; + ptr = comma + 1; + } + + return result; + } + + void AddTracks(JsepSessionImpl& side, + const std::vector<SdpMediaSection::MediaType>& mediatypes, + AddTrackMagic magic = ADDTRACK_MAGIC) { + std::string stream_id; + std::string track_id; + + ASSERT_TRUE(mUuidGen.Generate(&stream_id)); + + AddTracksToStream(side, stream_id, mediatypes, magic); + } + + void AddTracksToStream(JsepSessionImpl& side, const std::string stream_id, + const std::string& mediatypes, + AddTrackMagic magic = ADDTRACK_MAGIC) { + AddTracksToStream(side, stream_id, BuildTypes(mediatypes), magic); + } + + // A bit of a hack. JsepSessionImpl populates the track-id automatically, just + // in case, because the w3c spec requires msid to be set even when there's no + // send track. + bool IsNull(const JsepTrack& track) const { + return track.GetStreamIds().empty() && + (track.GetMediaType() != SdpMediaSection::MediaType::kApplication); + } + + void AddTracksToStream( + JsepSessionImpl& side, const std::string stream_id, + const std::vector<SdpMediaSection::MediaType>& mediatypes, + AddTrackMagic magic = ADDTRACK_MAGIC) + + { + std::string track_id; + + for (auto type : mediatypes) { + ASSERT_TRUE(mUuidGen.Generate(&track_id)); + + Maybe<JsepTransceiver> suitableTransceiver; + size_t i; + if (magic == ADDTRACK_MAGIC) { + // We're simulating addTrack. + for (i = 0; i < GetTransceivers(side).size(); ++i) { + auto transceiver = GetTransceivers(side)[i]; + if (transceiver.mSendTrack.GetMediaType() != type) { + continue; + } + + if (IsNull(transceiver.mSendTrack) || + transceiver.GetMediaType() == SdpMediaSection::kApplication) { + suitableTransceiver = Some(transceiver); + break; + } + } + } + + if (!suitableTransceiver) { + i = GetTransceivers(side).size(); + side.AddTransceiver(JsepTransceiver(type, mUuidGen)); + suitableTransceiver = Some(GetTransceivers(side).back()); + if (magic == ADDTRACK_MAGIC) { + suitableTransceiver->SetAddTrackMagic(); + } + } + + std::cerr << "Updating send track for transceiver " << i << std::endl; + suitableTransceiver->SetOnlyExistsBecauseOfSetRemote(false); + suitableTransceiver->mJsDirection |= + SdpDirectionAttribute::Direction::kSendonly; + suitableTransceiver->mSendTrack.UpdateStreamIds( + std::vector<std::string>(1, stream_id)); + side.SetTransceiver(*suitableTransceiver); + } + } + + bool HasMediaStream(const std::vector<JsepTrack>& tracks) const { + for (const auto& track : tracks) { + if (track.GetMediaType() != SdpMediaSection::kApplication) { + return true; + } + } + return false; + } + + const std::string GetFirstLocalStreamId(JsepSessionImpl& side) const { + auto tracks = GetLocalTracks(side); + return tracks.begin()->GetStreamIds()[0]; + } + + std::vector<JsepTrack> GetLocalTracks(const JsepSession& session) const { + std::vector<JsepTrack> result; + for (const auto& transceiver : GetTransceivers(session)) { + if (!IsNull(transceiver.mSendTrack)) { + result.push_back(transceiver.mSendTrack); + } + } + return result; + } + + std::vector<JsepTrack> GetRemoteTracks(const JsepSession& session) const { + std::vector<JsepTrack> result; + for (const auto& transceiver : GetTransceivers(session)) { + if (!IsNull(transceiver.mRecvTrack)) { + result.push_back(transceiver.mRecvTrack); + } + } + return result; + } + + JsepTransceiver* GetDatachannelTransceiver(JsepSession& side) { + for (auto& transceiver : GetTransceivers(side)) { + if (transceiver.mSendTrack.GetMediaType() == + SdpMediaSection::MediaType::kApplication) { + return &transceiver; + } + } + + return nullptr; + } + + JsepTransceiver* GetNegotiatedTransceiver(JsepSession& side, size_t index) { + for (auto& transceiver : GetTransceivers(side)) { + if (transceiver.mSendTrack.GetNegotiatedDetails() || + transceiver.mRecvTrack.GetNegotiatedDetails()) { + if (index) { + --index; + continue; + } + + return &transceiver; + } + } + + return nullptr; + } + + Maybe<JsepTransceiver> GetTransceiverByLevel( + const std::vector<JsepTransceiver>& transceivers, size_t level) { + for (auto& transceiver : transceivers) { + if (transceiver.HasLevel() && transceiver.GetLevel() == level) { + return Some(transceiver); + } + } + + return Nothing(); + } + + Maybe<JsepTransceiver> GetTransceiverByLevel(JsepSession& side, + size_t level) { + return GetTransceiverByLevel(GetTransceivers(side), level); + } + + std::vector<std::string> GetMediaStreamIds( + const std::vector<JsepTrack>& tracks) const { + std::vector<std::string> ids; + for (const auto& track : tracks) { + // data channels don't have msid's + if (track.GetMediaType() == SdpMediaSection::kApplication) { + continue; + } + ids.insert(ids.end(), track.GetStreamIds().begin(), + track.GetStreamIds().end()); + } + return ids; + } + + std::vector<std::string> GetLocalMediaStreamIds(JsepSessionImpl& side) const { + return GetMediaStreamIds(GetLocalTracks(side)); + } + + std::vector<std::string> GetRemoteMediaStreamIds( + JsepSessionImpl& side) const { + return GetMediaStreamIds(GetRemoteTracks(side)); + } + + std::vector<std::string> sortUniqueStrVector( + std::vector<std::string> in) const { + std::sort(in.begin(), in.end()); + auto it = std::unique(in.begin(), in.end()); + in.resize(std::distance(in.begin(), it)); + return in; + } + + std::vector<std::string> GetLocalUniqueStreamIds( + JsepSessionImpl& side) const { + return sortUniqueStrVector(GetLocalMediaStreamIds(side)); + } + + std::vector<std::string> GetRemoteUniqueStreamIds( + JsepSessionImpl& side) const { + return sortUniqueStrVector(GetRemoteMediaStreamIds(side)); + } + + JsepTrack GetTrack(JsepSessionImpl& side, SdpMediaSection::MediaType type, + size_t index) const { + for (const auto& transceiver : GetTransceivers(side)) { + if (IsNull(transceiver.mSendTrack) || + transceiver.mSendTrack.GetMediaType() != type) { + continue; + } + + if (index != 0) { + --index; + continue; + } + + return transceiver.mSendTrack; + } + + return JsepTrack(type, sdp::kSend); + } + + JsepTrack GetTrackOff(size_t index, SdpMediaSection::MediaType type) { + return GetTrack(*mSessionOff, type, index); + } + + JsepTrack GetTrackAns(size_t index, SdpMediaSection::MediaType type) { + return GetTrack(*mSessionAns, type, index); + } + + bool Equals(const SdpFingerprintAttributeList::Fingerprint& f1, + const SdpFingerprintAttributeList::Fingerprint& f2) const { + if (f1.hashFunc != f2.hashFunc) { + return false; + } + + if (f1.fingerprint != f2.fingerprint) { + return false; + } + + return true; + } + + bool Equals(const SdpFingerprintAttributeList& f1, + const SdpFingerprintAttributeList& f2) const { + if (f1.mFingerprints.size() != f2.mFingerprints.size()) { + return false; + } + + for (size_t i = 0; i < f1.mFingerprints.size(); ++i) { + if (!Equals(f1.mFingerprints[i], f2.mFingerprints[i])) { + return false; + } + } + + return true; + } + + bool Equals(const UniquePtr<JsepDtlsTransport>& t1, + const UniquePtr<JsepDtlsTransport>& t2) const { + if (!t1 && !t2) { + return true; + } + + if (!t1 || !t2) { + return false; + } + + if (!Equals(t1->GetFingerprints(), t2->GetFingerprints())) { + return false; + } + + if (t1->GetRole() != t2->GetRole()) { + return false; + } + + return true; + } + + bool Equals(const UniquePtr<JsepIceTransport>& t1, + const UniquePtr<JsepIceTransport>& t2) const { + if (!t1 && !t2) { + return true; + } + + if (!t1 || !t2) { + return false; + } + + if (t1->GetUfrag() != t2->GetUfrag()) { + return false; + } + + if (t1->GetPassword() != t2->GetPassword()) { + return false; + } + + return true; + } + + bool Equals(const JsepTransport& t1, const JsepTransport& t2) const { + if (t1.mTransportId != t2.mTransportId) { + std::cerr << "Transport id differs: " << t1.mTransportId << " vs " + << t2.mTransportId << std::endl; + return false; + } + + if (t1.mComponents != t2.mComponents) { + std::cerr << "Component count differs" << std::endl; + return false; + } + + if (!Equals(t1.mIce, t2.mIce)) { + std::cerr << "ICE differs" << std::endl; + return false; + } + + return true; + } + + bool Equals(const JsepTrack& t1, const JsepTrack& t2) const { + if (t1.GetMediaType() != t2.GetMediaType()) { + return false; + } + + if (t1.GetDirection() != t2.GetDirection()) { + return false; + } + + if (t1.GetStreamIds() != t2.GetStreamIds()) { + return false; + } + + if (t1.GetActive() != t2.GetActive()) { + return false; + } + + if (t1.GetCNAME() != t2.GetCNAME()) { + return false; + } + + if (t1.GetSsrcs() != t2.GetSsrcs()) { + return false; + } + + return true; + } + + bool Equals(const JsepTransceiver& p1, const JsepTransceiver& p2) const { + if (p1.HasLevel() != p2.HasLevel()) { + std::cerr << "One transceiver has a level, the other doesn't" + << std::endl; + return false; + } + + if (p1.HasLevel() && (p1.GetLevel() != p2.GetLevel())) { + std::cerr << "Level differs: " << p1.GetLevel() << " vs " << p2.GetLevel() + << std::endl; + return false; + } + + // We don't check things like BundleLevel(), since that can change without + // any changes to the transport, which is what we're really interested in. + + if (p1.IsStopped() != p2.IsStopped()) { + std::cerr << "One transceiver is stopped, the other is not" << std::endl; + return false; + } + + if (p1.IsAssociated() != p2.IsAssociated()) { + std::cerr << "One transceiver has a mid, the other doesn't" << std::endl; + return false; + } + + if (p1.IsAssociated() && (p1.GetMid() != p2.GetMid())) { + std::cerr << "mid differs: " << p1.GetMid() << " vs " << p2.GetMid() + << std::endl; + return false; + } + + if (!Equals(p1.mSendTrack, p2.mSendTrack)) { + std::cerr << "Send track differs" << std::endl; + return false; + } + + if (!Equals(p1.mRecvTrack, p2.mRecvTrack)) { + std::cerr << "Receive track differs" << std::endl; + return false; + } + + if (!Equals(p1.mTransport, p2.mTransport)) { + std::cerr << "Transport differs" << std::endl; + return false; + } + + return true; + } + + bool Equals(const std::vector<JsepTransceiver>& t1, + const std::vector<JsepTransceiver>& t2) const { + if (t1.size() != t2.size()) { + std::cerr << "Size differs: t1.size = " << t1.size() + << ", t2.size = " << t2.size() << std::endl; + return false; + } + + for (size_t i = 0; i < t1.size(); ++i) { + if (!Equals(t1[i], t2[i])) { + return false; + } + } + + return true; + } + + size_t GetTrackCount(JsepSessionImpl& side, + SdpMediaSection::MediaType type) const { + size_t result = 0; + for (const auto& track : GetLocalTracks(side)) { + if (track.GetMediaType() == type) { + ++result; + } + } + return result; + } + + UniquePtr<Sdp> GetParsedLocalDescription(const JsepSessionImpl& side) const { + return Parse(side.GetLocalDescription(kJsepDescriptionCurrent)); + } + + SdpMediaSection* GetMsection(Sdp& sdp, SdpMediaSection::MediaType type, + size_t index) const { + for (size_t i = 0; i < sdp.GetMediaSectionCount(); ++i) { + auto& msection = sdp.GetMediaSection(i); + if (msection.GetMediaType() != type) { + continue; + } + + if (index) { + --index; + continue; + } + + return &msection; + } + + return nullptr; + } + + void SetPayloadTypeNumber(JsepSession& session, const std::string& codecName, + const std::string& payloadType) { + for (auto& codec : session.Codecs()) { + if (codec->mName == codecName) { + codec->mDefaultPt = payloadType; + } + } + } + + void SetCodecEnabled(JsepSession& session, const std::string& codecName, + bool enabled) { + for (auto& codec : session.Codecs()) { + if (codec->mName == codecName) { + codec->mEnabled = enabled; + } + } + } + + void EnsureNegotiationFailure(SdpMediaSection::MediaType type, + const std::string& codecName) { + for (auto& codec : mSessionOff->Codecs()) { + if (codec->Type() == type && codec->mName != codecName) { + codec->mEnabled = false; + } + } + + for (auto& codec : mSessionAns->Codecs()) { + if (codec->Type() == type && codec->mName == codecName) { + codec->mEnabled = false; + } + } + } + + std::string CreateAnswer() { + std::vector<JsepTransceiver> transceiversBefore = + GetTransceivers(*mSessionAns); + + JsepAnswerOptions options; + std::string answer; + + JsepSession::Result result = mSessionAns->CreateAnswer(options, &answer); + EXPECT_FALSE(result.mError.isSome()); + + std::cerr << "ANSWER: " << answer << std::endl; + + ValidateTransport(*mAnswererTransport, answer, sdp::kAnswer); + CheckTransceiverInvariants(transceiversBefore, + GetTransceivers(*mSessionAns)); + + return answer; + } + + static const uint32_t NO_CHECKS = 0; + static const uint32_t CHECK_SUCCESS = 1; + static const uint32_t CHECK_TRACKS = 1 << 2; + static const uint32_t ALL_CHECKS = CHECK_SUCCESS | CHECK_TRACKS; + + void OfferAnswer(uint32_t checkFlags = ALL_CHECKS, + const Maybe<JsepOfferOptions>& options = Nothing()) { + std::string offer = CreateOffer(options); + SetLocalOffer(offer, checkFlags); + SetRemoteOffer(offer, checkFlags); + + std::string answer = CreateAnswer(); + SetLocalAnswer(answer, checkFlags); + SetRemoteAnswer(answer, checkFlags); + } + + void SetLocalOffer(const std::string& offer, + uint32_t checkFlags = ALL_CHECKS) { + std::vector<JsepTransceiver> transceiversBefore = + GetTransceivers(*mSessionOff); + + JsepSession::Result result = + mSessionOff->SetLocalDescription(kJsepSdpOffer, offer); + + CheckTransceiverInvariants(transceiversBefore, + GetTransceivers(*mSessionOff)); + + if (checkFlags & CHECK_SUCCESS) { + ASSERT_FALSE(result.mError.isSome()); + } + + if (checkFlags & CHECK_TRACKS) { + // This assumes no recvonly or inactive transceivers. + ASSERT_EQ(types.size(), GetTransceivers(*mSessionOff).size()); + for (const auto& transceiver : GetTransceivers(*mSessionOff)) { + if (!transceiver.HasLevel()) { + continue; + } + const auto& track(transceiver.mSendTrack); + size_t level = transceiver.GetLevel(); + ASSERT_FALSE(IsNull(track)); + ASSERT_EQ(types[level], track.GetMediaType()); + if (track.GetMediaType() != SdpMediaSection::kApplication) { + std::string msidAttr("a=msid:"); + msidAttr += track.GetStreamIds()[0]; + ASSERT_NE(std::string::npos, offer.find(msidAttr)) + << "Did not find " << msidAttr << " in offer"; + } + } + if (types.size() == 1 && types[0] == SdpMediaSection::kApplication) { + ASSERT_EQ(std::string::npos, offer.find("a=ssrc")) + << "Data channel should not contain SSRC"; + } + } + } + + void SetRemoteOffer(const std::string& offer, + uint32_t checkFlags = ALL_CHECKS) { + std::vector<JsepTransceiver> transceiversBefore = + GetTransceivers(*mSessionAns); + + JsepSession::Result result = + mSessionAns->SetRemoteDescription(kJsepSdpOffer, offer); + + CheckTransceiverInvariants(transceiversBefore, + GetTransceivers(*mSessionAns)); + + if (checkFlags & CHECK_SUCCESS) { + ASSERT_FALSE(result.mError.isSome()); + } + + if (checkFlags & CHECK_TRACKS) { + // This assumes no recvonly or inactive transceivers. + ASSERT_EQ(types.size(), GetTransceivers(*mSessionAns).size()); + for (const auto& transceiver : GetTransceivers(*mSessionAns)) { + if (!transceiver.HasLevel()) { + continue; + } + const auto& track(transceiver.mRecvTrack); + size_t level = transceiver.GetLevel(); + ASSERT_FALSE(IsNull(track)); + ASSERT_EQ(types[level], track.GetMediaType()); + if (track.GetMediaType() != SdpMediaSection::kApplication) { + std::string msidAttr("a=msid:"); + msidAttr += track.GetStreamIds()[0]; + // Track id will not match, and will eventually not be present at all + ASSERT_NE(std::string::npos, offer.find(msidAttr)) + << "Did not find " << msidAttr << " in offer"; + } + } + } + } + + void SetLocalAnswer(const std::string& answer, + uint32_t checkFlags = ALL_CHECKS) { + std::vector<JsepTransceiver> transceiversBefore = + GetTransceivers(*mSessionAns); + + JsepSession::Result result = + mSessionAns->SetLocalDescription(kJsepSdpAnswer, answer); + if (checkFlags & CHECK_SUCCESS) { + ASSERT_FALSE(result.mError.isSome()); + } + + CheckTransceiverInvariants(transceiversBefore, + GetTransceivers(*mSessionAns)); + + if (checkFlags & CHECK_TRACKS) { + // Verify that the right stuff is in the tracks. + ASSERT_EQ(types.size(), GetTransceivers(*mSessionAns).size()); + for (const auto& transceiver : GetTransceivers(*mSessionAns)) { + if (!transceiver.HasLevel()) { + continue; + } + const auto& sendTrack(transceiver.mSendTrack); + const auto& recvTrack(transceiver.mRecvTrack); + size_t level = transceiver.GetLevel(); + ASSERT_FALSE(IsNull(sendTrack)); + ASSERT_EQ(types[level], sendTrack.GetMediaType()); + // These might have been in the SDP, or might have been randomly + // chosen by JsepSessionImpl + ASSERT_FALSE(IsNull(recvTrack)); + ASSERT_EQ(types[level], recvTrack.GetMediaType()); + + if (recvTrack.GetMediaType() != SdpMediaSection::kApplication) { + std::string msidAttr("a=msid:"); + msidAttr += sendTrack.GetStreamIds()[0]; + ASSERT_NE(std::string::npos, answer.find(msidAttr)) + << "Did not find " << msidAttr << " in answer"; + } + } + if (types.size() == 1 && types[0] == SdpMediaSection::kApplication) { + ASSERT_EQ(std::string::npos, answer.find("a=ssrc")) + << "Data channel should not contain SSRC"; + } + } + std::cerr << "Answerer transceivers:" << std::endl; + DumpTransceivers(*mSessionAns); + } + + void SetRemoteAnswer(const std::string& answer, + uint32_t checkFlags = ALL_CHECKS) { + std::vector<JsepTransceiver> transceiversBefore = + GetTransceivers(*mSessionOff); + + JsepSession::Result result = + mSessionOff->SetRemoteDescription(kJsepSdpAnswer, answer); + if (checkFlags & CHECK_SUCCESS) { + ASSERT_FALSE(result.mError.isSome()); + } + + CheckTransceiverInvariants(transceiversBefore, + GetTransceivers(*mSessionOff)); + + if (checkFlags & CHECK_TRACKS) { + // Verify that the right stuff is in the tracks. + ASSERT_EQ(types.size(), GetTransceivers(*mSessionOff).size()); + for (const auto& transceiver : GetTransceivers(*mSessionOff)) { + if (!transceiver.HasLevel()) { + continue; + } + const auto& sendTrack(transceiver.mSendTrack); + const auto& recvTrack(transceiver.mRecvTrack); + size_t level = transceiver.GetLevel(); + ASSERT_FALSE(IsNull(sendTrack)); + ASSERT_EQ(types[level], sendTrack.GetMediaType()); + // These might have been in the SDP, or might have been randomly + // chosen by JsepSessionImpl + ASSERT_FALSE(IsNull(recvTrack)); + ASSERT_EQ(types[level], recvTrack.GetMediaType()); + + if (recvTrack.GetMediaType() != SdpMediaSection::kApplication) { + std::string msidAttr("a=msid:"); + msidAttr += recvTrack.GetStreamIds()[0]; + // Track id will not match, and will eventually not be present at all + ASSERT_NE(std::string::npos, answer.find(msidAttr)) + << "Did not find " << msidAttr << " in answer"; + } + } + } + std::cerr << "Offerer transceivers:" << std::endl; + DumpTransceivers(*mSessionOff); + } + + std::string GetTransportId(const JsepSession& session, size_t level) { + for (const auto& transceiver : GetTransceivers(session)) { + if (transceiver.HasLevel() && transceiver.GetLevel() == level) { + return transceiver.mTransport.mTransportId; + } + } + return std::string(); + } + + typedef enum { RTP = 1, RTCP = 2 } ComponentType; + + class CandidateSet { + public: + CandidateSet() {} + + void Gather(JsepSession& session, ComponentType maxComponent = RTCP) { + for (const auto& transceiver : GetTransceivers(session)) { + if (transceiver.HasOwnTransport()) { + Gather(session, transceiver.mTransport.mTransportId, RTP); + if (transceiver.mTransport.mComponents > 1) { + Gather(session, transceiver.mTransport.mTransportId, RTCP); + } + } + } + FinishGathering(session); + } + + void Gather(JsepSession& session, const std::string& transportId, + ComponentType component) { + static uint16_t port = 1000; + std::vector<std::string> candidates; + + for (size_t i = 0; i < kNumCandidatesPerComponent; ++i) { + ++port; + std::ostringstream candidate; + candidate << "0 " << static_cast<uint16_t>(component) + << " UDP 9999 192.168.0.1 " << port << " typ host"; + std::string mid; + uint16_t level = 0; + bool skipped; + session.AddLocalIceCandidate(kAEqualsCandidate + candidate.str(), + transportId, "", &level, &mid, &skipped); + if (!skipped) { + mCandidatesToTrickle.push_back(std::tuple<Level, Mid, Candidate>( + level, mid, kAEqualsCandidate + candidate.str())); + candidates.push_back(candidate.str()); + } + } + + // Stomp existing candidates + mCandidates[transportId][component] = candidates; + + // Stomp existing defaults + mDefaultCandidates[transportId][component] = + std::make_pair("192.168.0.1", port); + session.UpdateDefaultCandidate( + mDefaultCandidates[transportId][RTP].first, + mDefaultCandidates[transportId][RTP].second, + // Will be empty string if not present, which is how we indicate + // that there is no default for RTCP + mDefaultCandidates[transportId][RTCP].first, + mDefaultCandidates[transportId][RTCP].second, transportId); + } + + void FinishGathering(JsepSession& session) const { + // Copy so we can be terse and use [] + for (auto idAndCandidates : mDefaultCandidates) { + ASSERT_EQ(1U, idAndCandidates.second.count(RTP)); + // do a final UpdateDefaultCandidate here in case candidates were + // cleared during renegotiation. + session.UpdateDefaultCandidate( + idAndCandidates.second[RTP].first, + idAndCandidates.second[RTP].second, + // Will be empty string if not present, which is how we indicate + // that there is no default for RTCP + idAndCandidates.second[RTCP].first, + idAndCandidates.second[RTCP].second, idAndCandidates.first); + std::string mid; + uint16_t level = 0; + bool skipped; + session.AddLocalIceCandidate("", idAndCandidates.first, "", &level, + &mid, &skipped); + } + } + + void Trickle(JsepSession& session) { + std::string transportId; + for (const auto& levelMidAndCandidate : mCandidatesToTrickle) { + auto [level, mid, candidate] = levelMidAndCandidate; + std::cerr << "trickling candidate: " << candidate << " level: " << level + << " mid: " << mid << std::endl; + Maybe<unsigned long> lev = Some(level); + session.AddRemoteIceCandidate(candidate, mid, lev, "", &transportId); + } + session.AddRemoteIceCandidate("", "", Maybe<uint16_t>(), "", + &transportId); + mCandidatesToTrickle.clear(); + } + + void CheckRtpCandidates(bool expectRtpCandidates, + const SdpMediaSection& msection, + const std::string& transportId, + const std::string& context) const { + auto& attrs = msection.GetAttributeList(); + + ASSERT_EQ(expectRtpCandidates, + attrs.HasAttribute(SdpAttribute::kCandidateAttribute)) + << context << " (level " << msection.GetLevel() << ")"; + + if (expectRtpCandidates) { + // Copy so we can be terse and use [] + auto expectedCandidates = mCandidates; + ASSERT_LE(kNumCandidatesPerComponent, + expectedCandidates[transportId][RTP].size()); + + auto& candidates = attrs.GetCandidate(); + ASSERT_LE(kNumCandidatesPerComponent, candidates.size()) + << context << " (level " << msection.GetLevel() << ")"; + for (size_t i = 0; i < kNumCandidatesPerComponent; ++i) { + ASSERT_EQ(expectedCandidates[transportId][RTP][i], candidates[i]) + << context << " (level " << msection.GetLevel() << ")"; + } + } + } + + void CheckRtcpCandidates(bool expectRtcpCandidates, + const SdpMediaSection& msection, + const std::string& transportId, + const std::string& context) const { + auto& attrs = msection.GetAttributeList(); + + if (expectRtcpCandidates) { + // Copy so we can be terse and use [] + auto expectedCandidates = mCandidates; + ASSERT_LE(kNumCandidatesPerComponent, + expectedCandidates[transportId][RTCP].size()); + + ASSERT_TRUE(attrs.HasAttribute(SdpAttribute::kCandidateAttribute)) + << context << " (level " << msection.GetLevel() << ")"; + auto& candidates = attrs.GetCandidate(); + ASSERT_EQ(kNumCandidatesPerComponent * 2, candidates.size()) + << context << " (level " << msection.GetLevel() << ")"; + for (size_t i = 0; i < kNumCandidatesPerComponent; ++i) { + ASSERT_EQ(expectedCandidates[transportId][RTCP][i], + candidates[i + kNumCandidatesPerComponent]) + << context << " (level " << msection.GetLevel() << ")"; + } + } + } + + void CheckDefaultRtpCandidate(bool expectDefault, + const SdpMediaSection& msection, + const std::string& transportId, + const std::string& context) const { + Address expectedAddress = "0.0.0.0"; + Port expectedPort = 9U; + + if (expectDefault) { + // Copy so we can be terse and use [] + auto defaultCandidates = mDefaultCandidates; + expectedAddress = defaultCandidates[transportId][RTP].first; + expectedPort = defaultCandidates[transportId][RTP].second; + } + + // if bundle-only attribute is present, expect port 0 + const SdpAttributeList& attrs = msection.GetAttributeList(); + if (attrs.HasAttribute(SdpAttribute::kBundleOnlyAttribute)) { + expectedPort = 0U; + } + + ASSERT_EQ(expectedAddress, msection.GetConnection().GetAddress()) + << context << " (level " << msection.GetLevel() << ")"; + ASSERT_EQ(expectedPort, msection.GetPort()) + << context << " (level " << msection.GetLevel() << ")"; + } + + void CheckDefaultRtcpCandidate(bool expectDefault, + const SdpMediaSection& msection, + const std::string& transportId, + const std::string& context) const { + if (expectDefault) { + // Copy so we can be terse and use [] + auto defaultCandidates = mDefaultCandidates; + ASSERT_TRUE(msection.GetAttributeList().HasAttribute( + SdpAttribute::kRtcpAttribute)) + << context << " (level " << msection.GetLevel() << ")"; + auto& rtcpAttr = msection.GetAttributeList().GetRtcp(); + ASSERT_EQ(defaultCandidates[transportId][RTCP].second, rtcpAttr.mPort) + << context << " (level " << msection.GetLevel() << ")"; + ASSERT_EQ(sdp::kInternet, rtcpAttr.mNetType) + << context << " (level " << msection.GetLevel() << ")"; + ASSERT_EQ(sdp::kIPv4, rtcpAttr.mAddrType) + << context << " (level " << msection.GetLevel() << ")"; + ASSERT_EQ(defaultCandidates[transportId][RTCP].first, rtcpAttr.mAddress) + << context << " (level " << msection.GetLevel() << ")"; + } else { + ASSERT_FALSE(msection.GetAttributeList().HasAttribute( + SdpAttribute::kRtcpAttribute)) + << context << " (level " << msection.GetLevel() << ")"; + } + } + + private: + typedef size_t Level; + typedef std::string TransportId; + typedef std::string Mid; + typedef std::string Candidate; + typedef std::string Address; + typedef uint16_t Port; + // Default candidates are put into the m-line, c-line, and rtcp + // attribute for endpoints that don't support ICE. + std::map<TransportId, std::map<ComponentType, std::pair<Address, Port>>> + mDefaultCandidates; + std::map<TransportId, std::map<ComponentType, std::vector<Candidate>>> + mCandidates; + // Level/mid/candidate tuples that need to be trickled + std::vector<std::tuple<Level, Mid, Candidate>> mCandidatesToTrickle; + }; + + // For streaming parse errors + std::string GetParseErrors( + const UniquePtr<SdpParser::Results>& results) const { + std::stringstream output; + auto errors = std::move(results->Errors()); + for (auto error : errors) { + output << error.first << ": " << error.second << std::endl; + } + return output.str(); + } + + void CheckEndOfCandidates(bool expectEoc, const SdpMediaSection& msection, + const std::string& context) { + if (expectEoc) { + ASSERT_TRUE(msection.GetAttributeList().HasAttribute( + SdpAttribute::kEndOfCandidatesAttribute)) + << context << " (level " << msection.GetLevel() << ")"; + } else { + ASSERT_FALSE(msection.GetAttributeList().HasAttribute( + SdpAttribute::kEndOfCandidatesAttribute)) + << context << " (level " << msection.GetLevel() << ")"; + } + } + + void CheckTransceiversAreBundled(const JsepSession& session, + const std::string& context) { + for (const auto& transceiver : GetTransceivers(session)) { + ASSERT_TRUE(transceiver.HasBundleLevel()) + << context; + ASSERT_EQ(0U, transceiver.BundleLevel()) << context; + ASSERT_NE("", transceiver.mTransport.mTransportId); + } + } + + void DisableMsid(std::string* sdp) const { + while (true) { + size_t pos = sdp->find("a=msid"); + if (pos == std::string::npos) { + break; + } + (*sdp)[pos + 2] = 'X'; // garble, a=Xsid + } + } + + void DisableBundle(std::string* sdp) const { + size_t pos = sdp->find("a=group:BUNDLE"); + ASSERT_NE(std::string::npos, pos); + (*sdp)[pos + 11] = 'G'; // garble, a=group:BUNGLE + } + + void DisableMsection(std::string* sdp, size_t level) const { + UniquePtr<Sdp> parsed(Parse(*sdp)); + ASSERT_TRUE(parsed.get()); + ASSERT_LT(level, parsed->GetMediaSectionCount()); + SdpHelper::DisableMsection(parsed.get(), &parsed->GetMediaSection(level)); + (*sdp) = parsed->ToString(); + } + + void CopyTransportAttributes(std::string* sdp, size_t src_level, + size_t dst_level) { + UniquePtr<Sdp> parsed(Parse(*sdp)); + ASSERT_TRUE(parsed.get()); + ASSERT_LT(src_level, parsed->GetMediaSectionCount()); + ASSERT_LT(dst_level, parsed->GetMediaSectionCount()); + nsresult rv = + mSdpHelper.CopyTransportParams(2, parsed->GetMediaSection(src_level), + &parsed->GetMediaSection(dst_level)); + ASSERT_EQ(NS_OK, rv); + (*sdp) = parsed->ToString(); + } + + void ReplaceInSdp(std::string* sdp, const char* searchStr, + const char* replaceStr) const { + if (searchStr[0] == '\0') return; + size_t pos = 0; + while ((pos = sdp->find(searchStr, pos)) != std::string::npos) { + sdp->replace(pos, strlen(searchStr), replaceStr); + pos += strlen(replaceStr); + } + } + + void ValidateDisabledMSection(const SdpMediaSection* msection) { + ASSERT_EQ(1U, msection->GetFormats().size()); + + auto& attrs = msection->GetAttributeList(); + ASSERT_TRUE(attrs.HasAttribute(SdpAttribute::kMidAttribute)); + ASSERT_TRUE(attrs.HasAttribute(SdpAttribute::kDirectionAttribute)); + ASSERT_FALSE(attrs.HasAttribute(SdpAttribute::kBundleOnlyAttribute)); + ASSERT_EQ(SdpDirectionAttribute::kInactive, + msection->GetDirectionAttribute().mValue); + ASSERT_EQ(3U, attrs.Count()); + if (msection->GetMediaType() == SdpMediaSection::kAudio) { + ASSERT_EQ("0", msection->GetFormats()[0]); + const SdpRtpmapAttributeList::Rtpmap* rtpmap(msection->FindRtpmap("0")); + ASSERT_TRUE(rtpmap); + ASSERT_EQ("0", rtpmap->pt); + ASSERT_EQ("PCMU", rtpmap->name); + } else if (msection->GetMediaType() == SdpMediaSection::kVideo) { + ASSERT_EQ("120", msection->GetFormats()[0]); + const SdpRtpmapAttributeList::Rtpmap* rtpmap(msection->FindRtpmap("120")); + ASSERT_TRUE(rtpmap); + ASSERT_EQ("120", rtpmap->pt); + ASSERT_EQ("VP8", rtpmap->name); + } else if (msection->GetMediaType() == SdpMediaSection::kApplication) { + if (msection->GetProtocol() == SdpMediaSection::kUdpDtlsSctp || + msection->GetProtocol() == SdpMediaSection::kTcpDtlsSctp) { + // draft 21 format + ASSERT_EQ("webrtc-datachannel", msection->GetFormats()[0]); + ASSERT_FALSE(msection->GetSctpmap()); + ASSERT_EQ(0U, msection->GetSctpPort()); + } else { + // old draft 05 format + ASSERT_EQ("0", msection->GetFormats()[0]); + const SdpSctpmapAttributeList::Sctpmap* sctpmap(msection->GetSctpmap()); + ASSERT_TRUE(sctpmap); + ASSERT_EQ("0", sctpmap->pt); + ASSERT_EQ("rejected", sctpmap->name); + ASSERT_EQ(0U, sctpmap->streams); + } + } else { + // Not that we would have any test which tests this... + ASSERT_EQ("19", msection->GetFormats()[0]); + const SdpRtpmapAttributeList::Rtpmap* rtpmap(msection->FindRtpmap("19")); + ASSERT_TRUE(rtpmap); + ASSERT_EQ("19", rtpmap->pt); + ASSERT_EQ("reserved", rtpmap->name); + } + + ASSERT_FALSE(msection->GetAttributeList().HasAttribute( + SdpAttribute::kMsidAttribute)); + } + + void ValidateSetupAttribute(const JsepSessionImpl& side, + const SdpSetupAttribute::Role expectedRole) { + auto sdp = GetParsedLocalDescription(side); + for (size_t i = 0; sdp && i < sdp->GetMediaSectionCount(); ++i) { + if (sdp->GetMediaSection(i).GetAttributeList().HasAttribute( + SdpAttribute::kSetupAttribute)) { + auto role = sdp->GetMediaSection(i).GetAttributeList().GetSetup().mRole; + ASSERT_EQ(expectedRole, role); + } + } + } + + void DumpTrack(const JsepTrack& track) { + const JsepTrackNegotiatedDetails* details = track.GetNegotiatedDetails(); + std::cerr << " type=" << track.GetMediaType() << std::endl; + if (!details) { + std::cerr << " not negotiated" << std::endl; + return; + } + std::cerr << " encodings=" << std::endl; + for (size_t i = 0; i < details->GetEncodingCount(); ++i) { + const JsepTrackEncoding& encoding = details->GetEncoding(i); + std::cerr << " id=" << encoding.mRid << std::endl; + for (const auto& codec : encoding.GetCodecs()) { + std::cerr << " " << codec->mName << " enabled(" + << (codec->mEnabled ? "yes" : "no") << ")"; + if (track.GetMediaType() == SdpMediaSection::kAudio) { + const JsepAudioCodecDescription* audioCodec = + static_cast<const JsepAudioCodecDescription*>(codec.get()); + std::cerr << " dtmf(" << (audioCodec->mDtmfEnabled ? "yes" : "no") + << ")"; + } + if (track.GetMediaType() == SdpMediaSection::kVideo) { + const JsepVideoCodecDescription* videoCodec = + static_cast<const JsepVideoCodecDescription*>(codec.get()); + std::cerr << " rtx(" + << (videoCodec->mRtxEnabled ? videoCodec->mRtxPayloadType + : "no") + << ")"; + } + std::cerr << std::endl; + } + } + } + + void DumpTransport(const JsepTransport& transport) { + std::cerr << " id=" << transport.mTransportId << std::endl; + std::cerr << " components=" << transport.mComponents << std::endl; + } + + void DumpTransceivers(const JsepSessionImpl& session) { + for (const auto& transceiver : GetTransceivers(session)) { + std::cerr << "Transceiver "; + if (transceiver.HasLevel()) { + std::cerr << transceiver.GetLevel() << std::endl; + } else { + std::cerr << "<NO LEVEL>" << std::endl; + } + if (transceiver.HasBundleLevel()) { + std::cerr << "(bundle level is " << transceiver.BundleLevel() << ")" + << std::endl; + } + if (!IsNull(transceiver.mSendTrack)) { + std::cerr << "Sending-->" << std::endl; + DumpTrack(transceiver.mSendTrack); + } + if (!IsNull(transceiver.mRecvTrack)) { + std::cerr << "Receiving-->" << std::endl; + DumpTrack(transceiver.mRecvTrack); + } + std::cerr << "Transport-->" << std::endl; + DumpTransport(transceiver.mTransport); + } + } + + UniquePtr<Sdp> Parse(const std::string& sdp) const { + SipccSdpParser parser; + auto results = parser.Parse(sdp); + UniquePtr<Sdp> parsed = std::move(results->Sdp()); + EXPECT_TRUE(parsed.get()) << "Should have valid SDP" << std::endl + << "Errors were: " << GetParseErrors(results); + return parsed; + } + + std::string SetExtmap(const std::string& aSdp, const std::string& aUri, + uint16_t aId, uint16_t* aOldId = nullptr) { + UniquePtr<Sdp> munge(Parse(aSdp)); + for (size_t i = 0; i < munge->GetMediaSectionCount(); ++i) { + auto& attrs = munge->GetMediaSection(i).GetAttributeList(); + if (attrs.HasAttribute(SdpAttribute::kExtmapAttribute)) { + auto extmap = attrs.GetExtmap(); + for (auto it = extmap.mExtmaps.begin(); it != extmap.mExtmaps.end(); + ++it) { + if (it->extensionname == aUri) { + if (aOldId) { + *aOldId = it->entry; + } + + if (aId) { + it->entry = aId; + } else { + extmap.mExtmaps.erase(it); + } + break; + } + } + attrs.SetAttribute(extmap.Clone()); + } + } + return munge->ToString(); + } + + uint16_t GetExtmap(const std::string& aSdp, const std::string& aUri) { + UniquePtr<Sdp> parsed(Parse(aSdp)); + for (size_t i = 0; i < parsed->GetMediaSectionCount(); ++i) { + auto& attrs = parsed->GetMediaSection(i).GetAttributeList(); + if (attrs.HasAttribute(SdpAttribute::kExtmapAttribute)) { + auto extmap = attrs.GetExtmap(); + for (auto& ext : extmap.mExtmaps) { + if (ext.extensionname == aUri) { + return ext.entry; + } + } + } + } + return 0; + } + + void SwapOfferAnswerRoles() { + mSessionOff.swap(mSessionAns); + mOffCandidates.swap(mAnsCandidates); + mOffererTransport.swap(mAnswererTransport); + } + + UniquePtr<JsepSessionImpl> mSessionOff; + UniquePtr<CandidateSet> mOffCandidates; + UniquePtr<JsepSessionImpl> mSessionAns; + UniquePtr<CandidateSet> mAnsCandidates; + + std::vector<SdpMediaSection::MediaType> types; + std::vector<std::pair<std::string, uint16_t>> mGatheredCandidates; + + FakeUuidGenerator mUuidGen; + + private: + void ValidateTransport(TransportData& source, const std::string& sdp_str, + sdp::SdpType type) { + UniquePtr<Sdp> sdp(Parse(sdp_str)); + ASSERT_TRUE(!!sdp); + size_t num_m_sections = sdp->GetMediaSectionCount(); + for (size_t i = 0; i < num_m_sections; ++i) { + auto& msection = sdp->GetMediaSection(i); + + if (msection.GetMediaType() == SdpMediaSection::kApplication) { + if (!(msection.GetProtocol() == SdpMediaSection::kUdpDtlsSctp || + msection.GetProtocol() == SdpMediaSection::kTcpDtlsSctp)) { + // old draft 05 format + ASSERT_EQ(SdpMediaSection::kDtlsSctp, msection.GetProtocol()); + } + } else { + ASSERT_EQ(SdpMediaSection::kUdpTlsRtpSavpf, msection.GetProtocol()); + } + + const SdpAttributeList& attrs = msection.GetAttributeList(); + bool bundle_only = attrs.HasAttribute(SdpAttribute::kBundleOnlyAttribute); + + // port 0 only means disabled when the bundle-only attribute is missing + if (!bundle_only && msection.GetPort() == 0) { + ValidateDisabledMSection(&msection); + continue; + } + if (mSdpHelper.OwnsTransport(*sdp, i, type)) { + const SdpAttributeList& attrs = msection.GetAttributeList(); + + ASSERT_FALSE(attrs.GetIceUfrag().empty()); + ASSERT_FALSE(attrs.GetIcePwd().empty()); + const SdpFingerprintAttributeList& fps = attrs.GetFingerprint(); + for (auto fp = fps.mFingerprints.begin(); fp != fps.mFingerprints.end(); + ++fp) { + std::string alg_str = "None"; + + if (fp->hashFunc == SdpFingerprintAttributeList::kSha1) { + alg_str = "sha-1"; + } else if (fp->hashFunc == SdpFingerprintAttributeList::kSha256) { + alg_str = "sha-256"; + } + ASSERT_EQ(source.mFingerprints[alg_str], fp->fingerprint); + } + + ASSERT_EQ(source.mFingerprints.size(), fps.mFingerprints.size()); + } + } + } + + std::string mLastError; + SdpHelper mSdpHelper; + + UniquePtr<TransportData> mOffererTransport; + UniquePtr<TransportData> mAnswererTransport; +}; + +TEST_F(JsepSessionTestBase, CreateDestroy) {} + +TEST_P(JsepSessionTest, CreateOffer) { + AddTracks(*mSessionOff); + CreateOffer(); +} + +TEST_P(JsepSessionTest, CreateOfferSetLocal) { + AddTracks(*mSessionOff); + std::string offer = CreateOffer(); + SetLocalOffer(offer); +} + +TEST_P(JsepSessionTest, CreateOfferSetLocalSetRemote) { + AddTracks(*mSessionOff); + std::string offer = CreateOffer(); + SetLocalOffer(offer); + SetRemoteOffer(offer); +} + +TEST_P(JsepSessionTest, CreateOfferSetLocalSetRemoteCreateAnswer) { + AddTracks(*mSessionOff); + std::string offer = CreateOffer(); + SetLocalOffer(offer); + SetRemoteOffer(offer); + AddTracks(*mSessionAns); + std::string answer = CreateAnswer(); +} + +TEST_P(JsepSessionTest, CreateOfferSetLocalSetRemoteCreateAnswerSetLocal) { + AddTracks(*mSessionOff); + std::string offer = CreateOffer(); + SetLocalOffer(offer); + SetRemoteOffer(offer); + AddTracks(*mSessionAns); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); +} + +TEST_P(JsepSessionTest, FullCall) { + AddTracks(*mSessionOff); + std::string offer = CreateOffer(); + SetLocalOffer(offer); + SetRemoteOffer(offer); + AddTracks(*mSessionAns); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + SetRemoteAnswer(answer); +} + +TEST_P(JsepSessionTest, GetDescriptions) { + AddTracks(*mSessionOff); + std::string offer = CreateOffer(); + SetLocalOffer(offer); + std::string desc = mSessionOff->GetLocalDescription(kJsepDescriptionCurrent); + ASSERT_EQ(0U, desc.size()); + desc = mSessionOff->GetLocalDescription(kJsepDescriptionPending); + ASSERT_NE(0U, desc.size()); + desc = mSessionOff->GetLocalDescription(kJsepDescriptionPendingOrCurrent); + ASSERT_NE(0U, desc.size()); + desc = mSessionOff->GetRemoteDescription(kJsepDescriptionPendingOrCurrent); + ASSERT_EQ(0U, desc.size()); + desc = mSessionAns->GetLocalDescription(kJsepDescriptionPendingOrCurrent); + ASSERT_EQ(0U, desc.size()); + desc = mSessionAns->GetRemoteDescription(kJsepDescriptionPendingOrCurrent); + ASSERT_EQ(0U, desc.size()); + + SetRemoteOffer(offer); + desc = mSessionAns->GetRemoteDescription(kJsepDescriptionCurrent); + ASSERT_EQ(0U, desc.size()); + desc = mSessionAns->GetRemoteDescription(kJsepDescriptionPending); + ASSERT_NE(0U, desc.size()); + desc = mSessionAns->GetRemoteDescription(kJsepDescriptionPendingOrCurrent); + ASSERT_NE(0U, desc.size()); + desc = mSessionAns->GetLocalDescription(kJsepDescriptionPendingOrCurrent); + ASSERT_EQ(0U, desc.size()); + desc = mSessionOff->GetLocalDescription(kJsepDescriptionPendingOrCurrent); + ASSERT_NE(0U, desc.size()); + desc = mSessionOff->GetRemoteDescription(kJsepDescriptionPendingOrCurrent); + ASSERT_EQ(0U, desc.size()); + + AddTracks(*mSessionAns); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + desc = mSessionAns->GetLocalDescription(kJsepDescriptionCurrent); + ASSERT_NE(0U, desc.size()); + desc = mSessionAns->GetLocalDescription(kJsepDescriptionPending); + ASSERT_EQ(0U, desc.size()); + desc = mSessionAns->GetLocalDescription(kJsepDescriptionPendingOrCurrent); + ASSERT_NE(0U, desc.size()); + desc = mSessionAns->GetRemoteDescription(kJsepDescriptionCurrent); + ASSERT_NE(0U, desc.size()); + desc = mSessionAns->GetRemoteDescription(kJsepDescriptionPending); + ASSERT_EQ(0U, desc.size()); + desc = mSessionAns->GetRemoteDescription(kJsepDescriptionPendingOrCurrent); + ASSERT_NE(0U, desc.size()); + desc = mSessionOff->GetLocalDescription(kJsepDescriptionPendingOrCurrent); + ASSERT_NE(0U, desc.size()); + desc = mSessionOff->GetRemoteDescription(kJsepDescriptionPendingOrCurrent); + ASSERT_EQ(0U, desc.size()); + + SetRemoteAnswer(answer); + desc = mSessionOff->GetLocalDescription(kJsepDescriptionCurrent); + ASSERT_NE(0U, desc.size()); + desc = mSessionOff->GetLocalDescription(kJsepDescriptionPending); + ASSERT_EQ(0U, desc.size()); + desc = mSessionOff->GetLocalDescription(kJsepDescriptionPendingOrCurrent); + ASSERT_NE(0U, desc.size()); + desc = mSessionOff->GetRemoteDescription(kJsepDescriptionCurrent); + ASSERT_NE(0U, desc.size()); + desc = mSessionOff->GetRemoteDescription(kJsepDescriptionPending); + ASSERT_EQ(0U, desc.size()); + desc = mSessionOff->GetRemoteDescription(kJsepDescriptionPendingOrCurrent); + ASSERT_NE(0U, desc.size()); + desc = mSessionAns->GetLocalDescription(kJsepDescriptionPendingOrCurrent); + ASSERT_NE(0U, desc.size()); + desc = mSessionAns->GetRemoteDescription(kJsepDescriptionPendingOrCurrent); + ASSERT_NE(0U, desc.size()); +} + +TEST_P(JsepSessionTest, RenegotiationNoChange) { + AddTracks(*mSessionOff); + std::string offer = CreateOffer(); + SetLocalOffer(offer); + SetRemoteOffer(offer); + + AddTracks(*mSessionAns); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + SetRemoteAnswer(answer); + + ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); + ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive); + + std::vector<JsepTransceiver> origOffererTransceivers = + GetTransceivers(*mSessionOff); + std::vector<JsepTransceiver> origAnswererTransceivers = + GetTransceivers(*mSessionAns); + + std::string reoffer = CreateOffer(); + SetLocalOffer(reoffer); + SetRemoteOffer(reoffer); + + std::string reanswer = CreateAnswer(); + SetLocalAnswer(reanswer); + SetRemoteAnswer(reanswer); + + ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); + ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive); + + auto newOffererTransceivers = GetTransceivers(*mSessionOff); + auto newAnswererTransceivers = GetTransceivers(*mSessionAns); + + ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers)); + ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers)); +} + +// Disabled: See Bug 1329028 +TEST_P(JsepSessionTest, DISABLED_RenegotiationSwappedRolesNoChange) { + AddTracks(*mSessionOff); + std::string offer = CreateOffer(); + SetLocalOffer(offer); + SetRemoteOffer(offer); + + AddTracks(*mSessionAns); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + SetRemoteAnswer(answer); + + ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); + ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive); + + auto offererTransceivers = GetTransceivers(*mSessionOff); + auto answererTransceivers = GetTransceivers(*mSessionAns); + + SwapOfferAnswerRoles(); + + std::string reoffer = CreateOffer(); + SetLocalOffer(reoffer); + SetRemoteOffer(reoffer); + + std::string reanswer = CreateAnswer(); + SetLocalAnswer(reanswer); + SetRemoteAnswer(reanswer); + + ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); + ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kPassive); + + auto newOffererTransceivers = GetTransceivers(*mSessionOff); + auto newAnswererTransceivers = GetTransceivers(*mSessionAns); + + ASSERT_TRUE(Equals(offererTransceivers, newAnswererTransceivers)); + ASSERT_TRUE(Equals(answererTransceivers, newOffererTransceivers)); +} + +static void RemoveLastN(std::vector<JsepTransceiver>& aTransceivers, + size_t aNum) { + while (aNum--) { + // erase doesn't take reverse_iterator :( + aTransceivers.erase(--aTransceivers.end()); + } +} + +TEST_P(JsepSessionTest, RenegotiationOffererAddsTrack) { + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + + OfferAnswer(); + + ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); + ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive); + + std::vector<JsepTransceiver> origOffererTransceivers = + GetTransceivers(*mSessionOff); + std::vector<JsepTransceiver> origAnswererTransceivers = + GetTransceivers(*mSessionAns); + + std::vector<SdpMediaSection::MediaType> extraTypes; + extraTypes.push_back(SdpMediaSection::kAudio); + extraTypes.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionOff, extraTypes); + types.insert(types.end(), extraTypes.begin(), extraTypes.end()); + + OfferAnswer(CHECK_SUCCESS); + + ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); + ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive); + + auto newOffererTransceivers = GetTransceivers(*mSessionOff); + auto newAnswererTransceivers = GetTransceivers(*mSessionAns); + + ASSERT_LE(2U, newOffererTransceivers.size()); + RemoveLastN(newOffererTransceivers, 2); + ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers)); + + ASSERT_LE(2U, newAnswererTransceivers.size()); + RemoveLastN(newAnswererTransceivers, 2); + ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers)); +} + +TEST_P(JsepSessionTest, RenegotiationAnswererAddsTrack) { + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + + OfferAnswer(); + + ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); + ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive); + + std::vector<JsepTransceiver> origOffererTransceivers = + GetTransceivers(*mSessionOff); + std::vector<JsepTransceiver> origAnswererTransceivers = + GetTransceivers(*mSessionAns); + + std::vector<SdpMediaSection::MediaType> extraTypes; + extraTypes.push_back(SdpMediaSection::kAudio); + extraTypes.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionAns, extraTypes); + types.insert(types.end(), extraTypes.begin(), extraTypes.end()); + + // We need to add a recvonly m-section to the offer for this to work + mSessionOff->AddTransceiver( + JsepTransceiver(SdpMediaSection::kAudio, mUuidGen, + SdpDirectionAttribute::Direction::kRecvonly)); + mSessionOff->AddTransceiver( + JsepTransceiver(SdpMediaSection::kVideo, mUuidGen, + SdpDirectionAttribute::Direction::kRecvonly)); + + std::string offer = CreateOffer(); + SetLocalOffer(offer, CHECK_SUCCESS); + SetRemoteOffer(offer, CHECK_SUCCESS); + + std::string answer = CreateAnswer(); + SetLocalAnswer(answer, CHECK_SUCCESS); + SetRemoteAnswer(answer, CHECK_SUCCESS); + + ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); + ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive); + + auto newOffererTransceivers = GetTransceivers(*mSessionOff); + auto newAnswererTransceivers = GetTransceivers(*mSessionAns); + + ASSERT_LE(2U, newOffererTransceivers.size()); + RemoveLastN(newOffererTransceivers, 2); + ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers)); + + ASSERT_LE(2U, newAnswererTransceivers.size()); + RemoveLastN(newAnswererTransceivers, 2); + ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers)); +} + +TEST_P(JsepSessionTest, RenegotiationBothAddTrack) { + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + + OfferAnswer(); + + ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); + ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive); + + std::vector<JsepTransceiver> origOffererTransceivers = + GetTransceivers(*mSessionOff); + std::vector<JsepTransceiver> origAnswererTransceivers = + GetTransceivers(*mSessionAns); + + std::vector<SdpMediaSection::MediaType> extraTypes; + extraTypes.push_back(SdpMediaSection::kAudio); + extraTypes.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionAns, extraTypes); + AddTracks(*mSessionOff, extraTypes); + types.insert(types.end(), extraTypes.begin(), extraTypes.end()); + + OfferAnswer(CHECK_SUCCESS); + + ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); + ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive); + + auto newOffererTransceivers = GetTransceivers(*mSessionOff); + auto newAnswererTransceivers = GetTransceivers(*mSessionAns); + + ASSERT_LE(2U, newOffererTransceivers.size()); + RemoveLastN(newOffererTransceivers, 2); + ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers)); + + ASSERT_LE(2U, newAnswererTransceivers.size()); + RemoveLastN(newAnswererTransceivers, 2); + ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers)); +} + +TEST_P(JsepSessionTest, RenegotiationBothAddTracksToExistingStream) { + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + if (GetParam() == "datachannel") { + return; + } + + OfferAnswer(); + + auto oHasStream = HasMediaStream(GetLocalTracks(*mSessionOff)); + auto aHasStream = HasMediaStream(GetLocalTracks(*mSessionAns)); + ASSERT_EQ(oHasStream, !GetLocalUniqueStreamIds(*mSessionOff).empty()); + ASSERT_EQ(aHasStream, !GetLocalUniqueStreamIds(*mSessionAns).empty()); + ASSERT_EQ(aHasStream, !GetRemoteUniqueStreamIds(*mSessionOff).empty()); + ASSERT_EQ(oHasStream, !GetRemoteUniqueStreamIds(*mSessionAns).empty()); + + auto firstOffId = GetFirstLocalStreamId(*mSessionOff); + auto firstAnsId = GetFirstLocalStreamId(*mSessionAns); + + auto offererTransceivers = GetTransceivers(*mSessionOff); + auto answererTransceivers = GetTransceivers(*mSessionAns); + + std::vector<SdpMediaSection::MediaType> extraTypes; + extraTypes.push_back(SdpMediaSection::kAudio); + extraTypes.push_back(SdpMediaSection::kVideo); + AddTracksToStream(*mSessionOff, firstOffId, extraTypes); + AddTracksToStream(*mSessionAns, firstAnsId, extraTypes); + types.insert(types.end(), extraTypes.begin(), extraTypes.end()); + + OfferAnswer(CHECK_SUCCESS); + + oHasStream = HasMediaStream(GetLocalTracks(*mSessionOff)); + aHasStream = HasMediaStream(GetLocalTracks(*mSessionAns)); + + ASSERT_EQ(oHasStream, !GetLocalUniqueStreamIds(*mSessionOff).empty()); + ASSERT_EQ(aHasStream, !GetLocalUniqueStreamIds(*mSessionAns).empty()); + ASSERT_EQ(aHasStream, !GetRemoteUniqueStreamIds(*mSessionOff).empty()); + ASSERT_EQ(oHasStream, !GetRemoteUniqueStreamIds(*mSessionAns).empty()); + if (oHasStream) { + ASSERT_STREQ(firstOffId.c_str(), + GetFirstLocalStreamId(*mSessionOff).c_str()); + } + if (aHasStream) { + ASSERT_STREQ(firstAnsId.c_str(), + GetFirstLocalStreamId(*mSessionAns).c_str()); + + auto oHasStream = HasMediaStream(GetLocalTracks(*mSessionOff)); + auto aHasStream = HasMediaStream(GetLocalTracks(*mSessionAns)); + ASSERT_EQ(oHasStream, !GetLocalUniqueStreamIds(*mSessionOff).empty()); + ASSERT_EQ(aHasStream, !GetLocalUniqueStreamIds(*mSessionAns).empty()); + } +} + +// The JSEP draft explicitly forbids changing the msid on an m-section, but +// that is a bug. +TEST_P(JsepSessionTest, RenegotiationOffererChangesMsid) { + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + + OfferAnswer(); + + std::string offer = CreateOffer(); + SetLocalOffer(offer); + + JsepTransceiver* transceiver = GetNegotiatedTransceiver(*mSessionOff, 0); + ASSERT_TRUE(transceiver); + if (transceiver->GetMediaType() == SdpMediaSection::kApplication) { + return; + } + std::string streamId = transceiver->mSendTrack.GetStreamIds()[0]; + std::string msidToReplace("a=msid:"); + msidToReplace += streamId; + size_t msidOffset = offer.find(msidToReplace); + ASSERT_NE(std::string::npos, msidOffset); + offer.replace(msidOffset, msidToReplace.size(), "a=msid:foo"); + + SetRemoteOffer(offer); + transceiver = GetNegotiatedTransceiver(*mSessionAns, 0); + ASSERT_EQ("foo", transceiver->mRecvTrack.GetStreamIds()[0]); + + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + SetRemoteAnswer(answer); +} + +// The JSEP draft explicitly forbids changing the msid on an m-section, but +// that is a bug. +TEST_P(JsepSessionTest, RenegotiationAnswererChangesMsid) { + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + + OfferAnswer(); + + JsepTransceiver* transceiver = GetNegotiatedTransceiver(*mSessionOff, 0); + ASSERT_TRUE(transceiver); + if (transceiver->GetMediaType() == SdpMediaSection::kApplication) { + return; + } + + std::string offer = CreateOffer(); + SetLocalOffer(offer); + SetRemoteOffer(offer); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + + transceiver = GetNegotiatedTransceiver(*mSessionAns, 0); + ASSERT_TRUE(transceiver); + if (transceiver->GetMediaType() == SdpMediaSection::kApplication) { + return; + } + std::string streamId = transceiver->mSendTrack.GetStreamIds()[0]; + std::string msidToReplace("a=msid:"); + msidToReplace += streamId; + size_t msidOffset = answer.find(msidToReplace); + ASSERT_NE(std::string::npos, msidOffset); + answer.replace(msidOffset, msidToReplace.size(), "a=msid:foo"); + + SetRemoteAnswer(answer); + + transceiver = GetNegotiatedTransceiver(*mSessionOff, 0); + ASSERT_EQ("foo", transceiver->mRecvTrack.GetStreamIds()[0]); +} + +TEST_P(JsepSessionTest, RenegotiationOffererStopsTransceiver) { + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + if (types.back() == SdpMediaSection::kApplication) { + return; + } + + OfferAnswer(); + + std::vector<JsepTransceiver> origOffererTransceivers = + GetTransceivers(*mSessionOff); + std::vector<JsepTransceiver> origAnswererTransceivers = + GetTransceivers(*mSessionAns); + + JsepTransceiver lastTransceiver = GetTransceivers(*mSessionOff).back(); + // Avoid bundle transport side effects; don't stop the BUNDLE-tag! + lastTransceiver.Stop(); + mSessionOff->SetTransceiver(lastTransceiver); + JsepTrack removedTrack(lastTransceiver.mSendTrack); + + OfferAnswer(CHECK_SUCCESS); + + // Last m-section should be disabled + auto offer = GetParsedLocalDescription(*mSessionOff); + const SdpMediaSection* msection = + &offer->GetMediaSection(offer->GetMediaSectionCount() - 1); + ASSERT_TRUE(msection); + ValidateDisabledMSection(msection); + + // Last m-section should be disabled + auto answer = GetParsedLocalDescription(*mSessionAns); + msection = &answer->GetMediaSection(answer->GetMediaSectionCount() - 1); + ASSERT_TRUE(msection); + ValidateDisabledMSection(msection); + + auto newOffererTransceivers = GetTransceivers(*mSessionOff); + auto newAnswererTransceivers = GetTransceivers(*mSessionAns); + + ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size()); + + ASSERT_FALSE(origOffererTransceivers.back().IsStopped()); + ASSERT_TRUE(newOffererTransceivers.back().IsStopped()); + + ASSERT_FALSE(origAnswererTransceivers.back().IsStopped()); + ASSERT_TRUE(newAnswererTransceivers.back().IsStopped()); + RemoveLastN(origOffererTransceivers, 1); // Ignore this one + RemoveLastN(newOffererTransceivers, 1); // Ignore this one + RemoveLastN(origAnswererTransceivers, 1); // Ignore this one + RemoveLastN(newAnswererTransceivers, 1); // Ignore this one + + ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers)); + ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers)); +} + +TEST_P(JsepSessionTest, RenegotiationAnswererStopsTransceiver) { + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + if (types.back() == SdpMediaSection::kApplication) { + return; + } + + OfferAnswer(); + + std::vector<JsepTransceiver> origOffererTransceivers = + GetTransceivers(*mSessionOff); + std::vector<JsepTransceiver> origAnswererTransceivers = + GetTransceivers(*mSessionAns); + + // Avoid bundle transport side effects; don't stop the BUNDLE-tag! + GetTransceivers(*mSessionAns).back().Stop(); + JsepTrack removedTrack(GetTransceivers(*mSessionAns).back().mSendTrack); + + OfferAnswer(CHECK_SUCCESS); + + // Last m-section should be sendrecv + auto offer = GetParsedLocalDescription(*mSessionOff); + const SdpMediaSection* msection = + &offer->GetMediaSection(offer->GetMediaSectionCount() - 1); + ASSERT_TRUE(msection); + ASSERT_TRUE(msection->IsReceiving()); + ASSERT_TRUE(msection->IsSending()); + + // Last m-section should be disabled + auto answer = GetParsedLocalDescription(*mSessionAns); + msection = &answer->GetMediaSection(answer->GetMediaSectionCount() - 1); + ASSERT_TRUE(msection); + ValidateDisabledMSection(msection); + + auto newOffererTransceivers = GetTransceivers(*mSessionOff); + auto newAnswererTransceivers = GetTransceivers(*mSessionAns); + + ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size()); + + ASSERT_FALSE(origOffererTransceivers.back().IsStopped()); + ASSERT_TRUE(newOffererTransceivers.back().IsStopped()); + ASSERT_FALSE(origAnswererTransceivers.back().IsStopped()); + ASSERT_TRUE(newAnswererTransceivers.back().IsStopped()); + RemoveLastN(origOffererTransceivers, 1); // Ignore this one + RemoveLastN(newOffererTransceivers, 1); // Ignore this one + RemoveLastN(origAnswererTransceivers, 1); // Ignore this one + RemoveLastN(newAnswererTransceivers, 1); // Ignore this one + + ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers)); + ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers)); +} + +TEST_P(JsepSessionTest, RenegotiationBothStopSameTransceiver) { + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + if (types.back() == SdpMediaSection::kApplication) { + return; + } + + OfferAnswer(); + + std::vector<JsepTransceiver> origOffererTransceivers = + GetTransceivers(*mSessionOff); + std::vector<JsepTransceiver> origAnswererTransceivers = + GetTransceivers(*mSessionAns); + + // Avoid bundle transport side effects; don't stop the BUNDLE-tag! + GetTransceivers(*mSessionOff).back().Stop(); + JsepTrack removedTrackOffer(GetTransceivers(*mSessionOff).back().mSendTrack); + GetTransceivers(*mSessionAns).back().Stop(); + JsepTrack removedTrackAnswer(GetTransceivers(*mSessionAns).back().mSendTrack); + + OfferAnswer(CHECK_SUCCESS); + + // Last m-section should be disabled + auto offer = GetParsedLocalDescription(*mSessionOff); + const SdpMediaSection* msection = + &offer->GetMediaSection(offer->GetMediaSectionCount() - 1); + ASSERT_TRUE(msection); + ValidateDisabledMSection(msection); + + // Last m-section should be disabled + auto answer = GetParsedLocalDescription(*mSessionAns); + msection = &answer->GetMediaSection(answer->GetMediaSectionCount() - 1); + ASSERT_TRUE(msection); + ValidateDisabledMSection(msection); + + auto newOffererTransceivers = GetTransceivers(*mSessionOff); + auto newAnswererTransceivers = GetTransceivers(*mSessionAns); + + ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size()); + + ASSERT_FALSE(origOffererTransceivers.back().IsStopped()); + ASSERT_TRUE(newOffererTransceivers.back().IsStopped()); + ASSERT_FALSE(origAnswererTransceivers.back().IsStopped()); + ASSERT_TRUE(newAnswererTransceivers.back().IsStopped()); + RemoveLastN(origOffererTransceivers, 1); // Ignore this one + RemoveLastN(newOffererTransceivers, 1); // Ignore this one + RemoveLastN(origAnswererTransceivers, 1); // Ignore this one + RemoveLastN(newAnswererTransceivers, 1); // Ignore this one + + ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers)); + ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers)); +} + +TEST_P(JsepSessionTest, RenegotiationBothStopTransceiverThenAddTrack) { + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + if (types.back() == SdpMediaSection::kApplication) { + return; + } + + SdpMediaSection::MediaType removedType = types.back(); + + OfferAnswer(); + + // Avoid bundle transport side effects; don't stop the BUNDLE-tag! + GetTransceivers(*mSessionOff).back().Stop(); + JsepTrack removedTrackOffer(GetTransceivers(*mSessionOff).back().mSendTrack); + GetTransceivers(*mSessionOff).back().Stop(); + JsepTrack removedTrackAnswer(GetTransceivers(*mSessionOff).back().mSendTrack); + + OfferAnswer(CHECK_SUCCESS); + + std::vector<JsepTransceiver> origOffererTransceivers = + GetTransceivers(*mSessionOff); + std::vector<JsepTransceiver> origAnswererTransceivers = + GetTransceivers(*mSessionAns); + + std::vector<SdpMediaSection::MediaType> extraTypes; + extraTypes.push_back(removedType); + AddTracks(*mSessionAns, extraTypes); + AddTracks(*mSessionOff, extraTypes); + types.insert(types.end(), extraTypes.begin(), extraTypes.end()); + + OfferAnswer(CHECK_SUCCESS); + + auto newOffererTransceivers = GetTransceivers(*mSessionOff); + auto newAnswererTransceivers = GetTransceivers(*mSessionAns); + + ASSERT_EQ(origOffererTransceivers.size() + 1, newOffererTransceivers.size()); + ASSERT_EQ(origAnswererTransceivers.size() + 1, + newAnswererTransceivers.size()); + + // Ensure that the m-section was re-used; no gaps + ASSERT_EQ(origOffererTransceivers.back().GetLevel(), + newOffererTransceivers.back().GetLevel()); + + ASSERT_EQ(origAnswererTransceivers.back().GetLevel(), + newAnswererTransceivers.back().GetLevel()); +} + +TEST_P(JsepSessionTest, RenegotiationBothStopTransceiverDifferentMsection) { + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + + if (types.size() < 2) { + return; + } + + if (GetTransceivers(*mSessionOff)[0].GetMediaType() == + SdpMediaSection::kApplication || + GetTransceivers(*mSessionOff)[1].GetMediaType() == + SdpMediaSection::kApplication) { + return; + } + + OfferAnswer(); + + GetTransceivers(*mSessionOff)[0].Stop(); + GetTransceivers(*mSessionOff)[1].Stop(); + + OfferAnswer(CHECK_SUCCESS); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[1].IsStopped()); +} + +TEST_P(JsepSessionTest, RenegotiationOffererChangesStreamId) { + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + + if (GetTransceivers(*mSessionOff)[0].GetMediaType() == + SdpMediaSection::kApplication) { + return; + } + + OfferAnswer(); + + GetTransceivers(*mSessionOff)[0].mSendTrack.UpdateStreamIds( + std::vector<std::string>(1, "newstream")); + + OfferAnswer(CHECK_SUCCESS); + + ASSERT_EQ("newstream", + GetTransceivers(*mSessionAns)[0].mRecvTrack.GetStreamIds()[0]); +} + +TEST_P(JsepSessionTest, RenegotiationAnswererChangesStreamId) { + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + + if (GetTransceivers(*mSessionOff)[0].GetMediaType() == + SdpMediaSection::kApplication) { + return; + } + + OfferAnswer(); + + GetTransceivers(*mSessionAns)[0].mSendTrack.UpdateStreamIds( + std::vector<std::string>(1, "newstream")); + + OfferAnswer(CHECK_SUCCESS); + + ASSERT_EQ("newstream", + GetTransceivers(*mSessionOff)[0].mRecvTrack.GetStreamIds()[0]); +} + +// Tests whether auto-assigned remote msids (ie; what happens when the other +// side doesn't use msid attributes) are stable across renegotiation. +TEST_P(JsepSessionTest, RenegotiationAutoAssignedMsidIsStable) { + AddTracks(*mSessionOff); + std::string offer = CreateOffer(); + SetLocalOffer(offer); + SetRemoteOffer(offer); + AddTracks(*mSessionAns); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + + DisableMsid(&answer); + + SetRemoteAnswer(answer, CHECK_SUCCESS); + + std::vector<JsepTransceiver> origOffererTransceivers = + GetTransceivers(*mSessionOff); + std::vector<JsepTransceiver> origAnswererTransceivers = + GetTransceivers(*mSessionAns); + + ASSERT_EQ(origOffererTransceivers.size(), origAnswererTransceivers.size()); + for (size_t i = 0; i < origOffererTransceivers.size(); ++i) { + ASSERT_FALSE(IsNull(origOffererTransceivers[i].mRecvTrack)); + ASSERT_FALSE(IsNull(origAnswererTransceivers[i].mSendTrack)); + // These should not match since we've monkeyed with the msid + ASSERT_NE(origOffererTransceivers[i].mRecvTrack.GetStreamIds(), + origAnswererTransceivers[i].mSendTrack.GetStreamIds()); + } + + offer = CreateOffer(); + SetLocalOffer(offer); + SetRemoteOffer(offer); + answer = CreateAnswer(); + SetLocalAnswer(answer); + + DisableMsid(&answer); + + SetRemoteAnswer(answer, CHECK_SUCCESS); + + auto newOffererTransceivers = GetTransceivers(*mSessionOff); + + ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers)); +} + +TEST_P(JsepSessionTest, RenegotiationOffererDisablesTelephoneEvent) { + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + OfferAnswer(); + + // check all the audio tracks to make sure they have 2 codecs (109 and 101), + // and dtmf is enabled on all audio tracks + std::vector<JsepTrack> tracks; + for (const auto& transceiver : GetTransceivers(*mSessionOff)) { + tracks.push_back(transceiver.mSendTrack); + tracks.push_back(transceiver.mRecvTrack); + } + + for (const JsepTrack& track : tracks) { + if (track.GetMediaType() != SdpMediaSection::kAudio) { + continue; + } + const JsepTrackNegotiatedDetails* details = track.GetNegotiatedDetails(); + ASSERT_EQ(1U, details->GetEncodingCount()); + const JsepTrackEncoding& encoding = details->GetEncoding(0); + ASSERT_EQ(5U, encoding.GetCodecs().size()); + ASSERT_TRUE(encoding.HasFormat("109")); + ASSERT_TRUE(encoding.HasFormat("101")); + for (const auto& codec : encoding.GetCodecs()) { + ASSERT_TRUE(codec); + // we can cast here because we've already checked for audio track + const JsepAudioCodecDescription* audioCodec = + static_cast<const JsepAudioCodecDescription*>(codec.get()); + ASSERT_TRUE(audioCodec->mDtmfEnabled); + } + } + + std::string offer = CreateOffer(); + ReplaceInSdp(&offer, "8 101", "8"); + ReplaceInSdp(&offer, "a=fmtp:101 0-15\r\n", ""); + ReplaceInSdp(&offer, "a=rtpmap:101 telephone-event/8000/1\r\n", ""); + std::cerr << "modified OFFER: " << offer << std::endl; + + SetLocalOffer(offer); + SetRemoteOffer(offer); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + SetRemoteAnswer(answer); + + // check all the audio tracks to make sure they have 1 codec (109), + // and dtmf is disabled on all audio tracks + tracks.clear(); + for (const auto& transceiver : GetTransceivers(*mSessionOff)) { + tracks.push_back(transceiver.mSendTrack); + tracks.push_back(transceiver.mRecvTrack); + } + + for (const JsepTrack& track : tracks) { + if (track.GetMediaType() != SdpMediaSection::kAudio) { + continue; + } + const JsepTrackNegotiatedDetails* details = track.GetNegotiatedDetails(); + ASSERT_EQ(1U, details->GetEncodingCount()); + const JsepTrackEncoding& encoding = details->GetEncoding(0); + ASSERT_EQ(4U, encoding.GetCodecs().size()); + ASSERT_TRUE(encoding.HasFormat("109")); + // we can cast here because we've already checked for audio track + const JsepAudioCodecDescription* audioCodec = + static_cast<const JsepAudioCodecDescription*>( + encoding.GetCodecs()[0].get()); + ASSERT_TRUE(audioCodec); + ASSERT_FALSE(audioCodec->mDtmfEnabled); + } +} + +// Tests behavior when the answerer does not use msid in the initial exchange, +// but does on renegotiation. +TEST_P(JsepSessionTest, RenegotiationAnswererEnablesMsid) { + AddTracks(*mSessionOff); + std::string offer = CreateOffer(); + SetLocalOffer(offer); + SetRemoteOffer(offer); + AddTracks(*mSessionAns); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + + DisableMsid(&answer); + + SetRemoteAnswer(answer, CHECK_SUCCESS); + + std::vector<JsepTransceiver> origOffererTransceivers = + GetTransceivers(*mSessionOff); + std::vector<JsepTransceiver> origAnswererTransceivers = + GetTransceivers(*mSessionAns); + + offer = CreateOffer(); + SetLocalOffer(offer); + SetRemoteOffer(offer); + answer = CreateAnswer(); + SetLocalAnswer(answer); + SetRemoteAnswer(answer, CHECK_SUCCESS); + + auto newOffererTransceivers = GetTransceivers(*mSessionOff); + + ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size()); + for (size_t i = 0; i < origOffererTransceivers.size(); ++i) { + ASSERT_EQ(origOffererTransceivers[i].mRecvTrack.GetMediaType(), + newOffererTransceivers[i].mRecvTrack.GetMediaType()); + + ASSERT_TRUE(Equals(origOffererTransceivers[i].mSendTrack, + newOffererTransceivers[i].mSendTrack)); + ASSERT_TRUE(Equals(origOffererTransceivers[i].mTransport, + newOffererTransceivers[i].mTransport)); + + if (origOffererTransceivers[i].mRecvTrack.GetMediaType() == + SdpMediaSection::kApplication) { + ASSERT_TRUE(Equals(origOffererTransceivers[i].mRecvTrack, + newOffererTransceivers[i].mRecvTrack)); + } else { + // This should be the only difference + ASSERT_FALSE(Equals(origOffererTransceivers[i].mRecvTrack, + newOffererTransceivers[i].mRecvTrack)); + } + } +} + +TEST_P(JsepSessionTest, RenegotiationAnswererDisablesMsid) { + AddTracks(*mSessionOff); + std::string offer = CreateOffer(); + SetLocalOffer(offer); + SetRemoteOffer(offer); + AddTracks(*mSessionAns); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + SetRemoteAnswer(answer, CHECK_SUCCESS); + + std::vector<JsepTransceiver> origOffererTransceivers = + GetTransceivers(*mSessionOff); + std::vector<JsepTransceiver> origAnswererTransceivers = + GetTransceivers(*mSessionAns); + + offer = CreateOffer(); + SetLocalOffer(offer); + SetRemoteOffer(offer); + answer = CreateAnswer(); + SetLocalAnswer(answer); + + DisableMsid(&answer); + + SetRemoteAnswer(answer, CHECK_SUCCESS); + + auto newOffererTransceivers = GetTransceivers(*mSessionOff); + + ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size()); + for (size_t i = 0; i < origOffererTransceivers.size(); ++i) { + ASSERT_EQ(origOffererTransceivers[i].mRecvTrack.GetMediaType(), + newOffererTransceivers[i].mRecvTrack.GetMediaType()); + + ASSERT_TRUE(Equals(origOffererTransceivers[i].mSendTrack, + newOffererTransceivers[i].mSendTrack)); + ASSERT_TRUE(Equals(origOffererTransceivers[i].mTransport, + newOffererTransceivers[i].mTransport)); + + if (origOffererTransceivers[i].mRecvTrack.GetMediaType() == + SdpMediaSection::kApplication) { + ASSERT_TRUE(Equals(origOffererTransceivers[i].mRecvTrack, + newOffererTransceivers[i].mRecvTrack)); + } else { + // This should be the only difference + ASSERT_FALSE(Equals(origOffererTransceivers[i].mRecvTrack, + newOffererTransceivers[i].mRecvTrack)); + } + } +} + +// Tests behavior when offerer does not use bundle on the initial offer/answer, +// but does on renegotiation. +TEST_P(JsepSessionTest, RenegotiationOffererEnablesBundle) { + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + + if (types.size() < 2) { + // No bundle will happen here. + return; + } + + std::string offer = CreateOffer(); + + DisableBundle(&offer); + + SetLocalOffer(offer); + SetRemoteOffer(offer); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + SetRemoteAnswer(answer); + + std::vector<JsepTransceiver> origOffererTransceivers = + GetTransceivers(*mSessionOff); + std::vector<JsepTransceiver> origAnswererTransceivers = + GetTransceivers(*mSessionAns); + + OfferAnswer(); + + auto newOffererTransceivers = GetTransceivers(*mSessionOff); + auto newAnswererTransceivers = GetTransceivers(*mSessionAns); + + ASSERT_EQ(newOffererTransceivers.size(), newAnswererTransceivers.size()); + ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size()); + ASSERT_EQ(origAnswererTransceivers.size(), newAnswererTransceivers.size()); + + for (size_t i = 0; i < newOffererTransceivers.size(); ++i) { + // No bundle initially + ASSERT_FALSE(origOffererTransceivers[i].HasBundleLevel()); + ASSERT_FALSE(origAnswererTransceivers[i].HasBundleLevel()); + if (i != 0) { + ASSERT_FALSE(Equals(origOffererTransceivers[0].mTransport, + origOffererTransceivers[i].mTransport)); + ASSERT_FALSE(Equals(origAnswererTransceivers[0].mTransport, + origAnswererTransceivers[i].mTransport)); + } + + // Verify that bundle worked after renegotiation + ASSERT_TRUE(newOffererTransceivers[i].HasBundleLevel()); + ASSERT_TRUE(newAnswererTransceivers[i].HasBundleLevel()); + ASSERT_TRUE(Equals(newOffererTransceivers[0].mTransport, + newOffererTransceivers[i].mTransport)); + ASSERT_TRUE(Equals(newAnswererTransceivers[0].mTransport, + newAnswererTransceivers[i].mTransport)); + } +} + +TEST_P(JsepSessionTest, RenegotiationOffererDisablesBundleTransport) { + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + + if (types.size() < 2) { + return; + } + + OfferAnswer(); + + auto stopped = GetTransceiverByLevel(*mSessionOff, 0); + stopped->Stop(); + mSessionOff->SetTransceiver(*stopped); + + std::vector<JsepTransceiver> origOffererTransceivers = + GetTransceivers(*mSessionOff); + std::vector<JsepTransceiver> origAnswererTransceivers = + GetTransceivers(*mSessionAns); + + OfferAnswer(CHECK_SUCCESS); + + auto newOffererTransceivers = GetTransceivers(*mSessionOff); + auto newAnswererTransceivers = GetTransceivers(*mSessionAns); + + ASSERT_EQ(newOffererTransceivers.size(), newAnswererTransceivers.size()); + ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size()); + ASSERT_EQ(origAnswererTransceivers.size(), newAnswererTransceivers.size()); + + Maybe<JsepTransceiver> ot0 = GetTransceiverByLevel(newOffererTransceivers, 0); + Maybe<JsepTransceiver> at0 = + GetTransceiverByLevel(newAnswererTransceivers, 0); + ASSERT_FALSE(ot0->HasBundleLevel()); + ASSERT_FALSE(at0->HasBundleLevel()); + + ASSERT_FALSE( + Equals(ot0->mTransport, + GetTransceiverByLevel(origOffererTransceivers, 0)->mTransport)); + ASSERT_FALSE( + Equals(at0->mTransport, + GetTransceiverByLevel(origAnswererTransceivers, 0)->mTransport)); + + ASSERT_EQ(0U, ot0->mTransport.mComponents); + ASSERT_EQ(0U, at0->mTransport.mComponents); + + for (size_t i = 1; i < types.size() - 1; ++i) { + Maybe<JsepTransceiver> ot = + GetTransceiverByLevel(newOffererTransceivers, i); + Maybe<JsepTransceiver> at = + GetTransceiverByLevel(newAnswererTransceivers, i); + ASSERT_TRUE(ot->HasBundleLevel()); + ASSERT_TRUE(at->HasBundleLevel()); + ASSERT_EQ(1U, ot->BundleLevel()); + ASSERT_EQ(1U, at->BundleLevel()); + ASSERT_FALSE(Equals(ot0->mTransport, ot->mTransport)); + ASSERT_FALSE(Equals(at0->mTransport, at->mTransport)); + } +} + +TEST_P(JsepSessionTest, RenegotiationAnswererDisablesBundleTransport) { + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + + if (types.size() < 2) { + return; + } + + OfferAnswer(); + + std::vector<JsepTransceiver> origOffererTransceivers = + GetTransceivers(*mSessionOff); + std::vector<JsepTransceiver> origAnswererTransceivers = + GetTransceivers(*mSessionAns); + + auto stopped = GetTransceiverByLevel(*mSessionAns, 0); + stopped->Stop(); + mSessionAns->SetTransceiver(*stopped); + + OfferAnswer(CHECK_SUCCESS); + + auto newOffererTransceivers = GetTransceivers(*mSessionOff); + auto newAnswererTransceivers = GetTransceivers(*mSessionAns); + + ASSERT_EQ(newOffererTransceivers.size(), newAnswererTransceivers.size()); + ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size()); + ASSERT_EQ(origAnswererTransceivers.size(), newAnswererTransceivers.size()); + + Maybe<JsepTransceiver> ot0 = GetTransceiverByLevel(newOffererTransceivers, 0); + Maybe<JsepTransceiver> at0 = + GetTransceiverByLevel(newAnswererTransceivers, 0); + ASSERT_FALSE(ot0->HasBundleLevel()); + ASSERT_FALSE(at0->HasBundleLevel()); + + ASSERT_FALSE( + Equals(ot0->mTransport, + GetTransceiverByLevel(origOffererTransceivers, 0)->mTransport)); + ASSERT_FALSE( + Equals(at0->mTransport, + GetTransceiverByLevel(origAnswererTransceivers, 0)->mTransport)); + + ASSERT_EQ(0U, ot0->mTransport.mComponents); + ASSERT_EQ(0U, at0->mTransport.mComponents); + + // With bundle-policy "balanced", this ends up being somewhat complicated. + // The first m-section of each type is _not_ marked bundle-only, + // but subsequent m-sections of that type are. This means that if there is + // more than one type, there is more than one transport, which means there's + // a fallback when the bundle-tag is disabled (the first such transport). We + // should be using that fallback when it exists. + Maybe<size_t> fallbackTransport; + for (size_t level = 1; level != types.size(); ++level) { + if (types[level] != types[0]) { + fallbackTransport = Some(level); + break; + } + } + + for (size_t i = 1; i < newOffererTransceivers.size(); ++i) { + auto ot = GetTransceiverByLevel(newOffererTransceivers, i); + auto at = GetTransceiverByLevel(newAnswererTransceivers, i); + // If there is no fallback, the bundle level will be left pointing at the + // dead transport at index 0. + size_t expectedBundleLevel = fallbackTransport.valueOr(0); + auto otWithTransport = + GetTransceiverByLevel(newOffererTransceivers, expectedBundleLevel); + auto atWithTransport = + GetTransceiverByLevel(newAnswererTransceivers, expectedBundleLevel); + ASSERT_TRUE(ot->HasBundleLevel()); + ASSERT_TRUE(at->HasBundleLevel()); + ASSERT_EQ(expectedBundleLevel, ot->BundleLevel()); + ASSERT_EQ(expectedBundleLevel, at->BundleLevel()); + // TODO: When creating an answer where we have rejected the bundle + // transport, we do not do a good job of creating a sensible SDP. Mainly, + // when we remove the rejected mid from the bundle group, we can leave a + // bundle-only mid as the first one when others are available. + ASSERT_TRUE(Equals(otWithTransport->mTransport, ot->mTransport)); + ASSERT_TRUE(Equals(atWithTransport->mTransport, at->mTransport)); + } +} + +TEST_P(JsepSessionTest, ParseRejectsBadMediaFormat) { + AddTracks(*mSessionOff); + if (types.front() == SdpMediaSection::MediaType::kApplication) { + return; + } + std::string offer = CreateOffer(); + UniquePtr<Sdp> munge(Parse(offer)); + SdpMediaSection& mediaSection = munge->GetMediaSection(0); + mediaSection.AddCodec("75", "DummyFormatVal", 8000, 1); + std::string sdpString = munge->ToString(); + JsepSession::Result result = + mSessionOff->SetLocalDescription(kJsepSdpOffer, sdpString); + ASSERT_EQ(dom::PCError::OperationError, *result.mError); +} + +TEST_P(JsepSessionTest, FullCallWithCandidates) { + AddTracks(*mSessionOff); + std::string offer = CreateOffer(); + SetLocalOffer(offer); + mOffCandidates->Gather(*mSessionOff); + + UniquePtr<Sdp> localOffer( + Parse(mSessionOff->GetLocalDescription(kJsepDescriptionPending))); + for (size_t i = 0; i < localOffer->GetMediaSectionCount(); ++i) { + std::string id = GetTransportId(*mSessionOff, i); + bool bundleOnly = + localOffer->GetMediaSection(i).GetAttributeList().HasAttribute( + SdpAttribute::kBundleOnlyAttribute); + mOffCandidates->CheckRtpCandidates( + !bundleOnly, localOffer->GetMediaSection(i), id, + "Local offer after gathering should have RTP candidates " + "(unless bundle-only)"); + mOffCandidates->CheckDefaultRtpCandidate( + !bundleOnly, localOffer->GetMediaSection(i), id, + "Local offer after gathering should have a default RTP candidate " + "(unless bundle-only)"); + mOffCandidates->CheckRtcpCandidates( + !bundleOnly && types[i] != SdpMediaSection::kApplication, + localOffer->GetMediaSection(i), id, + "Local offer after gathering should have RTCP candidates " + "(unless m=application or bundle-only)"); + mOffCandidates->CheckDefaultRtcpCandidate( + !bundleOnly && types[i] != SdpMediaSection::kApplication, + localOffer->GetMediaSection(i), id, + "Local offer after gathering should have a default RTCP candidate " + "(unless m=application or bundle-only)"); + CheckEndOfCandidates( + !bundleOnly, localOffer->GetMediaSection(i), + "Local offer after gathering should have an end-of-candidates " + "(unless bundle-only)"); + } + + SetRemoteOffer(offer); + mOffCandidates->Trickle(*mSessionAns); + + UniquePtr<Sdp> remoteOffer( + Parse(mSessionAns->GetRemoteDescription(kJsepDescriptionPending))); + for (size_t i = 0; i < remoteOffer->GetMediaSectionCount(); ++i) { + std::string id = GetTransportId(*mSessionOff, i); + bool bundleOnly = + remoteOffer->GetMediaSection(i).GetAttributeList().HasAttribute( + SdpAttribute::kBundleOnlyAttribute); + mOffCandidates->CheckRtpCandidates( + !bundleOnly, remoteOffer->GetMediaSection(i), id, + "Remote offer after trickle should have RTP candidates " + "(unless bundle-only)"); + mOffCandidates->CheckDefaultRtpCandidate( + false, remoteOffer->GetMediaSection(i), id, + "Remote offer after trickle should not have a default RTP candidate."); + mOffCandidates->CheckRtcpCandidates( + !bundleOnly && types[i] != SdpMediaSection::kApplication, + remoteOffer->GetMediaSection(i), id, + "Remote offer after trickle should have RTCP candidates " + "(unless m=application or bundle-only)"); + mOffCandidates->CheckDefaultRtcpCandidate( + false, remoteOffer->GetMediaSection(i), id, + "Remote offer after trickle should not have a default RTCP candidate."); + CheckEndOfCandidates( + true, remoteOffer->GetMediaSection(i), + "Remote offer after trickle should have an end-of-candidates."); + } + + AddTracks(*mSessionAns); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + // This will gather candidates that mSessionAns knows it doesn't need. + // They should not be present in the SDP. + mAnsCandidates->Gather(*mSessionAns); + + UniquePtr<Sdp> localAnswer( + Parse(mSessionAns->GetLocalDescription(kJsepDescriptionCurrent))); + std::string id0 = GetTransportId(*mSessionAns, 0); + for (size_t i = 0; i < localAnswer->GetMediaSectionCount(); ++i) { + std::string id = GetTransportId(*mSessionAns, i); + mAnsCandidates->CheckRtpCandidates( + i == 0, localAnswer->GetMediaSection(i), id, + "Local answer after gathering should have RTP candidates on level 0."); + mAnsCandidates->CheckDefaultRtpCandidate( + true, localAnswer->GetMediaSection(i), id0, + "Local answer after gathering should have a default RTP candidate " + "on all levels that matches transport level 0."); + mAnsCandidates->CheckRtcpCandidates( + false, localAnswer->GetMediaSection(i), id, + "Local answer after gathering should not have RTCP candidates " + "(because we're answering with rtcp-mux)"); + mAnsCandidates->CheckDefaultRtcpCandidate( + false, localAnswer->GetMediaSection(i), id, + "Local answer after gathering should not have a default RTCP candidate " + "(because we're answering with rtcp-mux)"); + CheckEndOfCandidates( + i == 0, localAnswer->GetMediaSection(i), + "Local answer after gathering should have an end-of-candidates only for" + " level 0."); + } + + SetRemoteAnswer(answer); + mAnsCandidates->Trickle(*mSessionOff); + + UniquePtr<Sdp> remoteAnswer( + Parse(mSessionOff->GetRemoteDescription(kJsepDescriptionCurrent))); + for (size_t i = 0; i < remoteAnswer->GetMediaSectionCount(); ++i) { + std::string id = GetTransportId(*mSessionAns, i); + mAnsCandidates->CheckRtpCandidates( + i == 0, remoteAnswer->GetMediaSection(i), id, + "Remote answer after trickle should have RTP candidates on level 0."); + mAnsCandidates->CheckDefaultRtpCandidate( + false, remoteAnswer->GetMediaSection(i), id, + "Remote answer after trickle should not have a default RTP candidate."); + mAnsCandidates->CheckRtcpCandidates( + false, remoteAnswer->GetMediaSection(i), id, + "Remote answer after trickle should not have RTCP candidates " + "(because we're answering with rtcp-mux)"); + mAnsCandidates->CheckDefaultRtcpCandidate( + false, remoteAnswer->GetMediaSection(i), id, + "Remote answer after trickle should not have a default RTCP " + "candidate."); + CheckEndOfCandidates( + true, remoteAnswer->GetMediaSection(i), + "Remote answer after trickle should have an end-of-candidates."); + } +} + +TEST_P(JsepSessionTest, RenegotiationWithCandidates) { + AddTracks(*mSessionOff); + std::string offer = CreateOffer(); + SetLocalOffer(offer); + mOffCandidates->Gather(*mSessionOff); + SetRemoteOffer(offer); + mOffCandidates->Trickle(*mSessionAns); + AddTracks(*mSessionAns); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + mAnsCandidates->Gather(*mSessionAns); + SetRemoteAnswer(answer); + mAnsCandidates->Trickle(*mSessionOff); + + offer = CreateOffer(); + SetLocalOffer(offer); + + UniquePtr<Sdp> parsedOffer(Parse(offer)); + std::string id0 = GetTransportId(*mSessionOff, 0); + for (size_t i = 0; i < parsedOffer->GetMediaSectionCount(); ++i) { + std::string id = GetTransportId(*mSessionOff, i); + mOffCandidates->CheckRtpCandidates( + i == 0, parsedOffer->GetMediaSection(i), id, + "Local reoffer before gathering should have RTP candidates on level 0" + " only."); + mOffCandidates->CheckDefaultRtpCandidate( + i == 0, parsedOffer->GetMediaSection(i), id0, + "Local reoffer before gathering should have a default RTP candidate " + "on level 0 only."); + mOffCandidates->CheckRtcpCandidates( + false, parsedOffer->GetMediaSection(i), id, + "Local reoffer before gathering should not have RTCP candidates."); + mOffCandidates->CheckDefaultRtcpCandidate( + false, parsedOffer->GetMediaSection(i), id, + "Local reoffer before gathering should not have a default RTCP " + "candidate."); + CheckEndOfCandidates( + i == 0, parsedOffer->GetMediaSection(i), + "Local reoffer before gathering should have an end-of-candidates " + "(level 0 only)"); + } + + // mSessionAns should generate a reoffer that is similar + std::string otherOffer; + JsepOfferOptions defaultOptions; + JsepSession::Result result = + mSessionAns->CreateOffer(defaultOptions, &otherOffer); + ASSERT_FALSE(result.mError.isSome()); + parsedOffer = Parse(otherOffer); + id0 = GetTransportId(*mSessionAns, 0); + for (size_t i = 0; i < parsedOffer->GetMediaSectionCount(); ++i) { + std::string id = GetTransportId(*mSessionAns, i); + mAnsCandidates->CheckRtpCandidates( + i == 0, parsedOffer->GetMediaSection(i), id, + "Local reoffer before gathering should have RTP candidates on level 0" + " only. (previous answerer)"); + mAnsCandidates->CheckDefaultRtpCandidate( + i == 0, parsedOffer->GetMediaSection(i), id0, + "Local reoffer before gathering should have a default RTP candidate " + "on level 0 only. (previous answerer)"); + mAnsCandidates->CheckRtcpCandidates( + false, parsedOffer->GetMediaSection(i), id, + "Local reoffer before gathering should not have RTCP candidates." + " (previous answerer)"); + mAnsCandidates->CheckDefaultRtcpCandidate( + false, parsedOffer->GetMediaSection(i), id, + "Local reoffer before gathering should not have a default RTCP " + "candidate. (previous answerer)"); + CheckEndOfCandidates( + i == 0, parsedOffer->GetMediaSection(i), + "Local reoffer before gathering should have an end-of-candidates " + "(level 0 only)"); + } + + // Ok, let's continue with the renegotiation + SetRemoteOffer(offer); + + // PeerConnection will not re-gather for RTP, but it will for RTCP in case + // the answerer decides to turn off rtcp-mux. + if (types[0] != SdpMediaSection::kApplication) { + mOffCandidates->Gather(*mSessionOff, GetTransportId(*mSessionOff, 0), RTCP); + } + + // Since the remaining levels were bundled, PeerConnection will re-gather for + // both RTP and RTCP, in case the answerer rejects bundle, provided + // bundle-only isn't being used. + UniquePtr<Sdp> localOffer( + Parse(mSessionOff->GetLocalDescription(kJsepDescriptionPending))); + for (size_t level = 1; level < types.size(); ++level) { + std::string id = GetTransportId(*mSessionOff, level); + bool bundleOnly = + localOffer->GetMediaSection(level).GetAttributeList().HasAttribute( + SdpAttribute::kBundleOnlyAttribute); + if (!id.empty() && !bundleOnly) { + mOffCandidates->Gather(*mSessionOff, id, RTP); + if (types[level] != SdpMediaSection::kApplication) { + mOffCandidates->Gather(*mSessionOff, id, RTCP); + } + } + } + mOffCandidates->FinishGathering(*mSessionOff); + localOffer = Parse(mSessionOff->GetLocalDescription(kJsepDescriptionPending)); + + mOffCandidates->Trickle(*mSessionAns); + + for (size_t i = 0; i < localOffer->GetMediaSectionCount(); ++i) { + std::string id = GetTransportId(*mSessionOff, i); + bool bundleOnly = + localOffer->GetMediaSection(i).GetAttributeList().HasAttribute( + SdpAttribute::kBundleOnlyAttribute); + mOffCandidates->CheckRtpCandidates( + !bundleOnly, localOffer->GetMediaSection(i), id, + "Local reoffer after gathering should have RTP candidates " + "(unless bundle-only)"); + mOffCandidates->CheckDefaultRtpCandidate( + !bundleOnly, localOffer->GetMediaSection(i), id, + "Local reoffer after gathering should have a default RTP candidate " + "(unless bundle-only)"); + mOffCandidates->CheckRtcpCandidates( + !bundleOnly && (types[i] != SdpMediaSection::kApplication), + localOffer->GetMediaSection(i), id, + "Local reoffer after gathering should have RTCP candidates " + "(unless m=application or bundle-only)"); + mOffCandidates->CheckDefaultRtcpCandidate( + !bundleOnly && (types[i] != SdpMediaSection::kApplication), + localOffer->GetMediaSection(i), id, + "Local reoffer after gathering should have a default RTCP candidate " + "(unless m=application or bundle-only)"); + CheckEndOfCandidates( + !bundleOnly, localOffer->GetMediaSection(i), + "Local reoffer after gathering should have an end-of-candidates " + "(unless bundle-only)"); + } + + UniquePtr<Sdp> remoteOffer( + Parse(mSessionAns->GetRemoteDescription(kJsepDescriptionPending))); + for (size_t i = 0; i < remoteOffer->GetMediaSectionCount(); ++i) { + bool bundleOnly = + remoteOffer->GetMediaSection(i).GetAttributeList().HasAttribute( + SdpAttribute::kBundleOnlyAttribute); + std::string id = GetTransportId(*mSessionOff, i); + mOffCandidates->CheckRtpCandidates( + !bundleOnly, remoteOffer->GetMediaSection(i), id, + "Remote reoffer after trickle should have RTP candidates " + "(unless bundle-only)"); + mOffCandidates->CheckDefaultRtpCandidate( + i == 0, remoteOffer->GetMediaSection(i), id, + "Remote reoffer should have a default RTP candidate on level 0 " + "(because it was gathered last offer/answer)."); + mOffCandidates->CheckRtcpCandidates( + !bundleOnly && types[i] != SdpMediaSection::kApplication, + remoteOffer->GetMediaSection(i), id, + "Remote reoffer after trickle should have RTCP candidates " + "(unless m=application or bundle-only)"); + mOffCandidates->CheckDefaultRtcpCandidate( + false, remoteOffer->GetMediaSection(i), id, + "Remote reoffer should not have a default RTCP candidate."); + CheckEndOfCandidates(true, remoteOffer->GetMediaSection(i), + "Remote reoffer should have an end-of-candidates."); + } + + answer = CreateAnswer(); + SetLocalAnswer(answer); + SetRemoteAnswer(answer); + // No candidates should be gathered at the answerer, but default candidates + // should be set. + mAnsCandidates->FinishGathering(*mSessionAns); + + UniquePtr<Sdp> localAnswer( + Parse(mSessionAns->GetLocalDescription(kJsepDescriptionCurrent))); + id0 = GetTransportId(*mSessionAns, 0); + for (size_t i = 0; i < localAnswer->GetMediaSectionCount(); ++i) { + std::string id = GetTransportId(*mSessionAns, 0); + mAnsCandidates->CheckRtpCandidates( + i == 0, localAnswer->GetMediaSection(i), id, + "Local reanswer after gathering should have RTP candidates on level " + "0."); + mAnsCandidates->CheckDefaultRtpCandidate( + true, localAnswer->GetMediaSection(i), id0, + "Local reanswer after gathering should have a default RTP candidate " + "on all levels that matches transport level 0."); + mAnsCandidates->CheckRtcpCandidates( + false, localAnswer->GetMediaSection(i), id, + "Local reanswer after gathering should not have RTCP candidates " + "(because we're reanswering with rtcp-mux)"); + mAnsCandidates->CheckDefaultRtcpCandidate( + false, localAnswer->GetMediaSection(i), id, + "Local reanswer after gathering should not have a default RTCP " + "candidate (because we're reanswering with rtcp-mux)"); + CheckEndOfCandidates( + i == 0, localAnswer->GetMediaSection(i), + "Local reanswer after gathering should have an end-of-candidates only " + "for level 0."); + } + + UniquePtr<Sdp> remoteAnswer( + Parse(mSessionOff->GetRemoteDescription(kJsepDescriptionCurrent))); + for (size_t i = 0; i < localAnswer->GetMediaSectionCount(); ++i) { + std::string id = GetTransportId(*mSessionAns, 0); + mAnsCandidates->CheckRtpCandidates( + i == 0, remoteAnswer->GetMediaSection(i), id, + "Remote reanswer after trickle should have RTP candidates on level 0."); + mAnsCandidates->CheckDefaultRtpCandidate( + i == 0, remoteAnswer->GetMediaSection(i), id, + "Remote reanswer should have a default RTP candidate on level 0 " + "(because it was gathered last offer/answer)."); + mAnsCandidates->CheckRtcpCandidates( + false, remoteAnswer->GetMediaSection(i), id, + "Remote reanswer after trickle should not have RTCP candidates " + "(because we're reanswering with rtcp-mux)"); + mAnsCandidates->CheckDefaultRtcpCandidate( + false, remoteAnswer->GetMediaSection(i), id, + "Remote reanswer after trickle should not have a default RTCP " + "candidate."); + CheckEndOfCandidates(i == 0, remoteAnswer->GetMediaSection(i), + "Remote reanswer after trickle should have an " + "end-of-candidates on level 0 only."); + } +} + +TEST_P(JsepSessionTest, RenegotiationAnswererSendonly) { + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + OfferAnswer(); + + std::string offer = CreateOffer(); + SetLocalOffer(offer); + SetRemoteOffer(offer); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + + UniquePtr<Sdp> parsedAnswer(Parse(answer)); + for (size_t i = 0; i < parsedAnswer->GetMediaSectionCount(); ++i) { + SdpMediaSection& msection = parsedAnswer->GetMediaSection(i); + if (msection.GetMediaType() != SdpMediaSection::kApplication) { + msection.SetReceiving(false); + } + } + + answer = parsedAnswer->ToString(); + + SetRemoteAnswer(answer); + + for (const JsepTrack& track : GetLocalTracks(*mSessionOff)) { + if (track.GetMediaType() != SdpMediaSection::kApplication) { + ASSERT_FALSE(track.GetActive()); + } + } + + ASSERT_EQ(types.size(), GetTransceivers(*mSessionOff).size()); +} + +TEST_P(JsepSessionTest, RenegotiationAnswererInactive) { + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + OfferAnswer(); + + std::string offer = CreateOffer(); + SetLocalOffer(offer); + SetRemoteOffer(offer); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + + UniquePtr<Sdp> parsedAnswer(Parse(answer)); + for (size_t i = 0; i < parsedAnswer->GetMediaSectionCount(); ++i) { + SdpMediaSection& msection = parsedAnswer->GetMediaSection(i); + if (msection.GetMediaType() != SdpMediaSection::kApplication) { + msection.SetReceiving(false); + msection.SetSending(false); + } + } + + answer = parsedAnswer->ToString(); + + SetRemoteAnswer(answer, CHECK_SUCCESS); // Won't have answerer tracks + + for (const JsepTrack& track : GetLocalTracks(*mSessionOff)) { + if (track.GetMediaType() != SdpMediaSection::kApplication) { + ASSERT_FALSE(track.GetActive()); + } + } + + ASSERT_EQ(types.size(), GetTransceivers(*mSessionOff).size()); +} + +INSTANTIATE_TEST_SUITE_P( + Variants, JsepSessionTest, + ::testing::Values("audio", "video", "datachannel", "audio,video", + "video,audio", "audio,datachannel", "video,datachannel", + "video,audio,datachannel", "audio,video,datachannel", + "datachannel,audio", "datachannel,video", + "datachannel,audio,video", "datachannel,video,audio", + "audio,datachannel,video", "video,datachannel,audio", + "audio,audio", "video,video", "audio,audio,video", + "audio,video,video", "audio,audio,video,video", + "audio,audio,video,video,datachannel")); + +// offerToReceiveXxx variants + +TEST_F(JsepSessionTest, OfferAnswerRecvOnlyLines) { + mSessionOff->AddTransceiver(JsepTransceiver( + SdpMediaSection::kAudio, mUuidGen, SdpDirectionAttribute::kRecvonly)); + mSessionOff->AddTransceiver(JsepTransceiver( + SdpMediaSection::kVideo, mUuidGen, SdpDirectionAttribute::kRecvonly)); + mSessionOff->AddTransceiver(JsepTransceiver( + SdpMediaSection::kVideo, mUuidGen, SdpDirectionAttribute::kRecvonly)); + std::string offer = CreateOffer(); + + UniquePtr<Sdp> parsedOffer(Parse(offer)); + ASSERT_TRUE(!!parsedOffer); + + ASSERT_EQ(3U, parsedOffer->GetMediaSectionCount()); + ASSERT_EQ(SdpMediaSection::kAudio, + parsedOffer->GetMediaSection(0).GetMediaType()); + ASSERT_EQ(SdpDirectionAttribute::kRecvonly, + parsedOffer->GetMediaSection(0).GetAttributeList().GetDirection()); + ASSERT_TRUE(parsedOffer->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kSsrcAttribute)); + + ASSERT_EQ(SdpMediaSection::kVideo, + parsedOffer->GetMediaSection(1).GetMediaType()); + ASSERT_EQ(SdpDirectionAttribute::kRecvonly, + parsedOffer->GetMediaSection(1).GetAttributeList().GetDirection()); + ASSERT_TRUE(parsedOffer->GetMediaSection(1).GetAttributeList().HasAttribute( + SdpAttribute::kSsrcAttribute)); + + ASSERT_EQ(SdpMediaSection::kVideo, + parsedOffer->GetMediaSection(2).GetMediaType()); + ASSERT_EQ(SdpDirectionAttribute::kRecvonly, + parsedOffer->GetMediaSection(2).GetAttributeList().GetDirection()); + ASSERT_TRUE(parsedOffer->GetMediaSection(2).GetAttributeList().HasAttribute( + SdpAttribute::kSsrcAttribute)); + + ASSERT_TRUE(parsedOffer->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kRtcpMuxAttribute)); + ASSERT_TRUE(parsedOffer->GetMediaSection(1).GetAttributeList().HasAttribute( + SdpAttribute::kRtcpMuxAttribute)); + ASSERT_TRUE(parsedOffer->GetMediaSection(2).GetAttributeList().HasAttribute( + SdpAttribute::kRtcpMuxAttribute)); + + SetLocalOffer(offer, CHECK_SUCCESS); + + AddTracks(*mSessionAns, "audio,video"); + SetRemoteOffer(offer, CHECK_SUCCESS); + + std::string answer = CreateAnswer(); + UniquePtr<Sdp> parsedAnswer(Parse(answer)); + + ASSERT_EQ(3U, parsedAnswer->GetMediaSectionCount()); + ASSERT_EQ(SdpMediaSection::kAudio, + parsedAnswer->GetMediaSection(0).GetMediaType()); + ASSERT_EQ(SdpDirectionAttribute::kSendonly, + parsedAnswer->GetMediaSection(0).GetAttributeList().GetDirection()); + ASSERT_EQ(SdpMediaSection::kVideo, + parsedAnswer->GetMediaSection(1).GetMediaType()); + ASSERT_EQ(SdpDirectionAttribute::kSendonly, + parsedAnswer->GetMediaSection(1).GetAttributeList().GetDirection()); + ASSERT_EQ(SdpMediaSection::kVideo, + parsedAnswer->GetMediaSection(2).GetMediaType()); + ASSERT_EQ(SdpDirectionAttribute::kInactive, + parsedAnswer->GetMediaSection(2).GetAttributeList().GetDirection()); + + SetLocalAnswer(answer, CHECK_SUCCESS); + SetRemoteAnswer(answer, CHECK_SUCCESS); + + std::vector<JsepTransceiver> transceivers(GetTransceivers(*mSessionOff)); + ASSERT_EQ(3U, transceivers.size()); + for (const auto& transceiver : transceivers) { + const auto& msection = parsedOffer->GetMediaSection(transceiver.GetLevel()); + const auto& ssrcs = msection.GetAttributeList().GetSsrc().mSsrcs; + ASSERT_EQ(1U, ssrcs.size()); + } +} + +TEST_F(JsepSessionTest, OfferAnswerSendOnlyLines) { + AddTracks(*mSessionOff, "audio,video,video"); + + SetDirection(*mSessionOff, 0, SdpDirectionAttribute::kSendonly); + SetDirection(*mSessionOff, 2, SdpDirectionAttribute::kSendonly); + std::string offer = CreateOffer(); + + UniquePtr<Sdp> outputSdp(Parse(offer)); + ASSERT_TRUE(!!outputSdp); + + ASSERT_EQ(3U, outputSdp->GetMediaSectionCount()); + ASSERT_EQ(SdpMediaSection::kAudio, + outputSdp->GetMediaSection(0).GetMediaType()); + ASSERT_EQ(SdpDirectionAttribute::kSendonly, + outputSdp->GetMediaSection(0).GetAttributeList().GetDirection()); + ASSERT_EQ(SdpMediaSection::kVideo, + outputSdp->GetMediaSection(1).GetMediaType()); + ASSERT_EQ(SdpDirectionAttribute::kSendrecv, + outputSdp->GetMediaSection(1).GetAttributeList().GetDirection()); + ASSERT_EQ(SdpMediaSection::kVideo, + outputSdp->GetMediaSection(2).GetMediaType()); + ASSERT_EQ(SdpDirectionAttribute::kSendonly, + outputSdp->GetMediaSection(2).GetAttributeList().GetDirection()); + + ASSERT_TRUE(outputSdp->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kRtcpMuxAttribute)); + ASSERT_TRUE(outputSdp->GetMediaSection(1).GetAttributeList().HasAttribute( + SdpAttribute::kRtcpMuxAttribute)); + ASSERT_TRUE(outputSdp->GetMediaSection(2).GetAttributeList().HasAttribute( + SdpAttribute::kRtcpMuxAttribute)); + + SetLocalOffer(offer, CHECK_SUCCESS); + + AddTracks(*mSessionAns, "audio,video"); + SetRemoteOffer(offer, CHECK_SUCCESS); + + std::string answer = CreateAnswer(); + outputSdp = Parse(answer); + + ASSERT_EQ(3U, outputSdp->GetMediaSectionCount()); + ASSERT_EQ(SdpMediaSection::kAudio, + outputSdp->GetMediaSection(0).GetMediaType()); + ASSERT_EQ(SdpDirectionAttribute::kRecvonly, + outputSdp->GetMediaSection(0).GetAttributeList().GetDirection()); + ASSERT_EQ(SdpMediaSection::kVideo, + outputSdp->GetMediaSection(1).GetMediaType()); + ASSERT_EQ(SdpDirectionAttribute::kSendrecv, + outputSdp->GetMediaSection(1).GetAttributeList().GetDirection()); + ASSERT_EQ(SdpMediaSection::kVideo, + outputSdp->GetMediaSection(2).GetMediaType()); + ASSERT_EQ(SdpDirectionAttribute::kRecvonly, + outputSdp->GetMediaSection(2).GetAttributeList().GetDirection()); +} + +TEST_F(JsepSessionTest, OfferToReceiveAudioNotUsed) { + mSessionOff->AddTransceiver(JsepTransceiver( + SdpMediaSection::kAudio, mUuidGen, SdpDirectionAttribute::kRecvonly)); + + OfferAnswer(CHECK_SUCCESS); + + UniquePtr<Sdp> offer( + Parse(mSessionOff->GetLocalDescription(kJsepDescriptionCurrent))); + ASSERT_TRUE(offer.get()); + ASSERT_EQ(1U, offer->GetMediaSectionCount()); + ASSERT_EQ(SdpMediaSection::kAudio, offer->GetMediaSection(0).GetMediaType()); + ASSERT_EQ(SdpDirectionAttribute::kRecvonly, + offer->GetMediaSection(0).GetAttributeList().GetDirection()); + + UniquePtr<Sdp> answer( + Parse(mSessionAns->GetLocalDescription(kJsepDescriptionCurrent))); + ASSERT_TRUE(answer.get()); + ASSERT_EQ(1U, answer->GetMediaSectionCount()); + ASSERT_EQ(SdpMediaSection::kAudio, answer->GetMediaSection(0).GetMediaType()); + ASSERT_EQ(SdpDirectionAttribute::kInactive, + answer->GetMediaSection(0).GetAttributeList().GetDirection()); +} + +TEST_F(JsepSessionTest, OfferToReceiveVideoNotUsed) { + mSessionOff->AddTransceiver(JsepTransceiver( + SdpMediaSection::kVideo, mUuidGen, SdpDirectionAttribute::kRecvonly)); + + OfferAnswer(CHECK_SUCCESS); + + UniquePtr<Sdp> offer( + Parse(mSessionOff->GetLocalDescription(kJsepDescriptionCurrent))); + ASSERT_TRUE(offer.get()); + ASSERT_EQ(1U, offer->GetMediaSectionCount()); + ASSERT_EQ(SdpMediaSection::kVideo, offer->GetMediaSection(0).GetMediaType()); + ASSERT_EQ(SdpDirectionAttribute::kRecvonly, + offer->GetMediaSection(0).GetAttributeList().GetDirection()); + + UniquePtr<Sdp> answer( + Parse(mSessionAns->GetLocalDescription(kJsepDescriptionCurrent))); + ASSERT_TRUE(answer.get()); + ASSERT_EQ(1U, answer->GetMediaSectionCount()); + ASSERT_EQ(SdpMediaSection::kVideo, answer->GetMediaSection(0).GetMediaType()); + ASSERT_EQ(SdpDirectionAttribute::kInactive, + answer->GetMediaSection(0).GetAttributeList().GetDirection()); +} + +TEST_F(JsepSessionTest, CreateOfferNoDatachannelDefault) { + JsepTransceiver audio(SdpMediaSection::kAudio, mUuidGen); + audio.mSendTrack.UpdateStreamIds( + std::vector<std::string>(1, "offerer_stream")); + mSessionOff->AddTransceiver(audio); + + JsepTransceiver video(SdpMediaSection::kVideo, mUuidGen); + video.mSendTrack.UpdateStreamIds( + std::vector<std::string>(1, "offerer_stream")); + mSessionOff->AddTransceiver(video); + + std::string offer = CreateOffer(); + + UniquePtr<Sdp> outputSdp(Parse(offer)); + ASSERT_TRUE(!!outputSdp); + + ASSERT_EQ(2U, outputSdp->GetMediaSectionCount()); + ASSERT_EQ(SdpMediaSection::kAudio, + outputSdp->GetMediaSection(0).GetMediaType()); + ASSERT_EQ(SdpMediaSection::kVideo, + outputSdp->GetMediaSection(1).GetMediaType()); +} + +TEST_F(JsepSessionTest, ValidateOfferedVideoCodecParams) { + types.push_back(SdpMediaSection::kAudio); + types.push_back(SdpMediaSection::kVideo); + + JsepTransceiver audio(SdpMediaSection::kAudio, mUuidGen); + audio.mSendTrack.UpdateStreamIds( + std::vector<std::string>(1, "offerer_stream")); + mSessionOff->AddTransceiver(audio); + + JsepTransceiver video(SdpMediaSection::kVideo, mUuidGen); + video.mSendTrack.UpdateStreamIds( + std::vector<std::string>(1, "offerer_stream")); + mSessionOff->AddTransceiver(video); + + std::string offer = CreateOffer(); + + UniquePtr<Sdp> outputSdp(Parse(offer)); + ASSERT_TRUE(!!outputSdp); + + ASSERT_EQ(2U, outputSdp->GetMediaSectionCount()); + const auto& video_section = outputSdp->GetMediaSection(1); + ASSERT_EQ(SdpMediaSection::kVideo, video_section.GetMediaType()); + const auto& video_attrs = video_section.GetAttributeList(); + ASSERT_EQ(SdpDirectionAttribute::kSendrecv, video_attrs.GetDirection()); + + ASSERT_EQ(10U, video_section.GetFormats().size()); + ASSERT_EQ("120", video_section.GetFormats()[0]); + ASSERT_EQ("124", video_section.GetFormats()[1]); + ASSERT_EQ("121", video_section.GetFormats()[2]); + ASSERT_EQ("125", video_section.GetFormats()[3]); + ASSERT_EQ("126", video_section.GetFormats()[4]); + ASSERT_EQ("127", video_section.GetFormats()[5]); + ASSERT_EQ("97", video_section.GetFormats()[6]); + ASSERT_EQ("98", video_section.GetFormats()[7]); + ASSERT_EQ("123", video_section.GetFormats()[8]); + ASSERT_EQ("122", video_section.GetFormats()[9]); + + // Validate rtpmap + ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kRtpmapAttribute)); + auto& rtpmaps = video_attrs.GetRtpmap(); + ASSERT_TRUE(rtpmaps.HasEntry("120")); + ASSERT_TRUE(rtpmaps.HasEntry("124")); + ASSERT_TRUE(rtpmaps.HasEntry("121")); + ASSERT_TRUE(rtpmaps.HasEntry("125")); + ASSERT_TRUE(rtpmaps.HasEntry("126")); + ASSERT_TRUE(rtpmaps.HasEntry("127")); + ASSERT_TRUE(rtpmaps.HasEntry("97")); + ASSERT_TRUE(rtpmaps.HasEntry("98")); + ASSERT_TRUE(rtpmaps.HasEntry("123")); + ASSERT_TRUE(rtpmaps.HasEntry("122")); + + const auto& vp8_entry = rtpmaps.GetEntry("120"); + const auto& vp8_rtx_entry = rtpmaps.GetEntry("124"); + const auto& vp9_entry = rtpmaps.GetEntry("121"); + const auto& vp9_rtx_entry = rtpmaps.GetEntry("125"); + const auto& h264_1_entry = rtpmaps.GetEntry("126"); + const auto& h264_1_rtx_entry = rtpmaps.GetEntry("127"); + const auto& h264_0_entry = rtpmaps.GetEntry("97"); + const auto& h264_0_rtx_entry = rtpmaps.GetEntry("98"); + const auto& ulpfec_0_entry = rtpmaps.GetEntry("123"); + const auto& red_0_entry = rtpmaps.GetEntry("122"); + + ASSERT_EQ("VP8", vp8_entry.name); + ASSERT_EQ("rtx", vp8_rtx_entry.name); + ASSERT_EQ("VP9", vp9_entry.name); + ASSERT_EQ("rtx", vp9_rtx_entry.name); + ASSERT_EQ("H264", h264_1_entry.name); + ASSERT_EQ("rtx", h264_1_rtx_entry.name); + ASSERT_EQ("H264", h264_0_entry.name); + ASSERT_EQ("rtx", h264_0_rtx_entry.name); + ASSERT_EQ("red", red_0_entry.name); + ASSERT_EQ("ulpfec", ulpfec_0_entry.name); + + // Validate fmtps + ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kFmtpAttribute)); + auto& fmtps = video_attrs.GetFmtp().mFmtps; + + ASSERT_EQ(9U, fmtps.size()); + + // VP8 + const SdpFmtpAttributeList::Parameters* vp8_params = + video_section.FindFmtp("120"); + ASSERT_TRUE(vp8_params); + ASSERT_EQ(SdpRtpmapAttributeList::kVP8, vp8_params->codec_type); + + auto& parsed_vp8_params = + *static_cast<const SdpFmtpAttributeList::VP8Parameters*>(vp8_params); + + ASSERT_EQ((uint32_t)12288, parsed_vp8_params.max_fs); + ASSERT_EQ((uint32_t)60, parsed_vp8_params.max_fr); + + // VP8 RTX + const SdpFmtpAttributeList::Parameters* vp8_rtx_params = + video_section.FindFmtp("124"); + ASSERT_TRUE(vp8_rtx_params); + ASSERT_EQ(SdpRtpmapAttributeList::kRtx, vp8_rtx_params->codec_type); + + const auto& parsed_vp8_rtx_params = + *static_cast<const SdpFmtpAttributeList::RtxParameters*>(vp8_rtx_params); + + ASSERT_EQ((uint32_t)120, parsed_vp8_rtx_params.apt); + + // VP9 + const SdpFmtpAttributeList::Parameters* vp9_params = + video_section.FindFmtp("121"); + ASSERT_TRUE(vp9_params); + ASSERT_EQ(SdpRtpmapAttributeList::kVP9, vp9_params->codec_type); + + const auto& parsed_vp9_params = + *static_cast<const SdpFmtpAttributeList::VP8Parameters*>(vp9_params); + + ASSERT_EQ((uint32_t)12288, parsed_vp9_params.max_fs); + ASSERT_EQ((uint32_t)60, parsed_vp9_params.max_fr); + + // VP9 RTX + const SdpFmtpAttributeList::Parameters* vp9_rtx_params = + video_section.FindFmtp("125"); + ASSERT_TRUE(vp9_rtx_params); + ASSERT_EQ(SdpRtpmapAttributeList::kRtx, vp9_rtx_params->codec_type); + + const auto& parsed_vp9_rtx_params = + *static_cast<const SdpFmtpAttributeList::RtxParameters*>(vp9_rtx_params); + ASSERT_EQ((uint32_t)121, parsed_vp9_rtx_params.apt); + + // H264 packetization mode 1 + const SdpFmtpAttributeList::Parameters* h264_1_params = + video_section.FindFmtp("126"); + ASSERT_TRUE(h264_1_params); + ASSERT_EQ(SdpRtpmapAttributeList::kH264, h264_1_params->codec_type); + + const auto& parsed_h264_1_params = + *static_cast<const SdpFmtpAttributeList::H264Parameters*>(h264_1_params); + + ASSERT_EQ((uint32_t)0x42e00d, parsed_h264_1_params.profile_level_id); + ASSERT_TRUE(parsed_h264_1_params.level_asymmetry_allowed); + ASSERT_EQ(1U, parsed_h264_1_params.packetization_mode); + + // H264 packetization mode 1 RTX + const SdpFmtpAttributeList::Parameters* h264_1_rtx_params = + video_section.FindFmtp("127"); + ASSERT_TRUE(h264_1_rtx_params); + ASSERT_EQ(SdpRtpmapAttributeList::kRtx, h264_1_rtx_params->codec_type); + + const auto& parsed_h264_1_rtx_params = + *static_cast<const SdpFmtpAttributeList::RtxParameters*>( + h264_1_rtx_params); + + ASSERT_EQ((uint32_t)126, parsed_h264_1_rtx_params.apt); + + // H264 packetization mode 0 + const SdpFmtpAttributeList::Parameters* h264_0_params = + video_section.FindFmtp("97"); + ASSERT_TRUE(h264_0_params); + ASSERT_EQ(SdpRtpmapAttributeList::kH264, h264_0_params->codec_type); + + const auto& parsed_h264_0_params = + *static_cast<const SdpFmtpAttributeList::H264Parameters*>(h264_0_params); + + ASSERT_EQ((uint32_t)0x42e00d, parsed_h264_0_params.profile_level_id); + ASSERT_TRUE(parsed_h264_0_params.level_asymmetry_allowed); + ASSERT_EQ(0U, parsed_h264_0_params.packetization_mode); + + // H264 packetization mode 0 RTX + const SdpFmtpAttributeList::Parameters* h264_0_rtx_params = + video_section.FindFmtp("98"); + ASSERT_TRUE(h264_0_rtx_params); + ASSERT_EQ(SdpRtpmapAttributeList::kRtx, h264_0_rtx_params->codec_type); + + const auto& parsed_h264_0_rtx_params = + *static_cast<const SdpFmtpAttributeList::RtxParameters*>( + h264_0_rtx_params); + + ASSERT_EQ((uint32_t)97, parsed_h264_0_rtx_params.apt); + + // red + const SdpFmtpAttributeList::Parameters* red_params = + video_section.FindFmtp("122"); + ASSERT_TRUE(red_params); + ASSERT_EQ(SdpRtpmapAttributeList::kRed, red_params->codec_type); + + const auto& parsed_red_params = + *static_cast<const SdpFmtpAttributeList::RedParameters*>(red_params); + ASSERT_EQ(5U, parsed_red_params.encodings.size()); + ASSERT_EQ(120, parsed_red_params.encodings[0]); + ASSERT_EQ(121, parsed_red_params.encodings[1]); + ASSERT_EQ(126, parsed_red_params.encodings[2]); + ASSERT_EQ(97, parsed_red_params.encodings[3]); + ASSERT_EQ(123, parsed_red_params.encodings[4]); +} + +TEST_F(JsepSessionTest, ValidateOfferedAudioCodecParams) { + types.push_back(SdpMediaSection::kAudio); + types.push_back(SdpMediaSection::kVideo); + + JsepTransceiver audio(SdpMediaSection::kAudio, mUuidGen); + audio.mSendTrack.UpdateStreamIds( + std::vector<std::string>(1, "offerer_stream")); + mSessionOff->AddTransceiver(audio); + + JsepTransceiver video(SdpMediaSection::kVideo, mUuidGen); + video.mSendTrack.UpdateStreamIds( + std::vector<std::string>(1, "offerer_stream")); + mSessionOff->AddTransceiver(video); + + std::string offer = CreateOffer(); + + UniquePtr<Sdp> outputSdp(Parse(offer)); + ASSERT_TRUE(!!outputSdp); + + ASSERT_EQ(2U, outputSdp->GetMediaSectionCount()); + auto& audio_section = outputSdp->GetMediaSection(0); + ASSERT_EQ(SdpMediaSection::kAudio, audio_section.GetMediaType()); + auto& audio_attrs = audio_section.GetAttributeList(); + ASSERT_EQ(SdpDirectionAttribute::kSendrecv, audio_attrs.GetDirection()); + ASSERT_EQ(5U, audio_section.GetFormats().size()); + ASSERT_EQ("109", audio_section.GetFormats()[0]); + ASSERT_EQ("9", audio_section.GetFormats()[1]); + ASSERT_EQ("0", audio_section.GetFormats()[2]); + ASSERT_EQ("8", audio_section.GetFormats()[3]); + ASSERT_EQ("101", audio_section.GetFormats()[4]); + + // Validate rtpmap + ASSERT_TRUE(audio_attrs.HasAttribute(SdpAttribute::kRtpmapAttribute)); + auto& rtpmaps = audio_attrs.GetRtpmap(); + ASSERT_TRUE(rtpmaps.HasEntry("109")); + ASSERT_TRUE(rtpmaps.HasEntry("9")); + ASSERT_TRUE(rtpmaps.HasEntry("0")); + ASSERT_TRUE(rtpmaps.HasEntry("8")); + ASSERT_TRUE(rtpmaps.HasEntry("101")); + + auto& opus_entry = rtpmaps.GetEntry("109"); + auto& g722_entry = rtpmaps.GetEntry("9"); + auto& pcmu_entry = rtpmaps.GetEntry("0"); + auto& pcma_entry = rtpmaps.GetEntry("8"); + auto& telephone_event_entry = rtpmaps.GetEntry("101"); + + ASSERT_EQ("opus", opus_entry.name); + ASSERT_EQ("G722", g722_entry.name); + ASSERT_EQ("PCMU", pcmu_entry.name); + ASSERT_EQ("PCMA", pcma_entry.name); + ASSERT_EQ("telephone-event", telephone_event_entry.name); + + // Validate fmtps + ASSERT_TRUE(audio_attrs.HasAttribute(SdpAttribute::kFmtpAttribute)); + auto& fmtps = audio_attrs.GetFmtp().mFmtps; + + ASSERT_EQ(2U, fmtps.size()); + + // opus + const SdpFmtpAttributeList::Parameters* opus_params = + audio_section.FindFmtp("109"); + ASSERT_TRUE(opus_params); + ASSERT_EQ(SdpRtpmapAttributeList::kOpus, opus_params->codec_type); + + auto& parsed_opus_params = + *static_cast<const SdpFmtpAttributeList::OpusParameters*>(opus_params); + + ASSERT_EQ((uint32_t)48000, parsed_opus_params.maxplaybackrate); + ASSERT_EQ((uint32_t)1, parsed_opus_params.stereo); + ASSERT_EQ((uint32_t)0, parsed_opus_params.useInBandFec); + ASSERT_EQ((uint32_t)0, parsed_opus_params.maxAverageBitrate); + ASSERT_EQ((uint32_t)0, parsed_opus_params.useDTX); + ASSERT_EQ((uint32_t)0, parsed_opus_params.useCbr); + ASSERT_EQ((uint32_t)0, parsed_opus_params.frameSizeMs); + ASSERT_EQ((uint32_t)0, parsed_opus_params.minFrameSizeMs); + ASSERT_EQ((uint32_t)0, parsed_opus_params.maxFrameSizeMs); + + // dtmf + const SdpFmtpAttributeList::Parameters* dtmf_params = + audio_section.FindFmtp("101"); + ASSERT_TRUE(dtmf_params); + ASSERT_EQ(SdpRtpmapAttributeList::kTelephoneEvent, dtmf_params->codec_type); + + auto& parsed_dtmf_params = + *static_cast<const SdpFmtpAttributeList::TelephoneEventParameters*>( + dtmf_params); + + ASSERT_EQ("0-15", parsed_dtmf_params.dtmfTones); +} + +TEST_F(JsepSessionTest, ValidateNoFmtpLineForRedInOfferAndAnswer) { + types.push_back(SdpMediaSection::kAudio); + types.push_back(SdpMediaSection::kVideo); + + AddTracksToStream(*mSessionOff, "offerer_stream", "audio,video"); + + std::string offer = CreateOffer(); + + // look for line with fmtp:122 and remove it + size_t start = offer.find("a=fmtp:122"); + size_t end = offer.find("\r\n", start); + offer.replace(start, end + 2 - start, ""); + + SetLocalOffer(offer); + SetRemoteOffer(offer); + + AddTracksToStream(*mSessionAns, "answerer_stream", "audio,video"); + + std::string answer = CreateAnswer(); + // because parsing will throw out the malformed fmtp, make sure it is not + // in the answer sdp string + ASSERT_EQ(std::string::npos, answer.find("a=fmtp:122")); + + UniquePtr<Sdp> outputSdp(Parse(answer)); + ASSERT_TRUE(!!outputSdp); + + ASSERT_EQ(2U, outputSdp->GetMediaSectionCount()); + auto& video_section = outputSdp->GetMediaSection(1); + ASSERT_EQ(SdpMediaSection::kVideo, video_section.GetMediaType()); + auto& video_attrs = video_section.GetAttributeList(); + ASSERT_EQ(SdpDirectionAttribute::kSendrecv, video_attrs.GetDirection()); + + ASSERT_EQ(10U, video_section.GetFormats().size()); + ASSERT_EQ("120", video_section.GetFormats()[0]); + ASSERT_EQ("124", video_section.GetFormats()[1]); + ASSERT_EQ("121", video_section.GetFormats()[2]); + ASSERT_EQ("125", video_section.GetFormats()[3]); + ASSERT_EQ("126", video_section.GetFormats()[4]); + ASSERT_EQ("127", video_section.GetFormats()[5]); + ASSERT_EQ("97", video_section.GetFormats()[6]); + ASSERT_EQ("98", video_section.GetFormats()[7]); + ASSERT_EQ("123", video_section.GetFormats()[8]); + ASSERT_EQ("122", video_section.GetFormats()[9]); + + // Validate rtpmap + ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kRtpmapAttribute)); + auto& rtpmaps = video_attrs.GetRtpmap(); + ASSERT_TRUE(rtpmaps.HasEntry("120")); + ASSERT_TRUE(rtpmaps.HasEntry("124")); + ASSERT_TRUE(rtpmaps.HasEntry("121")); + ASSERT_TRUE(rtpmaps.HasEntry("125")); + ASSERT_TRUE(rtpmaps.HasEntry("126")); + ASSERT_TRUE(rtpmaps.HasEntry("127")); + ASSERT_TRUE(rtpmaps.HasEntry("97")); + ASSERT_TRUE(rtpmaps.HasEntry("98")); + ASSERT_TRUE(rtpmaps.HasEntry("123")); + ASSERT_TRUE(rtpmaps.HasEntry("122")); + + // Validate fmtps + ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kFmtpAttribute)); + auto& fmtps = video_attrs.GetFmtp().mFmtps; + + ASSERT_EQ(8U, fmtps.size()); + ASSERT_EQ("126", fmtps[0].format); + ASSERT_EQ("97", fmtps[1].format); + ASSERT_EQ("120", fmtps[2].format); + ASSERT_EQ("124", fmtps[3].format); + ASSERT_EQ("121", fmtps[4].format); + ASSERT_EQ("125", fmtps[5].format); + ASSERT_EQ("127", fmtps[6].format); + ASSERT_EQ("98", fmtps[7].format); + + SetLocalAnswer(answer); + SetRemoteAnswer(answer); + + auto offerTransceivers = GetTransceivers(*mSessionOff); + ASSERT_EQ(2U, offerTransceivers.size()); + ASSERT_FALSE(IsNull(offerTransceivers[1].mSendTrack)); + ASSERT_FALSE(IsNull(offerTransceivers[1].mRecvTrack)); + ASSERT_TRUE(offerTransceivers[1].mSendTrack.GetNegotiatedDetails()); + ASSERT_TRUE(offerTransceivers[1].mRecvTrack.GetNegotiatedDetails()); + ASSERT_EQ(6U, offerTransceivers[1] + .mSendTrack.GetNegotiatedDetails() + ->GetEncoding(0) + .GetCodecs() + .size()); + ASSERT_EQ(6U, offerTransceivers[1] + .mRecvTrack.GetNegotiatedDetails() + ->GetEncoding(0) + .GetCodecs() + .size()); + + auto answerTransceivers = GetTransceivers(*mSessionAns); + ASSERT_EQ(2U, answerTransceivers.size()); + ASSERT_FALSE(IsNull(answerTransceivers[1].mSendTrack)); + ASSERT_FALSE(IsNull(answerTransceivers[1].mRecvTrack)); + ASSERT_TRUE(answerTransceivers[1].mSendTrack.GetNegotiatedDetails()); + ASSERT_TRUE(answerTransceivers[1].mRecvTrack.GetNegotiatedDetails()); + ASSERT_EQ(6U, answerTransceivers[1] + .mSendTrack.GetNegotiatedDetails() + ->GetEncoding(0) + .GetCodecs() + .size()); + ASSERT_EQ(6U, answerTransceivers[1] + .mRecvTrack.GetNegotiatedDetails() + ->GetEncoding(0) + .GetCodecs() + .size()); +} + +TEST_F(JsepSessionTest, ValidateAnsweredCodecParamsNoRed) { + // TODO(bug 1099351): Once fixed, we can allow red in this offer, + // which will also cause multiple codecs in answer. For now, + // red/ulpfec for video are behind a pref to mitigate potential for + // errors. + SetCodecEnabled(*mSessionOff, "red", false); + for (auto& codec : mSessionAns->Codecs()) { + if (codec->mName == "H264") { + JsepVideoCodecDescription* h264 = + static_cast<JsepVideoCodecDescription*>(codec.get()); + h264->mProfileLevelId = 0x42a00d; + // Switch up the pts + if (h264->mDefaultPt == "126") { + h264->mDefaultPt = "97"; + } else { + h264->mDefaultPt = "126"; + } + } + } + + types.push_back(SdpMediaSection::kAudio); + types.push_back(SdpMediaSection::kVideo); + + AddTracksToStream(*mSessionOff, "offerer_stream", "audio,video"); + + std::string offer = CreateOffer(); + SetLocalOffer(offer); + SetRemoteOffer(offer); + + AddTracksToStream(*mSessionAns, "answerer_stream", "audio,video"); + + std::string answer = CreateAnswer(); + + UniquePtr<Sdp> outputSdp(Parse(answer)); + ASSERT_TRUE(!!outputSdp); + + ASSERT_EQ(2U, outputSdp->GetMediaSectionCount()); + auto& video_section = outputSdp->GetMediaSection(1); + ASSERT_EQ(SdpMediaSection::kVideo, video_section.GetMediaType()); + auto& video_attrs = video_section.GetAttributeList(); + ASSERT_EQ(SdpDirectionAttribute::kSendrecv, video_attrs.GetDirection()); + + ASSERT_EQ(4U, video_section.GetFormats().size()); + ASSERT_EQ("120", video_section.GetFormats()[0]); + ASSERT_EQ("124", video_section.GetFormats()[1]); + ASSERT_EQ("121", video_section.GetFormats()[2]); + ASSERT_EQ("125", video_section.GetFormats()[3]); + + // Validate rtpmap + ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kRtpmapAttribute)); + auto& rtpmaps = video_attrs.GetRtpmap(); + ASSERT_TRUE(rtpmaps.HasEntry("120")); + ASSERT_TRUE(rtpmaps.HasEntry("121")); + + auto& vp8_entry = rtpmaps.GetEntry("120"); + auto& vp9_entry = rtpmaps.GetEntry("121"); + + ASSERT_EQ("VP8", vp8_entry.name); + ASSERT_EQ("VP9", vp9_entry.name); + + // Validate fmtps + ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kFmtpAttribute)); + auto& fmtps = video_attrs.GetFmtp().mFmtps; + + ASSERT_EQ(4U, fmtps.size()); + + // VP8 + ASSERT_EQ("120", fmtps[0].format); + ASSERT_TRUE(!!fmtps[0].parameters); + ASSERT_EQ(SdpRtpmapAttributeList::kVP8, fmtps[0].parameters->codec_type); + + auto& parsed_vp8_params = + *static_cast<const SdpFmtpAttributeList::VP8Parameters*>( + fmtps[0].parameters.get()); + + ASSERT_EQ((uint32_t)12288, parsed_vp8_params.max_fs); + ASSERT_EQ((uint32_t)60, parsed_vp8_params.max_fr); + + // VP9 + ASSERT_EQ("121", fmtps[2].format); + ASSERT_TRUE(!!fmtps[2].parameters); + ASSERT_EQ(SdpRtpmapAttributeList::kVP9, fmtps[2].parameters->codec_type); + + auto& parsed_vp9_params = + *static_cast<const SdpFmtpAttributeList::VP8Parameters*>( + fmtps[2].parameters.get()); + + ASSERT_EQ((uint32_t)12288, parsed_vp9_params.max_fs); + ASSERT_EQ((uint32_t)60, parsed_vp9_params.max_fr); + + SetLocalAnswer(answer); + SetRemoteAnswer(answer); + + auto offerTransceivers = GetTransceivers(*mSessionOff); + ASSERT_EQ(2U, offerTransceivers.size()); + ASSERT_FALSE(IsNull(offerTransceivers[1].mSendTrack)); + ASSERT_FALSE(IsNull(offerTransceivers[1].mRecvTrack)); + ASSERT_TRUE(offerTransceivers[1].mSendTrack.GetNegotiatedDetails()); + ASSERT_TRUE(offerTransceivers[1].mRecvTrack.GetNegotiatedDetails()); + ASSERT_EQ(2U, offerTransceivers[1] + .mSendTrack.GetNegotiatedDetails() + ->GetEncoding(0) + .GetCodecs() + .size()); + ASSERT_EQ(2U, offerTransceivers[1] + .mRecvTrack.GetNegotiatedDetails() + ->GetEncoding(0) + .GetCodecs() + .size()); + + auto answerTransceivers = GetTransceivers(*mSessionAns); + ASSERT_EQ(2U, answerTransceivers.size()); + ASSERT_FALSE(IsNull(answerTransceivers[1].mSendTrack)); + ASSERT_FALSE(IsNull(answerTransceivers[1].mRecvTrack)); + ASSERT_TRUE(answerTransceivers[1].mSendTrack.GetNegotiatedDetails()); + ASSERT_TRUE(answerTransceivers[1].mRecvTrack.GetNegotiatedDetails()); + ASSERT_EQ(2U, answerTransceivers[1] + .mSendTrack.GetNegotiatedDetails() + ->GetEncoding(0) + .GetCodecs() + .size()); + ASSERT_EQ(2U, answerTransceivers[1] + .mRecvTrack.GetNegotiatedDetails() + ->GetEncoding(0) + .GetCodecs() + .size()); + +#if 0 + // H264 packetization mode 1 + ASSERT_EQ("126", fmtps[1].format); + ASSERT_TRUE(fmtps[1].parameters); + ASSERT_EQ(SdpRtpmapAttributeList::kH264, fmtps[1].parameters->codec_type); + + auto& parsed_h264_1_params = + *static_cast<const SdpFmtpAttributeList::H264Parameters*>( + fmtps[1].parameters.get()); + + ASSERT_EQ((uint32_t)0x42a00d, parsed_h264_1_params.profile_level_id); + ASSERT_TRUE(parsed_h264_1_params.level_asymmetry_allowed); + ASSERT_EQ(1U, parsed_h264_1_params.packetization_mode); + + // H264 packetization mode 0 + ASSERT_EQ("97", fmtps[2].format); + ASSERT_TRUE(fmtps[2].parameters); + ASSERT_EQ(SdpRtpmapAttributeList::kH264, fmtps[2].parameters->codec_type); + + auto& parsed_h264_0_params = + *static_cast<const SdpFmtpAttributeList::H264Parameters*>( + fmtps[2].parameters.get()); + + ASSERT_EQ((uint32_t)0x42a00d, parsed_h264_0_params.profile_level_id); + ASSERT_TRUE(parsed_h264_0_params.level_asymmetry_allowed); + ASSERT_EQ(0U, parsed_h264_0_params.packetization_mode); +#endif +} + +TEST_F(JsepSessionTest, OfferWithBundleGroupNoTags) { + AddTracks(*mSessionOff, "audio,video"); + AddTracks(*mSessionAns, "audio,video"); + + std::string offer = CreateOffer(); + size_t i = offer.find("a=group:BUNDLE"); + offer.insert(i, "a=group:BUNDLE\r\n"); + + SetLocalOffer(offer, CHECK_SUCCESS); + SetRemoteOffer(offer, CHECK_SUCCESS); + std::string answer(CreateAnswer()); +} + +static void Replace(const std::string& toReplace, const std::string& with, + std::string* in) { + size_t pos = in->find(toReplace); + ASSERT_NE(std::string::npos, pos); + in->replace(pos, toReplace.size(), with); +} + +static void ReplaceAll(const std::string& toReplace, const std::string& with, + std::string* in) { + while (in->find(toReplace) != std::string::npos) { + Replace(toReplace, with, in); + } +} + +static void GetCodec(JsepSession& session, size_t transceiverIndex, + sdp::Direction direction, size_t encodingIndex, + size_t codecIndex, + UniquePtr<JsepCodecDescription>* codecOut) { + codecOut->reset(); + ASSERT_LT(transceiverIndex, JsepSessionTest::GetTransceivers(session).size()); + JsepTransceiver transceiver( + JsepSessionTest::GetTransceivers(session)[transceiverIndex]); + JsepTrack& track = (direction == sdp::kSend) ? transceiver.mSendTrack + : transceiver.mRecvTrack; + ASSERT_TRUE(track.GetNegotiatedDetails()); + ASSERT_LT(encodingIndex, track.GetNegotiatedDetails()->GetEncodingCount()); + ASSERT_LT(codecIndex, track.GetNegotiatedDetails() + ->GetEncoding(encodingIndex) + .GetCodecs() + .size()); + codecOut->reset(track.GetNegotiatedDetails() + ->GetEncoding(encodingIndex) + .GetCodecs()[codecIndex] + ->Clone()); +} + +static void ForceH264(JsepSession& session, uint32_t profileLevelId) { + for (auto& codec : session.Codecs()) { + if (codec->mName == "H264") { + JsepVideoCodecDescription* h264 = + static_cast<JsepVideoCodecDescription*>(codec.get()); + h264->mProfileLevelId = profileLevelId; + } else { + codec->mEnabled = false; + } + } +} + +TEST_F(JsepSessionTest, TestH264Negotiation) { + ForceH264(*mSessionOff, 0x42e00b); + ForceH264(*mSessionAns, 0x42e00d); + + AddTracks(*mSessionOff, "video"); + AddTracks(*mSessionAns, "video"); + + std::string offer(CreateOffer()); + SetLocalOffer(offer, CHECK_SUCCESS); + + SetRemoteOffer(offer, CHECK_SUCCESS); + std::string answer(CreateAnswer()); + + SetRemoteAnswer(answer, CHECK_SUCCESS); + SetLocalAnswer(answer, CHECK_SUCCESS); + + UniquePtr<JsepCodecDescription> offererSendCodec; + GetCodec(*mSessionOff, 0, sdp::kSend, 0, 0, &offererSendCodec); + ASSERT_TRUE(offererSendCodec); + ASSERT_EQ("H264", offererSendCodec->mName); + const JsepVideoCodecDescription* offererVideoSendCodec( + static_cast<const JsepVideoCodecDescription*>(offererSendCodec.get())); + ASSERT_EQ((uint32_t)0x42e00d, offererVideoSendCodec->mProfileLevelId); + + UniquePtr<JsepCodecDescription> offererRecvCodec; + GetCodec(*mSessionOff, 0, sdp::kRecv, 0, 0, &offererRecvCodec); + ASSERT_EQ("H264", offererRecvCodec->mName); + const JsepVideoCodecDescription* offererVideoRecvCodec( + static_cast<const JsepVideoCodecDescription*>(offererRecvCodec.get())); + ASSERT_EQ((uint32_t)0x42e00b, offererVideoRecvCodec->mProfileLevelId); + + UniquePtr<JsepCodecDescription> answererSendCodec; + GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &answererSendCodec); + ASSERT_TRUE(answererSendCodec); + ASSERT_EQ("H264", answererSendCodec->mName); + const JsepVideoCodecDescription* answererVideoSendCodec( + static_cast<const JsepVideoCodecDescription*>(answererSendCodec.get())); + ASSERT_EQ((uint32_t)0x42e00b, answererVideoSendCodec->mProfileLevelId); + + UniquePtr<JsepCodecDescription> answererRecvCodec; + GetCodec(*mSessionAns, 0, sdp::kRecv, 0, 0, &answererRecvCodec); + ASSERT_EQ("H264", answererRecvCodec->mName); + const JsepVideoCodecDescription* answererVideoRecvCodec( + static_cast<const JsepVideoCodecDescription*>(answererRecvCodec.get())); + ASSERT_EQ((uint32_t)0x42e00d, answererVideoRecvCodec->mProfileLevelId); +} + +TEST_F(JsepSessionTest, TestH264NegotiationFails) { + ForceH264(*mSessionOff, 0x42000b); + ForceH264(*mSessionAns, 0x42e00d); + + AddTracks(*mSessionOff, "video"); + AddTracks(*mSessionAns, "video"); + + std::string offer(CreateOffer()); + SetLocalOffer(offer, CHECK_SUCCESS); + + SetRemoteOffer(offer, CHECK_SUCCESS); + std::string answer(CreateAnswer()); + + SetRemoteAnswer(answer, CHECK_SUCCESS); + SetLocalAnswer(answer, CHECK_SUCCESS); + + ASSERT_EQ(nullptr, GetNegotiatedTransceiver(*mSessionOff, 0)); + ASSERT_EQ(nullptr, GetNegotiatedTransceiver(*mSessionAns, 0)); +} + +TEST_F(JsepSessionTest, TestH264NegotiationOffererDefault) { + ForceH264(*mSessionOff, 0x42000d); + ForceH264(*mSessionAns, 0x42000d); + + AddTracks(*mSessionOff, "video"); + AddTracks(*mSessionAns, "video"); + + std::string offer(CreateOffer()); + SetLocalOffer(offer, CHECK_SUCCESS); + + Replace("profile-level-id=42000d", "some-unknown-param=0", &offer); + + SetRemoteOffer(offer, CHECK_SUCCESS); + std::string answer(CreateAnswer()); + + SetRemoteAnswer(answer, CHECK_SUCCESS); + SetLocalAnswer(answer, CHECK_SUCCESS); + + UniquePtr<JsepCodecDescription> answererSendCodec; + GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &answererSendCodec); + ASSERT_TRUE(answererSendCodec); + ASSERT_EQ("H264", answererSendCodec->mName); + const JsepVideoCodecDescription* answererVideoSendCodec( + static_cast<const JsepVideoCodecDescription*>(answererSendCodec.get())); + ASSERT_EQ((uint32_t)0x420010, answererVideoSendCodec->mProfileLevelId); +} + +TEST_F(JsepSessionTest, TestH264NegotiationOffererNoFmtp) { + ForceH264(*mSessionOff, 0x42000d); + ForceH264(*mSessionAns, 0x42001e); + + AddTracks(*mSessionOff, "video"); + AddTracks(*mSessionAns, "video"); + + std::string offer(CreateOffer()); + SetLocalOffer(offer, CHECK_SUCCESS); + + Replace("a=fmtp", "a=oops", &offer); + + SetRemoteOffer(offer, CHECK_SUCCESS); + std::string answer(CreateAnswer()); + + SetRemoteAnswer(answer, CHECK_SUCCESS); + SetLocalAnswer(answer, CHECK_SUCCESS); + + UniquePtr<JsepCodecDescription> answererSendCodec; + GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &answererSendCodec); + ASSERT_TRUE(answererSendCodec); + ASSERT_EQ("H264", answererSendCodec->mName); + const JsepVideoCodecDescription* answererVideoSendCodec( + static_cast<const JsepVideoCodecDescription*>(answererSendCodec.get())); + ASSERT_EQ((uint32_t)0x420010, answererVideoSendCodec->mProfileLevelId); + + UniquePtr<JsepCodecDescription> answererRecvCodec; + GetCodec(*mSessionAns, 0, sdp::kRecv, 0, 0, &answererRecvCodec); + ASSERT_EQ("H264", answererRecvCodec->mName); + const JsepVideoCodecDescription* answererVideoRecvCodec( + static_cast<const JsepVideoCodecDescription*>(answererRecvCodec.get())); + ASSERT_EQ((uint32_t)0x420010, answererVideoRecvCodec->mProfileLevelId); +} + +TEST_F(JsepSessionTest, TestH264LevelAsymmetryDisallowedByOffererWithLowLevel) { + ForceH264(*mSessionOff, 0x42e00b); + ForceH264(*mSessionAns, 0x42e00d); + + AddTracks(*mSessionOff, "video"); + AddTracks(*mSessionAns, "video"); + + std::string offer(CreateOffer()); + SetLocalOffer(offer, CHECK_SUCCESS); + + Replace("level-asymmetry-allowed=1", "level-asymmetry-allowed=0", &offer); + + SetRemoteOffer(offer, CHECK_SUCCESS); + std::string answer(CreateAnswer()); + + SetRemoteAnswer(answer, CHECK_SUCCESS); + SetLocalAnswer(answer, CHECK_SUCCESS); + + // Offerer doesn't know about the shenanigans we've pulled here, so will + // behave normally, and we test the normal behavior elsewhere. + + UniquePtr<JsepCodecDescription> answererSendCodec; + GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &answererSendCodec); + ASSERT_TRUE(answererSendCodec); + ASSERT_EQ("H264", answererSendCodec->mName); + const JsepVideoCodecDescription* answererVideoSendCodec( + static_cast<const JsepVideoCodecDescription*>(answererSendCodec.get())); + ASSERT_EQ((uint32_t)0x42e00b, answererVideoSendCodec->mProfileLevelId); + + UniquePtr<JsepCodecDescription> answererRecvCodec; + GetCodec(*mSessionAns, 0, sdp::kRecv, 0, 0, &answererRecvCodec); + ASSERT_EQ("H264", answererRecvCodec->mName); + const JsepVideoCodecDescription* answererVideoRecvCodec( + static_cast<const JsepVideoCodecDescription*>(answererRecvCodec.get())); + ASSERT_EQ((uint32_t)0x42e00b, answererVideoRecvCodec->mProfileLevelId); +} + +TEST_F(JsepSessionTest, + TestH264LevelAsymmetryDisallowedByOffererWithHighLevel) { + ForceH264(*mSessionOff, 0x42e00d); + ForceH264(*mSessionAns, 0x42e00b); + + AddTracks(*mSessionOff, "video"); + AddTracks(*mSessionAns, "video"); + + std::string offer(CreateOffer()); + SetLocalOffer(offer, CHECK_SUCCESS); + + Replace("level-asymmetry-allowed=1", "level-asymmetry-allowed=0", &offer); + + SetRemoteOffer(offer, CHECK_SUCCESS); + std::string answer(CreateAnswer()); + + SetRemoteAnswer(answer, CHECK_SUCCESS); + SetLocalAnswer(answer, CHECK_SUCCESS); + + // Offerer doesn't know about the shenanigans we've pulled here, so will + // behave normally, and we test the normal behavior elsewhere. + + UniquePtr<JsepCodecDescription> answererSendCodec; + GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &answererSendCodec); + ASSERT_TRUE(answererSendCodec); + ASSERT_EQ("H264", answererSendCodec->mName); + const JsepVideoCodecDescription* answererVideoSendCodec( + static_cast<const JsepVideoCodecDescription*>(answererSendCodec.get())); + ASSERT_EQ((uint32_t)0x42e00b, answererVideoSendCodec->mProfileLevelId); + + UniquePtr<JsepCodecDescription> answererRecvCodec; + GetCodec(*mSessionAns, 0, sdp::kRecv, 0, 0, &answererRecvCodec); + ASSERT_EQ("H264", answererRecvCodec->mName); + const JsepVideoCodecDescription* answererVideoRecvCodec( + static_cast<const JsepVideoCodecDescription*>(answererRecvCodec.get())); + ASSERT_EQ((uint32_t)0x42e00b, answererVideoRecvCodec->mProfileLevelId); +} + +TEST_F(JsepSessionTest, + TestH264LevelAsymmetryDisallowedByAnswererWithLowLevel) { + ForceH264(*mSessionOff, 0x42e00d); + ForceH264(*mSessionAns, 0x42e00b); + + AddTracks(*mSessionOff, "video"); + AddTracks(*mSessionAns, "video"); + + std::string offer(CreateOffer()); + SetLocalOffer(offer, CHECK_SUCCESS); + SetRemoteOffer(offer, CHECK_SUCCESS); + std::string answer(CreateAnswer()); + + Replace("level-asymmetry-allowed=1", "level-asymmetry-allowed=0", &answer); + + SetRemoteAnswer(answer, CHECK_SUCCESS); + SetLocalAnswer(answer, CHECK_SUCCESS); + + UniquePtr<JsepCodecDescription> offererSendCodec; + GetCodec(*mSessionOff, 0, sdp::kSend, 0, 0, &offererSendCodec); + ASSERT_TRUE(offererSendCodec); + ASSERT_EQ("H264", offererSendCodec->mName); + const JsepVideoCodecDescription* offererVideoSendCodec( + static_cast<const JsepVideoCodecDescription*>(offererSendCodec.get())); + ASSERT_EQ((uint32_t)0x42e00b, offererVideoSendCodec->mProfileLevelId); + + UniquePtr<JsepCodecDescription> offererRecvCodec; + GetCodec(*mSessionOff, 0, sdp::kRecv, 0, 0, &offererRecvCodec); + ASSERT_EQ("H264", offererRecvCodec->mName); + const JsepVideoCodecDescription* offererVideoRecvCodec( + static_cast<const JsepVideoCodecDescription*>(offererRecvCodec.get())); + ASSERT_EQ((uint32_t)0x42e00b, offererVideoRecvCodec->mProfileLevelId); + + // Answerer doesn't know we've pulled these shenanigans, it should act as if + // it did not set level-asymmetry-required, and we already check that + // elsewhere +} + +TEST_F(JsepSessionTest, + TestH264LevelAsymmetryDisallowedByAnswererWithHighLevel) { + ForceH264(*mSessionOff, 0x42e00b); + ForceH264(*mSessionAns, 0x42e00d); + + AddTracks(*mSessionOff, "video"); + AddTracks(*mSessionAns, "video"); + + std::string offer(CreateOffer()); + SetLocalOffer(offer, CHECK_SUCCESS); + SetRemoteOffer(offer, CHECK_SUCCESS); + std::string answer(CreateAnswer()); + + Replace("level-asymmetry-allowed=1", "level-asymmetry-allowed=0", &answer); + + SetRemoteAnswer(answer, CHECK_SUCCESS); + SetLocalAnswer(answer, CHECK_SUCCESS); + + UniquePtr<JsepCodecDescription> offererSendCodec; + GetCodec(*mSessionOff, 0, sdp::kSend, 0, 0, &offererSendCodec); + ASSERT_TRUE(offererSendCodec); + ASSERT_EQ("H264", offererSendCodec->mName); + const JsepVideoCodecDescription* offererVideoSendCodec( + static_cast<const JsepVideoCodecDescription*>(offererSendCodec.get())); + ASSERT_EQ((uint32_t)0x42e00b, offererVideoSendCodec->mProfileLevelId); + + UniquePtr<JsepCodecDescription> offererRecvCodec; + GetCodec(*mSessionOff, 0, sdp::kRecv, 0, 0, &offererRecvCodec); + ASSERT_EQ("H264", offererRecvCodec->mName); + const JsepVideoCodecDescription* offererVideoRecvCodec( + static_cast<const JsepVideoCodecDescription*>(offererRecvCodec.get())); + ASSERT_EQ((uint32_t)0x42e00b, offererVideoRecvCodec->mProfileLevelId); + + // Answerer doesn't know we've pulled these shenanigans, it should act as if + // it did not set level-asymmetry-required, and we already check that + // elsewhere +} + +TEST_P(JsepSessionTest, TestRejectMline) { + // We need to do this before adding tracks + types = BuildTypes(GetParam()); + + SdpMediaSection::MediaType type = types.front(); + + switch (type) { + case SdpMediaSection::kAudio: + // Sabotage audio + EnsureNegotiationFailure(types.front(), "opus"); + break; + case SdpMediaSection::kVideo: + // Sabotage video + EnsureNegotiationFailure(types.front(), "H264"); + break; + case SdpMediaSection::kApplication: + // Sabotage datachannel + EnsureNegotiationFailure(types.front(), "webrtc-datachannel"); + break; + default: + ASSERT_TRUE(false) + << "Unknown media type"; + } + + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + + std::string offer = CreateOffer(); + mSessionOff->SetLocalDescription(kJsepSdpOffer, offer); + mSessionAns->SetRemoteDescription(kJsepSdpOffer, offer); + + std::string answer = CreateAnswer(); + + UniquePtr<Sdp> outputSdp(Parse(answer)); + ASSERT_TRUE(!!outputSdp); + + ASSERT_NE(0U, outputSdp->GetMediaSectionCount()); + SdpMediaSection* failed_section = nullptr; + + for (size_t i = 0; i < outputSdp->GetMediaSectionCount(); ++i) { + if (outputSdp->GetMediaSection(i).GetMediaType() == type) { + failed_section = &outputSdp->GetMediaSection(i); + } + } + + ASSERT_TRUE(failed_section) + << "Failed type was entirely absent from SDP"; + auto& failed_attrs = failed_section->GetAttributeList(); + ASSERT_EQ(SdpDirectionAttribute::kInactive, failed_attrs.GetDirection()); + ASSERT_EQ(0U, failed_section->GetPort()); + + mSessionAns->SetLocalDescription(kJsepSdpAnswer, answer); + mSessionOff->SetRemoteDescription(kJsepSdpAnswer, answer); + + size_t numRejected = std::count(types.begin(), types.end(), type); + size_t numAccepted = types.size() - numRejected; + + if (type == SdpMediaSection::MediaType::kApplication) { + ASSERT_TRUE(GetDatachannelTransceiver(*mSessionOff)); + ASSERT_FALSE( + GetDatachannelTransceiver(*mSessionOff)->mRecvTrack.GetActive()); + ASSERT_TRUE(GetDatachannelTransceiver(*mSessionAns)); + ASSERT_FALSE( + GetDatachannelTransceiver(*mSessionAns)->mRecvTrack.GetActive()); + } else { + ASSERT_EQ(types.size(), GetLocalTracks(*mSessionOff).size()); + ASSERT_EQ(numAccepted, GetRemoteTracks(*mSessionOff).size()); + + ASSERT_EQ(types.size(), GetLocalTracks(*mSessionAns).size()); + ASSERT_EQ(types.size(), GetRemoteTracks(*mSessionAns).size()); + } +} + +TEST_F(JsepSessionTest, NegotiationNoMlines) { OfferAnswer(); } + +TEST_F(JsepSessionTest, TestIceLite) { + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + std::string offer = CreateOffer(); + SetLocalOffer(offer, CHECK_SUCCESS); + + UniquePtr<Sdp> parsedOffer(Parse(offer)); + parsedOffer->GetAttributeList().SetAttribute( + new SdpFlagAttribute(SdpAttribute::kIceLiteAttribute)); + + std::ostringstream os; + parsedOffer->Serialize(os); + SetRemoteOffer(os.str(), CHECK_SUCCESS); + + ASSERT_TRUE(mSessionAns->RemoteIsIceLite()); + ASSERT_FALSE(mSessionOff->RemoteIsIceLite()); +} + +TEST_F(JsepSessionTest, TestIceOptions) { + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + std::string offer = CreateOffer(); + SetLocalOffer(offer, CHECK_SUCCESS); + SetRemoteOffer(offer, CHECK_SUCCESS); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer, CHECK_SUCCESS); + SetRemoteAnswer(answer, CHECK_SUCCESS); + + ASSERT_EQ(1U, mSessionOff->GetIceOptions().size()); + ASSERT_EQ("trickle", mSessionOff->GetIceOptions()[0]); + + ASSERT_EQ(1U, mSessionAns->GetIceOptions().size()); + ASSERT_EQ("trickle", mSessionAns->GetIceOptions()[0]); +} + +TEST_F(JsepSessionTest, TestIceRestart) { + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + std::string offer = CreateOffer(); + SetLocalOffer(offer, CHECK_SUCCESS); + SetRemoteOffer(offer, CHECK_SUCCESS); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer, CHECK_SUCCESS); + SetRemoteAnswer(answer, CHECK_SUCCESS); + + JsepOfferOptions options; + options.mIceRestart = Some(true); + + std::string reoffer = CreateOffer(Some(options)); + SetLocalOffer(reoffer, CHECK_SUCCESS); + SetRemoteOffer(reoffer, CHECK_SUCCESS); + std::string reanswer = CreateAnswer(); + SetLocalAnswer(reanswer, CHECK_SUCCESS); + SetRemoteAnswer(reanswer, CHECK_SUCCESS); + + UniquePtr<Sdp> parsedOffer(Parse(offer)); + ASSERT_EQ(1U, parsedOffer->GetMediaSectionCount()); + UniquePtr<Sdp> parsedReoffer(Parse(reoffer)); + ASSERT_EQ(1U, parsedReoffer->GetMediaSectionCount()); + + UniquePtr<Sdp> parsedAnswer(Parse(answer)); + ASSERT_EQ(1U, parsedAnswer->GetMediaSectionCount()); + UniquePtr<Sdp> parsedReanswer(Parse(reanswer)); + ASSERT_EQ(1U, parsedReanswer->GetMediaSectionCount()); + + // verify ice pwd/ufrag are present in offer/answer and reoffer/reanswer + auto& offerMediaAttrs = parsedOffer->GetMediaSection(0).GetAttributeList(); + ASSERT_TRUE(offerMediaAttrs.HasAttribute(SdpAttribute::kIcePwdAttribute)); + ASSERT_TRUE(offerMediaAttrs.HasAttribute(SdpAttribute::kIceUfragAttribute)); + + auto& reofferMediaAttrs = + parsedReoffer->GetMediaSection(0).GetAttributeList(); + ASSERT_TRUE(reofferMediaAttrs.HasAttribute(SdpAttribute::kIcePwdAttribute)); + ASSERT_TRUE(reofferMediaAttrs.HasAttribute(SdpAttribute::kIceUfragAttribute)); + + auto& answerMediaAttrs = parsedAnswer->GetMediaSection(0).GetAttributeList(); + ASSERT_TRUE(answerMediaAttrs.HasAttribute(SdpAttribute::kIcePwdAttribute)); + ASSERT_TRUE(answerMediaAttrs.HasAttribute(SdpAttribute::kIceUfragAttribute)); + + auto& reanswerMediaAttrs = + parsedReanswer->GetMediaSection(0).GetAttributeList(); + ASSERT_TRUE(reanswerMediaAttrs.HasAttribute(SdpAttribute::kIcePwdAttribute)); + ASSERT_TRUE( + reanswerMediaAttrs.HasAttribute(SdpAttribute::kIceUfragAttribute)); + + // make sure offer/reoffer ice pwd/ufrag changed on ice restart + ASSERT_NE(offerMediaAttrs.GetIcePwd().c_str(), + reofferMediaAttrs.GetIcePwd().c_str()); + ASSERT_NE(offerMediaAttrs.GetIceUfrag().c_str(), + reofferMediaAttrs.GetIceUfrag().c_str()); + + // make sure answer/reanswer ice pwd/ufrag changed on ice restart + ASSERT_NE(answerMediaAttrs.GetIcePwd().c_str(), + reanswerMediaAttrs.GetIcePwd().c_str()); + ASSERT_NE(answerMediaAttrs.GetIceUfrag().c_str(), + reanswerMediaAttrs.GetIceUfrag().c_str()); + + auto offererTransceivers = GetTransceivers(*mSessionOff); + auto answererTransceivers = GetTransceivers(*mSessionAns); + ASSERT_EQ(reofferMediaAttrs.GetIceUfrag(), + offererTransceivers[0].mTransport.mLocalUfrag); + ASSERT_EQ(reofferMediaAttrs.GetIceUfrag(), + answererTransceivers[0].mTransport.mIce->GetUfrag()); + ASSERT_EQ(reofferMediaAttrs.GetIcePwd(), + offererTransceivers[0].mTransport.mLocalPwd); + ASSERT_EQ(reofferMediaAttrs.GetIcePwd(), + answererTransceivers[0].mTransport.mIce->GetPassword()); + + ASSERT_EQ(reanswerMediaAttrs.GetIceUfrag(), + answererTransceivers[0].mTransport.mLocalUfrag); + ASSERT_EQ(reanswerMediaAttrs.GetIceUfrag(), + offererTransceivers[0].mTransport.mIce->GetUfrag()); + ASSERT_EQ(reanswerMediaAttrs.GetIcePwd(), + answererTransceivers[0].mTransport.mLocalPwd); + ASSERT_EQ(reanswerMediaAttrs.GetIcePwd(), + offererTransceivers[0].mTransport.mIce->GetPassword()); +} + +TEST_F(JsepSessionTest, TestAnswererIndicatingIceRestart) { + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + std::string offer = CreateOffer(); + SetLocalOffer(offer, CHECK_SUCCESS); + SetRemoteOffer(offer, CHECK_SUCCESS); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer, CHECK_SUCCESS); + SetRemoteAnswer(answer, CHECK_SUCCESS); + + // reoffer, but we'll improperly indicate an ice restart in the answer by + // modifying the ice pwd and ufrag + std::string reoffer = CreateOffer(); + SetLocalOffer(reoffer, CHECK_SUCCESS); + SetRemoteOffer(reoffer, CHECK_SUCCESS); + std::string reanswer = CreateAnswer(); + + // change the ice pwd and ufrag + ReplaceInSdp(&reanswer, "a=ice-ufrag:", "a=ice-ufrag:bad-"); + ReplaceInSdp(&reanswer, "a=ice-pwd:", "a=ice-pwd:bad-"); + SetLocalAnswer(reanswer, CHECK_SUCCESS); + JsepSession::Result result = + mSessionOff->SetRemoteDescription(kJsepSdpAnswer, reanswer); + ASSERT_EQ(dom::PCError::InvalidAccessError, *result.mError); +} + +TEST_F(JsepSessionTest, TestExtmap) { + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + // ssrc-audio-level will be extmap 1 for both + // csrc-audio-level will be 2 for both + // mid will be 3 for both + // video related extensions take 4 - 7 + mSessionOff->AddAudioRtpExtension("foo"); // Default mapping of 8 + mSessionOff->AddAudioRtpExtension("bar"); // Default mapping of 9 + mSessionAns->AddAudioRtpExtension("bar"); // Default mapping of 8 + std::string offer = CreateOffer(); + SetLocalOffer(offer, CHECK_SUCCESS); + SetRemoteOffer(offer, CHECK_SUCCESS); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer, CHECK_SUCCESS); + SetRemoteAnswer(answer, CHECK_SUCCESS); + + UniquePtr<Sdp> parsedOffer(Parse(offer)); + ASSERT_EQ(1U, parsedOffer->GetMediaSectionCount()); + + auto& offerMediaAttrs = parsedOffer->GetMediaSection(0).GetAttributeList(); + ASSERT_TRUE(offerMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)); + auto& offerExtmap = offerMediaAttrs.GetExtmap().mExtmaps; + ASSERT_EQ(5U, offerExtmap.size()); + ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level", + offerExtmap[0].extensionname); + ASSERT_EQ(1U, offerExtmap[0].entry); + ASSERT_EQ("urn:ietf:params:rtp-hdrext:csrc-audio-level", + offerExtmap[1].extensionname); + ASSERT_EQ(2U, offerExtmap[1].entry); + ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid", + offerExtmap[2].extensionname); + ASSERT_EQ(3U, offerExtmap[2].entry); + ASSERT_EQ("foo", offerExtmap[3].extensionname); + ASSERT_EQ(8U, offerExtmap[3].entry); + ASSERT_EQ("bar", offerExtmap[4].extensionname); + ASSERT_EQ(9U, offerExtmap[4].entry); + + UniquePtr<Sdp> parsedAnswer(Parse(answer)); + ASSERT_EQ(1U, parsedAnswer->GetMediaSectionCount()); + + auto& answerMediaAttrs = parsedAnswer->GetMediaSection(0).GetAttributeList(); + ASSERT_TRUE(answerMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)); + auto& answerExtmap = answerMediaAttrs.GetExtmap().mExtmaps; + ASSERT_EQ(3U, answerExtmap.size()); + ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level", + answerExtmap[0].extensionname); + ASSERT_EQ(1U, answerExtmap[0].entry); + ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid", + answerExtmap[1].extensionname); + ASSERT_EQ(3U, answerExtmap[1].entry); + // We ensure that the entry for "bar" matches what was in the offer + ASSERT_EQ("bar", answerExtmap[2].extensionname); + ASSERT_EQ(9U, answerExtmap[2].entry); +} + +TEST_F(JsepSessionTest, TestExtmapDefaults) { + types.push_back(SdpMediaSection::kAudio); + types.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionOff, "audio,video"); + + std::string offer = CreateOffer(); + SetLocalOffer(offer, CHECK_SUCCESS); + SetRemoteOffer(offer, CHECK_SUCCESS); + + std::string answer = CreateAnswer(); + SetLocalAnswer(answer, CHECK_SUCCESS); + SetRemoteAnswer(answer, CHECK_SUCCESS); + + UniquePtr<Sdp> parsedOffer(Parse(offer)); + ASSERT_EQ(2U, parsedOffer->GetMediaSectionCount()); + + auto& offerAudioMediaAttrs = + parsedOffer->GetMediaSection(0).GetAttributeList(); + ASSERT_TRUE( + offerAudioMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)); + auto& offerAudioExtmap = offerAudioMediaAttrs.GetExtmap().mExtmaps; + ASSERT_EQ(3U, offerAudioExtmap.size()); + + ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level", + offerAudioExtmap[0].extensionname); + ASSERT_EQ(1U, offerAudioExtmap[0].entry); + ASSERT_EQ("urn:ietf:params:rtp-hdrext:csrc-audio-level", + offerAudioExtmap[1].extensionname); + ASSERT_EQ(2U, offerAudioExtmap[1].entry); + ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid", + offerAudioExtmap[2].extensionname); + + auto& offerVideoMediaAttrs = + parsedOffer->GetMediaSection(1).GetAttributeList(); + ASSERT_TRUE( + offerVideoMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)); + auto& offerVideoExtmap = offerVideoMediaAttrs.GetExtmap().mExtmaps; + ASSERT_EQ(5U, offerVideoExtmap.size()); + + ASSERT_EQ(3U, offerVideoExtmap[0].entry); + ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid", + offerVideoExtmap[0].extensionname); + ASSERT_EQ("http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", + offerVideoExtmap[1].extensionname); + ASSERT_EQ(4U, offerVideoExtmap[1].entry); + ASSERT_EQ("urn:ietf:params:rtp-hdrext:toffset", + offerVideoExtmap[2].extensionname); + ASSERT_EQ(5U, offerVideoExtmap[2].entry); + ASSERT_EQ("http://www.webrtc.org/experiments/rtp-hdrext/playout-delay", + offerVideoExtmap[3].extensionname); + ASSERT_EQ(6U, offerVideoExtmap[3].entry); + ASSERT_EQ( + "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-" + "extensions-01", + offerVideoExtmap[4].extensionname); + ASSERT_EQ(7U, offerVideoExtmap[4].entry); + + UniquePtr<Sdp> parsedAnswer(Parse(answer)); + ASSERT_EQ(2U, parsedAnswer->GetMediaSectionCount()); + + auto& answerAudioMediaAttrs = + parsedAnswer->GetMediaSection(0).GetAttributeList(); + ASSERT_TRUE( + answerAudioMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)); + auto& answerAudioExtmap = answerAudioMediaAttrs.GetExtmap().mExtmaps; + ASSERT_EQ(2U, answerAudioExtmap.size()); + + ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level", + answerAudioExtmap[0].extensionname); + ASSERT_EQ(1U, answerAudioExtmap[0].entry); + ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid", + answerAudioExtmap[1].extensionname); + ASSERT_EQ(3U, answerAudioExtmap[1].entry); + + auto& answerVideoMediaAttrs = + parsedAnswer->GetMediaSection(1).GetAttributeList(); + ASSERT_TRUE( + answerVideoMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)); + auto& answerVideoExtmap = answerVideoMediaAttrs.GetExtmap().mExtmaps; + ASSERT_EQ(4U, answerVideoExtmap.size()); + + ASSERT_EQ(3U, answerVideoExtmap[0].entry); + ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid", + answerVideoExtmap[0].extensionname); + ASSERT_EQ("http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", + answerVideoExtmap[1].extensionname); + ASSERT_EQ(4U, answerVideoExtmap[1].entry); + ASSERT_EQ("urn:ietf:params:rtp-hdrext:toffset", + answerVideoExtmap[2].extensionname); + ASSERT_EQ(5U, answerVideoExtmap[2].entry); + ASSERT_EQ( + "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-" + "extensions-01", + answerVideoExtmap[3].extensionname); + ASSERT_EQ(7U, answerVideoExtmap[3].entry); +} + +TEST_F(JsepSessionTest, TestExtmapWithDuplicates) { + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + // ssrc-audio-level will be extmap 1 for both + // csrc-audio-level will be 2 for both + // mid will be 3 for both + // video related extensions take 4 - 7 + mSessionOff->AddAudioRtpExtension("foo"); // Default mapping of 8 + mSessionOff->AddAudioRtpExtension("bar"); // Default mapping of 9 + mSessionOff->AddAudioRtpExtension("bar"); // Should be ignored + mSessionOff->AddAudioRtpExtension("bar"); // Should be ignored + mSessionOff->AddAudioRtpExtension("baz"); // Default mapping of 10 + mSessionOff->AddAudioRtpExtension("bar"); // Should be ignored + + std::string offer = CreateOffer(); + UniquePtr<Sdp> parsedOffer(Parse(offer)); + ASSERT_EQ(1U, parsedOffer->GetMediaSectionCount()); + + auto& offerMediaAttrs = parsedOffer->GetMediaSection(0).GetAttributeList(); + ASSERT_TRUE(offerMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)); + auto& offerExtmap = offerMediaAttrs.GetExtmap().mExtmaps; + ASSERT_EQ(6U, offerExtmap.size()); + ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level", + offerExtmap[0].extensionname); + ASSERT_EQ(1U, offerExtmap[0].entry); + ASSERT_EQ("urn:ietf:params:rtp-hdrext:csrc-audio-level", + offerExtmap[1].extensionname); + ASSERT_EQ(2U, offerExtmap[1].entry); + ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid", + offerExtmap[2].extensionname); + ASSERT_EQ(3U, offerExtmap[2].entry); + ASSERT_EQ("foo", offerExtmap[3].extensionname); + ASSERT_EQ(8U, offerExtmap[3].entry); + ASSERT_EQ("bar", offerExtmap[4].extensionname); + ASSERT_EQ(9U, offerExtmap[4].entry); + ASSERT_EQ("baz", offerExtmap[5].extensionname); + ASSERT_EQ(10U, offerExtmap[5].entry); +} + +TEST_F(JsepSessionTest, TestExtmapZeroId) { + AddTracks(*mSessionOff, "video"); + AddTracks(*mSessionAns, "video"); + + std::string sdp = + "v=0\r\n" + "o=- 6 2 IN IP4 1r\r\n" + "t=0 0a\r\n" + "a=ice-ufrag:Xp\r\n" + "a=ice-pwd:he\r\n" + "a=setup:actpass\r\n" + "a=fingerprint:sha-256 " + "DC:FC:25:56:2B:88:77:2F:E4:FA:97:4E:2E:F1:D6:34:A6:A0:11:E2:E4:38:B3:98:" + "08:D2:F7:9D:F5:E2:C1:15\r\n" + "m=video 9 UDP/TLS/RTP/SAVPF 100\r\n" + "c=IN IP4 0\r\n" + "a=rtpmap:100 VP8/90000\r\n" + "a=extmap:0 urn:ietf:params:rtp-hdrext:toffset\r\n"; + auto result = mSessionAns->SetRemoteDescription(kJsepSdpOffer, sdp); + ASSERT_TRUE(result.mError == Some(dom::PCError::OperationError)); + ASSERT_EQ( + "Description contains invalid extension id 0 on level 0 which is" + " unsupported until 2-byte rtp header extensions are supported in" + " webrtc.org", + mSessionAns->GetLastError()); +} + +TEST_F(JsepSessionTest, TestExtmapInvalidId) { + AddTracks(*mSessionAns, "video"); + + std::string sdp = + "v=0\r\n" + "o=- 6 2 IN IP4 1r\r\n" + "t=0 0a\r\n" + "a=ice-ufrag:Xp\r\n" + "a=ice-pwd:he\r\n" + "a=setup:actpass\r\n" + "a=fingerprint:sha-256 " + "DC:FC:25:56:2B:88:77:2F:E4:FA:97:4E:2E:F1:D6:34:A6:A0:11:E2:E4:38:B3:98:" + "08:D2:F7:9D:F5:E2:C1:15\r\n" + "m=video 9 UDP/TLS/RTP/SAVPF 100\r\n" + "c=IN IP4 0\r\n" + "a=rtpmap:100 VP8/90000\r\n" + "a=extmap:15 urn:ietf:params:rtp-hdrext:toffset\r\n"; + auto result = mSessionAns->SetRemoteDescription(kJsepSdpOffer, sdp); + ASSERT_TRUE(result.mError == Some(dom::PCError::OperationError)); + ASSERT_EQ( + "Description contains invalid extension id 15 on level 0 which is" + " unsupported until 2-byte rtp header extensions are supported in" + " webrtc.org", + mSessionAns->GetLastError()); +} + +TEST_F(JsepSessionTest, TestExtmapDuplicateId) { + AddTracks(*mSessionAns, "video"); + + std::string sdp = + "v=0\r\n" + "o=- 6 2 IN IP4 1r\r\n" + "t=0 0a\r\n" + "a=ice-ufrag:Xp\r\n" + "a=ice-pwd:he\r\n" + "a=setup:actpass\r\n" + "a=fingerprint:sha-256 " + "DC:FC:25:56:2B:88:77:2F:E4:FA:97:4E:2E:F1:D6:34:A6:A0:11:E2:E4:38:B3:98:" + "08:D2:F7:9D:F5:E2:C1:15\r\n" + "m=video 9 UDP/TLS/RTP/SAVPF 100\r\n" + "c=IN IP4 0\r\n" + "a=rtpmap:100 VP8/90000\r\n" + "a=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\n" + "a=extmap:2 " + "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n"; + auto result = mSessionAns->SetRemoteDescription(kJsepSdpOffer, sdp); + ASSERT_TRUE(result.mError == Some(dom::PCError::OperationError)); + ASSERT_EQ("Description contains duplicate extension id 2 on level 0", + mSessionAns->GetLastError()); +} + +TEST_F(JsepSessionTest, TestNegotiatedExtmapStability) { + AddTracks(*mSessionAns, "audio,video"); + + std::string sdp = + "v=0\r\n" + "o=- 6 2 IN IP4 1r\r\n" + "t=0 0a\r\n" + "a=ice-ufrag:Xp\r\n" + "a=ice-pwd:he\r\n" + "a=setup:actpass\r\n" + "a=fingerprint:sha-256 " + "DC:FC:25:56:2B:88:77:2F:E4:FA:97:4E:2E:F1:D6:34:A6:A0:11:E2:E4:38:B3:98:" + "08:D2:F7:9D:F5:E2:C1:15\r\n" + "m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n" + "c=IN IP4 51.81.107.13\r\n" + "a=sendrecv\r\n" + "a=extmap:11 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n" + "a=fmtp:111 maxplaybackrate=48000;stereo=1;useinbandfec=1\r\n" + "a=mid:audio\r\n" + "a=rtcp-mux\r\n" + "a=rtpmap:111 opus/48000/2\r\n" + "a=setup:active\r\n" + "a=ssrc:3463672643 cname:{ec9a356a-8d2c-504e-9977-99070a51f929}\r\n" + "m=video 9 UDP/TLS/RTP/SAVPF 100\r\n" + "c=IN IP4 0\r\n" + "a=sendrecv\r\n" + "a=rtpmap:100 VP8/90000\r\n" + "a=extmap:12 urn:ietf:params:rtp-hdrext:toffset\r\n" + "a=extmap:13 " + "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n"; + auto result = mSessionAns->SetRemoteDescription(kJsepSdpOffer, sdp); + ASSERT_FALSE(result.mError.isSome()); + auto answer = CreateAnswer(); + result = mSessionAns->SetLocalDescription(kJsepSdpAnswer, answer); + ASSERT_FALSE(result.mError.isSome()); + + // Verify that we've negotiated the right extmap based on the unusual values + // in the offer. + auto transceivers = GetTransceivers(*mSessionAns); + ASSERT_EQ(2U, transceivers.size()); + auto* audioSend = transceivers[0].mSendTrack.GetNegotiatedDetails(); + auto* audioRecv = transceivers[0].mRecvTrack.GetNegotiatedDetails(); + auto* videoSend = transceivers[1].mSendTrack.GetNegotiatedDetails(); + auto* videoRecv = transceivers[1].mRecvTrack.GetNegotiatedDetails(); + ASSERT_TRUE(audioSend); + ASSERT_TRUE(audioRecv); + ASSERT_TRUE(videoSend); + ASSERT_TRUE(videoRecv); + ASSERT_EQ( + 11U, + audioSend->GetExt("urn:ietf:params:rtp-hdrext:ssrc-audio-level")->entry); + ASSERT_EQ( + 11U, + audioRecv->GetExt("urn:ietf:params:rtp-hdrext:ssrc-audio-level")->entry); + ASSERT_EQ(12U, + videoSend->GetExt("urn:ietf:params:rtp-hdrext:toffset")->entry); + ASSERT_EQ(12U, + videoRecv->GetExt("urn:ietf:params:rtp-hdrext:toffset")->entry); + ASSERT_EQ( + 13U, + videoSend + ->GetExt("http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time") + ->entry); + ASSERT_EQ( + 13U, + videoRecv + ->GetExt("http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time") + ->entry); + + SwapOfferAnswerRoles(); + + // Make sure a reoffer uses the negotiated extmap + auto reoffer = CreateOffer(); + ASSERT_NE(std::string::npos, reoffer.find("a=extmap:11")); + ASSERT_NE(std::string::npos, reoffer.find("a=extmap:12")); + ASSERT_NE(std::string::npos, reoffer.find("a=extmap:13")); +} + +TEST_F(JsepSessionTest, TestNegotiatedExtmapCollision) { + AddTracks(*mSessionAns, "audio"); + // ssrc-audio-level will be extmap 1 for both + // csrc-audio-level will be 2 for both + // mid will be 3 for both + mSessionAns->AddAudioRtpExtension("foo"); + mSessionAns->AddAudioRtpExtension("bar"); + mSessionAns->AddAudioRtpExtension("baz"); + + // Set up an offer that uses the same extmap entries, but for different + // things, causing collisions. + std::string sdp = + "v=0\r\n" + "o=- 6 2 IN IP4 1r\r\n" + "t=0 0a\r\n" + "a=ice-ufrag:Xp\r\n" + "a=ice-pwd:he\r\n" + "a=setup:actpass\r\n" + "a=fingerprint:sha-256 " + "DC:FC:25:56:2B:88:77:2F:E4:FA:97:4E:2E:F1:D6:34:A6:A0:11:E2:E4:38:B3:98:" + "08:D2:F7:9D:F5:E2:C1:15\r\n" + "m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n" + "c=IN IP4 51.81.107.13\r\n" + "a=sendrecv\r\n" + "a=extmap:1 foo\r\n" + "a=extmap:2 bar\r\n" + "a=extmap:3 baz\r\n" + "a=extmap:11 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n" + "a=fmtp:111 maxplaybackrate=48000;stereo=1;useinbandfec=1\r\n" + "a=mid:audio\r\n" + "a=rtcp-mux\r\n" + "a=rtpmap:111 opus/48000/2\r\n" + "a=setup:active\r\n" + "a=ssrc:3463672643 cname:{ec9a356a-8d2c-504e-9977-99070a51f929}\r\n"; + auto result = mSessionAns->SetRemoteDescription(kJsepSdpOffer, sdp); + ASSERT_FALSE(result.mError.isSome()); + auto answer = CreateAnswer(); + result = mSessionAns->SetLocalDescription(kJsepSdpAnswer, answer); + ASSERT_FALSE(result.mError.isSome()); + + // Verify that we've negotiated the right extmap based on the unusual values + // in the offer. + auto transceivers = GetTransceivers(*mSessionAns); + ASSERT_EQ(1U, transceivers.size()); + auto* audioSend = transceivers[0].mSendTrack.GetNegotiatedDetails(); + auto* audioRecv = transceivers[0].mRecvTrack.GetNegotiatedDetails(); + ASSERT_TRUE(audioSend); + ASSERT_TRUE(audioRecv); + ASSERT_EQ(1U, audioSend->GetExt("foo")->entry); + ASSERT_EQ(1U, audioRecv->GetExt("foo")->entry); + ASSERT_EQ(2U, audioSend->GetExt("bar")->entry); + ASSERT_EQ(2U, audioRecv->GetExt("bar")->entry); + ASSERT_EQ(3U, audioSend->GetExt("baz")->entry); + ASSERT_EQ(3U, audioRecv->GetExt("baz")->entry); + ASSERT_EQ( + 11U, + audioSend->GetExt("urn:ietf:params:rtp-hdrext:ssrc-audio-level")->entry); + ASSERT_EQ( + 11U, + audioRecv->GetExt("urn:ietf:params:rtp-hdrext:ssrc-audio-level")->entry); + SwapOfferAnswerRoles(); + + // Make sure a reoffer uses the negotiated extmap + auto reoffer = CreateOffer(); + ASSERT_NE(std::string::npos, reoffer.find("a=extmap:1 foo")); + ASSERT_NE(std::string::npos, reoffer.find("a=extmap:2 bar")); + ASSERT_NE(std::string::npos, reoffer.find("a=extmap:3 baz")); + ASSERT_NE( + std::string::npos, + reoffer.find("a=extmap:11 urn:ietf:params:rtp-hdrext:ssrc-audio-level")); + + // Ensure no duplicates + ASSERT_EQ(std::string::npos, + reoffer.find("a=extmap:1 ", reoffer.find("a=extmap:1 ") + 1)); + ASSERT_EQ(std::string::npos, + reoffer.find("a=extmap:2 ", reoffer.find("a=extmap:2 ") + 1)); + ASSERT_EQ(std::string::npos, + reoffer.find("a=extmap:3 ", reoffer.find("a=extmap:3 ") + 1)); + ASSERT_EQ(std::string::npos, + reoffer.find("a=extmap:11 ", reoffer.find("a=extmap:11 ") + 1)); +} + +TEST_F(JsepSessionTest, TestExtmapAnswerChangesId) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + + std::string offer = CreateOffer(); + SetLocalOffer(offer, ALL_CHECKS); + SetRemoteOffer(offer, ALL_CHECKS); + + std::string answer = CreateAnswer(); + std::string mungedAnswer = + SetExtmap(answer, "urn:ietf:params:rtp-hdrext:sdes:mid", 14); + JsepSession::Result result = + mSessionOff->SetRemoteDescription(kJsepSdpAnswer, mungedAnswer); + ASSERT_TRUE(result.mError.isSome()); + ASSERT_EQ(dom::PCError::InvalidAccessError, *result.mError); + ASSERT_NE(mSessionOff->GetLastError().find( + "Answer changed id for extmap attribute at level 0"), + std::string::npos); +} + +TEST_F(JsepSessionTest, TestExtmapChangeId) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + + OfferAnswer(); + + // RFC 5285 does not seem to forbid giving a pre-existing extension a new id, + // as long as that id has never been used before + { + std::string offer = CreateOffer(); + SetLocalOffer(offer, ALL_CHECKS); + uint16_t oldId = 0; + std::string mungedOffer = + SetExtmap(offer, "urn:ietf:params:rtp-hdrext:sdes:mid", 14, &oldId); + ASSERT_NE(oldId, 0); + SetRemoteOffer(mungedOffer, ALL_CHECKS); + + std::string answer = CreateAnswer(); + SetLocalAnswer(answer, ALL_CHECKS); + + std::string mungedAnswer = + SetExtmap(answer, "urn:ietf:params:rtp-hdrext:sdes:mid", oldId); + SetRemoteAnswer(mungedAnswer, ALL_CHECKS); + } + + // Make sure going back to the previous id works + OfferAnswer(); +} + +TEST_F(JsepSessionTest, TestExtmapSwap) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + + OfferAnswer(); + + std::string offer = CreateOffer(); + uint16_t midId = GetExtmap(offer, "urn:ietf:params:rtp-hdrext:sdes:mid"); + uint16_t ssrcLevelId = 0; + std::string mungedOffer = + SetExtmap(offer, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", midId, + &ssrcLevelId); + mungedOffer = SetExtmap(mungedOffer, "urn:ietf:params:rtp-hdrext:sdes:mid", + ssrcLevelId); + + JsepSession::Result result = + mSessionAns->SetRemoteDescription(kJsepSdpOffer, mungedOffer); + ASSERT_TRUE(result.mError.isSome()); + ASSERT_EQ(dom::PCError::InvalidAccessError, *result.mError); + ASSERT_NE(mSessionAns->GetLastError().find( + "Remote description attempted to remap RTP extension id"), + std::string::npos); +} + +TEST_F(JsepSessionTest, TestExtmapReuse) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + + OfferAnswer(); + + std::string offer = CreateOffer(); + UniquePtr<Sdp> munge(Parse(offer)); + ASSERT_EQ(1U, munge->GetMediaSectionCount()); + + auto& offerMediaAttrs = munge->GetMediaSection(0).GetAttributeList(); + ASSERT_TRUE(offerMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)); + auto offerExtmap = offerMediaAttrs.GetExtmap(); + for (auto& ext : offerExtmap.mExtmaps) { + if (ext.extensionname == "urn:ietf:params:rtp-hdrext:ssrc-audio-level") { + ext.extensionname = "foo"; + } + } + + offerMediaAttrs.SetAttribute(offerExtmap.Clone()); + + std::string sdpString = munge->ToString(); + JsepSession::Result result = + mSessionAns->SetRemoteDescription(kJsepSdpOffer, sdpString); + ASSERT_TRUE(result.mError.isSome()); + ASSERT_EQ(dom::PCError::InvalidAccessError, *result.mError); + ASSERT_NE(mSessionAns->GetLastError().find( + "Remote description attempted to remap RTP extension id"), + std::string::npos); +} + +TEST_F(JsepSessionTest, TestExtmapReuseAfterRenegotiation) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + + OfferAnswer(); + + // Renegotiate without ssrc-audio-level + { + std::string offer = CreateOffer(); + SetLocalOffer(offer, ALL_CHECKS); + // Passing 0 removes urn:ietf:params:rtp-hdrext:ssrc-audio-level + std::string mungedOffer = + SetExtmap(offer, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", 0); + SetRemoteOffer(mungedOffer, ALL_CHECKS); + + std::string answer = CreateAnswer(); + SetLocalAnswer(answer, ALL_CHECKS); + SetRemoteAnswer(answer, ALL_CHECKS); + } + + // Make sure trying to reuse the id for ssrc-audio-level fails, even though we + // did not use it last round. + { + std::string offer = CreateOffer(); + UniquePtr<Sdp> munge(Parse(offer)); + ASSERT_EQ(1U, munge->GetMediaSectionCount()); + + auto& offerMediaAttrs = munge->GetMediaSection(0).GetAttributeList(); + ASSERT_TRUE(offerMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)); + auto offerExtmap = offerMediaAttrs.GetExtmap(); + for (auto& ext : offerExtmap.mExtmaps) { + if (ext.extensionname == "urn:ietf:params:rtp-hdrext:ssrc-audio-level") { + ext.extensionname = "foo"; + } + } + + offerMediaAttrs.SetAttribute(offerExtmap.Clone()); + + std::string sdpString = munge->ToString(); + JsepSession::Result result = + mSessionAns->SetRemoteDescription(kJsepSdpOffer, sdpString); + ASSERT_TRUE(result.mError.isSome()); + ASSERT_EQ(dom::PCError::InvalidAccessError, *result.mError); + ASSERT_NE(mSessionAns->GetLastError().find( + "Remote description attempted to remap RTP extension id"), + std::string::npos); + } +} + +TEST_F(JsepSessionTest, TestRtcpFbStar) { + AddTracks(*mSessionOff, "video"); + AddTracks(*mSessionAns, "video"); + + std::string offer = CreateOffer(); + + UniquePtr<Sdp> parsedOffer(Parse(offer)); + auto* rtcpfbs = new SdpRtcpFbAttributeList; + rtcpfbs->PushEntry("*", SdpRtcpFbAttributeList::kNack); + parsedOffer->GetMediaSection(0).GetAttributeList().SetAttribute(rtcpfbs); + offer = parsedOffer->ToString(); + + SetLocalOffer(offer, CHECK_SUCCESS); + SetRemoteOffer(offer, CHECK_SUCCESS); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer, CHECK_SUCCESS); + SetRemoteAnswer(answer, CHECK_SUCCESS); + + ASSERT_EQ(1U, GetRemoteTracks(*mSessionAns).size()); + JsepTrack track = GetRemoteTracks(*mSessionAns)[0]; + ASSERT_TRUE(track.GetNegotiatedDetails()); + auto* details = track.GetNegotiatedDetails(); + for (const auto& codec : details->GetEncoding(0).GetCodecs()) { + const JsepVideoCodecDescription* videoCodec = + static_cast<const JsepVideoCodecDescription*>(codec.get()); + ASSERT_EQ(1U, videoCodec->mNackFbTypes.size()); + ASSERT_EQ("", videoCodec->mNackFbTypes[0]); + } +} + +TEST_F(JsepSessionTest, TestUniquePayloadTypes) { + // The audio payload types will all appear more than once, but the video + // payload types will be unique. + AddTracks(*mSessionOff, "audio,audio,video"); + AddTracks(*mSessionAns, "audio,audio,video"); + + std::string offer = CreateOffer(); + SetLocalOffer(offer, CHECK_SUCCESS); + SetRemoteOffer(offer, CHECK_SUCCESS); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer, CHECK_SUCCESS); + SetRemoteAnswer(answer, CHECK_SUCCESS); + + auto offerTransceivers = GetTransceivers(*mSessionOff); + auto answerTransceivers = GetTransceivers(*mSessionAns); + ASSERT_EQ(3U, offerTransceivers.size()); + ASSERT_EQ(3U, answerTransceivers.size()); + + ASSERT_FALSE(IsNull(offerTransceivers[0].mRecvTrack)); + ASSERT_TRUE(offerTransceivers[0].mRecvTrack.GetNegotiatedDetails()); + ASSERT_EQ(0U, offerTransceivers[0] + .mRecvTrack.GetNegotiatedDetails() + ->GetUniquePayloadTypes() + .size()); + + ASSERT_FALSE(IsNull(offerTransceivers[1].mRecvTrack)); + ASSERT_TRUE(offerTransceivers[1].mRecvTrack.GetNegotiatedDetails()); + ASSERT_EQ(0U, offerTransceivers[1] + .mRecvTrack.GetNegotiatedDetails() + ->GetUniquePayloadTypes() + .size()); + + ASSERT_FALSE(IsNull(offerTransceivers[2].mRecvTrack)); + ASSERT_TRUE(offerTransceivers[2].mRecvTrack.GetNegotiatedDetails()); + ASSERT_NE(0U, offerTransceivers[2] + .mRecvTrack.GetNegotiatedDetails() + ->GetUniquePayloadTypes() + .size()); + + ASSERT_FALSE(IsNull(answerTransceivers[0].mRecvTrack)); + ASSERT_TRUE(answerTransceivers[0].mRecvTrack.GetNegotiatedDetails()); + ASSERT_EQ(0U, answerTransceivers[0] + .mRecvTrack.GetNegotiatedDetails() + ->GetUniquePayloadTypes() + .size()); + + ASSERT_FALSE(IsNull(answerTransceivers[1].mRecvTrack)); + ASSERT_TRUE(answerTransceivers[1].mRecvTrack.GetNegotiatedDetails()); + ASSERT_EQ(0U, answerTransceivers[1] + .mRecvTrack.GetNegotiatedDetails() + ->GetUniquePayloadTypes() + .size()); + + ASSERT_FALSE(IsNull(answerTransceivers[2].mRecvTrack)); + ASSERT_TRUE(answerTransceivers[2].mRecvTrack.GetNegotiatedDetails()); + ASSERT_NE(0U, answerTransceivers[2] + .mRecvTrack.GetNegotiatedDetails() + ->GetUniquePayloadTypes() + .size()); +} + +TEST_F(JsepSessionTest, UnknownFingerprintAlgorithm) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + + std::string offer(CreateOffer()); + SetLocalOffer(offer); + ReplaceAll("fingerprint:sha", "fingerprint:foo", &offer); + JsepSession::Result result = + mSessionAns->SetRemoteDescription(kJsepSdpOffer, offer); + ASSERT_EQ(dom::PCError::InvalidAccessError, *result.mError); + ASSERT_NE("", mSessionAns->GetLastError()); +} + +TEST(H264ProfileLevelIdTest, TestLevelComparisons) +{ + ASSERT_LT(JsepVideoCodecDescription::GetSaneH264Level(0x421D0B), // 1b + JsepVideoCodecDescription::GetSaneH264Level(0x420D0B)); // 1.1 + ASSERT_LT(JsepVideoCodecDescription::GetSaneH264Level(0x420D0A), // 1.0 + JsepVideoCodecDescription::GetSaneH264Level(0x421D0B)); // 1b + ASSERT_LT(JsepVideoCodecDescription::GetSaneH264Level(0x420D0A), // 1.0 + JsepVideoCodecDescription::GetSaneH264Level(0x420D0B)); // 1.1 + + ASSERT_LT(JsepVideoCodecDescription::GetSaneH264Level(0x640009), // 1b + JsepVideoCodecDescription::GetSaneH264Level(0x64000B)); // 1.1 + ASSERT_LT(JsepVideoCodecDescription::GetSaneH264Level(0x64000A), // 1.0 + JsepVideoCodecDescription::GetSaneH264Level(0x640009)); // 1b + ASSERT_LT(JsepVideoCodecDescription::GetSaneH264Level(0x64000A), // 1.0 + JsepVideoCodecDescription::GetSaneH264Level(0x64000B)); // 1.1 +} + +TEST(H264ProfileLevelIdTest, TestLevelSetting) +{ + uint32_t profileLevelId = 0x420D0A; + JsepVideoCodecDescription::SetSaneH264Level( + JsepVideoCodecDescription::GetSaneH264Level(0x42100B), &profileLevelId); + ASSERT_EQ((uint32_t)0x421D0B, profileLevelId); + + JsepVideoCodecDescription::SetSaneH264Level( + JsepVideoCodecDescription::GetSaneH264Level(0x42000A), &profileLevelId); + ASSERT_EQ((uint32_t)0x420D0A, profileLevelId); + + profileLevelId = 0x6E100A; + JsepVideoCodecDescription::SetSaneH264Level( + JsepVideoCodecDescription::GetSaneH264Level(0x640009), &profileLevelId); + ASSERT_EQ((uint32_t)0x6E1009, profileLevelId); + + JsepVideoCodecDescription::SetSaneH264Level( + JsepVideoCodecDescription::GetSaneH264Level(0x64000B), &profileLevelId); + ASSERT_EQ((uint32_t)0x6E100B, profileLevelId); +} + +TEST_F(JsepSessionTest, StronglyPreferredCodec) { + for (auto& codec : mSessionAns->Codecs()) { + if (codec->mName == "H264") { + codec->mStronglyPreferred = true; + } + } + + types.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionOff, "video"); + AddTracks(*mSessionAns, "video"); + + OfferAnswer(); + + UniquePtr<JsepCodecDescription> codec; + GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &codec); + ASSERT_TRUE(codec); + ASSERT_EQ("H264", codec->mName); + GetCodec(*mSessionAns, 0, sdp::kRecv, 0, 0, &codec); + ASSERT_TRUE(codec); + ASSERT_EQ("H264", codec->mName); +} + +TEST_F(JsepSessionTest, LowDynamicPayloadType) { + SetPayloadTypeNumber(*mSessionOff, "opus", "12"); + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + + OfferAnswer(); + UniquePtr<JsepCodecDescription> codec; + GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &codec); + ASSERT_TRUE(codec); + ASSERT_EQ("opus", codec->mName); + ASSERT_EQ("12", codec->mDefaultPt); + GetCodec(*mSessionAns, 0, sdp::kRecv, 0, 0, &codec); + ASSERT_TRUE(codec); + ASSERT_EQ("opus", codec->mName); + ASSERT_EQ("12", codec->mDefaultPt); +} + +TEST_F(JsepSessionTest, TestOfferPTAsymmetry) { + SetPayloadTypeNumber(*mSessionAns, "opus", "105"); + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + JsepOfferOptions options; + + // Ensure that mSessionAns is appropriately configured. Also ensure that + // creating an offer with 105 does not prompt mSessionAns to ignore the + // PT in the offer. + std::string offer; + JsepSession::Result result = mSessionAns->CreateOffer(options, &offer); + ASSERT_FALSE(result.mError.isSome()); + ASSERT_NE(std::string::npos, offer.find("a=rtpmap:105 opus")) << offer; + + OfferAnswer(); + + // Answerer should use what the offerer suggested + UniquePtr<JsepCodecDescription> codec; + GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &codec); + ASSERT_TRUE(codec); + ASSERT_EQ("opus", codec->mName); + ASSERT_EQ("109", codec->mDefaultPt); + GetCodec(*mSessionAns, 0, sdp::kRecv, 0, 0, &codec); + ASSERT_TRUE(codec); + ASSERT_EQ("opus", codec->mName); + ASSERT_EQ("109", codec->mDefaultPt); + + // Answerer should not change when it reoffers + result = mSessionAns->CreateOffer(options, &offer); + ASSERT_FALSE(result.mError.isSome()); + ASSERT_NE(std::string::npos, offer.find("a=rtpmap:109 opus")) << offer; +} + +TEST_F(JsepSessionTest, TestAnswerPTAsymmetry) { + // JsepSessionImpl will never answer with an asymmetric payload type + // (tested in TestOfferPTAsymmetry), so we have to rewrite SDP a little. + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + + std::string offer = CreateOffer(); + SetLocalOffer(offer); + + Replace("a=rtpmap:109 opus", "a=rtpmap:105 opus", &offer); + Replace("m=audio 9 UDP/TLS/RTP/SAVPF 109", "m=audio 9 UDP/TLS/RTP/SAVPF 105", + &offer); + ReplaceAll("a=fmtp:109", "a=fmtp:105", &offer); + SetRemoteOffer(offer); + + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + SetRemoteAnswer(answer); + + UniquePtr<JsepCodecDescription> codec; + GetCodec(*mSessionOff, 0, sdp::kSend, 0, 0, &codec); + ASSERT_TRUE(codec); + ASSERT_EQ("opus", codec->mName); + ASSERT_EQ("105", codec->mDefaultPt); + GetCodec(*mSessionOff, 0, sdp::kRecv, 0, 0, &codec); + ASSERT_TRUE(codec); + ASSERT_EQ("opus", codec->mName); + ASSERT_EQ("109", codec->mDefaultPt); + + GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &codec); + ASSERT_TRUE(codec); + ASSERT_EQ("opus", codec->mName); + ASSERT_EQ("105", codec->mDefaultPt); + GetCodec(*mSessionAns, 0, sdp::kRecv, 0, 0, &codec); + ASSERT_TRUE(codec); + ASSERT_EQ("opus", codec->mName); + ASSERT_EQ("105", codec->mDefaultPt); + + // Offerer should use 105 for reoffers + offer = CreateOffer(); + ASSERT_NE(std::string::npos, offer.find("a=rtpmap:105 opus")) << offer; + ASSERT_EQ(std::string::npos, offer.find("a=rtpmap:109 opus")) << offer; + ASSERT_NE(std::string::npos, offer.find("a=fmtp:105")) << offer; + ASSERT_EQ(std::string::npos, offer.find("a=fmtp:109")) << offer; +} + +TEST_F(JsepSessionTest, PayloadTypeClash) { + // Set up a scenario where mSessionAns' favorite codec (opus) is unsupported + // by mSessionOff, and mSessionOff uses that payload type for something else. + SetCodecEnabled(*mSessionOff, "opus", false); + SetPayloadTypeNumber(*mSessionOff, "opus", "0"); + SetPayloadTypeNumber(*mSessionOff, "G722", "109"); + SetPayloadTypeNumber(*mSessionAns, "opus", "109"); + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + + OfferAnswer(); + UniquePtr<JsepCodecDescription> codec; + GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &codec); + ASSERT_TRUE(codec); + ASSERT_EQ("G722", codec->mName); + ASSERT_EQ("109", codec->mDefaultPt); + GetCodec(*mSessionAns, 0, sdp::kRecv, 0, 0, &codec); + ASSERT_TRUE(codec); + ASSERT_EQ("G722", codec->mName); + ASSERT_EQ("109", codec->mDefaultPt); + + // Now, make sure that mSessionAns does not put a=rtpmap:109 opus in a + // reoffer, since pt 109 is taken for PCMU (the answerer still supports opus, + // and will reoffer it, but it should choose a new payload type for it) + JsepOfferOptions options; + std::string reoffer; + JsepSession::Result result = mSessionAns->CreateOffer(options, &reoffer); + ASSERT_FALSE(result.mError.isSome()); + ASSERT_EQ(std::string::npos, reoffer.find("a=rtpmap:109 opus")) << reoffer; + ASSERT_NE(std::string::npos, reoffer.find(" opus")) << reoffer; +} + +TEST_P(JsepSessionTest, TestGlareRollback) { + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + JsepOfferOptions options; + + std::string offer; + ASSERT_FALSE(mSessionAns->CreateOffer(options, &offer).mError.isSome()); + ASSERT_FALSE( + mSessionAns->SetLocalDescription(kJsepSdpOffer, offer).mError.isSome()); + ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionAns->GetState()); + + ASSERT_FALSE(mSessionOff->CreateOffer(options, &offer).mError.isSome()); + ASSERT_FALSE( + mSessionOff->SetLocalDescription(kJsepSdpOffer, offer).mError.isSome()); + ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionOff->GetState()); + + ASSERT_EQ(dom::PCError::InvalidStateError, + *mSessionAns->SetRemoteDescription(kJsepSdpOffer, offer).mError); + ASSERT_FALSE( + mSessionAns->SetLocalDescription(kJsepSdpRollback, "").mError.isSome()); + ASSERT_EQ(kJsepStateStable, mSessionAns->GetState()); + + SetRemoteOffer(offer); + + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + SetRemoteAnswer(answer); +} + +TEST_P(JsepSessionTest, TestRejectOfferRollback) { + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + + std::string offer = CreateOffer(); + SetLocalOffer(offer); + SetRemoteOffer(offer); + + ASSERT_FALSE( + mSessionAns->SetRemoteDescription(kJsepSdpRollback, "").mError.isSome()); + ASSERT_EQ(kJsepStateStable, mSessionAns->GetState()); + for (const auto& transceiver : GetTransceivers(*mSessionAns)) { + ASSERT_EQ(0U, transceiver.mRecvTrack.GetStreamIds().size()); + } + + ASSERT_FALSE( + mSessionOff->SetLocalDescription(kJsepSdpRollback, "").mError.isSome()); + ASSERT_EQ(kJsepStateStable, mSessionOff->GetState()); + + OfferAnswer(); +} + +TEST_P(JsepSessionTest, TestInvalidRollback) { + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + + ASSERT_EQ(dom::PCError::InvalidStateError, + *mSessionOff->SetLocalDescription(kJsepSdpRollback, "").mError); + ASSERT_EQ(dom::PCError::InvalidStateError, + *mSessionOff->SetRemoteDescription(kJsepSdpRollback, "").mError); + + std::string offer = CreateOffer(); + ASSERT_EQ(dom::PCError::InvalidStateError, + *mSessionOff->SetLocalDescription(kJsepSdpRollback, "").mError); + ASSERT_EQ(dom::PCError::InvalidStateError, + *mSessionOff->SetRemoteDescription(kJsepSdpRollback, "").mError); + + SetLocalOffer(offer); + ASSERT_EQ(dom::PCError::InvalidStateError, + *mSessionOff->SetRemoteDescription(kJsepSdpRollback, "").mError); + + SetRemoteOffer(offer); + ASSERT_EQ(dom::PCError::InvalidStateError, + *mSessionAns->SetLocalDescription(kJsepSdpRollback, "").mError); + + std::string answer = CreateAnswer(); + ASSERT_EQ(dom::PCError::InvalidStateError, + *mSessionAns->SetLocalDescription(kJsepSdpRollback, "").mError); + + SetLocalAnswer(answer); + ASSERT_EQ(dom::PCError::InvalidStateError, + *mSessionAns->SetLocalDescription(kJsepSdpRollback, "").mError); + ASSERT_EQ(dom::PCError::InvalidStateError, + *mSessionAns->SetRemoteDescription(kJsepSdpRollback, "").mError); + + SetRemoteAnswer(answer); + ASSERT_EQ(dom::PCError::InvalidStateError, + *mSessionOff->SetLocalDescription(kJsepSdpRollback, "").mError); + ASSERT_EQ(dom::PCError::InvalidStateError, + *mSessionOff->SetRemoteDescription(kJsepSdpRollback, "").mError); +} + +size_t GetActiveTransportCount(const JsepSession& session) { + size_t activeTransportCount = 0; + for (const auto& transceiver : JsepSessionTest::GetTransceivers(session)) { + if (!transceiver.HasBundleLevel() || + (transceiver.BundleLevel() == transceiver.GetLevel())) { + activeTransportCount += transceiver.mTransport.mComponents; + } + } + return activeTransportCount; +} + +TEST_P(JsepSessionTest, TestBalancedBundle) { + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + + mSessionOff->SetBundlePolicy(kBundleBalanced); + + std::string offer = CreateOffer(); + UniquePtr<Sdp> parsedOffer = std::move(SipccSdpParser().Parse(offer)->Sdp()); + + ASSERT_TRUE(parsedOffer.get()); + + std::map<SdpMediaSection::MediaType, SdpMediaSection*> firstByType; + + for (size_t i = 0; i < parsedOffer->GetMediaSectionCount(); ++i) { + SdpMediaSection& msection(parsedOffer->GetMediaSection(i)); + bool firstOfType = !firstByType.count(msection.GetMediaType()); + if (firstOfType) { + firstByType[msection.GetMediaType()] = &msection; + } + ASSERT_EQ(!firstOfType, msection.GetAttributeList().HasAttribute( + SdpAttribute::kBundleOnlyAttribute)); + } + + SetLocalOffer(offer); + SetRemoteOffer(offer); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + SetRemoteAnswer(answer); + + CheckTransceiversAreBundled(*mSessionOff, "Offerer transceivers"); + CheckTransceiversAreBundled(*mSessionAns, "Answerer transceivers"); + EXPECT_EQ(1U, GetActiveTransportCount(*mSessionOff)); + EXPECT_EQ(1U, GetActiveTransportCount(*mSessionAns)); +} + +TEST_P(JsepSessionTest, TestMaxBundle) { + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + + mSessionOff->SetBundlePolicy(kBundleMaxBundle); + std::string offer = CreateOffer(); + UniquePtr<Sdp> parsedOffer = std::move(SipccSdpParser().Parse(offer)->Sdp()); + + ASSERT_TRUE(parsedOffer.get()); + + ASSERT_FALSE(parsedOffer->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kBundleOnlyAttribute)); + ASSERT_NE(0U, parsedOffer->GetMediaSection(0).GetPort()); + for (size_t i = 1; i < parsedOffer->GetMediaSectionCount(); ++i) { + ASSERT_TRUE(parsedOffer->GetMediaSection(i).GetAttributeList().HasAttribute( + SdpAttribute::kBundleOnlyAttribute)); + ASSERT_EQ(0U, parsedOffer->GetMediaSection(i).GetPort()); + } + + SetLocalOffer(offer); + for (const auto& transceiver : GetTransceivers(*mSessionOff)) { + if (transceiver.GetLevel() == 0) { + // We do not set the bundle-level in have-local-offer unless the + // m-section is bundle-only. + ASSERT_FALSE(transceiver.HasBundleLevel()); + } else { + ASSERT_TRUE(transceiver.HasBundleLevel()); + ASSERT_EQ(0U, transceiver.BundleLevel()); + } + ASSERT_NE("", transceiver.mTransport.mTransportId); + } + + SetRemoteOffer(offer); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + SetRemoteAnswer(answer); + + CheckTransceiversAreBundled(*mSessionOff, "Offerer transceivers"); + CheckTransceiversAreBundled(*mSessionAns, "Answerer transceivers"); + EXPECT_EQ(1U, GetActiveTransportCount(*mSessionOff)); + EXPECT_EQ(1U, GetActiveTransportCount(*mSessionAns)); +} + +TEST_F(JsepSessionTest, TestNonDefaultProtocol) { + AddTracks(*mSessionOff, "audio,video,datachannel"); + AddTracks(*mSessionAns, "audio,video,datachannel"); + + std::string offer; + ASSERT_FALSE( + mSessionOff->CreateOffer(JsepOfferOptions(), &offer).mError.isSome()); + offer.replace(offer.find("UDP/TLS/RTP/SAVPF"), strlen("UDP/TLS/RTP/SAVPF"), + "RTP/SAVPF"); + offer.replace(offer.find("UDP/TLS/RTP/SAVPF"), strlen("UDP/TLS/RTP/SAVPF"), + "RTP/SAVPF"); + mSessionOff->SetLocalDescription(kJsepSdpOffer, offer); + mSessionAns->SetRemoteDescription(kJsepSdpOffer, offer); + + std::string answer; + mSessionAns->CreateAnswer(JsepAnswerOptions(), &answer); + UniquePtr<Sdp> parsedAnswer = Parse(answer); + ASSERT_EQ(3U, parsedAnswer->GetMediaSectionCount()); + ASSERT_EQ(SdpMediaSection::kRtpSavpf, + parsedAnswer->GetMediaSection(0).GetProtocol()); + ASSERT_EQ(SdpMediaSection::kRtpSavpf, + parsedAnswer->GetMediaSection(1).GetProtocol()); + + mSessionAns->SetLocalDescription(kJsepSdpAnswer, answer); + mSessionOff->SetRemoteDescription(kJsepSdpAnswer, answer); + + // Make sure reoffer uses the same protocol as before + mSessionOff->CreateOffer(JsepOfferOptions(), &offer); + UniquePtr<Sdp> parsedOffer = Parse(offer); + ASSERT_EQ(3U, parsedOffer->GetMediaSectionCount()); + ASSERT_EQ(SdpMediaSection::kRtpSavpf, + parsedOffer->GetMediaSection(0).GetProtocol()); + ASSERT_EQ(SdpMediaSection::kRtpSavpf, + parsedOffer->GetMediaSection(1).GetProtocol()); + + // Make sure reoffer from other side uses the same protocol as before + mSessionAns->CreateOffer(JsepOfferOptions(), &offer); + parsedOffer = Parse(offer); + ASSERT_EQ(3U, parsedOffer->GetMediaSectionCount()); + ASSERT_EQ(SdpMediaSection::kRtpSavpf, + parsedOffer->GetMediaSection(0).GetProtocol()); + ASSERT_EQ(SdpMediaSection::kRtpSavpf, + parsedOffer->GetMediaSection(1).GetProtocol()); +} + +TEST_F(JsepSessionTest, CreateOfferNoVideoStreamRecvVideo) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + + JsepOfferOptions options; + options.mOfferToReceiveAudio = Some(static_cast<size_t>(1U)); + options.mOfferToReceiveVideo = Some(static_cast<size_t>(1U)); + + CreateOffer(Some(options)); +} + +TEST_F(JsepSessionTest, CreateOfferNoAudioStreamRecvAudio) { + types.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionOff, "video"); + + JsepOfferOptions options; + options.mOfferToReceiveAudio = Some(static_cast<size_t>(1U)); + options.mOfferToReceiveVideo = Some(static_cast<size_t>(1U)); + + CreateOffer(Some(options)); +} + +TEST_F(JsepSessionTest, CreateOfferNoVideoStream) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + + JsepOfferOptions options; + options.mOfferToReceiveAudio = Some(static_cast<size_t>(1U)); + options.mOfferToReceiveVideo = Some(static_cast<size_t>(0U)); + + CreateOffer(Some(options)); +} + +TEST_F(JsepSessionTest, CreateOfferNoAudioStream) { + types.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionOff, "video"); + + JsepOfferOptions options; + options.mOfferToReceiveAudio = Some(static_cast<size_t>(0U)); + options.mOfferToReceiveVideo = Some(static_cast<size_t>(1U)); + + CreateOffer(Some(options)); +} + +TEST_F(JsepSessionTest, CreateOfferDontReceiveAudio) { + types.push_back(SdpMediaSection::kAudio); + types.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionOff, "audio,video"); + + JsepOfferOptions options; + options.mOfferToReceiveAudio = Some(static_cast<size_t>(0U)); + options.mOfferToReceiveVideo = Some(static_cast<size_t>(1U)); + + CreateOffer(Some(options)); +} + +TEST_F(JsepSessionTest, CreateOfferDontReceiveVideo) { + types.push_back(SdpMediaSection::kAudio); + types.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionOff, "audio,video"); + + JsepOfferOptions options; + options.mOfferToReceiveAudio = Some(static_cast<size_t>(1U)); + options.mOfferToReceiveVideo = Some(static_cast<size_t>(0U)); + + CreateOffer(Some(options)); +} + +TEST_F(JsepSessionTest, CreateOfferRemoveAudioTrack) { + types.push_back(SdpMediaSection::kAudio); + types.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionOff, "audio,video"); + + SetDirection(*mSessionOff, 1, SdpDirectionAttribute::kSendonly); + JsepTrack removedTrack = RemoveTrack(*mSessionOff, 0); + ASSERT_FALSE(IsNull(removedTrack)); + + CreateOffer(); +} + +TEST_F(JsepSessionTest, CreateOfferDontReceiveAudioRemoveAudioTrack) { + types.push_back(SdpMediaSection::kAudio); + types.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionOff, "audio,video"); + + SetDirection(*mSessionOff, 0, SdpDirectionAttribute::kSendonly); + JsepTrack removedTrack = RemoveTrack(*mSessionOff, 0); + ASSERT_FALSE(IsNull(removedTrack)); + + CreateOffer(); +} + +TEST_F(JsepSessionTest, CreateOfferDontReceiveVideoRemoveVideoTrack) { + types.push_back(SdpMediaSection::kAudio); + types.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionOff, "audio,video"); + + JsepOfferOptions options; + options.mOfferToReceiveAudio = Some(static_cast<size_t>(1U)); + options.mOfferToReceiveVideo = Some(static_cast<size_t>(0U)); + + JsepTrack removedTrack = RemoveTrack(*mSessionOff, 0); + ASSERT_FALSE(IsNull(removedTrack)); + + CreateOffer(Some(options)); +} + +static const std::string strSampleCandidate = + "a=candidate:1 1 UDP 2130706431 192.168.2.1 50005 typ host\r\n"; + +static const unsigned short nSamplelevel = 2; + +TEST_F(JsepSessionTest, CreateOfferAddCandidate) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + std::string offer = CreateOffer(); + SetLocalOffer(offer); + + uint16_t level; + std::string mid; + bool skipped; + nsresult rv; + rv = mSessionOff->AddLocalIceCandidate(strSampleCandidate, + GetTransportId(*mSessionOff, 0), "", + &level, &mid, &skipped); + ASSERT_EQ(NS_OK, rv); +} + +TEST_F(JsepSessionTest, AddIceCandidateEarly) { + uint16_t level; + std::string mid; + bool skipped; + nsresult rv; + rv = mSessionOff->AddLocalIceCandidate(strSampleCandidate, + GetTransportId(*mSessionOff, 0), "", + &level, &mid, &skipped); + + // This can't succeed without a local description + ASSERT_NE(NS_OK, rv); +} + +TEST_F(JsepSessionTest, OfferAnswerDontAddAudioStreamOnAnswerNoOptions) { + types.push_back(SdpMediaSection::kAudio); + types.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionOff, "audio,video"); + AddTracks(*mSessionAns, "video"); + + JsepOfferOptions options; + options.mOfferToReceiveAudio = Some(static_cast<size_t>(1U)); + options.mOfferToReceiveVideo = Some(static_cast<size_t>(1U)); + + CreateOffer(Some(options)); + std::string offer = CreateOffer(Some(options)); + SetLocalOffer(offer); + SetRemoteOffer(offer); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer, CHECK_SUCCESS); + SetRemoteAnswer(answer, CHECK_SUCCESS); +} + +TEST_F(JsepSessionTest, OfferAnswerDontAddVideoStreamOnAnswerNoOptions) { + types.push_back(SdpMediaSection::kAudio); + types.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionOff, "audio,video"); + AddTracks(*mSessionAns, "audio"); + + JsepOfferOptions options; + options.mOfferToReceiveAudio = Some(static_cast<size_t>(1U)); + options.mOfferToReceiveVideo = Some(static_cast<size_t>(1U)); + + CreateOffer(Some(options)); + std::string offer = CreateOffer(Some(options)); + SetLocalOffer(offer); + SetRemoteOffer(offer); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer, CHECK_SUCCESS); + SetRemoteAnswer(answer, CHECK_SUCCESS); +} + +TEST_F(JsepSessionTest, OfferAnswerDontAddAudioVideoStreamsOnAnswerNoOptions) { + types.push_back(SdpMediaSection::kAudio); + types.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionOff, "audio,video"); + AddTracks(*mSessionAns); + + JsepOfferOptions options; + options.mOfferToReceiveAudio = Some(static_cast<size_t>(1U)); + options.mOfferToReceiveVideo = Some(static_cast<size_t>(1U)); + + OfferAnswer(); +} + +TEST_F(JsepSessionTest, OfferAndAnswerWithExtraCodec) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + std::string offer = CreateOffer(); + SetLocalOffer(offer); + SetRemoteOffer(offer); + std::string answer = CreateAnswer(); + + UniquePtr<Sdp> munge = Parse(answer); + SdpMediaSection& mediaSection = munge->GetMediaSection(0); + mediaSection.AddCodec("8", "PCMA", 8000, 1); + std::string sdpString = munge->ToString(); + + SetLocalAnswer(sdpString); + SetRemoteAnswer(answer); +} + +TEST_F(JsepSessionTest, AddCandidateInHaveLocalOffer) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + std::string offer = CreateOffer(); + SetLocalOffer(offer); + + std::string mid; + std::string transportId; + JsepSession::Result result = mSessionOff->AddRemoteIceCandidate( + strSampleCandidate, mid, Some(nSamplelevel), "", &transportId); + ASSERT_EQ(dom::PCError::InvalidStateError, *result.mError); +} + +TEST_F(JsepSessionTest, SetLocalWithoutCreateOffer) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + + std::string offer = CreateOffer(); + JsepSession::Result result = + mSessionAns->SetLocalDescription(kJsepSdpOffer, offer); + ASSERT_EQ(dom::PCError::InvalidModificationError, *result.mError); +} + +TEST_F(JsepSessionTest, SetLocalWithoutCreateAnswer) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + + std::string offer = CreateOffer(); + SetRemoteOffer(offer); + JsepSession::Result result = + mSessionAns->SetLocalDescription(kJsepSdpAnswer, offer); + ASSERT_EQ(dom::PCError::InvalidModificationError, *result.mError); +} + +// Test for Bug 843595 +TEST_F(JsepSessionTest, missingUfrag) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + std::string offer = CreateOffer(); + std::string ufrag = "ice-ufrag"; + std::size_t pos = offer.find(ufrag); + ASSERT_NE(pos, std::string::npos); + offer.replace(pos, ufrag.length(), "ice-ufrog"); + JsepSession::Result result = + mSessionAns->SetRemoteDescription(kJsepSdpOffer, offer); + ASSERT_EQ(dom::PCError::InvalidAccessError, *result.mError); +} + +TEST_F(JsepSessionTest, AudioOnlyCalleeNoRtcpMux) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + std::string offer = CreateOffer(); + std::string rtcp_mux = "a=rtcp-mux\r\n"; + std::size_t pos = offer.find(rtcp_mux); + ASSERT_NE(pos, std::string::npos); + offer.replace(pos, rtcp_mux.length(), ""); + SetLocalOffer(offer); + SetRemoteOffer(offer); + std::string answer = CreateAnswer(); + pos = answer.find(rtcp_mux); + ASSERT_EQ(pos, std::string::npos); +} + +// This test comes from Bug 810220 +TEST_F(JsepSessionTest, AudioOnlyG711Call) { + std::string offer = + "v=0\r\n" + "o=- 1 1 IN IP4 148.147.200.251\r\n" + "s=-\r\n" + "b=AS:64\r\n" + "t=0 0\r\n" + "a=fingerprint:sha-256 F3:FA:20:C0:CD:48:C4:5F:02:5F:A5:D3:21:D0:2D:48:" + "7B:31:60:5C:5A:D8:0D:CD:78:78:6C:6D:CE:CC:0C:67\r\n" + "m=audio 9000 UDP/TLS/RTP/SAVPF 0 8 126\r\n" + "c=IN IP4 148.147.200.251\r\n" + "b=TIAS:64000\r\n" + "a=rtpmap:0 PCMU/8000\r\n" + "a=rtpmap:8 PCMA/8000\r\n" + "a=rtpmap:126 telephone-event/8000\r\n" + "a=candidate:0 1 udp 2130706432 148.147.200.251 9000 typ host\r\n" + "a=candidate:0 2 udp 2130706432 148.147.200.251 9005 typ host\r\n" + "a=ice-ufrag:cYuakxkEKH+RApYE\r\n" + "a=ice-pwd:bwtpzLZD+3jbu8vQHvEa6Xuq\r\n" + "a=setup:active\r\n" + "a=sendrecv\r\n"; + + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionAns, "audio"); + SetRemoteOffer(offer, CHECK_SUCCESS); + std::string answer = CreateAnswer(); + + // They didn't offer opus, so our answer shouldn't include it. + ASSERT_EQ(answer.find(" opus/"), std::string::npos); + + // They also didn't offer video or application + ASSERT_EQ(answer.find("video"), std::string::npos); + ASSERT_EQ(answer.find("application"), std::string::npos); + + // We should answer with PCMU and telephone-event + ASSERT_NE(answer.find(" PCMU/8000"), std::string::npos); + + // Double-check the directionality + ASSERT_NE(answer.find("\r\na=sendrecv"), std::string::npos); +} + +TEST_F(JsepSessionTest, AudioOnlyG722Only) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + std::string offer = CreateOffer(); + SetLocalOffer(offer); + + std::string audio = "m=audio 9 UDP/TLS/RTP/SAVPF 109 9 0 8 101\r\n"; + std::size_t pos = offer.find(audio); + ASSERT_NE(pos, std::string::npos); + offer.replace(pos, audio.length(), "m=audio 65375 UDP/TLS/RTP/SAVPF 9\r\n"); + SetRemoteOffer(offer); + + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + ASSERT_NE(mSessionAns->GetLocalDescription(kJsepDescriptionCurrent) + .find("UDP/TLS/RTP/SAVPF 9\r"), + std::string::npos); + ASSERT_NE(mSessionAns->GetLocalDescription(kJsepDescriptionCurrent) + .find("a=rtpmap:9 G722/8000"), + std::string::npos); +} + +TEST_F(JsepSessionTest, AudioOnlyG722Rejected) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + std::string offer = CreateOffer(); + SetLocalOffer(offer); + + std::string audio = "m=audio 9 UDP/TLS/RTP/SAVPF 109 9 0 8 101\r\n"; + std::size_t pos = offer.find(audio); + ASSERT_NE(pos, std::string::npos); + offer.replace(pos, audio.length(), "m=audio 65375 UDP/TLS/RTP/SAVPF 0 8\r\n"); + SetRemoteOffer(offer); + + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + SetRemoteAnswer(answer); + + ASSERT_NE(mSessionAns->GetLocalDescription(kJsepDescriptionCurrent) + .find("UDP/TLS/RTP/SAVPF 0 8\r"), + std::string::npos); + ASSERT_NE(mSessionAns->GetLocalDescription(kJsepDescriptionCurrent) + .find("a=rtpmap:0 PCMU/8000"), + std::string::npos); + ASSERT_EQ(mSessionAns->GetLocalDescription(kJsepDescriptionCurrent) + .find("a=rtpmap:109 opus/48000/2"), + std::string::npos); + ASSERT_EQ(mSessionAns->GetLocalDescription(kJsepDescriptionCurrent) + .find("a=rtpmap:9 G722/8000"), + std::string::npos); +} + +// This test doesn't make sense for bundle +TEST_F(JsepSessionTest, DISABLED_FullCallAudioNoMuxVideoMux) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio,video"); + AddTracks(*mSessionAns, "audio,video"); + std::string offer = CreateOffer(); + SetLocalOffer(offer); + std::string rtcp_mux = "a=rtcp-mux\r\n"; + std::size_t pos = offer.find(rtcp_mux); + ASSERT_NE(pos, std::string::npos); + offer.replace(pos, rtcp_mux.length(), ""); + SetRemoteOffer(offer); + std::string answer = CreateAnswer(); + + size_t match = mSessionAns->GetLocalDescription(kJsepDescriptionCurrent) + .find("\r\na=rtcp-mux"); + ASSERT_NE(match, std::string::npos); + match = mSessionAns->GetLocalDescription(kJsepDescriptionCurrent) + .find("\r\na=rtcp-mux", match + 1); + ASSERT_EQ(match, std::string::npos); +} + +// Disabled pending resolution of bug 818640. +// Actually, this test is completely broken; you can't just call +// SetRemote/CreateAnswer over and over again. +TEST_F(JsepSessionTest, DISABLED_OfferAllDynamicTypes) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionAns, "audio"); + + std::string offer; + for (int i = 96; i < 128; i++) { + std::stringstream ss; + ss << i; + std::cout << "Trying dynamic pt = " << i << std::endl; + offer = + "v=0\r\n" + "o=- 1 1 IN IP4 148.147.200.251\r\n" + "s=-\r\n" + "b=AS:64\r\n" + "t=0 0\r\n" + "a=fingerprint:sha-256 F3:FA:20:C0:CD:48:C4:5F:02:5F:A5:D3:21:D0:2D:48:" + "7B:31:60:5C:5A:D8:0D:CD:78:78:6C:6D:CE:CC:0C:67\r\n" + "m=audio 9000 RTP/AVP " + + ss.str() + + "\r\n" + "c=IN IP4 148.147.200.251\r\n" + "b=TIAS:64000\r\n" + "a=rtpmap:" + + ss.str() + + " opus/48000/2\r\n" + "a=candidate:0 1 udp 2130706432 148.147.200.251 9000 typ host\r\n" + "a=candidate:0 2 udp 2130706432 148.147.200.251 9005 typ host\r\n" + "a=ice-ufrag:cYuakxkEKH+RApYE\r\n" + "a=ice-pwd:bwtpzLZD+3jbu8vQHvEa6Xuq\r\n" + "a=sendrecv\r\n"; + + SetRemoteOffer(offer, CHECK_SUCCESS); + std::string answer = CreateAnswer(); + ASSERT_NE(answer.find(ss.str() + " opus/"), std::string::npos); + } +} + +TEST_F(JsepSessionTest, ipAddrAnyOffer) { + std::string offer = + "v=0\r\n" + "o=- 1 1 IN IP4 127.0.0.1\r\n" + "s=-\r\n" + "b=AS:64\r\n" + "t=0 0\r\n" + "a=fingerprint:sha-256 F3:FA:20:C0:CD:48:C4:5F:02:5F:A5:D3:21:D0:2D:48:" + "7B:31:60:5C:5A:D8:0D:CD:78:78:6C:6D:CE:CC:0C:67\r\n" + "m=audio 9000 UDP/TLS/RTP/SAVPF 99\r\n" + "c=IN IP4 0.0.0.0\r\n" + "a=rtpmap:99 opus/48000/2\r\n" + "a=ice-ufrag:cYuakxkEKH+RApYE\r\n" + "a=ice-pwd:bwtpzLZD+3jbu8vQHvEa6Xuq\r\n" + "a=setup:active\r\n" + "a=sendrecv\r\n"; + + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionAns, "audio"); + SetRemoteOffer(offer, CHECK_SUCCESS); + std::string answer = CreateAnswer(); + + ASSERT_NE(answer.find("a=sendrecv"), std::string::npos); +} + +static void CreateSDPForBigOTests(std::string& offer, + const std::string& number) { + offer = + "v=0\r\n" + "o=- "; + offer += number; + offer += " "; + offer += number; + offer += + " IN IP4 127.0.0.1\r\n" + "s=-\r\n" + "b=AS:64\r\n" + "t=0 0\r\n" + "a=fingerprint:sha-256 F3:FA:20:C0:CD:48:C4:5F:02:5F:A5:D3:21:D0:2D:48:" + "7B:31:60:5C:5A:D8:0D:CD:78:78:6C:6D:CE:CC:0C:67\r\n" + "m=audio 9000 RTP/AVP 99\r\n" + "c=IN IP4 0.0.0.0\r\n" + "a=rtpmap:99 opus/48000/2\r\n" + "a=ice-ufrag:cYuakxkEKH+RApYE\r\n" + "a=ice-pwd:bwtpzLZD+3jbu8vQHvEa6Xuq\r\n" + "a=setup:active\r\n" + "a=sendrecv\r\n"; +} + +TEST_F(JsepSessionTest, BigOValues) { + std::string offer; + + CreateSDPForBigOTests(offer, "12345678901234567"); + + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionAns, "audio"); + SetRemoteOffer(offer, CHECK_SUCCESS); +} + +TEST_F(JsepSessionTest, BigOValuesExtraChars) { + std::string offer; + + CreateSDPForBigOTests(offer, "12345678901234567FOOBAR"); + + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionAns, "audio"); + // The signaling state will remain "stable" because the unparsable + // SDP leads to a failure in SetRemoteDescription. + SetRemoteOffer(offer, NO_CHECKS); + ASSERT_EQ(kJsepStateStable, mSessionAns->GetState()); +} + +TEST_F(JsepSessionTest, BigOValuesTooBig) { + std::string offer; + + CreateSDPForBigOTests(offer, "18446744073709551615"); + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionAns, "audio"); + + // The signaling state will remain "stable" because the unparsable + // SDP leads to a failure in SetRemoteDescription. + SetRemoteOffer(offer, NO_CHECKS); + ASSERT_EQ(kJsepStateStable, mSessionAns->GetState()); +} + +TEST_F(JsepSessionTest, SetLocalAnswerInStable) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + std::string offer = CreateOffer(); + + // The signaling state will remain "stable" because the + // SetLocalDescription call fails. + SetLocalAnswer(offer, NO_CHECKS); + ASSERT_EQ(kJsepStateStable, mSessionOff->GetState()); +} + +TEST_F(JsepSessionTest, SetRemoteAnswerInStable) { + const std::string answer = + "v=0\r\n" + "o=Mozilla-SIPUA 4949 0 IN IP4 10.86.255.143\r\n" + "s=SIP Call\r\n" + "t=0 0\r\n" + "a=ice-ufrag:qkEP\r\n" + "a=ice-pwd:ed6f9GuHjLcoCN6sC/Eh7fVl\r\n" + "m=audio 16384 RTP/AVP 0 8 9 101\r\n" + "c=IN IP4 10.86.255.143\r\n" + "a=rtpmap:0 PCMU/8000\r\n" + "a=rtpmap:8 PCMA/8000\r\n" + "a=rtpmap:9 G722/8000\r\n" + "a=rtpmap:101 telephone-event/8000\r\n" + "a=fmtp:101 0-15\r\n" + "a=sendrecv\r\n" + "a=candidate:1 1 UDP 2130706431 192.168.2.1 50005 typ host\r\n" + "a=candidate:2 2 UDP 2130706431 192.168.2.2 50006 typ host\r\n" + "m=video 1024 RTP/AVP 97\r\n" + "c=IN IP4 10.86.255.143\r\n" + "a=rtpmap:120 VP8/90000\r\n" + "a=fmtp:97 profile-level-id=42E00C\r\n" + "a=sendrecv\r\n" + "a=candidate:1 1 UDP 2130706431 192.168.2.3 50007 typ host\r\n" + "a=candidate:2 2 UDP 2130706431 192.168.2.4 50008 typ host\r\n"; + + // The signaling state will remain "stable" because the + // SetRemoteDescription call fails. + JsepSession::Result result = + mSessionOff->SetRemoteDescription(kJsepSdpAnswer, answer); + ASSERT_EQ(dom::PCError::InvalidStateError, *result.mError); + ASSERT_EQ(kJsepStateStable, mSessionOff->GetState()); +} + +TEST_F(JsepSessionTest, SetLocalAnswerInHaveLocalOffer) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + std::string offer = CreateOffer(); + + SetLocalOffer(offer); + ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionOff->GetState()); + + // The signaling state will remain "have-local-offer" because the + // SetLocalDescription call fails. + JsepSession::Result result = + mSessionOff->SetLocalDescription(kJsepSdpAnswer, offer); + ASSERT_EQ(dom::PCError::InvalidModificationError, *result.mError); + ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionOff->GetState()); +} + +TEST_F(JsepSessionTest, SetRemoteOfferInHaveLocalOffer) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + std::string offer = CreateOffer(); + + SetLocalOffer(offer); + ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionOff->GetState()); + + // The signaling state will remain "have-local-offer" because the + // SetRemoteDescription call fails. + JsepSession::Result result = + mSessionOff->SetRemoteDescription(kJsepSdpOffer, offer); + ASSERT_EQ(dom::PCError::InvalidStateError, *result.mError); + ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionOff->GetState()); +} + +TEST_F(JsepSessionTest, SetLocalOfferInHaveRemoteOffer) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + std::string offer = CreateOffer(); + + SetRemoteOffer(offer); + ASSERT_EQ(kJsepStateHaveRemoteOffer, mSessionAns->GetState()); + + // The signaling state will remain "have-remote-offer" because the + // SetLocalDescription call fails. + JsepSession::Result result = + mSessionAns->SetLocalDescription(kJsepSdpOffer, offer); + ASSERT_EQ(dom::PCError::InvalidModificationError, *result.mError); + ASSERT_EQ(kJsepStateHaveRemoteOffer, mSessionAns->GetState()); +} + +TEST_F(JsepSessionTest, SetRemoteAnswerInHaveRemoteOffer) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + std::string offer = CreateOffer(); + + SetRemoteOffer(offer); + ASSERT_EQ(kJsepStateHaveRemoteOffer, mSessionAns->GetState()); + + // The signaling state will remain "have-remote-offer" because the + // SetRemoteDescription call fails. + JsepSession::Result result = + mSessionAns->SetRemoteDescription(kJsepSdpAnswer, offer); + ASSERT_EQ(dom::PCError::InvalidStateError, *result.mError); + + ASSERT_EQ(kJsepStateHaveRemoteOffer, mSessionAns->GetState()); +} + +TEST_F(JsepSessionTest, RtcpFbInOffer) { + types.push_back(SdpMediaSection::kAudio); + types.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionOff, "audio,video"); + std::string offer = CreateOffer(); + + std::map<std::string, bool> expected; + expected["nack"] = false; + expected["nack pli"] = false; + expected["ccm fir"] = false; + + size_t prev = 0; + size_t found = 0; + for (;;) { + found = offer.find('\n', found + 1); + if (found == std::string::npos) break; + + std::string line = offer.substr(prev, (found - prev)); + + // ensure no other rtcp-fb values are present + if (line.find("a=rtcp-fb:") != std::string::npos) { + size_t space = line.find(' '); + // strip trailing \r\n + std::string value = line.substr(space + 1, line.length() - space - 2); + std::map<std::string, bool>::iterator entry = expected.find(value); + ASSERT_NE(entry, expected.end()); + entry->second = true; + } + + prev = found + 1; + } + + // ensure all values are present + for (std::map<std::string, bool>::iterator it = expected.begin(); + it != expected.end(); ++it) { + ASSERT_EQ(it->second, true); + } +} + +// In this test we will change the offer SDP's a=setup value +// from actpass to passive. This will force the answer to do active. +TEST_F(JsepSessionTest, AudioCallForceDtlsRoles) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + std::string offer = CreateOffer(); + + std::string actpass = "\r\na=setup:actpass"; + size_t match = offer.find(actpass); + ASSERT_NE(match, std::string::npos); + offer.replace(match, actpass.length(), "\r\na=setup:passive"); + + SetLocalOffer(offer); + SetRemoteOffer(offer); + ASSERT_EQ(kJsepStateHaveRemoteOffer, mSessionAns->GetState()); + std::string answer = CreateAnswer(); + match = answer.find("\r\na=setup:active"); + ASSERT_NE(match, std::string::npos); + + SetLocalAnswer(answer); + SetRemoteAnswer(answer); + ASSERT_EQ(kJsepStateStable, mSessionAns->GetState()); +} + +// In this test we will change the offer SDP's a=setup value +// from actpass to active. This will force the answer to do passive. +TEST_F(JsepSessionTest, AudioCallReverseDtlsRoles) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + std::string offer = CreateOffer(); + + std::string actpass = "\r\na=setup:actpass"; + size_t match = offer.find(actpass); + ASSERT_NE(match, std::string::npos); + offer.replace(match, actpass.length(), "\r\na=setup:active"); + + SetLocalOffer(offer); + SetRemoteOffer(offer); + ASSERT_EQ(kJsepStateHaveRemoteOffer, mSessionAns->GetState()); + std::string answer = CreateAnswer(); + match = answer.find("\r\na=setup:passive"); + ASSERT_NE(match, std::string::npos); + + SetLocalAnswer(answer); + SetRemoteAnswer(answer); + ASSERT_EQ(kJsepStateStable, mSessionAns->GetState()); +} + +// In this test we will change the answer SDP's a=setup value +// from active to passive. This will make both sides do +// active and should not connect. +TEST_F(JsepSessionTest, AudioCallMismatchDtlsRoles) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + std::string offer = CreateOffer(); + + std::string actpass = "\r\na=setup:actpass"; + size_t match = offer.find(actpass); + ASSERT_NE(match, std::string::npos); + SetLocalOffer(offer); + SetRemoteOffer(offer); + ASSERT_EQ(kJsepStateHaveRemoteOffer, mSessionAns->GetState()); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + + std::string active = "\r\na=setup:active"; + match = answer.find(active); + ASSERT_NE(match, std::string::npos); + answer.replace(match, active.length(), "\r\na=setup:passive"); + SetRemoteAnswer(answer); + + // This is as good as it gets in a JSEP test (w/o starting DTLS) + ASSERT_EQ(JsepDtlsTransport::kJsepDtlsClient, + GetTransceivers(*mSessionOff)[0].mTransport.mDtls->GetRole()); + ASSERT_EQ(JsepDtlsTransport::kJsepDtlsClient, + GetTransceivers(*mSessionAns)[0].mTransport.mDtls->GetRole()); +} + +// Verify that missing a=setup in offer gets rejected +TEST_F(JsepSessionTest, AudioCallOffererNoSetup) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + std::string offer = CreateOffer(); + SetLocalOffer(offer); + + std::string actpass = "\r\na=setup:actpass"; + size_t match = offer.find(actpass); + ASSERT_NE(match, std::string::npos); + offer.replace(match, actpass.length(), ""); + + // The signaling state will remain "stable" because the unparsable + // SDP leads to a failure in SetRemoteDescription. + SetRemoteOffer(offer, NO_CHECKS); + ASSERT_EQ(kJsepStateStable, mSessionAns->GetState()); + ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionOff->GetState()); +} + +// In this test we will change the answer SDP to remove the +// a=setup line, which results in active being assumed. +TEST_F(JsepSessionTest, AudioCallAnswerNoSetup) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + std::string offer = CreateOffer(); + size_t match = offer.find("\r\na=setup:actpass"); + ASSERT_NE(match, std::string::npos); + + SetLocalOffer(offer); + SetRemoteOffer(offer); + ASSERT_EQ(kJsepStateHaveRemoteOffer, mSessionAns->GetState()); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + + std::string active = "\r\na=setup:active"; + match = answer.find(active); + ASSERT_NE(match, std::string::npos); + answer.replace(match, active.length(), ""); + SetRemoteAnswer(answer); + ASSERT_EQ(kJsepStateStable, mSessionAns->GetState()); + + // This is as good as it gets in a JSEP test (w/o starting DTLS) + ASSERT_EQ(JsepDtlsTransport::kJsepDtlsServer, + GetTransceivers(*mSessionOff)[0].mTransport.mDtls->GetRole()); + ASSERT_EQ(JsepDtlsTransport::kJsepDtlsClient, + GetTransceivers(*mSessionAns)[0].mTransport.mDtls->GetRole()); +} + +// Verify that 'holdconn' gets rejected +TEST_F(JsepSessionTest, AudioCallDtlsRoleHoldconn) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + std::string offer = CreateOffer(); + SetLocalOffer(offer); + + std::string actpass = "\r\na=setup:actpass"; + size_t match = offer.find(actpass); + ASSERT_NE(match, std::string::npos); + offer.replace(match, actpass.length(), "\r\na=setup:holdconn"); + + // The signaling state will remain "stable" because the unparsable + // SDP leads to a failure in SetRemoteDescription. + SetRemoteOffer(offer, NO_CHECKS); + ASSERT_EQ(kJsepStateStable, mSessionAns->GetState()); + ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionOff->GetState()); +} + +// Verify that 'actpass' in answer gets rejected +TEST_F(JsepSessionTest, AudioCallAnswererUsesActpass) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + std::string offer = CreateOffer(); + SetLocalOffer(offer); + SetRemoteOffer(offer); + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + + std::string active = "\r\na=setup:active"; + size_t match = answer.find(active); + ASSERT_NE(match, std::string::npos); + answer.replace(match, active.length(), "\r\na=setup:actpass"); + + // The signaling state will remain "stable" because the unparsable + // SDP leads to a failure in SetRemoteDescription. + SetRemoteAnswer(answer, NO_CHECKS); + ASSERT_EQ(kJsepStateStable, mSessionAns->GetState()); + ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionOff->GetState()); +} + +// Verify that 'actpass' in reoffer from previous answerer doesn't result +// in a role switch. +TEST_F(JsepSessionTest, AudioCallPreviousAnswererUsesActpassInReoffer) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + + OfferAnswer(); + + ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); + ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive); + + SwapOfferAnswerRoles(); + + OfferAnswer(); + + ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); + ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kPassive); +} + +// Disabled: See Bug 1329028 +TEST_F(JsepSessionTest, DISABLED_AudioCallOffererAttemptsSetupRoleSwitch) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + + OfferAnswer(); + + ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); + ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive); + + std::string reoffer = CreateOffer(); + SetLocalOffer(reoffer); + + std::string actpass = "\r\na=setup:actpass"; + size_t match = reoffer.find(actpass); + ASSERT_NE(match, std::string::npos); + reoffer.replace(match, actpass.length(), "\r\na=setup:active"); + + // This is expected to fail. + SetRemoteOffer(reoffer, NO_CHECKS); + ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionOff->GetState()); + ASSERT_EQ(kJsepStateStable, mSessionAns->GetState()); +} + +// Disabled: See Bug 1329028 +TEST_F(JsepSessionTest, DISABLED_AudioCallAnswererAttemptsSetupRoleSwitch) { + types.push_back(SdpMediaSection::kAudio); + AddTracks(*mSessionOff, "audio"); + AddTracks(*mSessionAns, "audio"); + + OfferAnswer(); + + ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass); + ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive); + + std::string reoffer = CreateOffer(); + SetLocalOffer(reoffer); + SetRemoteOffer(reoffer); + + std::string reanswer = CreateAnswer(); + SetLocalAnswer(reanswer); + + std::string actpass = "\r\na=setup:active"; + size_t match = reanswer.find(actpass); + ASSERT_NE(match, std::string::npos); + reanswer.replace(match, actpass.length(), "\r\na=setup:passive"); + + // This is expected to fail. + SetRemoteAnswer(reanswer, NO_CHECKS); + ASSERT_EQ(kJsepStateHaveLocalOffer, mSessionOff->GetState()); + ASSERT_EQ(kJsepStateStable, mSessionAns->GetState()); +} + +// Remove H.264 P1 and VP8 from offer, check answer negotiates H.264 P0 +TEST_F(JsepSessionTest, OfferWithOnlyH264P0) { + for (auto& codec : mSessionOff->Codecs()) { + if (codec->mName != "H264" || codec->mDefaultPt == "126") { + codec->mEnabled = false; + } + } + + types.push_back(SdpMediaSection::kAudio); + types.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionOff, "audio,video"); + AddTracks(*mSessionAns, "audio,video"); + std::string offer = CreateOffer(); + + ASSERT_EQ(offer.find("a=rtpmap:126 H264/90000"), std::string::npos); + ASSERT_EQ(offer.find("a=rtpmap:120 VP8/90000"), std::string::npos); + + SetLocalOffer(offer); + SetRemoteOffer(offer); + std::string answer = CreateAnswer(); + size_t match = answer.find("\r\na=setup:active"); + ASSERT_NE(match, std::string::npos); + + // validate answer SDP + ASSERT_NE(answer.find("a=rtpmap:97 H264/90000"), std::string::npos); + ASSERT_NE(answer.find("a=rtcp-fb:97 nack"), std::string::npos); + ASSERT_NE(answer.find("a=rtcp-fb:97 nack pli"), std::string::npos); + ASSERT_NE(answer.find("a=rtcp-fb:97 ccm fir"), std::string::npos); + // Ensure VP8 and P1 removed + ASSERT_EQ(answer.find("a=rtpmap:126 H264/90000"), std::string::npos); + ASSERT_EQ(answer.find("a=rtpmap:120 VP8/90000"), std::string::npos); + ASSERT_EQ(answer.find("a=rtcp-fb:120"), std::string::npos); + ASSERT_EQ(answer.find("a=rtcp-fb:126"), std::string::npos); +} + +// Test negotiating an answer which has only H.264 P1 +// Which means replace VP8 with H.264 P1 in answer +TEST_F(JsepSessionTest, AnswerWithoutVP8) { + types.push_back(SdpMediaSection::kAudio); + types.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionOff, "audio,video"); + AddTracks(*mSessionAns, "audio,video"); + std::string offer = CreateOffer(); + SetLocalOffer(offer); + SetRemoteOffer(offer); + + for (auto& codec : mSessionOff->Codecs()) { + if (codec->mName != "H264" || codec->mDefaultPt == "126") { + codec->mEnabled = false; + } + } + + std::string answer = CreateAnswer(); + + SetLocalAnswer(answer); + SetRemoteAnswer(answer); +} + +// Ok. Hear me out. +// The JSEP spec specifies very different behavior for the following two cases: +// 1. AddTrack caused a transceiver to be created. +// 2. The transceiver was not created as a side-effect of AddTrack. +// +// All together now... +// +// SADFACE :( +// +// Ok, enough of that. The upshot is we need to test two different codepaths for +// the same thing here. Most of this unit-test suite tests the "magic" case +// (case 1 above). Case 2 (the non-magic case) is simpler, so we have just a +// handful of tests. +TEST_F(JsepSessionTest, OffererNoAddTrackMagic) { + types = BuildTypes("audio,video"); + AddTracks(*mSessionOff, NO_ADDTRACK_MAGIC); + AddTracks(*mSessionAns); + + // Offerer's transceivers aren't "magic"; they will not associate with the + // remote side's m-sections automatically. But, since they went into the + // offer, everything works normally. + OfferAnswer(); + + ASSERT_EQ(2U, GetTransceivers(*mSessionOff).size()); + ASSERT_EQ(2U, GetTransceivers(*mSessionAns).size()); +} + +TEST_F(JsepSessionTest, AnswererNoAddTrackMagic) { + types = BuildTypes("audio,video"); + AddTracks(*mSessionOff); + AddTracks(*mSessionAns, NO_ADDTRACK_MAGIC); + + OfferAnswer(CHECK_SUCCESS); + + ASSERT_EQ(2U, GetTransceivers(*mSessionOff).size()); + // Since answerer's transceivers aren't "magic", they cannot automatically be + // attached to the offerer's m-sections. + ASSERT_EQ(4U, GetTransceivers(*mSessionAns).size()); + + SwapOfferAnswerRoles(); + + OfferAnswer(CHECK_SUCCESS); + ASSERT_EQ(4U, GetTransceivers(*mSessionOff).size()); + ASSERT_EQ(4U, GetTransceivers(*mSessionAns).size()); +} + +// JSEP has rules about when a disabled m-section can be reused; the gist is +// that the m-section has to be negotiated disabled, then it becomes a candidate +// for reuse on the next renegotiation. Stopping a transceiver does not allow +// you to reuse on the next negotiation. +TEST_F(JsepSessionTest, OffererRecycle) { + types = BuildTypes("audio,video"); + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + + OfferAnswer(); + + ASSERT_EQ(2U, GetTransceivers(*mSessionOff).size()); + ASSERT_EQ(2U, GetTransceivers(*mSessionAns).size()); + GetTransceivers(*mSessionOff)[0].Stop(); + AddTracks(*mSessionOff, "audio"); + ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); + + OfferAnswer(CHECK_SUCCESS); + + // It is too soon to recycle msection 0, so the new track should have been + // given a new msection. + ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); + ASSERT_EQ(0U, GetTransceivers(*mSessionOff)[0].GetLevel()); + ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsStopped()); + ASSERT_EQ(1U, GetTransceivers(*mSessionOff)[1].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[1].IsStopped()); + ASSERT_EQ(2U, GetTransceivers(*mSessionOff)[2].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsStopped()); + + ASSERT_EQ(3U, GetTransceivers(*mSessionAns).size()); + ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[0].GetLevel()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsStopped()); + ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); + ASSERT_EQ(2U, GetTransceivers(*mSessionAns)[2].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); + + UniquePtr<Sdp> offer = GetParsedLocalDescription(*mSessionOff); + ASSERT_EQ(3U, offer->GetMediaSectionCount()); + ValidateDisabledMSection(&offer->GetMediaSection(0)); + + UniquePtr<Sdp> answer = GetParsedLocalDescription(*mSessionAns); + ASSERT_EQ(3U, answer->GetMediaSectionCount()); + ValidateDisabledMSection(&answer->GetMediaSection(0)); + + // Ok. Now renegotiating should recycle m-section 0. + AddTracks(*mSessionOff, "audio"); + ASSERT_EQ(4U, GetTransceivers(*mSessionOff).size()); + OfferAnswer(CHECK_SUCCESS); + + // Transceiver 3 should now be attached to m-section 0 + ASSERT_EQ(4U, GetTransceivers(*mSessionOff).size()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].HasLevel()); + ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsStopped()); + ASSERT_EQ(1U, GetTransceivers(*mSessionOff)[1].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[1].IsStopped()); + ASSERT_EQ(2U, GetTransceivers(*mSessionOff)[2].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsStopped()); + ASSERT_EQ(0U, GetTransceivers(*mSessionOff)[3].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[3].IsStopped()); + + ASSERT_EQ(4U, GetTransceivers(*mSessionAns).size()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].HasLevel()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsStopped()); + ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); + ASSERT_EQ(2U, GetTransceivers(*mSessionAns)[2].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); + ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[3].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[3].IsStopped()); +} + +TEST_F(JsepSessionTest, RecycleAnswererStopsTransceiver) { + types = BuildTypes("audio,video"); + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + + OfferAnswer(); + + ASSERT_EQ(2U, GetTransceivers(*mSessionOff).size()); + ASSERT_EQ(2U, GetTransceivers(*mSessionAns).size()); + GetTransceivers(*mSessionAns)[0].Stop(); + + OfferAnswer(CHECK_SUCCESS); + + ASSERT_EQ(2U, GetTransceivers(*mSessionOff).size()); + ASSERT_EQ(0U, GetTransceivers(*mSessionOff)[0].GetLevel()); + ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsStopped()); + ASSERT_EQ(1U, GetTransceivers(*mSessionOff)[1].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[1].IsStopped()); + + ASSERT_EQ(2U, GetTransceivers(*mSessionAns).size()); + ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[0].GetLevel()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsStopped()); + ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); + + UniquePtr<Sdp> offer = GetParsedLocalDescription(*mSessionOff); + ASSERT_EQ(2U, offer->GetMediaSectionCount()); + + UniquePtr<Sdp> answer = GetParsedLocalDescription(*mSessionAns); + ASSERT_EQ(2U, answer->GetMediaSectionCount()); + ValidateDisabledMSection(&answer->GetMediaSection(0)); + + // Renegotiating should recycle m-section 0. + AddTracks(*mSessionOff, "audio"); + ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); + OfferAnswer(CHECK_SUCCESS); + + // Transceiver 3 should now be attached to m-section 0 + ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].HasLevel()); + ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsStopped()); + ASSERT_EQ(1U, GetTransceivers(*mSessionOff)[1].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[1].IsStopped()); + ASSERT_EQ(0U, GetTransceivers(*mSessionOff)[2].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsStopped()); + + ASSERT_EQ(3U, GetTransceivers(*mSessionAns).size()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].HasLevel()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsStopped()); + ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); + ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[2].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); +} + +// TODO: Have a test where offerer stops, and answerer adds a track and reoffers +// once Nils' role swap code lands. + +// TODO: Have a test where answerer stops and adds a track. + +TEST_F(JsepSessionTest, OffererRecycleNoMagic) { + types = BuildTypes("audio,video"); + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + + OfferAnswer(); + + ASSERT_EQ(2U, GetTransceivers(*mSessionOff).size()); + ASSERT_EQ(2U, GetTransceivers(*mSessionAns).size()); + GetTransceivers(*mSessionOff)[0].Stop(); + + OfferAnswer(CHECK_SUCCESS); + + // Ok. Now renegotiating should recycle m-section 0. + AddTracks(*mSessionOff, "audio", NO_ADDTRACK_MAGIC); + ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); + OfferAnswer(CHECK_SUCCESS); + + // Transceiver 2 should now be attached to m-section 0 + ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].HasLevel()); + ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsStopped()); + ASSERT_EQ(1U, GetTransceivers(*mSessionOff)[1].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[1].IsStopped()); + ASSERT_EQ(0U, GetTransceivers(*mSessionOff)[2].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsStopped()); + + ASSERT_EQ(3U, GetTransceivers(*mSessionAns).size()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].HasLevel()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsStopped()); + ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); + ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[2].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); +} + +TEST_F(JsepSessionTest, OffererRecycleNoMagicAnswererStopsTransceiver) { + types = BuildTypes("audio,video"); + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + + OfferAnswer(); + + ASSERT_EQ(2U, GetTransceivers(*mSessionOff).size()); + ASSERT_EQ(2U, GetTransceivers(*mSessionAns).size()); + GetTransceivers(*mSessionAns)[0].Stop(); + + OfferAnswer(CHECK_SUCCESS); + + // Ok. Now renegotiating should recycle m-section 0. + AddTracks(*mSessionOff, "audio", NO_ADDTRACK_MAGIC); + ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); + OfferAnswer(CHECK_SUCCESS); + + // Transceiver 2 should now be attached to m-section 0 + ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].HasLevel()); + ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsStopped()); + ASSERT_EQ(1U, GetTransceivers(*mSessionOff)[1].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[1].IsStopped()); + ASSERT_EQ(0U, GetTransceivers(*mSessionOff)[2].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsStopped()); + + ASSERT_EQ(3U, GetTransceivers(*mSessionAns).size()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].HasLevel()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsStopped()); + ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); + ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[2].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); +} + +TEST_F(JsepSessionTest, RecycleRollback) { + types = BuildTypes("audio,video"); + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + + OfferAnswer(); + + ASSERT_EQ(2U, GetTransceivers(*mSessionOff).size()); + ASSERT_EQ(2U, GetTransceivers(*mSessionAns).size()); + GetTransceivers(*mSessionOff)[0].Stop(); + + OfferAnswer(CHECK_SUCCESS); + + AddTracks(*mSessionOff, "audio"); + + ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); + ASSERT_EQ(0U, GetTransceivers(*mSessionOff)[0].GetLevel()); + ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsStopped()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].IsAssociated()); + ASSERT_EQ(1U, GetTransceivers(*mSessionOff)[1].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[1].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionOff)[1].IsAssociated()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].HasLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsStopped()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsAssociated()); + + std::string offer = CreateOffer(); + ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].HasLevel()); + ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsStopped()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].IsAssociated()); + ASSERT_EQ(1U, GetTransceivers(*mSessionOff)[1].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[1].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionOff)[1].IsAssociated()); + ASSERT_EQ(0U, GetTransceivers(*mSessionOff)[2].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsStopped()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsAssociated()); + + SetLocalOffer(offer, CHECK_SUCCESS); + + ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].HasLevel()); + ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsStopped()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].IsAssociated()); + ASSERT_EQ(1U, GetTransceivers(*mSessionOff)[1].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[1].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionOff)[1].IsAssociated()); + ASSERT_EQ(0U, GetTransceivers(*mSessionOff)[2].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsStopped()); + // This should now be associated + ASSERT_TRUE(GetTransceivers(*mSessionOff)[2].IsAssociated()); + + ASSERT_FALSE( + mSessionOff->SetLocalDescription(kJsepSdpRollback, "").mError.isSome()); + + // Rollback should not change the levels of any of these, since those are set + // in CreateOffer. + ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].HasLevel()); + ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsStopped()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].IsAssociated()); + ASSERT_EQ(1U, GetTransceivers(*mSessionOff)[1].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[1].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionOff)[1].IsAssociated()); + ASSERT_EQ(0U, GetTransceivers(*mSessionOff)[2].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsStopped()); + // This should no longer be associated + ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsAssociated()); +} + +TEST_F(JsepSessionTest, AddTrackMagicWithNullReplaceTrack) { + types = BuildTypes("audio,video"); + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + + OfferAnswer(); + + ASSERT_EQ(2U, GetTransceivers(*mSessionOff).size()); + ASSERT_EQ(2U, GetTransceivers(*mSessionAns).size()); + + AddTracks(*mSessionAns, "audio"); + AddTracks(*mSessionOff, "audio"); + + ASSERT_EQ(3U, GetTransceivers(*mSessionAns).size()); + ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[0].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsAssociated()); + ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[1].IsAssociated()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsAssociated()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[2].HasAddTrackMagic()); + + // Ok, transceiver 2 is "magical". Ensure it still has this "magical" + // auto-matching property even if we null it out with replaceTrack. + GetTransceivers(*mSessionAns)[2].mSendTrack.ClearStreamIds(); + GetTransceivers(*mSessionAns)[2].mJsDirection = + SdpDirectionAttribute::Direction::kRecvonly; + + OfferAnswer(CHECK_SUCCESS); + + ASSERT_EQ(3U, GetTransceivers(*mSessionAns).size()); + ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[0].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsAssociated()); + ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[1].IsAssociated()); + ASSERT_EQ(2U, GetTransceivers(*mSessionAns)[2].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[2].IsAssociated()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[2].HasAddTrackMagic()); + + ASSERT_EQ(3U, GetTransceivers(*mSessionOff).size()); + ASSERT_EQ(0U, GetTransceivers(*mSessionOff)[0].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsAssociated()); + ASSERT_EQ(1U, GetTransceivers(*mSessionOff)[1].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[1].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionOff)[1].IsAssociated()); + ASSERT_EQ(2U, GetTransceivers(*mSessionOff)[2].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[2].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionOff)[2].IsAssociated()); + ASSERT_TRUE(GetTransceivers(*mSessionOff)[2].HasAddTrackMagic()); +} + +// Flipside of AddTrackMagicWithNullReplaceTrack; we want to check that +// auto-matching does not work for transceivers that were created without a +// track, but were later given a track with replaceTrack. +TEST_F(JsepSessionTest, NoAddTrackMagicReplaceTrack) { + types = BuildTypes("audio,video"); + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + + OfferAnswer(); + + ASSERT_EQ(2U, GetTransceivers(*mSessionOff).size()); + ASSERT_EQ(2U, GetTransceivers(*mSessionAns).size()); + AddTracks(*mSessionOff, "audio"); + mSessionAns->AddTransceiver( + JsepTransceiver(SdpMediaSection::MediaType::kAudio, mUuidGen)); + + GetTransceivers(*mSessionAns)[2].mSendTrack.UpdateStreamIds({"newstream"}); + + ASSERT_EQ(3U, GetTransceivers(*mSessionAns).size()); + ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[0].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsAssociated()); + ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[1].IsAssociated()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsAssociated()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasAddTrackMagic()); + + OfferAnswer(CHECK_SUCCESS); + + ASSERT_EQ(4U, GetTransceivers(*mSessionAns).size()); + ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[0].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsAssociated()); + ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[1].IsAssociated()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsAssociated()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasAddTrackMagic()); + ASSERT_EQ(2U, GetTransceivers(*mSessionAns)[3].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[3].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[3].IsAssociated()); +} + +TEST_F(JsepSessionTest, AddTrackDoesNotMakeTransceiverMagical) { + types = BuildTypes("audio,video"); + AddTracks(*mSessionOff); + AddTracks(*mSessionAns); + + OfferAnswer(); + + ASSERT_EQ(2U, GetTransceivers(*mSessionOff).size()); + ASSERT_EQ(2U, GetTransceivers(*mSessionAns).size()); + AddTracks(*mSessionOff, "audio"); + mSessionAns->AddTransceiver( + JsepTransceiver(SdpMediaSection::MediaType::kAudio, mUuidGen)); + + ASSERT_EQ(3U, GetTransceivers(*mSessionAns).size()); + ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[0].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsAssociated()); + ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[1].IsAssociated()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsAssociated()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasAddTrackMagic()); + + // This attaches a track to the third transceiver, but does _not_ set the + // addTrack magic bit, meaning it will not auto-pair with the track added + // to the offerer. + AddTracks(*mSessionAns, "audio"); + + ASSERT_EQ(3U, GetTransceivers(*mSessionAns).size()); + ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[0].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsAssociated()); + ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[1].IsAssociated()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsAssociated()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasAddTrackMagic()); + + OfferAnswer(CHECK_SUCCESS); + + // The offer's new transceiver does not pair up with the transceiver we added + ASSERT_EQ(4U, GetTransceivers(*mSessionAns).size()); + ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[0].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsAssociated()); + ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[1].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[1].IsAssociated()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsAssociated()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasAddTrackMagic()); + ASSERT_EQ(2U, GetTransceivers(*mSessionAns)[3].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[3].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[3].IsAssociated()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[3].HasAddTrackMagic()); +} + +TEST_F(JsepSessionTest, ComplicatedRemoteRollback) { + AddTracks(*mSessionOff, "audio,audio,audio,video"); + AddTracks(*mSessionAns, "video,video"); + + std::string offer = CreateOffer(); + SetLocalOffer(offer, CHECK_SUCCESS); + SetRemoteOffer(offer, CHECK_SUCCESS); + + // Three recvonly for audio, one sendrecv for video, and one (unmapped) for + // the second video track. + ASSERT_EQ(5U, GetTransceivers(*mSessionAns).size()); + // First video transceiver; auto matched with offer + ASSERT_EQ(3U, GetTransceivers(*mSessionAns)[0].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].IsAssociated()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].HasAddTrackMagic()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].OnlyExistsBecauseOfSetRemote()); + + // Second video transceiver, not matched with offer + ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].HasLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsAssociated()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[1].HasAddTrackMagic()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].OnlyExistsBecauseOfSetRemote()); + + // Audio transceiver, created due to application of SetRemote + ASSERT_EQ(0U, GetTransceivers(*mSessionAns)[2].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[2].IsAssociated()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasAddTrackMagic()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[2].OnlyExistsBecauseOfSetRemote()); + + // Audio transceiver, created due to application of SetRemote + ASSERT_EQ(1U, GetTransceivers(*mSessionAns)[3].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[3].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[3].IsAssociated()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[3].HasAddTrackMagic()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[3].OnlyExistsBecauseOfSetRemote()); + + // Audio transceiver, created due to application of SetRemote + ASSERT_EQ(2U, GetTransceivers(*mSessionAns)[4].GetLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[4].IsStopped()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[4].IsAssociated()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[4].HasAddTrackMagic()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[4].OnlyExistsBecauseOfSetRemote()); + + // This will prevent rollback from eating this transceiver, even though we + // call replaceTrack(null) on it. + AddTracks(*mSessionAns, "audio"); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasAddTrackMagic()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].OnlyExistsBecauseOfSetRemote()); + GetTransceivers(*mSessionAns)[2].mSendTrack.ClearStreamIds(); + GetTransceivers(*mSessionAns)[2].mJsDirection = + SdpDirectionAttribute::Direction::kRecvonly; + + // We do nothing with the second audio transceiver; when we rollback, it will + // be marked as removed. + + // This will not cause the third audio transceiver to stick around; having a + // track is _not_ enough to preserve it. It must have addTrack "magic"! + GetTransceivers(*mSessionAns)[4].mSendTrack.UpdateStreamIds({"newstream"}); + + // Create a fourth audio transceiver. Rollback will leave it alone, since we + // created it. + mSessionAns->AddTransceiver( + JsepTransceiver(SdpMediaSection::MediaType::kAudio, mUuidGen, + SdpDirectionAttribute::Direction::kRecvonly)); + + ASSERT_FALSE( + mSessionAns->SetRemoteDescription(kJsepSdpRollback, "").mError.isSome()); + + // Two of these (3 and 4) will be marked removed, if this all worked + ASSERT_EQ(6U, GetTransceivers(*mSessionAns).size()); + + // First video transceiver + ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].HasLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].IsStopped()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].IsAssociated()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[0].HasAddTrackMagic()); + ASSERT_FALSE(IsNull(GetTransceivers(*mSessionAns)[0].mSendTrack)); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[0].IsRemoved()); + + // Second video transceiver + ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].HasLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsStopped()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsAssociated()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[1].HasAddTrackMagic()); + ASSERT_FALSE(IsNull(GetTransceivers(*mSessionAns)[1].mSendTrack)); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[1].IsRemoved()); + + // First audio transceiver, kept because AddTrack touched it, even though we + // removed the send track after. + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsStopped()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsAssociated()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].HasAddTrackMagic()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].OnlyExistsBecauseOfSetRemote()); + ASSERT_TRUE(IsNull(GetTransceivers(*mSessionAns)[2].mSendTrack)); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[2].IsRemoved()); + + // Second audio transceiver should be gone. + ASSERT_FALSE(GetTransceivers(*mSessionAns)[3].HasLevel()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[3].IsStopped()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[3].IsAssociated()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[3].HasAddTrackMagic()); + ASSERT_TRUE(IsNull(GetTransceivers(*mSessionAns)[3].mSendTrack)); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[3].IsRemoved()); + + // Third audio transceiver should also be gone. + ASSERT_FALSE(GetTransceivers(*mSessionAns)[4].HasLevel()); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[4].IsStopped()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[4].IsAssociated()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[4].HasAddTrackMagic()); + ASSERT_TRUE(IsNull(GetTransceivers(*mSessionAns)[4].mSendTrack)); + ASSERT_TRUE(GetTransceivers(*mSessionAns)[4].IsRemoved()); + + // Fourth audio transceiver, created after SetRemote + ASSERT_FALSE(GetTransceivers(*mSessionAns)[5].HasLevel()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[5].IsStopped()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[5].IsAssociated()); + ASSERT_FALSE(GetTransceivers(*mSessionAns)[5].HasAddTrackMagic()); + ASSERT_TRUE( + GetTransceivers(*mSessionAns)[5].mSendTrack.GetStreamIds().empty()); +} + +TEST_F(JsepSessionTest, LocalRollback) { + AddTracks(*mSessionOff, "audio,video"); + AddTracks(*mSessionAns, "audio,video"); + + std::string offer = CreateOffer(); + SetLocalOffer(offer, CHECK_SUCCESS); + + ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsAssociated()); + ASSERT_TRUE(GetTransceivers(*mSessionOff)[1].IsAssociated()); + ASSERT_FALSE( + mSessionOff->SetLocalDescription(kJsepSdpRollback, "").mError.isSome()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].IsAssociated()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[1].IsAssociated()); +} + +TEST_F(JsepSessionTest, JsStopsTransceiverBeforeAnswer) { + AddTracks(*mSessionOff, "audio,video"); + AddTracks(*mSessionAns, "audio,video"); + + std::string offer = CreateOffer(); + SetLocalOffer(offer, CHECK_SUCCESS); + SetRemoteOffer(offer, CHECK_SUCCESS); + + std::string answer = CreateAnswer(); + SetLocalAnswer(answer, CHECK_SUCCESS); + + // Now JS decides to stop a transceiver. Make sure transport stuff is still + // ready to go when the answer is set. This should only prevent the flow of + // media for that transceiver. + + GetTransceivers(*mSessionOff)[0].Stop(); + SetRemoteAnswer(answer, CHECK_SUCCESS); + + ASSERT_TRUE(GetTransceivers(*mSessionOff)[0].IsStopped()); + ASSERT_EQ(1U, GetTransceivers(*mSessionOff)[0].mTransport.mComponents); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].mSendTrack.GetActive()); + ASSERT_FALSE(GetTransceivers(*mSessionOff)[0].mRecvTrack.GetActive()); +} + +TEST_F(JsepSessionTest, TestOfferPTAsymmetryRtxApt) { + for (auto& codec : mSessionAns->Codecs()) { + if (codec->mName == "VP8") { + JsepVideoCodecDescription* vp8 = + static_cast<JsepVideoCodecDescription*>(codec.get()); + vp8->EnableRtx("42"); + break; + } + } + + types.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionOff, "video"); + AddTracks(*mSessionAns, "video"); + JsepOfferOptions options; + + // Ensure that mSessionAns is appropriately configured. + std::string offer; + JsepSession::Result result = mSessionAns->CreateOffer(options, &offer); + ASSERT_FALSE(result.mError.isSome()); + ASSERT_NE(std::string::npos, offer.find("a=rtpmap:42 rtx")) << offer; + + OfferAnswer(); + + // Answerer should use what the offerer suggested + UniquePtr<JsepCodecDescription> codec; + GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &codec); + ASSERT_TRUE(codec); + ASSERT_EQ("VP8", codec->mName); + JsepVideoCodecDescription* vp8 = + static_cast<JsepVideoCodecDescription*>(codec.get()); + ASSERT_EQ("120", vp8->mDefaultPt); + ASSERT_EQ("124", vp8->mRtxPayloadType); + GetCodec(*mSessionAns, 0, sdp::kRecv, 0, 0, &codec); + ASSERT_TRUE(codec); + ASSERT_EQ("VP8", codec->mName); + vp8 = static_cast<JsepVideoCodecDescription*>(codec.get()); + ASSERT_EQ("120", vp8->mDefaultPt); + ASSERT_EQ("124", vp8->mRtxPayloadType); + + // Answerer should not change back when it reoffers + result = mSessionAns->CreateOffer(options, &offer); + ASSERT_FALSE(result.mError.isSome()); + ASSERT_NE(std::string::npos, offer.find("a=rtpmap:124 rtx")) << offer; +} + +TEST_F(JsepSessionTest, TestAnswerPTAsymmetryRtx) { + // JsepSessionImpl will never answer with an asymmetric payload type + // (tested in TestOfferPTAsymmetry), so we have to rewrite SDP a little. + types.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionOff, "video"); + AddTracks(*mSessionAns, "video"); + + std::string offer = CreateOffer(); + SetLocalOffer(offer); + + Replace("a=rtpmap:120 VP8", "a=rtpmap:119 VP8", &offer); + Replace("m=video 9 UDP/TLS/RTP/SAVPF 120", "m=video 9 UDP/TLS/RTP/SAVPF 119", + &offer); + ReplaceAll("a=fmtp:120", "a=fmtp:119", &offer); + ReplaceAll("a=fmtp:122 120", "a=fmtp:122 119", &offer); + ReplaceAll("a=fmtp:124 apt=120", "a=fmtp:124 apt=119", &offer); + ReplaceAll("a=rtcp-fb:120", "a=rtcp-fb:119", &offer); + + SetRemoteOffer(offer); + + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + SetRemoteAnswer(answer); + + UniquePtr<JsepCodecDescription> codec; + GetCodec(*mSessionOff, 0, sdp::kSend, 0, 0, &codec); + ASSERT_TRUE(codec); + ASSERT_EQ("VP8", codec->mName); + ASSERT_EQ("119", codec->mDefaultPt); + JsepVideoCodecDescription* vp8 = + static_cast<JsepVideoCodecDescription*>(codec.get()); + ASSERT_EQ("124", vp8->mRtxPayloadType); + GetCodec(*mSessionOff, 0, sdp::kRecv, 0, 0, &codec); + ASSERT_TRUE(codec); + ASSERT_EQ("VP8", codec->mName); + ASSERT_EQ("120", codec->mDefaultPt); + vp8 = static_cast<JsepVideoCodecDescription*>(codec.get()); + ASSERT_EQ("124", vp8->mRtxPayloadType); + + GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &codec); + ASSERT_TRUE(codec); + ASSERT_EQ("VP8", codec->mName); + ASSERT_EQ("119", codec->mDefaultPt); + vp8 = static_cast<JsepVideoCodecDescription*>(codec.get()); + ASSERT_EQ("124", vp8->mRtxPayloadType); + GetCodec(*mSessionAns, 0, sdp::kRecv, 0, 0, &codec); + ASSERT_TRUE(codec); + ASSERT_EQ("VP8", codec->mName); + ASSERT_EQ("119", codec->mDefaultPt); + vp8 = static_cast<JsepVideoCodecDescription*>(codec.get()); + ASSERT_EQ("124", vp8->mRtxPayloadType); +} + +TEST_F(JsepSessionTest, TestAnswerPTAsymmetryRtxApt) { + // JsepSessionImpl will never answer with an asymmetric payload type + // so we have to rewrite SDP a little. + types.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionOff, "video"); + AddTracks(*mSessionAns, "video"); + + std::string offer = CreateOffer(); + SetLocalOffer(offer); + + Replace("a=rtpmap:124 rtx", "a=rtpmap:42 rtx", &offer); + Replace("m=video 9 UDP/TLS/RTP/SAVPF 120 124", + "m=video 9 UDP/TLS/RTP/SAVPF 120 42", &offer); + ReplaceAll("a=fmtp:124", "a=fmtp:42", &offer); + + SetRemoteOffer(offer); + + std::string answer = CreateAnswer(); + SetLocalAnswer(answer); + SetRemoteAnswer(answer); + + UniquePtr<JsepCodecDescription> codec; + GetCodec(*mSessionOff, 0, sdp::kSend, 0, 0, &codec); + ASSERT_TRUE(codec); + ASSERT_EQ("VP8", codec->mName); + ASSERT_EQ("120", codec->mDefaultPt); + JsepVideoCodecDescription* vp8 = + static_cast<JsepVideoCodecDescription*>(codec.get()); + ASSERT_EQ("42", vp8->mRtxPayloadType); + GetCodec(*mSessionOff, 0, sdp::kRecv, 0, 0, &codec); + ASSERT_TRUE(codec); + ASSERT_EQ("VP8", codec->mName); + ASSERT_EQ("120", codec->mDefaultPt); + vp8 = static_cast<JsepVideoCodecDescription*>(codec.get()); + ASSERT_EQ("124", vp8->mRtxPayloadType); + + GetCodec(*mSessionAns, 0, sdp::kSend, 0, 0, &codec); + ASSERT_TRUE(codec); + ASSERT_EQ("VP8", codec->mName); + vp8 = static_cast<JsepVideoCodecDescription*>(codec.get()); + ASSERT_EQ("120", vp8->mDefaultPt); + ASSERT_EQ("42", vp8->mRtxPayloadType); + GetCodec(*mSessionAns, 0, sdp::kRecv, 0, 0, &codec); + ASSERT_TRUE(codec); + ASSERT_EQ("VP8", codec->mName); + vp8 = static_cast<JsepVideoCodecDescription*>(codec.get()); + ASSERT_EQ("120", vp8->mDefaultPt); + ASSERT_EQ("42", vp8->mRtxPayloadType); +} + +TEST_F(JsepSessionTest, TestOfferNoRtx) { + for (auto& codec : mSessionOff->Codecs()) { + if (codec->Type() == SdpMediaSection::kVideo) { + JsepVideoCodecDescription* videoCodec = + static_cast<JsepVideoCodecDescription*>(codec.get()); + videoCodec->mRtxEnabled = false; + } + } + + types.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionOff, "video"); + AddTracks(*mSessionAns, "video"); + JsepOfferOptions options; + + std::string offer; + JsepSession::Result result = mSessionOff->CreateOffer(options, &offer); + ASSERT_FALSE(result.mError.isSome()); + ASSERT_EQ(std::string::npos, offer.find("rtx")) << offer; + + OfferAnswer(); + + // Answerer should use what the offerer suggested + UniquePtr<JsepCodecDescription> codec; + for (size_t i = 0; i < 4; ++i) { + GetCodec(*mSessionAns, 0, sdp::kSend, 0, i, &codec); + ASSERT_TRUE(codec); + JsepVideoCodecDescription* videoCodec = + static_cast<JsepVideoCodecDescription*>(codec.get()); + ASSERT_FALSE(videoCodec->mRtxEnabled); + GetCodec(*mSessionAns, 0, sdp::kRecv, 0, i, &codec); + ASSERT_TRUE(codec); + videoCodec = static_cast<JsepVideoCodecDescription*>(codec.get()); + ASSERT_FALSE(videoCodec->mRtxEnabled); + } +} + +TEST_F(JsepSessionTest, TestOneWayRtx) { + for (auto& codec : mSessionAns->Codecs()) { + if (codec->Type() == SdpMediaSection::kVideo) { + JsepVideoCodecDescription* videoCodec = + static_cast<JsepVideoCodecDescription*>(codec.get()); + videoCodec->mRtxEnabled = false; + } + } + + types.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionOff, "video"); + AddTracks(*mSessionAns, "video"); + JsepOfferOptions options; + + std::string offer; + JsepSession::Result result = mSessionAns->CreateOffer(options, &offer); + ASSERT_FALSE(result.mError.isSome()); + ASSERT_EQ(std::string::npos, offer.find("rtx")) << offer; + + OfferAnswer(); + + // If the answerer does not support rtx, the offerer should not send it, + // but it is too late to turn off recv on the offerer side. + UniquePtr<JsepCodecDescription> codec; + for (size_t i = 0; i < 4; ++i) { + GetCodec(*mSessionOff, 0, sdp::kSend, 0, i, &codec); + ASSERT_TRUE(codec); + JsepVideoCodecDescription* videoCodec = + static_cast<JsepVideoCodecDescription*>(codec.get()); + ASSERT_FALSE(videoCodec->mRtxEnabled); + GetCodec(*mSessionOff, 0, sdp::kRecv, 0, i, &codec); + ASSERT_TRUE(codec); + videoCodec = static_cast<JsepVideoCodecDescription*>(codec.get()); + ASSERT_TRUE(videoCodec->mRtxEnabled); + } +} + +TEST_F(JsepSessionTest, TestRtxNoSsrcGroup) { + mSessionOff->AddTransceiver(JsepTransceiver( + SdpMediaSection::kVideo, mUuidGen, SdpDirectionAttribute::kRecvonly)); + + OfferAnswer(CHECK_SUCCESS); + + std::string offer = mSessionOff->GetLocalDescription(kJsepDescriptionCurrent); + ASSERT_EQ(std::string::npos, offer.find("FID")) << offer; + + std::string answer = + mSessionOff->GetRemoteDescription(kJsepDescriptionCurrent); + ASSERT_EQ(std::string::npos, answer.find("FID")) << answer; +} + +TEST_F(JsepSessionTest, TestRtxSsrcGroupOnlyOffered) { + mSessionOff->AddTransceiver(JsepTransceiver( + SdpMediaSection::kVideo, mUuidGen, SdpDirectionAttribute::kSendonly)); + + OfferAnswer(CHECK_SUCCESS); + + std::string offer = mSessionOff->GetLocalDescription(kJsepDescriptionCurrent); + ASSERT_NE(std::string::npos, offer.find("FID")) << offer; + + std::string answer = + mSessionOff->GetRemoteDescription(kJsepDescriptionCurrent); + ASSERT_EQ(std::string::npos, answer.find("FID")) << answer; +} + +TEST_F(JsepSessionTest, TestOfferRtxNoMsid) { + for (auto& codec : mSessionOff->Codecs()) { + if (codec->mName == "VP8") { + JsepVideoCodecDescription* vp8 = + static_cast<JsepVideoCodecDescription*>(codec.get()); + vp8->EnableRtx("42"); + break; + } + } + + types.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionOff, "video"); + + mSessionOff->ForEachTransceiver([this](JsepTransceiver& aTransceiver) { + if (!IsNull(aTransceiver.mSendTrack)) { + std::vector<std::string> empty; + aTransceiver.mSendTrack.UpdateStreamIds(empty); + } + }); + + // MSID stream absence should not influence FID ssrc-group + JsepOfferOptions options; + std::string offer; + JsepSession::Result result = mSessionOff->CreateOffer(options, &offer); + ASSERT_FALSE(result.mError.isSome()); + ASSERT_NE(std::string::npos, offer.find("FID")) << offer; +} + +TEST_F(JsepSessionTest, TestDuplicatePayloadTypes) { + for (auto& codec : mSessionOff->Codecs()) { + if (codec->Type() == SdpMediaSection::kVideo) { + JsepVideoCodecDescription* videoCodec = + static_cast<JsepVideoCodecDescription*>(codec.get()); + videoCodec->mRtxPayloadType = "97"; + videoCodec->EnableFec("97", "97"); + } + } + + types.push_back(SdpMediaSection::kVideo); + AddTracks(*mSessionOff, "video"); + AddTracks(*mSessionAns, "video"); + + OfferAnswer(); + + std::vector<sdp::Direction> directions = {sdp::kSend, sdp::kRecv}; + for (auto direction : directions) { + UniquePtr<JsepCodecDescription> codec; + std::set<std::string> payloadTypes; + std::string redPt, ulpfecPt; + for (size_t i = 0; i < 4; ++i) { + GetCodec(*mSessionOff, 0, direction, 0, i, &codec); + ASSERT_TRUE(codec); + JsepVideoCodecDescription* videoCodec = + static_cast<JsepVideoCodecDescription*>(codec.get()); + ASSERT_TRUE(payloadTypes.insert(videoCodec->mDefaultPt).second); + ASSERT_TRUE(payloadTypes.insert(videoCodec->mRtxPayloadType).second); + // ULPFEC and RED payload types are the same for each codec, so we only + // check them for the first one. + if (i == 0) { + ASSERT_TRUE(payloadTypes.insert(videoCodec->mREDPayloadType).second); + ASSERT_TRUE(payloadTypes.insert(videoCodec->mULPFECPayloadType).second); + redPt = videoCodec->mREDPayloadType; + ulpfecPt = videoCodec->mULPFECPayloadType; + } else { + ASSERT_TRUE(redPt == videoCodec->mREDPayloadType); + ASSERT_TRUE(ulpfecPt == videoCodec->mULPFECPayloadType); + } + } + } +} + +TEST_F(JsepSessionTest, TestTransportAttributeValidation) { + const std::string sdpTemplate = + "v=0\r\n" + "o=- 6 2 IN IP4 1r\r\n" + "t=0 0a\r\n" + "a=group:BUNDLE audio video\r\n" + "m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n" + "c=IN IP4 51.81.107.13\r\n" + "a=ice-ufrag:Xp\r\n" + "a=ice-pwd:he\r\n" + "a=setup:actpass\r\n" + "a=fingerprint:sha-256 " + "DC:FC:25:56:2B:88:77:2F:E4:FA:97:4E:2E:F1:D6:34:A6:A0:11:E2:E4:38:B3:98:" + "08:D2:F7:9D:F5:E2:C1:15\r\n" + "a=sendrecv\r\n" + "a=extmap:11 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n" + "a=fmtp:111 maxplaybackrate=48000;stereo=1;useinbandfec=1\r\n" + "a=mid:audio\r\n" + "a=rtcp-mux\r\n" + "a=rtpmap:111 opus/48000/2\r\n" + "a=ssrc:3463672643 cname:{ec9a356a-8d2c-504e-9977-99070a51f929}\r\n" + "m=video 9 UDP/TLS/RTP/SAVPF 100\r\n" + "c=IN IP4 51.81.107.13\r\n" + "a=rtpmap:100 VP8/90000\r\n" + "a=extmap:1 urn:ietf:params:rtp-hdrext:toffset\r\n" + "a=mid:video\r\n" + "a=ice-ufrag:Xp\r\n" + "a=ice-pwd:he\r\n" + "a=setup:actpass\r\n" + "a=fingerprint:sha-256 " + "DC:FC:25:56:2B:88:77:2F:E4:FA:97:4E:2E:F1:D6:34:A6:A0:11:E2:E4:38:B3:98:" + "08:D2:F7:9D:F5:E2:C1:15\r\n"; + + // Control case + { + auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdpTemplate); + ASSERT_FALSE(result.mError.isSome()); + } + + // Missing ufrag + { + UniquePtr<Sdp> parsed = Parse(sdpTemplate); + parsed->GetMediaSection(0).GetAttributeList().RemoveAttribute( + SdpAttribute::kIceUfragAttribute); + auto sdp = parsed->ToString(); + auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdp); + ASSERT_TRUE(result.mError.isSome()); + } + + // Missing pwd, bundle tag + { + UniquePtr<Sdp> parsed = Parse(sdpTemplate); + parsed->GetMediaSection(0).GetAttributeList().RemoveAttribute( + SdpAttribute::kIcePwdAttribute); + auto sdp = parsed->ToString(); + auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdp); + ASSERT_TRUE(result.mError.isSome()); + } + + // Missing setup, bundle tag + { + UniquePtr<Sdp> parsed = Parse(sdpTemplate); + parsed->GetMediaSection(0).GetAttributeList().RemoveAttribute( + SdpAttribute::kSetupAttribute); + auto sdp = parsed->ToString(); + auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdp); + ASSERT_TRUE(result.mError.isSome()); + } + + // Invalid setup attribute (holdconn), bundle tag + { + UniquePtr<Sdp> parsed = Parse(sdpTemplate); + parsed->GetMediaSection(0).GetAttributeList().SetAttribute( + new SdpSetupAttribute(SdpSetupAttribute::kHoldconn)); + auto sdp = parsed->ToString(); + auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdp); + ASSERT_TRUE(result.mError.isSome()); + } + + // Missing fingerprint, bundle tag + { + UniquePtr<Sdp> parsed = Parse(sdpTemplate); + parsed->GetMediaSection(0).GetAttributeList().RemoveAttribute( + SdpAttribute::kFingerprintAttribute); + auto sdp = parsed->ToString(); + auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdp); + ASSERT_TRUE(result.mError.isSome()); + } + + // Unknown fingerprint algorithm + { + std::string mungedSdp = sdpTemplate; + ReplaceAll("fingerprint:sha", "fingerprint:foo", &mungedSdp); + UniquePtr<Sdp> parsed = Parse(mungedSdp); + auto sdp = parsed->ToString(); + auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdp); + ASSERT_TRUE(result.mError.isSome()); + } + + // Missing pwd, bundled msection without bundle-only + { + UniquePtr<Sdp> parsed = Parse(sdpTemplate); + parsed->GetMediaSection(1).GetAttributeList().RemoveAttribute( + SdpAttribute::kIcePwdAttribute); + auto sdp = parsed->ToString(); + auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdp); + ASSERT_TRUE(result.mError.isSome()); + } + + // Missing setup, bundled msection without bundle-only + { + UniquePtr<Sdp> parsed = Parse(sdpTemplate); + parsed->GetMediaSection(1).GetAttributeList().RemoveAttribute( + SdpAttribute::kSetupAttribute); + auto sdp = parsed->ToString(); + auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdp); + ASSERT_TRUE(result.mError.isSome()); + } + + // Missing fingerprint, bundled msection without bundle-only + { + UniquePtr<Sdp> parsed = Parse(sdpTemplate); + parsed->GetMediaSection(1).GetAttributeList().RemoveAttribute( + SdpAttribute::kFingerprintAttribute); + auto sdp = parsed->ToString(); + auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdp); + ASSERT_TRUE(result.mError.isSome()); + } + + // Missing ufrag attribute, bundle-only msection + { + UniquePtr<Sdp> parsed = Parse(sdpTemplate); + parsed->GetMediaSection(1).GetAttributeList().RemoveAttribute( + SdpAttribute::kIceUfragAttribute); + parsed->GetMediaSection(1).GetAttributeList().SetAttribute( + new SdpFlagAttribute(SdpAttribute::kBundleOnlyAttribute)); + auto sdp = parsed->ToString(); + auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdp); + ASSERT_FALSE(result.mError.isSome()); + } + + // Missing pwd attribute, bundle-only msection + { + UniquePtr<Sdp> parsed = Parse(sdpTemplate); + parsed->GetMediaSection(1).GetAttributeList().RemoveAttribute( + SdpAttribute::kIcePwdAttribute); + parsed->GetMediaSection(1).GetAttributeList().SetAttribute( + new SdpFlagAttribute(SdpAttribute::kBundleOnlyAttribute)); + auto sdp = parsed->ToString(); + auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdp); + ASSERT_FALSE(result.mError.isSome()); + } + + // Missing fingerprint attribute, bundle-only msection + { + UniquePtr<Sdp> parsed = Parse(sdpTemplate); + parsed->GetMediaSection(1).GetAttributeList().RemoveAttribute( + SdpAttribute::kFingerprintAttribute); + parsed->GetMediaSection(1).GetAttributeList().SetAttribute( + new SdpFlagAttribute(SdpAttribute::kBundleOnlyAttribute)); + auto sdp = parsed->ToString(); + auto result = mSessionOff->SetRemoteDescription(kJsepSdpOffer, sdp); + ASSERT_FALSE(result.mError.isSome()); + } +} +} // namespace mozilla diff --git a/media/webrtc/signaling/gtest/jsep_track_unittest.cpp b/media/webrtc/signaling/gtest/jsep_track_unittest.cpp new file mode 100644 index 0000000000..778b87102e --- /dev/null +++ b/media/webrtc/signaling/gtest/jsep_track_unittest.cpp @@ -0,0 +1,1748 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nss.h" +#include "ssl.h" + +#define GTEST_HAS_RTTI 0 +#include "gtest/gtest.h" + +#include "jsep/JsepTrack.h" +#include "sdp/SipccSdp.h" +#include "sdp/SipccSdpParser.h" +#include "sdp/SdpHelper.h" + +namespace mozilla { + +class JsepTrackTestBase : public ::testing::Test { + public: + static void SetUpTestCase() { + NSS_NoDB_Init(nullptr); + NSS_SetDomesticPolicy(); + } +}; + +class JsepTrackTest : public JsepTrackTestBase { + public: + JsepTrackTest() + : mSendOff(SdpMediaSection::kAudio, sdp::kSend), + mRecvOff(SdpMediaSection::kAudio, sdp::kRecv), + mSendAns(SdpMediaSection::kAudio, sdp::kSend), + mRecvAns(SdpMediaSection::kAudio, sdp::kRecv) {} + + void TearDown() override { + if (::testing::UnitTest::GetInstance() + ->current_test_info() + ->result() + ->Failed()) { + if (mOffer) { + std::cerr << "Offer SDP: " << std::endl; + mOffer->Serialize(std::cerr); + } + + if (mAnswer) { + std::cerr << "Answer SDP: " << std::endl; + mAnswer->Serialize(std::cerr); + } + } + } + + std::vector<UniquePtr<JsepCodecDescription>> MakeCodecs( + bool addFecCodecs = false, bool preferRed = false, + bool addDtmfCodec = false) const { + std::vector<UniquePtr<JsepCodecDescription>> results; + results.emplace_back(JsepAudioCodecDescription::CreateDefaultOpus()); + results.emplace_back(JsepAudioCodecDescription::CreateDefaultG722()); + if (addDtmfCodec) { + results.emplace_back( + JsepAudioCodecDescription::CreateDefaultTelephoneEvent()); + } + + if (addFecCodecs && preferRed) { + results.emplace_back(JsepVideoCodecDescription::CreateDefaultRed()); + } + + results.emplace_back(JsepVideoCodecDescription::CreateDefaultVP8(false)); + results.emplace_back(JsepVideoCodecDescription::CreateDefaultH264_1(false)); + + if (addFecCodecs) { + if (!preferRed) { + results.emplace_back(JsepVideoCodecDescription::CreateDefaultRed()); + } + results.emplace_back(JsepVideoCodecDescription::CreateDefaultUlpFec()); + } + + results.emplace_back(new JsepApplicationCodecDescription( + "webrtc-datachannel", 256, 5999, 499)); + + // if we're doing something with red, it needs + // to update the redundant encodings list + for (auto& codec : results) { + if (codec->mName == "red") { + JsepVideoCodecDescription& red = + static_cast<JsepVideoCodecDescription&>(*codec); + red.UpdateRedundantEncodings(results); + } + } + + return results; + } + + void Init(SdpMediaSection::MediaType type) { + InitCodecs(); + InitTracks(type); + InitSdp(type); + } + + void InitCodecs() { + mOffCodecs = MakeCodecs(); + mAnsCodecs = MakeCodecs(); + } + + void InitTracks(SdpMediaSection::MediaType type) { + mSendOff = JsepTrack(type, sdp::kSend); + if (type != SdpMediaSection::MediaType::kApplication) { + mSendOff.UpdateStreamIds(std::vector<std::string>(1, "stream_id")); + } + mRecvOff = JsepTrack(type, sdp::kRecv); + mSendOff.PopulateCodecs(mOffCodecs); + mRecvOff.PopulateCodecs(mOffCodecs); + + mSendAns = JsepTrack(type, sdp::kSend); + if (type != SdpMediaSection::MediaType::kApplication) { + mSendAns.UpdateStreamIds(std::vector<std::string>(1, "stream_id")); + } + mRecvAns = JsepTrack(type, sdp::kRecv); + mSendAns.PopulateCodecs(mAnsCodecs); + mRecvAns.PopulateCodecs(mAnsCodecs); + } + + void InitSdp(SdpMediaSection::MediaType type) { + std::vector<std::string> msids(1, "*"); + std::string error; + SdpHelper helper(&error); + + mOffer.reset(new SipccSdp(SdpOrigin("", 0, 0, sdp::kIPv4, ""))); + mOffer->AddMediaSection(type, SdpDirectionAttribute::kSendrecv, 0, + SdpHelper::GetProtocolForMediaType(type), + sdp::kIPv4, "0.0.0.0"); + // JsepTrack doesn't set msid-semantic + helper.SetupMsidSemantic(msids, mOffer.get()); + + mAnswer.reset(new SipccSdp(SdpOrigin("", 0, 0, sdp::kIPv4, ""))); + mAnswer->AddMediaSection(type, SdpDirectionAttribute::kSendrecv, 0, + SdpHelper::GetProtocolForMediaType(type), + sdp::kIPv4, "0.0.0.0"); + // JsepTrack doesn't set msid-semantic + helper.SetupMsidSemantic(msids, mAnswer.get()); + } + + SdpMediaSection& GetOffer() { return mOffer->GetMediaSection(0); } + + SdpMediaSection& GetAnswer() { return mAnswer->GetMediaSection(0); } + + void CreateOffer() { + mSendOff.AddToOffer(mSsrcGenerator, &GetOffer()); + mRecvOff.AddToOffer(mSsrcGenerator, &GetOffer()); + } + + void CreateAnswer() { + if (mRecvAns.GetMediaType() != SdpMediaSection::MediaType::kApplication) { + mRecvAns.RecvTrackSetRemote(*mOffer, GetOffer()); + mSendAns.SendTrackSetRemote(mSsrcGenerator, GetOffer()); + } + + mSendAns.AddToAnswer(GetOffer(), mSsrcGenerator, &GetAnswer()); + mRecvAns.AddToAnswer(GetOffer(), mSsrcGenerator, &GetAnswer()); + } + + void Negotiate() { + if (mRecvOff.GetMediaType() != SdpMediaSection::MediaType::kApplication) { + mRecvOff.RecvTrackSetRemote(*mAnswer, GetAnswer()); + mSendOff.SendTrackSetRemote(mSsrcGenerator, GetAnswer()); + } + + if (GetAnswer().IsSending()) { + mSendAns.Negotiate(GetAnswer(), GetOffer(), GetAnswer()); + mRecvOff.Negotiate(GetAnswer(), GetAnswer(), GetOffer()); + } + + if (GetAnswer().IsReceiving()) { + mRecvAns.Negotiate(GetAnswer(), GetOffer(), GetAnswer()); + mSendOff.Negotiate(GetAnswer(), GetAnswer(), GetOffer()); + } + } + + void OfferAnswer() { + CreateOffer(); + CreateAnswer(); + Negotiate(); + SanityCheck(); + } + + // TODO: Look into writing a macro that wraps an ASSERT_ and returns false + // if it fails (probably requires writing a bool-returning function that + // takes a void-returning lambda with a bool outparam, which will in turn + // invokes the ASSERT_) + static void CheckEncodingCount(size_t expected, const JsepTrack& send, + const JsepTrack& recv) { + if (expected) { + ASSERT_TRUE(send.GetNegotiatedDetails()); + ASSERT_TRUE(recv.GetNegotiatedDetails()); + } + + if (!send.GetStreamIds().empty() && send.GetNegotiatedDetails()) { + ASSERT_EQ(expected, send.GetNegotiatedDetails()->GetEncodingCount()); + } + + if (!recv.GetStreamIds().empty() && recv.GetNegotiatedDetails()) { + ASSERT_EQ(expected, recv.GetNegotiatedDetails()->GetEncodingCount()); + } + } + + void CheckOffEncodingCount(size_t expected) const { + CheckEncodingCount(expected, mSendOff, mRecvAns); + } + + void CheckAnsEncodingCount(size_t expected) const { + CheckEncodingCount(expected, mSendAns, mRecvOff); + } + + UniquePtr<JsepCodecDescription> GetCodec(const JsepTrack& track, + SdpMediaSection::MediaType type, + size_t expectedSize, + size_t codecIndex) const { + if (!track.GetNegotiatedDetails() || + track.GetNegotiatedDetails()->GetEncodingCount() != 1U || + track.GetMediaType() != type) { + return nullptr; + } + const auto& codecs = + track.GetNegotiatedDetails()->GetEncoding(0).GetCodecs(); + // it should not be possible for codecs to have a different type + // than the track, but we'll check the codec here just in case. + if (codecs.size() != expectedSize || codecIndex >= expectedSize || + codecs[codecIndex]->Type() != type) { + return nullptr; + } + return UniquePtr<JsepCodecDescription>(codecs[codecIndex]->Clone()); + } + + UniquePtr<JsepVideoCodecDescription> GetVideoCodec( + const JsepTrack& track, size_t expectedSize = 1, + size_t codecIndex = 0) const { + auto codec = + GetCodec(track, SdpMediaSection::kVideo, expectedSize, codecIndex); + return UniquePtr<JsepVideoCodecDescription>( + static_cast<JsepVideoCodecDescription*>(codec.release())); + } + + UniquePtr<JsepAudioCodecDescription> GetAudioCodec( + const JsepTrack& track, size_t expectedSize = 1, + size_t codecIndex = 0) const { + auto codec = + GetCodec(track, SdpMediaSection::kAudio, expectedSize, codecIndex); + return UniquePtr<JsepAudioCodecDescription>( + static_cast<JsepAudioCodecDescription*>(codec.release())); + } + + void CheckOtherFbExists(const JsepVideoCodecDescription& videoCodec, + SdpRtcpFbAttributeList::Type type) const { + for (const auto& fb : videoCodec.mOtherFbTypes) { + if (fb.type == type) { + return; // found the RtcpFb type, so stop looking + } + } + FAIL(); // RtcpFb type not found + } + + void SanityCheckRtcpFbs(const JsepVideoCodecDescription& a, + const JsepVideoCodecDescription& b) const { + ASSERT_EQ(a.mNackFbTypes.size(), b.mNackFbTypes.size()); + ASSERT_EQ(a.mAckFbTypes.size(), b.mAckFbTypes.size()); + ASSERT_EQ(a.mCcmFbTypes.size(), b.mCcmFbTypes.size()); + ASSERT_EQ(a.mOtherFbTypes.size(), b.mOtherFbTypes.size()); + } + + void SanityCheckCodecs(const JsepCodecDescription& a, + const JsepCodecDescription& b) const { +#define MSG \ + "For codecs " << a.mName << " (" << a.mDirection << ") and " << b.mName \ + << " (" << b.mDirection << ")" + ASSERT_EQ(a.Type(), b.Type()) << MSG; + if (a.Type() != SdpMediaSection::kApplication) { + ASSERT_EQ(a.mDefaultPt, b.mDefaultPt) << MSG; + } + ASSERT_EQ(a.mName, b.mName); + if (!mExpectDifferingFmtp) { + ASSERT_EQ(a.mSdpFmtpLine, b.mSdpFmtpLine) << MSG; + } + ASSERT_EQ(a.mClock, b.mClock) << MSG; + ASSERT_EQ(a.mChannels, b.mChannels) << MSG; + ASSERT_NE(a.mDirection, b.mDirection) << MSG; + // These constraints are for fmtp and rid, which _are_ signaled + ASSERT_EQ(a.mConstraints, b.mConstraints) << MSG; +#undef MSG + + if (a.Type() == SdpMediaSection::kVideo) { + SanityCheckRtcpFbs(static_cast<const JsepVideoCodecDescription&>(a), + static_cast<const JsepVideoCodecDescription&>(b)); + } + } + + void SanityCheckEncodings(const JsepTrackEncoding& a, + const JsepTrackEncoding& b) const { + ASSERT_EQ(a.GetCodecs().size(), b.GetCodecs().size()); + for (size_t i = 0; i < a.GetCodecs().size(); ++i) { + SanityCheckCodecs(*a.GetCodecs()[i], *b.GetCodecs()[i]); + } + + ASSERT_EQ(a.mRid, b.mRid); + // mConstraints will probably differ, since they are not signaled to the + // other side. + } + + void SanityCheckNegotiatedDetails(const JsepTrackNegotiatedDetails& a, + const JsepTrackNegotiatedDetails& b) const { + ASSERT_EQ(a.GetEncodingCount(), b.GetEncodingCount()); + for (size_t i = 0; i < a.GetEncodingCount(); ++i) { + SanityCheckEncodings(a.GetEncoding(i), b.GetEncoding(i)); + } + + ASSERT_EQ(a.GetUniquePayloadTypes().size(), + b.GetUniquePayloadTypes().size()); + for (size_t i = 0; i < a.GetUniquePayloadTypes().size(); ++i) { + ASSERT_EQ(a.GetUniquePayloadTypes()[i], b.GetUniquePayloadTypes()[i]); + } + } + + void SanityCheckTracks(const JsepTrack& a, const JsepTrack& b) const { + if (!a.GetNegotiatedDetails()) { + ASSERT_FALSE(!!b.GetNegotiatedDetails()); + return; + } + + ASSERT_TRUE(!!a.GetNegotiatedDetails()); + ASSERT_TRUE(!!b.GetNegotiatedDetails()); + ASSERT_EQ(a.GetMediaType(), b.GetMediaType()); + ASSERT_EQ(a.GetStreamIds(), b.GetStreamIds()); + ASSERT_EQ(a.GetCNAME(), b.GetCNAME()); + ASSERT_NE(a.GetDirection(), b.GetDirection()); + ASSERT_EQ(a.GetSsrcs().size(), b.GetSsrcs().size()); + for (size_t i = 0; i < a.GetSsrcs().size(); ++i) { + ASSERT_EQ(a.GetSsrcs()[i], b.GetSsrcs()[i]); + } + + SanityCheckNegotiatedDetails(*a.GetNegotiatedDetails(), + *b.GetNegotiatedDetails()); + } + + void SanityCheck() const { + SanityCheckTracks(mSendOff, mRecvAns); + SanityCheckTracks(mRecvOff, mSendAns); + } + + protected: + JsepTrack mSendOff; + JsepTrack mRecvOff; + JsepTrack mSendAns; + JsepTrack mRecvAns; + std::vector<UniquePtr<JsepCodecDescription>> mOffCodecs; + std::vector<UniquePtr<JsepCodecDescription>> mAnsCodecs; + UniquePtr<Sdp> mOffer; + UniquePtr<Sdp> mAnswer; + SsrcGenerator mSsrcGenerator; + bool mExpectDifferingFmtp = false; +}; + +TEST_F(JsepTrackTestBase, CreateDestroy) {} + +TEST_F(JsepTrackTest, CreateDestroy) { Init(SdpMediaSection::kAudio); } + +TEST_F(JsepTrackTest, AudioNegotiation) { + Init(SdpMediaSection::kAudio); + OfferAnswer(); + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); +} + +TEST_F(JsepTrackTest, VideoNegotiation) { + Init(SdpMediaSection::kVideo); + OfferAnswer(); + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); +} + +class CheckForCodecType { + public: + explicit CheckForCodecType(SdpMediaSection::MediaType type, bool* result) + : mResult(result), mType(type) {} + + void operator()(const UniquePtr<JsepCodecDescription>& codec) { + if (codec->Type() == mType) { + *mResult = true; + } + } + + private: + bool* mResult; + SdpMediaSection::MediaType mType; +}; + +TEST_F(JsepTrackTest, CheckForMismatchedAudioCodecAndVideoTrack) { + std::vector<UniquePtr<JsepCodecDescription>> offerCodecs; + + // make codecs including telephone-event (an audio codec) + offerCodecs = MakeCodecs(false, false, true); + JsepTrack videoTrack(SdpMediaSection::kVideo, sdp::kSend); + videoTrack.UpdateStreamIds(std::vector<std::string>(1, "stream_id")); + // populate codecs and then make sure we don't have any audio codecs + // in the video track + videoTrack.PopulateCodecs(offerCodecs); + + bool found = false; + videoTrack.ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found)); + ASSERT_FALSE(found); + + found = false; + videoTrack.ForEachCodec(CheckForCodecType(SdpMediaSection::kVideo, &found)); + ASSERT_TRUE(found); // for sanity, make sure we did find video codecs +} + +TEST_F(JsepTrackTest, CheckVideoTrackWithHackedDtmfSdp) { + Init(SdpMediaSection::kVideo); + CreateOffer(); + // make sure we don't find sdp containing telephone-event in video track + ASSERT_EQ(mOffer->ToString().find("a=rtpmap:101 telephone-event"), + std::string::npos); + // force audio codec telephone-event into video m= section of offer + GetOffer().AddCodec("101", "telephone-event", 8000, 1); + // make sure we _do_ find sdp containing telephone-event in video track + ASSERT_NE(mOffer->ToString().find("a=rtpmap:101 telephone-event"), + std::string::npos); + + CreateAnswer(); + // make sure we don't find sdp containing telephone-event in video track + ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:101 telephone-event"), + std::string::npos); + // force audio codec telephone-event into video m= section of answer + GetAnswer().AddCodec("101", "telephone-event", 8000, 1); + // make sure we _do_ find sdp containing telephone-event in video track + ASSERT_NE(mAnswer->ToString().find("a=rtpmap:101 telephone-event"), + std::string::npos); + + Negotiate(); + SanityCheck(); + + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); + + // make sure we still don't find any audio codecs in the video track after + // hacking the sdp + bool found = false; + mSendOff.ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found)); + ASSERT_FALSE(found); + mRecvOff.ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found)); + ASSERT_FALSE(found); + mSendAns.ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found)); + ASSERT_FALSE(found); + mRecvAns.ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found)); + ASSERT_FALSE(found); +} + +TEST_F(JsepTrackTest, AudioNegotiationOffererDtmf) { + mOffCodecs = MakeCodecs(false, false, true); + mAnsCodecs = MakeCodecs(false, false, false); + + InitTracks(SdpMediaSection::kAudio); + InitSdp(SdpMediaSection::kAudio); + OfferAnswer(); + + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); + + ASSERT_NE(mOffer->ToString().find("a=rtpmap:101 telephone-event"), + std::string::npos); + ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:101 telephone-event"), + std::string::npos); + + ASSERT_NE(mOffer->ToString().find("a=fmtp:101 0-15"), std::string::npos); + ASSERT_EQ(mAnswer->ToString().find("a=fmtp:101"), std::string::npos); + + UniquePtr<JsepAudioCodecDescription> track; + ASSERT_TRUE((track = GetAudioCodec(mSendOff, 2, 0))); + ASSERT_EQ("109", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 2, 0))); + ASSERT_EQ("109", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mSendAns, 2, 0))); + ASSERT_EQ("109", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 2, 0))); + ASSERT_EQ("109", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mSendOff, 2, 1))); + ASSERT_EQ("9", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 2, 1))); + ASSERT_EQ("9", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mSendAns, 2, 1))); + ASSERT_EQ("9", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 2, 1))); + ASSERT_EQ("9", track->mDefaultPt); +} + +TEST_F(JsepTrackTest, AudioNegotiationAnswererDtmf) { + mOffCodecs = MakeCodecs(false, false, false); + mAnsCodecs = MakeCodecs(false, false, true); + + InitTracks(SdpMediaSection::kAudio); + InitSdp(SdpMediaSection::kAudio); + OfferAnswer(); + + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); + + ASSERT_EQ(mOffer->ToString().find("a=rtpmap:101 telephone-event"), + std::string::npos); + ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:101 telephone-event"), + std::string::npos); + + ASSERT_EQ(mOffer->ToString().find("a=fmtp:101 0-15"), std::string::npos); + ASSERT_EQ(mAnswer->ToString().find("a=fmtp:101"), std::string::npos); + + UniquePtr<JsepAudioCodecDescription> track; + ASSERT_TRUE((track = GetAudioCodec(mSendOff, 2, 0))); + ASSERT_EQ("109", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 2, 0))); + ASSERT_EQ("109", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mSendAns, 2, 0))); + ASSERT_EQ("109", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 2, 0))); + ASSERT_EQ("109", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mSendOff, 2, 1))); + ASSERT_EQ("9", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 2, 1))); + ASSERT_EQ("9", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mSendAns, 2, 1))); + ASSERT_EQ("9", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 2, 1))); + ASSERT_EQ("9", track->mDefaultPt); +} + +TEST_F(JsepTrackTest, AudioNegotiationOffererAnswererDtmf) { + mOffCodecs = MakeCodecs(false, false, true); + mAnsCodecs = MakeCodecs(false, false, true); + + InitTracks(SdpMediaSection::kAudio); + InitSdp(SdpMediaSection::kAudio); + OfferAnswer(); + + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); + + ASSERT_NE(mOffer->ToString().find("a=rtpmap:101 telephone-event"), + std::string::npos); + ASSERT_NE(mAnswer->ToString().find("a=rtpmap:101 telephone-event"), + std::string::npos); + + ASSERT_NE(mOffer->ToString().find("a=fmtp:101 0-15"), std::string::npos); + ASSERT_NE(mAnswer->ToString().find("a=fmtp:101 0-15"), std::string::npos); + + UniquePtr<JsepAudioCodecDescription> track; + ASSERT_TRUE((track = GetAudioCodec(mSendOff, 3, 0))); + ASSERT_EQ("109", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 0))); + ASSERT_EQ("109", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mSendAns, 3, 0))); + ASSERT_EQ("109", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 3, 0))); + ASSERT_EQ("109", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mSendOff, 3, 1))); + ASSERT_EQ("9", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 1))); + ASSERT_EQ("9", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mSendAns, 3, 1))); + ASSERT_EQ("9", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 3, 1))); + ASSERT_EQ("9", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mSendOff, 3, 2))); + ASSERT_EQ("101", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 2))); + ASSERT_EQ("101", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mSendAns, 3, 2))); + ASSERT_EQ("101", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 3, 2))); + ASSERT_EQ("101", track->mDefaultPt); +} + +TEST_F(JsepTrackTest, AudioNegotiationDtmfOffererNoFmtpAnswererFmtp) { + mOffCodecs = MakeCodecs(false, false, true); + mAnsCodecs = MakeCodecs(false, false, true); + + mExpectDifferingFmtp = true; + + InitTracks(SdpMediaSection::kAudio); + InitSdp(SdpMediaSection::kAudio); + + CreateOffer(); + GetOffer().RemoveFmtp("101"); + + CreateAnswer(); + + Negotiate(); + SanityCheck(); + + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); + + ASSERT_NE(mOffer->ToString().find("a=rtpmap:101 telephone-event"), + std::string::npos); + ASSERT_NE(mAnswer->ToString().find("a=rtpmap:101 telephone-event"), + std::string::npos); + + ASSERT_EQ(mOffer->ToString().find("a=fmtp:101"), std::string::npos); + ASSERT_NE(mAnswer->ToString().find("a=fmtp:101 0-15"), std::string::npos); + + UniquePtr<JsepAudioCodecDescription> track; + ASSERT_TRUE((track = GetAudioCodec(mSendOff, 3, 0))); + ASSERT_EQ("109", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 0))); + ASSERT_EQ("109", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mSendAns, 3, 0))); + ASSERT_EQ("109", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 3, 0))); + ASSERT_EQ("109", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mSendOff, 3, 1))); + ASSERT_EQ("9", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 1))); + ASSERT_EQ("9", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mSendAns, 3, 1))); + ASSERT_EQ("9", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 3, 1))); + ASSERT_EQ("9", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mSendOff, 3, 2))); + ASSERT_EQ("101", track->mDefaultPt); + ASSERT_EQ("0-15", track->mSdpFmtpLine.valueOr("nothing")); + ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 2))); + ASSERT_EQ("101", track->mDefaultPt); + ASSERT_EQ("nothing", track->mSdpFmtpLine.valueOr("nothing")); + ASSERT_TRUE((track = GetAudioCodec(mSendAns, 3, 2))); + ASSERT_EQ("101", track->mDefaultPt); + ASSERT_EQ("nothing", track->mSdpFmtpLine.valueOr("nothing")); + ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 3, 2))); + ASSERT_EQ("101", track->mDefaultPt); + ASSERT_EQ("0-15", track->mSdpFmtpLine.valueOr("nothing")); +} + +TEST_F(JsepTrackTest, AudioNegotiationDtmfOffererFmtpAnswererNoFmtp) { + mOffCodecs = MakeCodecs(false, false, true); + mAnsCodecs = MakeCodecs(false, false, true); + + mExpectDifferingFmtp = true; + + InitTracks(SdpMediaSection::kAudio); + InitSdp(SdpMediaSection::kAudio); + + CreateOffer(); + + CreateAnswer(); + GetAnswer().RemoveFmtp("101"); + + Negotiate(); + SanityCheck(); + + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); + + ASSERT_NE(mOffer->ToString().find("a=rtpmap:101 telephone-event"), + std::string::npos); + ASSERT_NE(mAnswer->ToString().find("a=rtpmap:101 telephone-event"), + std::string::npos); + + ASSERT_NE(mOffer->ToString().find("a=fmtp:101 0-15"), std::string::npos); + ASSERT_EQ(mAnswer->ToString().find("a=fmtp:101"), std::string::npos); + + UniquePtr<JsepAudioCodecDescription> track; + ASSERT_TRUE((track = GetAudioCodec(mSendOff, 3, 0))); + ASSERT_EQ("109", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 0))); + ASSERT_EQ("109", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mSendAns, 3, 0))); + ASSERT_EQ("109", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 3, 0))); + ASSERT_EQ("109", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mSendOff, 3, 1))); + ASSERT_EQ("9", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 1))); + ASSERT_EQ("9", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mSendAns, 3, 1))); + ASSERT_EQ("9", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 3, 1))); + ASSERT_EQ("9", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mSendOff, 3, 2))); + ASSERT_EQ("101", track->mDefaultPt); + ASSERT_EQ("nothing", track->mSdpFmtpLine.valueOr("nothing")); + ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 2))); + ASSERT_EQ("101", track->mDefaultPt); + ASSERT_EQ("0-15", track->mSdpFmtpLine.valueOr("nothing")); + ASSERT_TRUE((track = GetAudioCodec(mSendAns, 3, 2))); + ASSERT_EQ("101", track->mDefaultPt); + ASSERT_EQ("0-15", track->mSdpFmtpLine.valueOr("nothing")); + ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 3, 2))); + ASSERT_EQ("101", track->mDefaultPt); + ASSERT_EQ("nothing", track->mSdpFmtpLine.valueOr("nothing")); +} + +TEST_F(JsepTrackTest, AudioNegotiationDtmfOffererNoFmtpAnswererNoFmtp) { + mOffCodecs = MakeCodecs(false, false, true); + mAnsCodecs = MakeCodecs(false, false, true); + + mExpectDifferingFmtp = true; + + InitTracks(SdpMediaSection::kAudio); + InitSdp(SdpMediaSection::kAudio); + + CreateOffer(); + GetOffer().RemoveFmtp("101"); + + CreateAnswer(); + GetAnswer().RemoveFmtp("101"); + + Negotiate(); + SanityCheck(); + + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); + + ASSERT_NE(mOffer->ToString().find("a=rtpmap:101 telephone-event"), + std::string::npos); + ASSERT_NE(mAnswer->ToString().find("a=rtpmap:101 telephone-event"), + std::string::npos); + + ASSERT_EQ(mOffer->ToString().find("a=fmtp:101"), std::string::npos); + ASSERT_EQ(mAnswer->ToString().find("a=fmtp:101"), std::string::npos); + + UniquePtr<JsepAudioCodecDescription> track; + ASSERT_TRUE((track = GetAudioCodec(mSendOff, 3, 0))); + ASSERT_EQ("109", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 0))); + ASSERT_EQ("109", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mSendAns, 3, 0))); + ASSERT_EQ("109", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 3, 0))); + ASSERT_EQ("109", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mSendOff, 3, 1))); + ASSERT_EQ("9", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 1))); + ASSERT_EQ("9", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mSendAns, 3, 1))); + ASSERT_EQ("9", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 3, 1))); + ASSERT_EQ("9", track->mDefaultPt); + ASSERT_TRUE((track = GetAudioCodec(mSendOff, 3, 2))); + ASSERT_EQ("101", track->mDefaultPt); + ASSERT_EQ("nothing", track->mSdpFmtpLine.valueOr("nothing")); + ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 3, 2))); + ASSERT_EQ("101", track->mDefaultPt); + ASSERT_EQ("nothing", track->mSdpFmtpLine.valueOr("nothing")); + ASSERT_TRUE((track = GetAudioCodec(mSendAns, 3, 2))); + ASSERT_EQ("101", track->mDefaultPt); + ASSERT_EQ("nothing", track->mSdpFmtpLine.valueOr("nothing")); + ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 3, 2))); + ASSERT_EQ("101", track->mDefaultPt); + ASSERT_EQ("nothing", track->mSdpFmtpLine.valueOr("nothing")); +} + +TEST_F(JsepTrackTest, VideoNegotationOffererFEC) { + mOffCodecs = MakeCodecs(true); + mAnsCodecs = MakeCodecs(false); + + InitTracks(SdpMediaSection::kVideo); + InitSdp(SdpMediaSection::kVideo); + OfferAnswer(); + + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); + + ASSERT_NE(mOffer->ToString().find("a=rtpmap:122 red"), std::string::npos); + ASSERT_NE(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos); + ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos); + ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos); + + ASSERT_NE(mOffer->ToString().find("a=fmtp:122 120/126/123"), + std::string::npos); + ASSERT_EQ(mAnswer->ToString().find("a=fmtp:122"), std::string::npos); + + UniquePtr<JsepVideoCodecDescription> track; + ASSERT_TRUE((track = GetVideoCodec(mSendOff, 2, 0))); + ASSERT_EQ("120", track->mDefaultPt); + ASSERT_TRUE((track = GetVideoCodec(mRecvOff, 2, 0))); + ASSERT_EQ("120", track->mDefaultPt); + ASSERT_TRUE((track = GetVideoCodec(mSendAns, 2, 0))); + ASSERT_EQ("120", track->mDefaultPt); + ASSERT_TRUE((track = GetVideoCodec(mRecvAns, 2, 0))); + ASSERT_EQ("120", track->mDefaultPt); + ASSERT_TRUE((track = GetVideoCodec(mSendOff, 2, 1))); + ASSERT_EQ("126", track->mDefaultPt); + ASSERT_TRUE((track = GetVideoCodec(mRecvOff, 2, 1))); + ASSERT_EQ("126", track->mDefaultPt); + ASSERT_TRUE((track = GetVideoCodec(mSendAns, 2, 1))); + ASSERT_EQ("126", track->mDefaultPt); + ASSERT_TRUE((track = GetVideoCodec(mRecvAns, 2, 1))); + ASSERT_EQ("126", track->mDefaultPt); +} + +TEST_F(JsepTrackTest, VideoNegotationAnswererFEC) { + mOffCodecs = MakeCodecs(false); + mAnsCodecs = MakeCodecs(true); + + InitTracks(SdpMediaSection::kVideo); + InitSdp(SdpMediaSection::kVideo); + OfferAnswer(); + + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); + + ASSERT_EQ(mOffer->ToString().find("a=rtpmap:122 red"), std::string::npos); + ASSERT_EQ(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos); + ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos); + ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos); + + ASSERT_EQ(mOffer->ToString().find("a=fmtp:122"), std::string::npos); + ASSERT_EQ(mAnswer->ToString().find("a=fmtp:122"), std::string::npos); + + UniquePtr<JsepVideoCodecDescription> track; + ASSERT_TRUE((track = GetVideoCodec(mSendOff, 2, 0))); + ASSERT_EQ("120", track->mDefaultPt); + ASSERT_TRUE((track = GetVideoCodec(mRecvOff, 2, 0))); + ASSERT_EQ("120", track->mDefaultPt); + ASSERT_TRUE((track = GetVideoCodec(mSendAns, 2, 0))); + ASSERT_EQ("120", track->mDefaultPt); + ASSERT_TRUE((track = GetVideoCodec(mRecvAns, 2, 0))); + ASSERT_EQ("120", track->mDefaultPt); + ASSERT_TRUE((track = GetVideoCodec(mSendOff, 2, 1))); + ASSERT_EQ("126", track->mDefaultPt); + ASSERT_TRUE((track = GetVideoCodec(mRecvOff, 2, 1))); + ASSERT_EQ("126", track->mDefaultPt); + ASSERT_TRUE((track = GetVideoCodec(mSendAns, 2, 1))); + ASSERT_EQ("126", track->mDefaultPt); + ASSERT_TRUE((track = GetVideoCodec(mRecvAns, 2, 1))); + ASSERT_EQ("126", track->mDefaultPt); +} + +TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFEC) { + mOffCodecs = MakeCodecs(true); + mAnsCodecs = MakeCodecs(true); + + InitTracks(SdpMediaSection::kVideo); + InitSdp(SdpMediaSection::kVideo); + OfferAnswer(); + + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); + + ASSERT_NE(mOffer->ToString().find("a=rtpmap:122 red"), std::string::npos); + ASSERT_NE(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos); + ASSERT_NE(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos); + ASSERT_NE(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos); + + ASSERT_NE(mOffer->ToString().find("a=fmtp:122 120/126/123"), + std::string::npos); + ASSERT_NE(mAnswer->ToString().find("a=fmtp:122 120/126/123"), + std::string::npos); + + UniquePtr<JsepVideoCodecDescription> track; + ASSERT_TRUE((track = GetVideoCodec(mSendOff, 4))); + ASSERT_EQ("120", track->mDefaultPt); + ASSERT_TRUE((track = GetVideoCodec(mRecvOff, 4))); + ASSERT_EQ("120", track->mDefaultPt); + ASSERT_TRUE((track = GetVideoCodec(mSendAns, 4))); + ASSERT_EQ("120", track->mDefaultPt); + ASSERT_TRUE((track = GetVideoCodec(mRecvAns, 4))); + ASSERT_EQ("120", track->mDefaultPt); +} + +TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFECPreferred) { + mOffCodecs = MakeCodecs(true, true); + mAnsCodecs = MakeCodecs(true); + + InitTracks(SdpMediaSection::kVideo); + InitSdp(SdpMediaSection::kVideo); + OfferAnswer(); + + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); + + ASSERT_NE(mOffer->ToString().find("a=rtpmap:122 red"), std::string::npos); + ASSERT_NE(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos); + ASSERT_NE(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos); + ASSERT_NE(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos); + + ASSERT_NE(mOffer->ToString().find("a=fmtp:122 120/126/123"), + std::string::npos); + ASSERT_NE(mAnswer->ToString().find("a=fmtp:122 120/126/123"), + std::string::npos); + + UniquePtr<JsepVideoCodecDescription> track; + ASSERT_TRUE((track = GetVideoCodec(mSendOff, 4))); + ASSERT_EQ("122", track->mDefaultPt); + ASSERT_TRUE((track = GetVideoCodec(mRecvOff, 4))); + ASSERT_EQ("122", track->mDefaultPt); + ASSERT_TRUE((track = GetVideoCodec(mSendAns, 4))); + ASSERT_EQ("122", track->mDefaultPt); + ASSERT_TRUE((track = GetVideoCodec(mRecvAns, 4))); + ASSERT_EQ("122", track->mDefaultPt); +} + +// Make sure we only put the right things in the fmtp:122 120/.... line +TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFECMismatch) { + mOffCodecs = MakeCodecs(true, true); + mAnsCodecs = MakeCodecs(true); + // remove h264 from answer codecs + ASSERT_EQ("H264", mAnsCodecs[3]->mName); + mAnsCodecs.erase(mAnsCodecs.begin() + 3); + + InitTracks(SdpMediaSection::kVideo); + InitSdp(SdpMediaSection::kVideo); + OfferAnswer(); + + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); + + ASSERT_NE(mOffer->ToString().find("a=rtpmap:122 red"), std::string::npos); + ASSERT_NE(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos); + ASSERT_NE(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos); + ASSERT_NE(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos); + + ASSERT_NE(mOffer->ToString().find("a=fmtp:122 120/126/123"), + std::string::npos); + ASSERT_NE(mAnswer->ToString().find("a=fmtp:122 120/123"), std::string::npos); + + UniquePtr<JsepVideoCodecDescription> track; + ASSERT_TRUE((track = GetVideoCodec(mSendOff, 3))); + ASSERT_EQ("122", track->mDefaultPt); + ASSERT_TRUE((track = GetVideoCodec(mRecvOff, 3))); + ASSERT_EQ("122", track->mDefaultPt); + ASSERT_TRUE((track = GetVideoCodec(mSendAns, 3))); + ASSERT_EQ("122", track->mDefaultPt); + ASSERT_TRUE((track = GetVideoCodec(mRecvAns, 3))); + ASSERT_EQ("122", track->mDefaultPt); +} + +TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFECZeroVP9Codec) { + mOffCodecs = MakeCodecs(true); + auto vp9 = JsepVideoCodecDescription::CreateDefaultVP9(false); + vp9->mDefaultPt = "0"; + mOffCodecs.push_back(std::move(vp9)); + + ASSERT_EQ(8U, mOffCodecs.size()); + JsepVideoCodecDescription& red = + static_cast<JsepVideoCodecDescription&>(*mOffCodecs[4]); + ASSERT_EQ("red", red.mName); + // rebuild the redundant encodings with our newly added "wacky" VP9 + red.mRedundantEncodings.clear(); + red.UpdateRedundantEncodings(mOffCodecs); + + mAnsCodecs = MakeCodecs(true); + + InitTracks(SdpMediaSection::kVideo); + InitSdp(SdpMediaSection::kVideo); + OfferAnswer(); + + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); + + ASSERT_NE(mOffer->ToString().find("a=rtpmap:122 red"), std::string::npos); + ASSERT_NE(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos); + ASSERT_NE(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos); + ASSERT_NE(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos); + + ASSERT_NE(mOffer->ToString().find("a=fmtp:122 120/126/123/0"), + std::string::npos); + ASSERT_NE(mAnswer->ToString().find("a=fmtp:122 120/126/123\r\n"), + std::string::npos); +} + +TEST_F(JsepTrackTest, VideoNegotiationOfferRemb) { + InitCodecs(); + // enable remb on the offer codecs + ((JsepVideoCodecDescription&)*mOffCodecs[2]).EnableRemb(); + InitTracks(SdpMediaSection::kVideo); + InitSdp(SdpMediaSection::kVideo); + OfferAnswer(); + + // make sure REMB is on offer and not on answer + ASSERT_NE(mOffer->ToString().find("a=rtcp-fb:120 goog-remb"), + std::string::npos); + ASSERT_EQ(mAnswer->ToString().find("a=rtcp-fb:120 goog-remb"), + std::string::npos); + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); + + UniquePtr<JsepVideoCodecDescription> codec; + ASSERT_TRUE((codec = GetVideoCodec(mSendOff, 2, 0))); + ASSERT_EQ(codec->mOtherFbTypes.size(), 0U); + ASSERT_TRUE((codec = GetVideoCodec(mRecvAns, 2, 0))); + ASSERT_EQ(codec->mOtherFbTypes.size(), 0U); + ASSERT_TRUE((codec = GetVideoCodec(mSendAns, 2, 0))); + ASSERT_EQ(codec->mOtherFbTypes.size(), 0U); + ASSERT_TRUE((codec = GetVideoCodec(mRecvOff, 2, 0))); + ASSERT_EQ(codec->mOtherFbTypes.size(), 0U); +} + +TEST_F(JsepTrackTest, VideoNegotiationAnswerRemb) { + InitCodecs(); + // enable remb on the answer codecs + ((JsepVideoCodecDescription&)*mAnsCodecs[2]).EnableRemb(); + InitTracks(SdpMediaSection::kVideo); + InitSdp(SdpMediaSection::kVideo); + OfferAnswer(); + + // make sure REMB is not on offer and not on answer + ASSERT_EQ(mOffer->ToString().find("a=rtcp-fb:120 goog-remb"), + std::string::npos); + ASSERT_EQ(mAnswer->ToString().find("a=rtcp-fb:120 goog-remb"), + std::string::npos); + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); + + UniquePtr<JsepVideoCodecDescription> codec; + ASSERT_TRUE((codec = GetVideoCodec(mSendOff, 2, 0))); + ASSERT_EQ(codec->mOtherFbTypes.size(), 0U); + ASSERT_TRUE((codec = GetVideoCodec(mRecvAns, 2, 0))); + ASSERT_EQ(codec->mOtherFbTypes.size(), 0U); + ASSERT_TRUE((codec = GetVideoCodec(mSendAns, 2, 0))); + ASSERT_EQ(codec->mOtherFbTypes.size(), 0U); + ASSERT_TRUE((codec = GetVideoCodec(mRecvOff, 2, 0))); + ASSERT_EQ(codec->mOtherFbTypes.size(), 0U); +} + +TEST_F(JsepTrackTest, VideoNegotiationOfferAnswerRemb) { + InitCodecs(); + // enable remb on the offer and answer codecs + ((JsepVideoCodecDescription&)*mOffCodecs[2]).EnableRemb(); + ((JsepVideoCodecDescription&)*mAnsCodecs[2]).EnableRemb(); + InitTracks(SdpMediaSection::kVideo); + InitSdp(SdpMediaSection::kVideo); + OfferAnswer(); + + // make sure REMB is on offer and on answer + ASSERT_NE(mOffer->ToString().find("a=rtcp-fb:120 goog-remb"), + std::string::npos); + ASSERT_NE(mAnswer->ToString().find("a=rtcp-fb:120 goog-remb"), + std::string::npos); + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); + + UniquePtr<JsepVideoCodecDescription> codec; + ASSERT_TRUE((codec = GetVideoCodec(mSendOff, 2, 0))); + ASSERT_EQ(codec->mOtherFbTypes.size(), 1U); + CheckOtherFbExists(*codec, SdpRtcpFbAttributeList::kRemb); + ASSERT_TRUE((codec = GetVideoCodec(mRecvAns, 2, 0))); + ASSERT_EQ(codec->mOtherFbTypes.size(), 1U); + CheckOtherFbExists(*codec, SdpRtcpFbAttributeList::kRemb); + ASSERT_TRUE((codec = GetVideoCodec(mSendAns, 2, 0))); + ASSERT_EQ(codec->mOtherFbTypes.size(), 1U); + CheckOtherFbExists(*codec, SdpRtcpFbAttributeList::kRemb); + ASSERT_TRUE((codec = GetVideoCodec(mRecvOff, 2, 0))); + ASSERT_EQ(codec->mOtherFbTypes.size(), 1U); + CheckOtherFbExists(*codec, SdpRtcpFbAttributeList::kRemb); +} + +TEST_F(JsepTrackTest, VideoNegotiationOfferTransportCC) { + InitCodecs(); + // enable TransportCC on the offer codecs + ((JsepVideoCodecDescription&)*mOffCodecs[2]).EnableTransportCC(); + InitTracks(SdpMediaSection::kVideo); + InitSdp(SdpMediaSection::kVideo); + OfferAnswer(); + + // make sure TransportCC is on offer and not on answer + ASSERT_NE(mOffer->ToString().find("a=rtcp-fb:120 transport-cc"), + std::string::npos); + ASSERT_EQ(mAnswer->ToString().find("a=rtcp-fb:120 transport-cc"), + std::string::npos); + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); + + UniquePtr<JsepVideoCodecDescription> codec; + ASSERT_TRUE((codec = GetVideoCodec(mSendOff, 2, 0))); + ASSERT_EQ(codec->mOtherFbTypes.size(), 0U); + ASSERT_TRUE((codec = GetVideoCodec(mRecvAns, 2, 0))); + ASSERT_EQ(codec->mOtherFbTypes.size(), 0U); + ASSERT_TRUE((codec = GetVideoCodec(mSendAns, 2, 0))); + ASSERT_EQ(codec->mOtherFbTypes.size(), 0U); + ASSERT_TRUE((codec = GetVideoCodec(mRecvOff, 2, 0))); + ASSERT_EQ(codec->mOtherFbTypes.size(), 0U); +} + +TEST_F(JsepTrackTest, VideoNegotiationAnswerTransportCC) { + InitCodecs(); + // enable TransportCC on the answer codecs + ((JsepVideoCodecDescription&)*mAnsCodecs[2]).EnableTransportCC(); + InitTracks(SdpMediaSection::kVideo); + InitSdp(SdpMediaSection::kVideo); + OfferAnswer(); + + // make sure TransportCC is not on offer and not on answer + ASSERT_EQ(mOffer->ToString().find("a=rtcp-fb:120 transport-cc"), + std::string::npos); + ASSERT_EQ(mAnswer->ToString().find("a=rtcp-fb:120 transport-cc"), + std::string::npos); + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); + + UniquePtr<JsepVideoCodecDescription> codec; + ASSERT_TRUE((codec = GetVideoCodec(mSendOff, 2, 0))); + ASSERT_EQ(codec->mOtherFbTypes.size(), 0U); + ASSERT_TRUE((codec = GetVideoCodec(mRecvAns, 2, 0))); + ASSERT_EQ(codec->mOtherFbTypes.size(), 0U); + ASSERT_TRUE((codec = GetVideoCodec(mSendAns, 2, 0))); + ASSERT_EQ(codec->mOtherFbTypes.size(), 0U); + ASSERT_TRUE((codec = GetVideoCodec(mRecvOff, 2, 0))); + ASSERT_EQ(codec->mOtherFbTypes.size(), 0U); +} + +TEST_F(JsepTrackTest, VideoNegotiationOfferAnswerTransportCC) { + InitCodecs(); + // enable TransportCC on the offer and answer codecs + ((JsepVideoCodecDescription&)*mOffCodecs[2]).EnableTransportCC(); + ((JsepVideoCodecDescription&)*mAnsCodecs[2]).EnableTransportCC(); + InitTracks(SdpMediaSection::kVideo); + InitSdp(SdpMediaSection::kVideo); + OfferAnswer(); + + // make sure TransportCC is on offer and on answer + ASSERT_NE(mOffer->ToString().find("a=rtcp-fb:120 transport-cc"), + std::string::npos); + ASSERT_NE(mAnswer->ToString().find("a=rtcp-fb:120 transport-cc"), + std::string::npos); + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); + + UniquePtr<JsepVideoCodecDescription> codec; + ASSERT_TRUE((codec = GetVideoCodec(mSendOff, 2, 0))); + ASSERT_EQ(codec->mOtherFbTypes.size(), 1U); + CheckOtherFbExists(*codec, SdpRtcpFbAttributeList::kTransportCC); + ASSERT_TRUE((codec = GetVideoCodec(mRecvAns, 2, 0))); + ASSERT_EQ(codec->mOtherFbTypes.size(), 1U); + CheckOtherFbExists(*codec, SdpRtcpFbAttributeList::kTransportCC); + ASSERT_TRUE((codec = GetVideoCodec(mSendAns, 2, 0))); + ASSERT_EQ(codec->mOtherFbTypes.size(), 1U); + CheckOtherFbExists(*codec, SdpRtcpFbAttributeList::kTransportCC); + ASSERT_TRUE((codec = GetVideoCodec(mRecvOff, 2, 0))); + ASSERT_EQ(codec->mOtherFbTypes.size(), 1U); + CheckOtherFbExists(*codec, SdpRtcpFbAttributeList::kTransportCC); +} + +TEST_F(JsepTrackTest, AudioOffSendonlyAnsRecvonly) { + Init(SdpMediaSection::kAudio); + GetOffer().SetDirection(SdpDirectionAttribute::kSendonly); + GetAnswer().SetDirection(SdpDirectionAttribute::kRecvonly); + OfferAnswer(); + CheckOffEncodingCount(1); + CheckAnsEncodingCount(0); +} + +TEST_F(JsepTrackTest, VideoOffSendonlyAnsRecvonly) { + Init(SdpMediaSection::kVideo); + GetOffer().SetDirection(SdpDirectionAttribute::kSendonly); + GetAnswer().SetDirection(SdpDirectionAttribute::kRecvonly); + OfferAnswer(); + CheckOffEncodingCount(1); + CheckAnsEncodingCount(0); +} + +TEST_F(JsepTrackTest, AudioOffSendrecvAnsRecvonly) { + Init(SdpMediaSection::kAudio); + GetAnswer().SetDirection(SdpDirectionAttribute::kRecvonly); + OfferAnswer(); + CheckOffEncodingCount(1); + CheckAnsEncodingCount(0); +} + +TEST_F(JsepTrackTest, VideoOffSendrecvAnsRecvonly) { + Init(SdpMediaSection::kVideo); + GetAnswer().SetDirection(SdpDirectionAttribute::kRecvonly); + OfferAnswer(); + CheckOffEncodingCount(1); + CheckAnsEncodingCount(0); +} + +TEST_F(JsepTrackTest, AudioOffRecvonlyAnsSendonly) { + Init(SdpMediaSection::kAudio); + GetOffer().SetDirection(SdpDirectionAttribute::kRecvonly); + GetAnswer().SetDirection(SdpDirectionAttribute::kSendonly); + OfferAnswer(); + CheckOffEncodingCount(0); + CheckAnsEncodingCount(1); +} + +TEST_F(JsepTrackTest, VideoOffRecvonlyAnsSendonly) { + Init(SdpMediaSection::kVideo); + GetOffer().SetDirection(SdpDirectionAttribute::kRecvonly); + GetAnswer().SetDirection(SdpDirectionAttribute::kSendonly); + OfferAnswer(); + CheckOffEncodingCount(0); + CheckAnsEncodingCount(1); +} + +TEST_F(JsepTrackTest, AudioOffSendrecvAnsSendonly) { + Init(SdpMediaSection::kAudio); + GetAnswer().SetDirection(SdpDirectionAttribute::kSendonly); + OfferAnswer(); + CheckOffEncodingCount(0); + CheckAnsEncodingCount(1); +} + +TEST_F(JsepTrackTest, VideoOffSendrecvAnsSendonly) { + Init(SdpMediaSection::kVideo); + GetAnswer().SetDirection(SdpDirectionAttribute::kSendonly); + OfferAnswer(); + CheckOffEncodingCount(0); + CheckAnsEncodingCount(1); +} + +TEST_F(JsepTrackTest, DataChannelDraft05) { + mOffCodecs = MakeCodecs(false, false, false); + mAnsCodecs = MakeCodecs(false, false, false); + InitTracks(SdpMediaSection::kApplication); + + mOffer.reset(new SipccSdp(SdpOrigin("", 0, 0, sdp::kIPv4, ""))); + mOffer->AddMediaSection(SdpMediaSection::kApplication, + SdpDirectionAttribute::kSendrecv, 0, + SdpMediaSection::kDtlsSctp, sdp::kIPv4, "0.0.0.0"); + mAnswer.reset(new SipccSdp(SdpOrigin("", 0, 0, sdp::kIPv4, ""))); + mAnswer->AddMediaSection(SdpMediaSection::kApplication, + SdpDirectionAttribute::kSendrecv, 0, + SdpMediaSection::kDtlsSctp, sdp::kIPv4, "0.0.0.0"); + + OfferAnswer(); + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); + + ASSERT_NE(std::string::npos, + mOffer->ToString().find("a=sctpmap:5999 webrtc-datachannel 256")); + ASSERT_NE(std::string::npos, + mAnswer->ToString().find("a=sctpmap:5999 webrtc-datachannel 256")); + // Note: this is testing for a workaround, see bug 1335262 for details + ASSERT_NE(std::string::npos, + mOffer->ToString().find("a=max-message-size:499")); + ASSERT_NE(std::string::npos, + mAnswer->ToString().find("a=max-message-size:499")); + ASSERT_EQ(std::string::npos, mOffer->ToString().find("a=sctp-port")); + ASSERT_EQ(std::string::npos, mAnswer->ToString().find("a=sctp-port")); +} + +TEST_F(JsepTrackTest, DataChannelDraft21) { + Init(SdpMediaSection::kApplication); + OfferAnswer(); + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); + + ASSERT_NE(std::string::npos, mOffer->ToString().find("a=sctp-port:5999")); + ASSERT_NE(std::string::npos, mAnswer->ToString().find("a=sctp-port:5999")); + ASSERT_NE(std::string::npos, + mOffer->ToString().find("a=max-message-size:499")); + ASSERT_NE(std::string::npos, + mAnswer->ToString().find("a=max-message-size:499")); + ASSERT_EQ(std::string::npos, mOffer->ToString().find("a=sctpmap")); + ASSERT_EQ(std::string::npos, mAnswer->ToString().find("a=sctpmap")); +} + +TEST_F(JsepTrackTest, DataChannelDraft21AnswerWithDifferentPort) { + mOffCodecs = MakeCodecs(false, false, false); + mAnsCodecs = MakeCodecs(false, false, false); + + mOffCodecs.pop_back(); + mOffCodecs.emplace_back(new JsepApplicationCodecDescription( + "webrtc-datachannel", 256, 4555, 10544)); + + InitTracks(SdpMediaSection::kApplication); + InitSdp(SdpMediaSection::kApplication); + + OfferAnswer(); + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); + + ASSERT_NE(std::string::npos, mOffer->ToString().find("a=sctp-port:4555")); + ASSERT_NE(std::string::npos, mAnswer->ToString().find("a=sctp-port:5999")); + ASSERT_NE(std::string::npos, + mOffer->ToString().find("a=max-message-size:10544")); + ASSERT_NE(std::string::npos, + mAnswer->ToString().find("a=max-message-size:499")); + ASSERT_EQ(std::string::npos, mOffer->ToString().find("a=sctpmap")); + ASSERT_EQ(std::string::npos, mAnswer->ToString().find("a=sctpmap")); +} + +TEST_F(JsepTrackTest, SimulcastRejected) { + Init(SdpMediaSection::kVideo); + std::vector<std::string> rids; + rids.push_back("foo"); + rids.push_back("bar"); + mSendOff.SetRids(rids); + OfferAnswer(); + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); +} + +TEST_F(JsepTrackTest, SimulcastPrevented) { + Init(SdpMediaSection::kVideo); + std::vector<std::string> rids; + rids.push_back("foo"); + rids.push_back("bar"); + mSendAns.SetRids(rids); + OfferAnswer(); + CheckOffEncodingCount(1); + CheckAnsEncodingCount(1); +} + +TEST_F(JsepTrackTest, SimulcastOfferer) { + Init(SdpMediaSection::kVideo); + std::vector<std::string> rids; + rids.push_back("foo"); + rids.push_back("bar"); + mSendOff.SetRids(rids); + CreateOffer(); + CreateAnswer(); + // Add simulcast/rid to answer + mRecvAns.AddToMsection(rids, sdp::kRecv, mSsrcGenerator, false, &GetAnswer()); + Negotiate(); + ASSERT_TRUE(mSendOff.GetNegotiatedDetails()); + ASSERT_EQ(2U, mSendOff.GetNegotiatedDetails()->GetEncodingCount()); + ASSERT_EQ("foo", mSendOff.GetNegotiatedDetails()->GetEncoding(0).mRid); + ASSERT_EQ("bar", mSendOff.GetNegotiatedDetails()->GetEncoding(1).mRid); + ASSERT_NE(std::string::npos, + mOffer->ToString().find("a=simulcast:send foo;bar")); + ASSERT_NE(std::string::npos, + mAnswer->ToString().find("a=simulcast:recv foo;bar")); + ASSERT_NE(std::string::npos, mOffer->ToString().find("a=rid:foo send")); + ASSERT_NE(std::string::npos, mOffer->ToString().find("a=rid:bar send")); + ASSERT_NE(std::string::npos, mAnswer->ToString().find("a=rid:foo recv")); + ASSERT_NE(std::string::npos, mAnswer->ToString().find("a=rid:bar recv")); +} + +TEST_F(JsepTrackTest, SimulcastOffererWithRtx) { + Init(SdpMediaSection::kVideo); + std::vector<std::string> rids; + rids.push_back("foo"); + rids.push_back("bar"); + rids.push_back("pop"); + mSendOff.SetRids(rids); + mSendOff.AddToMsection(rids, sdp::kSend, mSsrcGenerator, true, &GetOffer()); + mRecvOff.AddToMsection(rids, sdp::kSend, mSsrcGenerator, true, &GetOffer()); + CreateAnswer(); + // Add simulcast/rid to answer + mRecvAns.AddToMsection(rids, sdp::kRecv, mSsrcGenerator, false, &GetAnswer()); + Negotiate(); + + ASSERT_EQ(3U, mSendOff.GetSsrcs().size()); + const auto posSsrc0 = + mOffer->ToString().find(std::to_string(mSendOff.GetSsrcs()[0])); + const auto posSsrc1 = + mOffer->ToString().find(std::to_string(mSendOff.GetSsrcs()[1])); + const auto posSsrc2 = + mOffer->ToString().find(std::to_string(mSendOff.GetSsrcs()[2])); + ASSERT_NE(std::string::npos, posSsrc0); + ASSERT_NE(std::string::npos, posSsrc1); + ASSERT_NE(std::string::npos, posSsrc2); + ASSERT_GT(posSsrc1, posSsrc0); + ASSERT_GT(posSsrc2, posSsrc0); + ASSERT_GT(posSsrc2, posSsrc1); +} + +TEST_F(JsepTrackTest, SimulcastAnswerer) { + Init(SdpMediaSection::kVideo); + std::vector<std::string> rids; + rids.push_back("foo"); + rids.push_back("bar"); + mSendAns.SetRids(rids); + CreateOffer(); + // Add simulcast/rid to offer + mRecvOff.AddToMsection(rids, sdp::kRecv, mSsrcGenerator, false, &GetOffer()); + CreateAnswer(); + Negotiate(); + ASSERT_TRUE(mSendAns.GetNegotiatedDetails()); + ASSERT_EQ(2U, mSendAns.GetNegotiatedDetails()->GetEncodingCount()); + ASSERT_EQ("foo", mSendAns.GetNegotiatedDetails()->GetEncoding(0).mRid); + ASSERT_EQ("bar", mSendAns.GetNegotiatedDetails()->GetEncoding(1).mRid); + ASSERT_NE(std::string::npos, + mOffer->ToString().find("a=simulcast:recv foo;bar")); + ASSERT_NE(std::string::npos, + mAnswer->ToString().find("a=simulcast:send foo;bar")); + ASSERT_NE(std::string::npos, mOffer->ToString().find("a=rid:foo recv")); + ASSERT_NE(std::string::npos, mOffer->ToString().find("a=rid:bar recv")); + ASSERT_NE(std::string::npos, mAnswer->ToString().find("a=rid:foo send")); + ASSERT_NE(std::string::npos, mAnswer->ToString().find("a=rid:bar send")); +} + +#define VERIFY_OPUS_MAX_PLAYBACK_RATE(track, expectedRate) \ + { \ + JsepTrack& copy(track); \ + ASSERT_TRUE(copy.GetNegotiatedDetails()); \ + ASSERT_TRUE(copy.GetNegotiatedDetails()->GetEncodingCount()); \ + for (const auto& codec : \ + copy.GetNegotiatedDetails()->GetEncoding(0).GetCodecs()) { \ + if (codec->mName == "opus") { \ + JsepAudioCodecDescription& audioCodec = \ + static_cast<JsepAudioCodecDescription&>(*codec); \ + ASSERT_EQ((expectedRate), audioCodec.mMaxPlaybackRate); \ + } \ + }; \ + } + +#define VERIFY_OPUS_FORCE_MONO(track, expected) \ + { \ + JsepTrack& copy(track); \ + ASSERT_TRUE(copy.GetNegotiatedDetails()); \ + ASSERT_TRUE(copy.GetNegotiatedDetails()->GetEncodingCount()); \ + for (const auto& codec : \ + copy.GetNegotiatedDetails()->GetEncoding(0).GetCodecs()) { \ + if (codec->mName == "opus") { \ + JsepAudioCodecDescription& audioCodec = \ + static_cast<JsepAudioCodecDescription&>(*codec); \ + /* gtest has some compiler warnings when using ASSERT_EQ with \ + * booleans. */ \ + ASSERT_EQ((int)(expected), (int)audioCodec.mForceMono); \ + } \ + }; \ + } + +TEST_F(JsepTrackTest, DefaultOpusParameters) { + Init(SdpMediaSection::kAudio); + OfferAnswer(); + + VERIFY_OPUS_MAX_PLAYBACK_RATE( + mSendOff, SdpFmtpAttributeList::OpusParameters::kDefaultMaxPlaybackRate); + VERIFY_OPUS_MAX_PLAYBACK_RATE( + mSendAns, SdpFmtpAttributeList::OpusParameters::kDefaultMaxPlaybackRate); + VERIFY_OPUS_MAX_PLAYBACK_RATE(mRecvOff, 0U); + VERIFY_OPUS_FORCE_MONO(mRecvOff, false); + VERIFY_OPUS_MAX_PLAYBACK_RATE(mRecvAns, 0U); + VERIFY_OPUS_FORCE_MONO(mRecvAns, false); +} + +TEST_F(JsepTrackTest, NonDefaultOpusParameters) { + InitCodecs(); + for (auto& codec : mAnsCodecs) { + if (codec->mName == "opus") { + JsepAudioCodecDescription* audioCodec = + static_cast<JsepAudioCodecDescription*>(codec.get()); + audioCodec->mMaxPlaybackRate = 16000; + audioCodec->mForceMono = true; + } + } + InitTracks(SdpMediaSection::kAudio); + InitSdp(SdpMediaSection::kAudio); + OfferAnswer(); + + VERIFY_OPUS_MAX_PLAYBACK_RATE(mSendOff, 16000U); + VERIFY_OPUS_FORCE_MONO(mSendOff, true); + VERIFY_OPUS_MAX_PLAYBACK_RATE( + mSendAns, SdpFmtpAttributeList::OpusParameters::kDefaultMaxPlaybackRate); + VERIFY_OPUS_FORCE_MONO(mSendAns, false); + VERIFY_OPUS_MAX_PLAYBACK_RATE(mRecvOff, 0U); + VERIFY_OPUS_FORCE_MONO(mRecvOff, false); + VERIFY_OPUS_MAX_PLAYBACK_RATE(mRecvAns, 16000U); + VERIFY_OPUS_FORCE_MONO(mRecvAns, true); +} + +TEST_F(JsepTrackTest, RtcpFbWithPayloadTypeAsymmetry) { + std::vector<std::string> expectedAckFbTypes; + std::vector<std::string> expectedNackFbTypes{"", "pli"}; + std::vector<std::string> expectedCcmFbTypes{"fir"}; + std::vector<SdpRtcpFbAttributeList::Feedback> expectedOtherFbTypes{ + {"", SdpRtcpFbAttributeList::kRemb, "", ""}, + {"", SdpRtcpFbAttributeList::kTransportCC, "", ""}}; + + InitCodecs(); + + // On offerer, configure to support remb and transport-cc on video codecs + for (auto& codec : mOffCodecs) { + if (codec->Type() == SdpMediaSection::kVideo) { + auto& videoCodec = static_cast<JsepVideoCodecDescription&>(*codec); + videoCodec.EnableRemb(); + videoCodec.EnableTransportCC(); + } + } + + InitTracks(SdpMediaSection::kVideo); + InitSdp(SdpMediaSection::kVideo); + + CreateOffer(); + // We do not bother trying to bamboozle the answerer into doing asymmetric + // payload types, we just use a raw SDP. + const std::string answer = + "v=0\r\n" + "o=- 0 0 IN IP4 127.0.0.1\r\n" + "s=-\r\n" + "t=0 0\r\n" + "a=msid-semantic:WMS *\r\n" + "m=video 0 UDP/TLS/RTP/SAVPF 136\r\n" + "c=IN IP4 0.0.0.0\r\n" + "a=sendrecv\r\n" + "a=fmtp:136 " + "profile-level-id=42e00d;level-asymmetry-allowed=1;packetization-mode=" + "1\r\n" + "a=msid:stream_id\r\n" + "a=rtcp-fb:136 nack\r\n" + "a=rtcp-fb:136 nack pli\r\n" + "a=rtcp-fb:136 ccm fir\r\n" + "a=rtcp-fb:136 goog-remb\r\n" + "a=rtcp-fb:136 transport-cc\r\n" + "a=rtpmap:136 H264/90000\r\n" + "a=ssrc:2025549043 cname:\r\n"; + + UniquePtr<SdpParser> parser(new SipccSdpParser); + mAnswer = std::move(parser->Parse(answer)->Sdp()); + ASSERT_TRUE(mAnswer); + + mRecvOff.RecvTrackSetRemote(*mAnswer, GetAnswer()); + mRecvOff.Negotiate(GetAnswer(), GetAnswer(), GetOffer()); + mSendOff.Negotiate(GetAnswer(), GetAnswer(), GetOffer()); + + ASSERT_TRUE(mSendOff.GetNegotiatedDetails()); + ASSERT_TRUE(mRecvOff.GetNegotiatedDetails()); + + UniquePtr<JsepVideoCodecDescription> codec; + ASSERT_TRUE((codec = GetVideoCodec(mSendOff))); + ASSERT_EQ("136", codec->mDefaultPt) + << "Offerer should have seen answer asymmetry!"; + ASSERT_TRUE((codec = GetVideoCodec(mRecvOff))); + ASSERT_EQ("126", codec->mDefaultPt); + ASSERT_EQ(expectedAckFbTypes, codec->mAckFbTypes); + ASSERT_EQ(expectedNackFbTypes, codec->mNackFbTypes); + ASSERT_EQ(expectedCcmFbTypes, codec->mCcmFbTypes); + ASSERT_EQ(expectedOtherFbTypes, codec->mOtherFbTypes); +} + +TEST_F(JsepTrackTest, AudioSdpFmtpLine) { + mOffCodecs = MakeCodecs(true, true, true); + mAnsCodecs = MakeCodecs(true, true, true); + InitTracks(SdpMediaSection::kAudio); + InitSdp(SdpMediaSection::kAudio); + OfferAnswer(); + + // SanityCheck checks that the sdpFmtpLine for a local codec matches that of + // the corresponding remote codec. + UniquePtr<JsepAudioCodecDescription> codec; + EXPECT_TRUE((codec = GetAudioCodec(mSendOff, 3, 0))); + EXPECT_EQ("opus", codec->mName); + EXPECT_EQ("maxplaybackrate=48000;stereo=1;useinbandfec=0", + codec->mSdpFmtpLine.valueOr("nothing")); + EXPECT_TRUE((codec = GetAudioCodec(mSendAns, 3, 0))); + EXPECT_EQ("opus", codec->mName); + EXPECT_EQ("maxplaybackrate=48000;stereo=1;useinbandfec=0", + codec->mSdpFmtpLine.valueOr("nothing")); + + EXPECT_TRUE((codec = GetAudioCodec(mSendOff, 3, 1))); + EXPECT_EQ("G722", codec->mName); + EXPECT_EQ("nothing", codec->mSdpFmtpLine.valueOr("nothing")); + EXPECT_TRUE((codec = GetAudioCodec(mSendAns, 3, 1))); + EXPECT_EQ("G722", codec->mName); + EXPECT_EQ("nothing", codec->mSdpFmtpLine.valueOr("nothing")); + + EXPECT_TRUE((codec = GetAudioCodec(mSendOff, 3, 2))); + EXPECT_EQ("telephone-event", codec->mName); + EXPECT_EQ("0-15", codec->mSdpFmtpLine.valueOr("nothing")); + EXPECT_TRUE((codec = GetAudioCodec(mSendAns, 3, 2))); + EXPECT_EQ("telephone-event", codec->mName); + EXPECT_EQ("0-15", codec->mSdpFmtpLine.valueOr("nothing")); +} + +TEST_F(JsepTrackTest, NonDefaultAudioSdpFmtpLine) { + mOffCodecs = MakeCodecs(true, true, true); + mAnsCodecs = MakeCodecs(true, true, true); + + for (auto& codec : mOffCodecs) { + if (codec->mName == "opus") { + auto* audio = static_cast<JsepAudioCodecDescription*>(codec.get()); + audio->mForceMono = true; + audio->mMaxPlaybackRate = 32000; + } + } + + for (auto& codec : mAnsCodecs) { + if (codec->mName == "opus") { + auto* audio = static_cast<JsepAudioCodecDescription*>(codec.get()); + audio->mFECEnabled = true; + audio->mCbrEnabled = true; + audio->mDTXEnabled = true; + audio->mFrameSizeMs = 10; + audio->mMinFrameSizeMs = 5; + audio->mMaxFrameSizeMs = 20; + } + } + + InitTracks(SdpMediaSection::kAudio); + InitSdp(SdpMediaSection::kAudio); + + { + // telephone-event doesn't store any params in JsepAudioCodecDescription. + // Set them directly in the offer sdp instead. + auto params = MakeUnique<SdpFmtpAttributeList::TelephoneEventParameters>(); + params->dtmfTones = "2-9"; + GetOffer().SetFmtp({"101", *params}); + } + + { + // telephone-event doesn't store any params in JsepAudioCodecDescription. + // Set them directly in the answer sdp instead. + auto params = MakeUnique<SdpFmtpAttributeList::TelephoneEventParameters>(); + params->dtmfTones = "0-3,10"; + GetAnswer().SetFmtp({"101", *params}); + } + + OfferAnswer(); + + // SanityCheck checks that the sdpFmtpLine for a local codec matches that of + // the corresponding remote codec. + UniquePtr<JsepAudioCodecDescription> codec; + EXPECT_TRUE((codec = GetAudioCodec(mSendOff, 3, 0))); + EXPECT_EQ("opus", codec->mName); + EXPECT_EQ( + "maxplaybackrate=48000;stereo=1;useinbandfec=1;usedtx=1;ptime=10;" + "minptime=5;maxptime=20;cbr=1", + codec->mSdpFmtpLine.valueOr("nothing")); + EXPECT_TRUE((codec = GetAudioCodec(mSendAns, 3, 0))); + EXPECT_EQ("opus", codec->mName); + EXPECT_EQ("maxplaybackrate=32000;stereo=0;useinbandfec=0", + codec->mSdpFmtpLine.valueOr("nothing")); + + EXPECT_TRUE((codec = GetAudioCodec(mSendOff, 3, 1))); + EXPECT_EQ("G722", codec->mName); + EXPECT_EQ("nothing", codec->mSdpFmtpLine.valueOr("nothing")); + EXPECT_TRUE((codec = GetAudioCodec(mSendAns, 3, 1))); + EXPECT_EQ("G722", codec->mName); + EXPECT_EQ("nothing", codec->mSdpFmtpLine.valueOr("nothing")); + + EXPECT_TRUE((codec = GetAudioCodec(mSendOff, 3, 2))); + EXPECT_EQ("telephone-event", codec->mName); + EXPECT_EQ("0-3,10", codec->mSdpFmtpLine.valueOr("nothing")); + EXPECT_TRUE((codec = GetAudioCodec(mSendAns, 3, 2))); + EXPECT_EQ("telephone-event", codec->mName); + EXPECT_EQ("2-9", codec->mSdpFmtpLine.valueOr("nothing")); +} + +TEST_F(JsepTrackTest, VideoSdpFmtpLine) { + mOffCodecs = MakeCodecs(true, true, true); + mAnsCodecs = MakeCodecs(true, true, true); + InitTracks(SdpMediaSection::kVideo); + InitSdp(SdpMediaSection::kVideo); + OfferAnswer(); + + // SanityCheck checks that the sdpFmtpLine for a local codec matches that of + // the corresponding remote codec. + UniquePtr<JsepVideoCodecDescription> codec; + EXPECT_TRUE((codec = GetVideoCodec(mSendOff, 4, 0))); + EXPECT_EQ("red", codec->mName); + EXPECT_EQ("120/126/123", codec->mSdpFmtpLine.valueOr("nothing")); + EXPECT_TRUE((codec = GetVideoCodec(mSendAns, 4, 0))); + EXPECT_EQ("red", codec->mName); + EXPECT_EQ("120/126/123", codec->mSdpFmtpLine.valueOr("nothing")); + + EXPECT_TRUE((codec = GetVideoCodec(mSendOff, 4, 1))); + EXPECT_EQ("VP8", codec->mName); + EXPECT_EQ("max-fs=12288;max-fr=60", codec->mSdpFmtpLine.valueOr("nothing")); + EXPECT_TRUE((codec = GetVideoCodec(mSendAns, 4, 1))); + EXPECT_EQ("VP8", codec->mName); + EXPECT_EQ("max-fs=12288;max-fr=60", codec->mSdpFmtpLine.valueOr("nothing")); + + EXPECT_TRUE((codec = GetVideoCodec(mSendOff, 4, 2))); + EXPECT_EQ("H264", codec->mName); + EXPECT_EQ( + "profile-level-id=42e00d;level-asymmetry-allowed=1;packetization-mode=1", + codec->mSdpFmtpLine.valueOr("nothing")); + EXPECT_TRUE((codec = GetVideoCodec(mSendAns, 4, 2))); + EXPECT_EQ("H264", codec->mName); + EXPECT_EQ( + "profile-level-id=42e00d;level-asymmetry-allowed=1;packetization-mode=1", + codec->mSdpFmtpLine.valueOr("nothing")); + + EXPECT_TRUE((codec = GetVideoCodec(mSendOff, 4, 3))); + EXPECT_EQ("ulpfec", codec->mName); + EXPECT_EQ("nothing", codec->mSdpFmtpLine.valueOr("nothing")); + EXPECT_TRUE((codec = GetVideoCodec(mSendAns, 4, 3))); + EXPECT_EQ("ulpfec", codec->mName); + EXPECT_EQ("nothing", codec->mSdpFmtpLine.valueOr("nothing")); +} + +TEST_F(JsepTrackTest, NonDefaultVideoSdpFmtpLine) { + mOffCodecs = MakeCodecs(true, true, true); + mAnsCodecs = MakeCodecs(true, true, true); + + for (auto& codec : mOffCodecs) { + if (codec->mName == "VP8" || codec->mName == "H264") { + auto* video = static_cast<JsepVideoCodecDescription*>(codec.get()); + video->mConstraints.maxFs = 1200; + if (codec->mName == "VP8") { + video->mConstraints.maxFps = Some(15); + } else { + video->mConstraints.maxDpb = 6400; + video->mConstraints.maxBr = 1000; + JsepVideoCodecDescription::SetSaneH264Level(0x1F0, + &video->mProfileLevelId); + } + } + } + + for (auto& codec : mAnsCodecs) { + if (codec->mName == "VP8" || codec->mName == "H264") { + auto* video = static_cast<JsepVideoCodecDescription*>(codec.get()); + video->mConstraints.maxFs = 32400; + if (codec->mName == "VP8") { + video->mConstraints.maxFps = Some(60); + } else { + video->mConstraints.maxMbps = 1944000; + video->mConstraints.maxCpb = 800000; + video->mConstraints.maxDpb = 128000; + JsepVideoCodecDescription::SetSaneH264Level(0xAB, + &video->mProfileLevelId); + video->mPacketizationMode = 1; + } + } + } + + InitTracks(SdpMediaSection::kVideo); + InitSdp(SdpMediaSection::kVideo); + OfferAnswer(); + + // SanityCheck checks that the sdpFmtpLine for a local codec matches that of + // the corresponding remote codec. + UniquePtr<JsepVideoCodecDescription> codec; + EXPECT_TRUE((codec = GetVideoCodec(mSendOff, 4, 0))); + EXPECT_EQ("red", codec->mName); + EXPECT_EQ("120/126/123", codec->mSdpFmtpLine.valueOr("nothing")); + EXPECT_TRUE((codec = GetVideoCodec(mSendAns, 4, 0))); + EXPECT_EQ("red", codec->mName); + EXPECT_EQ("120/126/123", codec->mSdpFmtpLine.valueOr("nothing")); + + EXPECT_TRUE((codec = GetVideoCodec(mSendOff, 4, 1))); + EXPECT_EQ("VP8", codec->mName); + EXPECT_EQ("max-fs=32400;max-fr=60", codec->mSdpFmtpLine.valueOr("nothing")); + EXPECT_TRUE((codec = GetVideoCodec(mSendAns, 4, 1))); + EXPECT_EQ("VP8", codec->mName); + EXPECT_EQ("max-fs=1200;max-fr=15", codec->mSdpFmtpLine.valueOr("nothing")); + + EXPECT_TRUE((codec = GetVideoCodec(mSendOff, 4, 2))); + EXPECT_EQ("H264", codec->mName); + EXPECT_EQ( + "profile-level-id=42f00b;level-asymmetry-allowed=1;packetization-mode=1;" + "max-mbps=1944000;max-fs=32400;max-cpb=800000;max-dpb=128000", + codec->mSdpFmtpLine.valueOr("nothing")); + EXPECT_TRUE((codec = GetVideoCodec(mSendAns, 4, 2))); + EXPECT_EQ("H264", codec->mName); + EXPECT_EQ( + "profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode=1;" + "max-fs=1200;max-dpb=6400;max-br=1000", + codec->mSdpFmtpLine.valueOr("nothing")); + + EXPECT_TRUE((codec = GetVideoCodec(mSendOff, 4, 3))); + EXPECT_EQ("ulpfec", codec->mName); + EXPECT_EQ("nothing", codec->mSdpFmtpLine.valueOr("nothing")); + EXPECT_TRUE((codec = GetVideoCodec(mSendAns, 4, 3))); + EXPECT_EQ("ulpfec", codec->mName); + EXPECT_EQ("nothing", codec->mSdpFmtpLine.valueOr("nothing")); +} + +} // namespace mozilla diff --git a/media/webrtc/signaling/gtest/mediapipeline_unittest.cpp b/media/webrtc/signaling/gtest/mediapipeline_unittest.cpp new file mode 100644 index 0000000000..2ee6e96ef4 --- /dev/null +++ b/media/webrtc/signaling/gtest/mediapipeline_unittest.cpp @@ -0,0 +1,697 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Original author: ekr@rtfm.com + +#include "logging.h" +#include "nss.h" +#include "ssl.h" + +#include "api/audio_codecs/builtin_audio_decoder_factory.h" +#include "api/scoped_refptr.h" +#include "AudioSegment.h" +#include "Canonicals.h" +#include "modules/audio_device/include/fake_audio_device.h" +#include "modules/audio_mixer/audio_mixer_impl.h" +#include "modules/audio_processing/include/audio_processing.h" +#include "mozilla/Mutex.h" +#include "mozilla/RefPtr.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "MediaConduitInterface.h" +#include "MediaPipeline.h" +#include "MediaPipelineFilter.h" +#include "MediaTrackGraph.h" +#include "MediaTrackListener.h" +#include "TaskQueueWrapper.h" +#include "mtransport_test_utils.h" +#include "SharedBuffer.h" +#include "MediaTransportHandler.h" +#include "WebrtcCallWrapper.h" +#include "PeerConnectionCtx.h" +#include "WaitFor.h" + +#define GTEST_HAS_RTTI 0 +#include "gtest/gtest.h" + +using namespace mozilla; +MOZ_MTLOG_MODULE("transportbridge") + +static MtransportTestUtils* test_utils; + +namespace { +class MainAsCurrent : public TaskQueueWrapper<DeletionPolicy::NonBlocking> { + public: + MainAsCurrent() + : TaskQueueWrapper( + TaskQueue::Create(do_AddRef(GetMainThreadSerialEventTarget()), + "MainAsCurrentTaskQueue"), + "MainAsCurrent"_ns), + mSetter(this) { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + } + + ~MainAsCurrent() = default; + + private: + CurrentTaskQueueSetter mSetter; +}; + +class FakeAudioTrack : public ProcessedMediaTrack { + public: + FakeAudioTrack() + : ProcessedMediaTrack(44100, MediaSegment::AUDIO, nullptr), + mMutex("Fake AudioTrack") { + NS_NewTimerWithFuncCallback( + getter_AddRefs(mTimer), FakeAudioTrackGenerateData, this, 20, + nsITimer::TYPE_REPEATING_SLACK, + "FakeAudioTrack::FakeAudioTrackGenerateData", test_utils->sts_target()); + } + + void Destroy() override { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(!mMainThreadDestroyed); + mMainThreadDestroyed = true; + mTimer->Cancel(); + mTimer = nullptr; + } + + void QueueSetAutoend(bool) override {} + + void AddListener(MediaTrackListener* aListener) override { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(!mListener); + mListener = aListener; + } + + RefPtr<GenericPromise> RemoveListener( + MediaTrackListener* aListener) override { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mListener == aListener); + mListener = nullptr; + return GenericPromise::CreateAndResolve(true, __func__); + } + + void ProcessInput(GraphTime aFrom, GraphTime aTo, uint32_t aFlags) override {} + + uint32_t NumberOfChannels() const override { return NUM_CHANNELS; } + + private: + Mutex mMutex MOZ_UNANNOTATED; + MediaTrackListener* mListener = nullptr; + nsCOMPtr<nsITimer> mTimer; + int mCount = 0; + + static const int AUDIO_BUFFER_SIZE = 1600; + static const int NUM_CHANNELS = 2; + static void FakeAudioTrackGenerateData(nsITimer* timer, void* closure) { + auto t = static_cast<FakeAudioTrack*>(closure); + MutexAutoLock lock(t->mMutex); + if (t->mMainThreadDestroyed) { + return; + } + CheckedInt<size_t> bufferSize(sizeof(int16_t)); + bufferSize *= NUM_CHANNELS; + bufferSize *= AUDIO_BUFFER_SIZE; + RefPtr<SharedBuffer> samples = SharedBuffer::Create(bufferSize); + int16_t* data = reinterpret_cast<int16_t*>(samples->Data()); + for (int i = 0; i < (AUDIO_BUFFER_SIZE * NUM_CHANNELS); i++) { + // saw tooth audio sample + data[i] = ((t->mCount % 8) * 4000) - (7 * 4000) / 2; + t->mCount++; + } + + AudioSegment segment; + AutoTArray<const int16_t*, 1> channels; + channels.AppendElement(data); + segment.AppendFrames(samples.forget(), channels, AUDIO_BUFFER_SIZE, + PRINCIPAL_HANDLE_NONE); + + if (t->mListener) { + t->mListener->NotifyQueuedChanges(nullptr, 0, segment); + } + } +}; + +template <typename Function> +void RunOnSts(Function&& aFunction) { + MOZ_ALWAYS_SUCCEEDS(test_utils->SyncDispatchToSTS( + NS_NewRunnableFunction(__func__, [&] { aFunction(); }))); +} + +class LoopbackTransport : public MediaTransportHandler { + public: + LoopbackTransport() : MediaTransportHandler(nullptr) { + RunOnSts([&] { + SetState("mux", TransportLayer::TS_INIT); + SetRtcpState("mux", TransportLayer::TS_INIT); + SetState("non-mux", TransportLayer::TS_INIT); + SetRtcpState("non-mux", TransportLayer::TS_INIT); + }); + } + + static void InitAndConnect(LoopbackTransport& client, + LoopbackTransport& server) { + client.Connect(&server); + server.Connect(&client); + } + + void Connect(LoopbackTransport* peer) { peer_ = peer; } + + void Shutdown() { peer_ = nullptr; } + + RefPtr<IceLogPromise> GetIceLog(const nsCString& aPattern) override { + return nullptr; + } + + void ClearIceLog() override {} + void EnterPrivateMode() override {} + void ExitPrivateMode() override {} + + void CreateIceCtx(const std::string& aName) override {} + + nsresult SetIceConfig(const nsTArray<dom::RTCIceServer>& aIceServers, + dom::RTCIceTransportPolicy aIcePolicy) override { + return NS_OK; + } + + void Destroy() override {} + + // We will probably be able to move the proxy lookup stuff into + // this class once we move mtransport to its own process. + void SetProxyConfig(NrSocketProxyConfig&& aProxyConfig) override {} + + void EnsureProvisionalTransport(const std::string& aTransportId, + const std::string& aLocalUfrag, + const std::string& aLocalPwd, + int aComponentCount) override {} + + void SetTargetForDefaultLocalAddressLookup(const std::string& aTargetIp, + uint16_t aTargetPort) override {} + + // We set default-route-only as late as possible because it depends on what + // capture permissions have been granted on the window, which could easily + // change between Init (ie; when the PC is created) and StartIceGathering + // (ie; when we set the local description). + void StartIceGathering(bool aDefaultRouteOnly, bool aObfuscateAddresses, + // TODO: It probably makes sense to look + // this up internally + const nsTArray<NrIceStunAddr>& aStunAddrs) override {} + + void ActivateTransport( + const std::string& aTransportId, const std::string& aLocalUfrag, + const std::string& aLocalPwd, size_t aComponentCount, + const std::string& aUfrag, const std::string& aPassword, + const nsTArray<uint8_t>& aKeyDer, const nsTArray<uint8_t>& aCertDer, + SSLKEAType aAuthType, bool aDtlsClient, const DtlsDigestList& aDigests, + bool aPrivacyRequested) override {} + + void RemoveTransportsExcept( + const std::set<std::string>& aTransportIds) override {} + + void StartIceChecks(bool aIsControlling, + const std::vector<std::string>& aIceOptions) override {} + + void AddIceCandidate(const std::string& aTransportId, + const std::string& aCandidate, const std::string& aUfrag, + const std::string& aObfuscatedAddress) override {} + + void UpdateNetworkState(bool aOnline) override {} + + RefPtr<dom::RTCStatsPromise> GetIceStats(const std::string& aTransportId, + DOMHighResTimeStamp aNow) override { + return nullptr; + } + + void SendPacket(const std::string& aTransportId, + MediaPacket&& aPacket) override { + peer_->SignalPacketReceived(aTransportId, aPacket); + } + + void SetState(const std::string& aTransportId, TransportLayer::State aState) { + MediaTransportHandler::OnStateChange(aTransportId, aState); + } + + void SetRtcpState(const std::string& aTransportId, + TransportLayer::State aState) { + MediaTransportHandler::OnRtcpStateChange(aTransportId, aState); + } + + private: + RefPtr<MediaTransportHandler> peer_; +}; + +class TestAgent { + public: + explicit TestAgent(const RefPtr<SharedWebrtcState>& aSharedState) + : control_(aSharedState->mCallWorkerThread), + audio_config_(109, "opus", 48000, 2, false), + call_(WebrtcCallWrapper::Create( + mozilla::dom::RTCStatsTimestampMaker::Create(), nullptr, + aSharedState)), + audio_conduit_( + AudioSessionConduit::Create(call_, test_utils->sts_target())), + audio_pipeline_(), + transport_(new LoopbackTransport) { + Unused << WaitFor(InvokeAsync(call_->mCallThread, __func__, [&] { + audio_conduit_->InitControl(&control_); + return GenericPromise::CreateAndResolve(true, "TestAgent()"); + })); + } + + static void Connect(TestAgent* client, TestAgent* server) { + LoopbackTransport::InitAndConnect(*client->transport_, *server->transport_); + } + + virtual void CreatePipeline(const std::string& aTransportId) = 0; + + void SetState_s(const std::string& aTransportId, + TransportLayer::State aState) { + transport_->SetState(aTransportId, aState); + } + + void SetRtcpState_s(const std::string& aTransportId, + TransportLayer::State aState) { + transport_->SetRtcpState(aTransportId, aState); + } + + void UpdateTransport_s(const std::string& aTransportId, + UniquePtr<MediaPipelineFilter>&& aFilter) { + audio_pipeline_->UpdateTransport_s(aTransportId, std::move(aFilter)); + } + + void Stop() { + MOZ_MTLOG(ML_DEBUG, "Stopping"); + + control_.Update([](auto& aControl) { + aControl.mTransmitting = false; + aControl.mReceiving = false; + }); + } + + void Shutdown_s() { transport_->Shutdown(); } + + void Shutdown() { + if (audio_pipeline_) { + audio_pipeline_->Shutdown(); + } + if (audio_conduit_) { + Unused << WaitFor(audio_conduit_->Shutdown()); + } + if (call_) { + call_->Destroy(); + } + if (audio_track_) { + audio_track_->Destroy(); + audio_track_ = nullptr; + } + + test_utils->SyncDispatchToSTS(WrapRunnable(this, &TestAgent::Shutdown_s)); + } + + uint32_t GetRemoteSSRC() { + return audio_conduit_->GetRemoteSSRC().valueOr(0); + } + + uint32_t GetLocalSSRC() { + std::vector<uint32_t> res; + res = audio_conduit_->GetLocalSSRCs(); + return res.empty() ? 0 : res[0]; + } + + int GetAudioRtpCountSent() { return audio_pipeline_->RtpPacketsSent(); } + + int GetAudioRtpCountReceived() { + return audio_pipeline_->RtpPacketsReceived(); + } + + int GetAudioRtcpCountSent() { return audio_pipeline_->RtcpPacketsSent(); } + + int GetAudioRtcpCountReceived() { + return audio_pipeline_->RtcpPacketsReceived(); + } + + protected: + ConcreteControl control_; + AudioCodecConfig audio_config_; + RefPtr<WebrtcCallWrapper> call_; + RefPtr<AudioSessionConduit> audio_conduit_; + RefPtr<FakeAudioTrack> audio_track_; + // TODO(bcampen@mozilla.com): Right now this does not let us test RTCP in + // both directions; only the sender's RTCP is sent, but the receiver should + // be sending it too. + RefPtr<MediaPipeline> audio_pipeline_; + RefPtr<LoopbackTransport> transport_; +}; + +class TestAgentSend : public TestAgent { + public: + explicit TestAgentSend(const RefPtr<SharedWebrtcState>& aSharedState) + : TestAgent(aSharedState) { + control_.Update([&](auto& aControl) { + aControl.mAudioSendCodec = Some(audio_config_); + }); + audio_track_ = new FakeAudioTrack(); + } + + virtual void CreatePipeline(const std::string& aTransportId) { + std::string test_pc; + + auto audio_pipeline = MakeRefPtr<MediaPipelineTransmit>( + test_pc, transport_, AbstractThread::MainThread(), + test_utils->sts_target(), false, audio_conduit_); + Unused << WaitFor(InvokeAsync(call_->mCallThread, __func__, [&] { + audio_pipeline->InitControl(&control_); + return GenericPromise::CreateAndResolve(true, __func__); + })); + + audio_pipeline->SetSendTrackOverride(audio_track_); + control_.Update([](auto& aControl) { aControl.mTransmitting = true; }); + audio_pipeline->UpdateTransport_m(aTransportId, nullptr); + audio_pipeline_ = audio_pipeline; + } +}; + +class TestAgentReceive : public TestAgent { + public: + explicit TestAgentReceive(const RefPtr<SharedWebrtcState>& aSharedState) + : TestAgent(aSharedState) { + control_.Update([&](auto& aControl) { + std::vector<AudioCodecConfig> codecs; + codecs.push_back(audio_config_); + aControl.mAudioRecvCodecs = codecs; + }); + } + + virtual void CreatePipeline(const std::string& aTransportId) { + std::string test_pc; + + auto audio_pipeline = MakeRefPtr<MediaPipelineReceiveAudio>( + test_pc, transport_, AbstractThread::MainThread(), + test_utils->sts_target(), + static_cast<AudioSessionConduit*>(audio_conduit_.get()), nullptr, + TrackingId(), PRINCIPAL_HANDLE_NONE, PrincipalPrivacy::NonPrivate); + Unused << WaitFor(InvokeAsync(call_->mCallThread, __func__, [&] { + audio_pipeline->InitControl(&control_); + return GenericPromise::CreateAndResolve(true, __func__); + })); + + control_.Update([](auto& aControl) { aControl.mReceiving = true; }); + audio_pipeline->UpdateTransport_m(aTransportId, std::move(bundle_filter_)); + audio_pipeline_ = audio_pipeline; + } + + void SetBundleFilter(UniquePtr<MediaPipelineFilter>&& filter) { + bundle_filter_ = std::move(filter); + } + + void UpdateTransport_s(const std::string& aTransportId, + UniquePtr<MediaPipelineFilter>&& filter) { + audio_pipeline_->UpdateTransport_s(aTransportId, std::move(filter)); + } + + private: + UniquePtr<MediaPipelineFilter> bundle_filter_; +}; + +void WaitFor(TimeDuration aDuration) { + bool done = false; + NS_DelayedDispatchToCurrentThread( + NS_NewRunnableFunction(__func__, [&] { done = true; }), + aDuration.ToMilliseconds()); + SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>( + "WaitFor(TimeDuration aDuration)"_ns, [&] { return done; }); +} + +webrtc::AudioState::Config CreateAudioStateConfig() { + webrtc::AudioState::Config audio_state_config; + audio_state_config.audio_mixer = webrtc::AudioMixerImpl::Create(); + webrtc::AudioProcessingBuilder audio_processing_builder; + audio_state_config.audio_processing = audio_processing_builder.Create(); + audio_state_config.audio_device_module = new webrtc::FakeAudioDeviceModule(); + return audio_state_config; +} + +class MediaPipelineTest : public ::testing::Test { + public: + MediaPipelineTest() + : main_task_queue_( + WrapUnique<TaskQueueWrapper<DeletionPolicy::NonBlocking>>( + new MainAsCurrent())), + shared_state_(MakeAndAddRef<SharedWebrtcState>( + AbstractThread::MainThread(), CreateAudioStateConfig(), + already_AddRefed( + webrtc::CreateBuiltinAudioDecoderFactory().release()), + WrapUnique(new webrtc::NoTrialsConfig()))), + p1_(shared_state_), + p2_(shared_state_) {} + + ~MediaPipelineTest() { + p1_.Shutdown(); + p2_.Shutdown(); + } + + static void SetUpTestCase() { + test_utils = new MtransportTestUtils(); + NSS_NoDB_Init(nullptr); + NSS_SetDomesticPolicy(); + } + + // Setup transport. + void InitTransports() { + test_utils->SyncDispatchToSTS( + WrapRunnableNM(&TestAgent::Connect, &p2_, &p1_)); + } + + // Verify RTP and RTCP + void TestAudioSend(bool aIsRtcpMux, + UniquePtr<MediaPipelineFilter>&& initialFilter = nullptr, + UniquePtr<MediaPipelineFilter>&& refinedFilter = nullptr, + unsigned int ms_until_filter_update = 500, + unsigned int ms_of_traffic_after_answer = 10000) { + bool bundle = !!(initialFilter); + // We do not support testing bundle without rtcp mux, since that doesn't + // make any sense. + ASSERT_FALSE(!aIsRtcpMux && bundle); + + p2_.SetBundleFilter(std::move(initialFilter)); + + // Setup transport flows + InitTransports(); + + std::string transportId = aIsRtcpMux ? "mux" : "non-mux"; + p1_.CreatePipeline(transportId); + p2_.CreatePipeline(transportId); + + // Set state of transports to CONNECTING. MediaPipeline doesn't really care + // about this transition, but we're trying to simluate what happens in a + // real case. + RunOnSts([&] { + p1_.SetState_s(transportId, TransportLayer::TS_CONNECTING); + p1_.SetRtcpState_s(transportId, TransportLayer::TS_CONNECTING); + p2_.SetState_s(transportId, TransportLayer::TS_CONNECTING); + p2_.SetRtcpState_s(transportId, TransportLayer::TS_CONNECTING); + }); + + WaitFor(TimeDuration::FromMilliseconds(10)); + + // Set state of transports to OPEN (ie; connected). This should result in + // media flowing. + RunOnSts([&] { + p1_.SetState_s(transportId, TransportLayer::TS_OPEN); + p1_.SetRtcpState_s(transportId, TransportLayer::TS_OPEN); + p2_.SetState_s(transportId, TransportLayer::TS_OPEN); + p2_.SetRtcpState_s(transportId, TransportLayer::TS_OPEN); + }); + + if (bundle) { + WaitFor(TimeDuration::FromMilliseconds(ms_until_filter_update)); + + // Leaving refinedFilter not set implies we want to just update with + // the other side's SSRC + if (!refinedFilter) { + refinedFilter = MakeUnique<MediaPipelineFilter>(); + // Might not be safe, strictly speaking. + refinedFilter->AddRemoteSSRC(p1_.GetLocalSSRC()); + } + + RunOnSts([&] { + p2_.UpdateTransport_s(transportId, std::move(refinedFilter)); + }); + } + + // wait for some RTP/RTCP tx and rx to happen + WaitFor(TimeDuration::FromMilliseconds(ms_of_traffic_after_answer)); + + p1_.Stop(); + p2_.Stop(); + + // wait for any packets in flight to arrive + WaitFor(TimeDuration::FromMilliseconds(200)); + + p1_.Shutdown(); + p2_.Shutdown(); + + if (!bundle) { + // If we are filtering, allow the test-case to do this checking. + ASSERT_GE(p1_.GetAudioRtpCountSent(), 40); + ASSERT_EQ(p1_.GetAudioRtpCountReceived(), p2_.GetAudioRtpCountSent()); + ASSERT_EQ(p1_.GetAudioRtpCountSent(), p2_.GetAudioRtpCountReceived()); + } + + // No RTCP packets should have been dropped, because we do not filter them. + // Calling ShutdownMedia_m on both pipelines does not stop the flow of + // RTCP. So, we might be off by one here. + ASSERT_LE(p2_.GetAudioRtcpCountReceived(), p1_.GetAudioRtcpCountSent()); + ASSERT_GE(p2_.GetAudioRtcpCountReceived() + 1, p1_.GetAudioRtcpCountSent()); + } + + void TestAudioReceiverBundle( + bool bundle_accepted, UniquePtr<MediaPipelineFilter>&& initialFilter, + UniquePtr<MediaPipelineFilter>&& refinedFilter = nullptr, + unsigned int ms_until_answer = 500, + unsigned int ms_of_traffic_after_answer = 10000) { + TestAudioSend(true, std::move(initialFilter), std::move(refinedFilter), + ms_until_answer, ms_of_traffic_after_answer); + } + + protected: + // main_task_queue_ has this type to make sure it goes through Delete() when + // we're destroyed. + UniquePtr<TaskQueueWrapper<DeletionPolicy::NonBlocking>> main_task_queue_; + const RefPtr<SharedWebrtcState> shared_state_; + TestAgentSend p1_; + TestAgentReceive p2_; +}; + +class MediaPipelineFilterTest : public ::testing::Test { + public: + bool Filter(MediaPipelineFilter& filter, uint32_t ssrc, uint8_t payload_type, + const Maybe<std::string>& mid = Nothing()) { + webrtc::RTPHeader header; + header.ssrc = ssrc; + header.payloadType = payload_type; + mid.apply([&](const auto& mid) { header.extension.mid = mid; }); + return filter.Filter(header); + } +}; + +TEST_F(MediaPipelineFilterTest, TestConstruct) { MediaPipelineFilter filter; } + +TEST_F(MediaPipelineFilterTest, TestDefault) { + MediaPipelineFilter filter; + EXPECT_FALSE(Filter(filter, 233, 110)); +} + +TEST_F(MediaPipelineFilterTest, TestSSRCFilter) { + MediaPipelineFilter filter; + filter.AddRemoteSSRC(555); + EXPECT_TRUE(Filter(filter, 555, 110)); + EXPECT_FALSE(Filter(filter, 556, 110)); +} + +#define SSRC(ssrc) \ + ((ssrc >> 24) & 0xFF), ((ssrc >> 16) & 0xFF), ((ssrc >> 8) & 0xFF), \ + (ssrc & 0xFF) +#define REPORT_FRAGMENT(ssrc) \ + SSRC(ssrc), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + +#define RTCP_TYPEINFO(num_rrs, type, size) 0x80 + num_rrs, type, 0, size + +TEST_F(MediaPipelineFilterTest, TestMidFilter) { + MediaPipelineFilter filter; + const auto mid = Some(std::string("mid0")); + filter.SetRemoteMediaStreamId(mid); + + EXPECT_FALSE(Filter(filter, 16, 110)); + EXPECT_TRUE(Filter(filter, 16, 110, mid)); + EXPECT_TRUE(Filter(filter, 16, 110)); + EXPECT_FALSE(Filter(filter, 17, 110)); + + // The mid filter maintains a set of SSRCs. Adding a new SSRC should work + // and still allow previous SSRCs to work. Unrecognized SSRCs should still be + // filtered out. + EXPECT_TRUE(Filter(filter, 18, 111, mid)); + EXPECT_TRUE(Filter(filter, 18, 111)); + EXPECT_TRUE(Filter(filter, 16, 110)); + EXPECT_FALSE(Filter(filter, 17, 110)); +} + +TEST_F(MediaPipelineFilterTest, TestPayloadTypeFilter) { + MediaPipelineFilter filter; + filter.AddUniquePT(110); + EXPECT_TRUE(Filter(filter, 555, 110)); + EXPECT_FALSE(Filter(filter, 556, 111)); +} + +TEST_F(MediaPipelineFilterTest, TestSSRCMovedWithMid) { + MediaPipelineFilter filter; + const auto mid0 = Some(std::string("mid0")); + const auto mid1 = Some(std::string("mid1")); + filter.SetRemoteMediaStreamId(mid0); + ASSERT_TRUE(Filter(filter, 555, 110, mid0)); + ASSERT_TRUE(Filter(filter, 555, 110)); + // Present a new MID binding + ASSERT_FALSE(Filter(filter, 555, 110, mid1)); + ASSERT_FALSE(Filter(filter, 555, 110)); +} + +TEST_F(MediaPipelineFilterTest, TestRemoteSDPNoSSRCs) { + // If the remote SDP doesn't have SSRCs, right now this is a no-op and + // there is no point of even incorporating a filter, but we make the + // behavior consistent to avoid confusion. + MediaPipelineFilter filter; + const auto mid = Some(std::string("mid0")); + filter.SetRemoteMediaStreamId(mid); + filter.AddUniquePT(111); + EXPECT_TRUE(Filter(filter, 555, 110, mid)); + EXPECT_TRUE(Filter(filter, 555, 110)); + + // Update but remember binding./ + MediaPipelineFilter filter2; + + filter.Update(filter2); + + // Ensure that the old SSRC still works. + EXPECT_TRUE(Filter(filter, 555, 110)); + + // Forget the previous binding + MediaPipelineFilter filter3; + filter3.SetRemoteMediaStreamId(Some(std::string("mid1"))); + filter.Update(filter3); + + ASSERT_FALSE(Filter(filter, 555, 110)); +} + +TEST_F(MediaPipelineTest, TestAudioSendNoMux) { TestAudioSend(false); } + +TEST_F(MediaPipelineTest, TestAudioSendMux) { TestAudioSend(true); } + +TEST_F(MediaPipelineTest, TestAudioSendBundle) { + auto filter = MakeUnique<MediaPipelineFilter>(); + // These durations have to be _extremely_ long to have any assurance that + // some RTCP will be sent at all. This is because the first RTCP packet + // is sometimes sent before the transports are ready, which causes it to + // be dropped. + TestAudioReceiverBundle( + true, std::move(filter), + // We do not specify the filter for the remote description, so it will be + // set to something sane after a short time. + nullptr, 10000, 10000); + + // Some packets should have been dropped, but not all + ASSERT_GT(p1_.GetAudioRtpCountSent(), p2_.GetAudioRtpCountReceived()); + ASSERT_GT(p2_.GetAudioRtpCountReceived(), 40); + ASSERT_GT(p1_.GetAudioRtcpCountSent(), 1); +} + +TEST_F(MediaPipelineTest, TestAudioSendEmptyBundleFilter) { + auto filter = MakeUnique<MediaPipelineFilter>(); + auto bad_answer_filter = MakeUnique<MediaPipelineFilter>(); + TestAudioReceiverBundle(true, std::move(filter), + std::move(bad_answer_filter)); + // Filter is empty, so should drop everything. + ASSERT_EQ(0, p2_.GetAudioRtpCountReceived()); +} + +} // end namespace diff --git a/media/webrtc/signaling/gtest/moz.build b/media/webrtc/signaling/gtest/moz.build new file mode 100644 index 0000000000..d975e81422 --- /dev/null +++ b/media/webrtc/signaling/gtest/moz.build @@ -0,0 +1,54 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +include("/dom/media/webrtc/third_party_build/webrtc.mozbuild") + +if CONFIG["OS_TARGET"] == "WINNT": + DEFINES["SIP_OS_WINDOWS"] = True +elif CONFIG["OS_TARGET"] == "Darwin": + DEFINES["SIP_OS_OSX"] = True +else: + DEFINES["SIP_OS_LINUX"] = True + +# TODO: bug 1172551 - get these tests working on iOS +# Also, these lengthy tests are prone to OOM on Windows ASan. +if ( + CONFIG["MOZ_WIDGET_TOOLKIT"] != "uikit" + and CONFIG["OS_TARGET"] != "Android" + and not (CONFIG["OS_TARGET"] == "WINNT" and CONFIG["MOZ_ASAN"]) +): + LOCAL_INCLUDES += [ + "!/ipc/ipdl/_ipdlheaders", + "/dom/media", + "/dom/media/gtest", + "/dom/media/systemservices", + "/dom/media/webrtc", + "/dom/media/webrtc/common", + "/dom/media/webrtc/common/time_profiling", + "/dom/media/webrtc/jsapi", + "/dom/media/webrtc/libwebrtcglue", + "/dom/media/webrtc/transport", + "/dom/media/webrtc/transport/test", + "/dom/media/webrtc/transport/third_party/nrappkit/src/registry", + "/dom/media/webrtc/transportbridge", + "/ipc/chromium/src", + "/media/webrtc/", + "/third_party/libsrtp/src/include", + "/third_party/libwebrtc", + "/third_party/libwebrtc/third_party/abseil-cpp", + "/third_party/sipcc", + ] + + SOURCES += [ + "audioconduit_unittests.cpp", + "jsep_session_unittest.cpp", + "jsep_track_unittest.cpp", + "mediapipeline_unittest.cpp", + "MockCall.cpp", + "sdp_unittests.cpp", + "videoconduit_unittests.cpp", + ] + + FINAL_LIBRARY = "xul-gtest" diff --git a/media/webrtc/signaling/gtest/sdp_unittests.cpp b/media/webrtc/signaling/gtest/sdp_unittests.cpp new file mode 100644 index 0000000000..df0b85f1dc --- /dev/null +++ b/media/webrtc/signaling/gtest/sdp_unittests.cpp @@ -0,0 +1,5915 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <string> + +#define GTEST_HAS_RTTI 0 +#include "gtest/gtest.h" + +#include "nss.h" +#include "ssl.h" + +#include "sdp/RsdparsaSdpParser.h" +#include "sdp/SipccSdpParser.h" +#include "sdp/SdpMediaSection.h" +#include "sdp/SdpAttribute.h" +#include "sdp/ParsingResultComparer.h" + +extern "C" { +#include "sipcc_sdp.h" +#include "sdp_private.h" +} + +#ifdef CRLF +# undef CRLF +#endif +#define CRLF "\r\n" + +#define SKIP_TEST_WITH_RUST_PARSER \ + if (!::testing::get<1>(GetParam())) { \ + return; \ + } +#define SKIP_TEST_WITH_SIPCC_PARSER \ + if (ResultsAreFromSipcc()) { \ + return; \ + } + +// If you want to see the SDP as it is parsed +// #define DEBUG_DISPLAY_SDP + +using namespace mozilla; + +namespace test { + +class SdpTest : public ::testing::Test { + public: + SdpTest() : final_level_(0), sdp_ptr_(nullptr) {} + + ~SdpTest() { sdp_free_description(sdp_ptr_); } + + static void SetUpTestCase() { + NSS_NoDB_Init(nullptr); + NSS_SetDomesticPolicy(); + } + + void SetUp() { + final_level_ = 0; + sdp_ptr_ = nullptr; + } + + static void TearDownTestCase() {} + + void ResetSdp() { + if (!sdp_ptr_) { + sdp_free_description(sdp_ptr_); + } + + sdp_media_e supported_media[] = { + SDP_MEDIA_AUDIO, SDP_MEDIA_VIDEO, SDP_MEDIA_APPLICATION, + SDP_MEDIA_DATA, SDP_MEDIA_CONTROL, SDP_MEDIA_NAS_RADIUS, + SDP_MEDIA_NAS_TACACS, SDP_MEDIA_NAS_DIAMETER, SDP_MEDIA_NAS_L2TP, + SDP_MEDIA_NAS_LOGIN, SDP_MEDIA_NAS_NONE, SDP_MEDIA_IMAGE, + }; + + sdp_conf_options_t* config_p = sdp_init_config(); + unsigned int i; + for (i = 0; i < sizeof(supported_media) / sizeof(sdp_media_e); i++) { + sdp_media_supported(config_p, supported_media[i], true); + } + sdp_nettype_supported(config_p, SDP_NT_INTERNET, true); + sdp_addrtype_supported(config_p, SDP_AT_IP4, true); + sdp_addrtype_supported(config_p, SDP_AT_IP6, true); + sdp_transport_supported(config_p, SDP_TRANSPORT_RTPSAVPF, true); + sdp_transport_supported(config_p, SDP_TRANSPORT_UDPTL, true); + sdp_require_session_name(config_p, false); + + sdp_ptr_ = sdp_init_description(config_p); + if (!sdp_ptr_) { + sdp_free_config(config_p); + } + } + + void ParseSdp(const std::string& sdp_str) { + const char* buf = sdp_str.data(); + ResetSdp(); + ASSERT_EQ(sdp_parse(sdp_ptr_, buf, sdp_str.size()), SDP_SUCCESS); + } + + void InitLocalSdp() { + ResetSdp(); + ASSERT_EQ(sdp_set_version(sdp_ptr_, 0), SDP_SUCCESS); + ASSERT_EQ(sdp_set_owner_username(sdp_ptr_, "-"), SDP_SUCCESS); + ASSERT_EQ(sdp_set_owner_sessionid(sdp_ptr_, "132954853"), SDP_SUCCESS); + ASSERT_EQ(sdp_set_owner_version(sdp_ptr_, "0"), SDP_SUCCESS); + ASSERT_EQ(sdp_set_owner_network_type(sdp_ptr_, SDP_NT_INTERNET), + SDP_SUCCESS); + ASSERT_EQ(sdp_set_owner_address_type(sdp_ptr_, SDP_AT_IP4), SDP_SUCCESS); + ASSERT_EQ(sdp_set_owner_address(sdp_ptr_, "198.51.100.7"), SDP_SUCCESS); + ASSERT_EQ(sdp_set_session_name(sdp_ptr_, "SDP Unit Test"), SDP_SUCCESS); + ASSERT_EQ(sdp_set_time_start(sdp_ptr_, "0"), SDP_SUCCESS); + ASSERT_EQ(sdp_set_time_stop(sdp_ptr_, "0"), SDP_SUCCESS); + } + + std::string SerializeSdp() { + flex_string fs; + flex_string_init(&fs); + EXPECT_EQ(sdp_build(sdp_ptr_, &fs), SDP_SUCCESS); + std::string body(fs.buffer); + flex_string_free(&fs); + return body; + } + + // Returns "level" for new media section + int AddNewMedia(sdp_media_e type) { + final_level_++; + EXPECT_EQ(sdp_insert_media_line(sdp_ptr_, final_level_), SDP_SUCCESS); + EXPECT_EQ(sdp_set_conn_nettype(sdp_ptr_, final_level_, SDP_NT_INTERNET), + SDP_SUCCESS); + EXPECT_EQ(sdp_set_conn_addrtype(sdp_ptr_, final_level_, SDP_AT_IP4), + SDP_SUCCESS); + EXPECT_EQ(sdp_set_conn_address(sdp_ptr_, final_level_, "198.51.100.7"), + SDP_SUCCESS); + EXPECT_EQ(sdp_set_media_type(sdp_ptr_, final_level_, SDP_MEDIA_VIDEO), + SDP_SUCCESS); + EXPECT_EQ( + sdp_set_media_transport(sdp_ptr_, final_level_, SDP_TRANSPORT_RTPAVP), + SDP_SUCCESS); + EXPECT_EQ(sdp_set_media_portnum(sdp_ptr_, final_level_, 12345, 0), + SDP_SUCCESS); + EXPECT_EQ(sdp_add_media_payload_type(sdp_ptr_, final_level_, 120, + SDP_PAYLOAD_NUMERIC), + SDP_SUCCESS); + return final_level_; + } + + uint16_t AddNewRtcpFbAck(int level, sdp_rtcp_fb_ack_type_e type, + uint16_t payload = SDP_ALL_PAYLOADS) { + uint16_t inst_num = 0; + EXPECT_EQ(sdp_add_new_attr(sdp_ptr_, level, 0, SDP_ATTR_RTCP_FB, &inst_num), + SDP_SUCCESS); + EXPECT_EQ( + sdp_attr_set_rtcp_fb_ack(sdp_ptr_, level, payload, inst_num, type), + SDP_SUCCESS); + return inst_num; + } + + uint16_t AddNewRtcpFbNack(int level, sdp_rtcp_fb_nack_type_e type, + uint16_t payload = SDP_ALL_PAYLOADS) { + uint16_t inst_num = 0; + EXPECT_EQ(sdp_add_new_attr(sdp_ptr_, level, 0, SDP_ATTR_RTCP_FB, &inst_num), + SDP_SUCCESS); + EXPECT_EQ( + sdp_attr_set_rtcp_fb_nack(sdp_ptr_, level, payload, inst_num, type), + SDP_SUCCESS); + return inst_num; + } + + uint16_t AddNewRtcpFbTrrInt(int level, uint32_t interval, + uint16_t payload = SDP_ALL_PAYLOADS) { + uint16_t inst_num = 0; + EXPECT_EQ(sdp_add_new_attr(sdp_ptr_, level, 0, SDP_ATTR_RTCP_FB, &inst_num), + SDP_SUCCESS); + EXPECT_EQ(sdp_attr_set_rtcp_fb_trr_int(sdp_ptr_, level, payload, inst_num, + interval), + SDP_SUCCESS); + return inst_num; + } + + uint16_t AddNewRtcpFbRemb(int level, uint16_t payload = SDP_ALL_PAYLOADS) { + uint16_t inst_num = 0; + EXPECT_EQ(sdp_add_new_attr(sdp_ptr_, level, 0, SDP_ATTR_RTCP_FB, &inst_num), + SDP_SUCCESS); + EXPECT_EQ(sdp_attr_set_rtcp_fb_remb(sdp_ptr_, level, payload, inst_num), + SDP_SUCCESS); + return inst_num; + } + + uint16_t AddNewRtcpFbCcm(int level, sdp_rtcp_fb_ccm_type_e type, + uint16_t payload = SDP_ALL_PAYLOADS) { + uint16_t inst_num = 0; + EXPECT_EQ(sdp_add_new_attr(sdp_ptr_, level, 0, SDP_ATTR_RTCP_FB, &inst_num), + SDP_SUCCESS); + EXPECT_EQ( + sdp_attr_set_rtcp_fb_ccm(sdp_ptr_, level, payload, inst_num, type), + SDP_SUCCESS); + return inst_num; + } + uint16_t AddNewExtMap(int level, const char* uri) { + uint16_t inst_num = 0; + EXPECT_EQ(sdp_add_new_attr(sdp_ptr_, level, 0, SDP_ATTR_EXTMAP, &inst_num), + SDP_SUCCESS); + EXPECT_EQ(sdp_attr_set_extmap(sdp_ptr_, level, inst_num, uri, inst_num), + SDP_SUCCESS); + return inst_num; + } + + uint16_t AddNewFmtpMaxFs(int level, uint32_t max_fs) { + uint16_t inst_num = 0; + EXPECT_EQ(sdp_add_new_attr(sdp_ptr_, level, 0, SDP_ATTR_FMTP, &inst_num), + SDP_SUCCESS); + EXPECT_EQ(sdp_attr_set_fmtp_payload_type(sdp_ptr_, level, 0, inst_num, 120), + SDP_SUCCESS); + EXPECT_EQ(sdp_attr_set_fmtp_max_fs(sdp_ptr_, level, 0, inst_num, max_fs), + SDP_SUCCESS); + return inst_num; + } + + uint16_t AddNewFmtpMaxFr(int level, uint32_t max_fr) { + uint16_t inst_num = 0; + EXPECT_EQ(sdp_add_new_attr(sdp_ptr_, level, 0, SDP_ATTR_FMTP, &inst_num), + SDP_SUCCESS); + EXPECT_EQ(sdp_attr_set_fmtp_payload_type(sdp_ptr_, level, 0, inst_num, 120), + SDP_SUCCESS); + EXPECT_EQ(sdp_attr_set_fmtp_max_fr(sdp_ptr_, level, 0, inst_num, max_fr), + SDP_SUCCESS); + return inst_num; + } + + uint16_t AddNewFmtpMaxFsFr(int level, uint32_t max_fs, uint32_t max_fr) { + uint16_t inst_num = 0; + EXPECT_EQ(sdp_add_new_attr(sdp_ptr_, level, 0, SDP_ATTR_FMTP, &inst_num), + SDP_SUCCESS); + EXPECT_EQ(sdp_attr_set_fmtp_payload_type(sdp_ptr_, level, 0, inst_num, 120), + SDP_SUCCESS); + EXPECT_EQ(sdp_attr_set_fmtp_max_fs(sdp_ptr_, level, 0, inst_num, max_fs), + SDP_SUCCESS); + EXPECT_EQ(sdp_attr_set_fmtp_max_fr(sdp_ptr_, level, 0, inst_num, max_fr), + SDP_SUCCESS); + return inst_num; + } + + protected: + int final_level_; + sdp_t* sdp_ptr_; +}; + +static const std::string kVideoSdp = + "v=0\r\n" + "o=- 4294967296 2 IN IP4 127.0.0.1\r\n" + "s=SIP Call\r\n" + "c=IN IP4 198.51.100.7\r\n" + "t=0 0\r\n" + "m=video 56436 RTP/SAVPF 120\r\n" + "a=rtpmap:120 VP8/90000\r\n"; + +TEST_F(SdpTest, parseRtcpFbAckRpsi) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 ack rpsi\r\n"); + ASSERT_EQ(sdp_attr_get_rtcp_fb_ack(sdp_ptr_, 1, 120, 1), + SDP_RTCP_FB_ACK_RPSI); +} + +TEST_F(SdpTest, parseRtcpFbAckApp) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 ack app\r\n"); + ASSERT_EQ(sdp_attr_get_rtcp_fb_ack(sdp_ptr_, 1, 120, 1), SDP_RTCP_FB_ACK_APP); +} + +TEST_F(SdpTest, parseRtcpFbAckAppFoo) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 ack app foo\r\n"); + ASSERT_EQ(sdp_attr_get_rtcp_fb_ack(sdp_ptr_, 1, 120, 1), SDP_RTCP_FB_ACK_APP); +} + +TEST_F(SdpTest, parseRtcpFbAckFooBar) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 ack foo bar\r\n"); + ASSERT_EQ(sdp_attr_get_rtcp_fb_ack(sdp_ptr_, 1, 120, 1), + SDP_RTCP_FB_ACK_UNKNOWN); +} + +TEST_F(SdpTest, parseRtcpFbAckFooBarBaz) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 ack foo bar baz\r\n"); + ASSERT_EQ(sdp_attr_get_rtcp_fb_ack(sdp_ptr_, 1, 120, 1), + SDP_RTCP_FB_ACK_UNKNOWN); +} + +TEST_F(SdpTest, parseRtcpFbNack) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 nack\r\n"); + ASSERT_EQ(sdp_attr_get_rtcp_fb_nack(sdp_ptr_, 1, 120, 1), + SDP_RTCP_FB_NACK_BASIC); +} + +TEST_F(SdpTest, parseRtcpFbNackPli) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 nack pli\r\n"); +} + +TEST_F(SdpTest, parseRtcpFbNackSli) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 nack sli\r\n"); + ASSERT_EQ(sdp_attr_get_rtcp_fb_nack(sdp_ptr_, 1, 120, 1), + SDP_RTCP_FB_NACK_SLI); +} + +TEST_F(SdpTest, parseRtcpFbNackRpsi) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 nack rpsi\r\n"); + ASSERT_EQ(sdp_attr_get_rtcp_fb_nack(sdp_ptr_, 1, 120, 1), + SDP_RTCP_FB_NACK_RPSI); +} + +TEST_F(SdpTest, parseRtcpFbNackApp) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 nack app\r\n"); + ASSERT_EQ(sdp_attr_get_rtcp_fb_nack(sdp_ptr_, 1, 120, 1), + SDP_RTCP_FB_NACK_APP); +} + +TEST_F(SdpTest, parseRtcpFbNackAppFoo) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 nack app foo\r\n"); + ASSERT_EQ(sdp_attr_get_rtcp_fb_nack(sdp_ptr_, 1, 120, 1), + SDP_RTCP_FB_NACK_APP); +} + +TEST_F(SdpTest, parseRtcpFbNackAppFooBar) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 nack app foo bar\r\n"); + ASSERT_EQ(sdp_attr_get_rtcp_fb_nack(sdp_ptr_, 1, 120, 1), + SDP_RTCP_FB_NACK_APP); +} + +TEST_F(SdpTest, parseRtcpFbNackFooBarBaz) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 nack foo bar baz\r\n"); + ASSERT_EQ(sdp_attr_get_rtcp_fb_nack(sdp_ptr_, 1, 120, 1), + SDP_RTCP_FB_NACK_UNKNOWN); +} + +TEST_F(SdpTest, parseRtcpFbRemb) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 goog-remb\r\n"); + ASSERT_EQ((bool)sdp_attr_get_rtcp_fb_remb_enabled(sdp_ptr_, 1, 120), true); +} + +TEST_F(SdpTest, parseRtcpRbRembAllPt) { + ParseSdp(kVideoSdp + "a=rtcp-fb:* goog-remb\r\n"); + ASSERT_EQ( + (bool)sdp_attr_get_rtcp_fb_remb_enabled(sdp_ptr_, 1, SDP_ALL_PAYLOADS), + true); +} + +TEST_F(SdpTest, parseRtcpFbTrrInt0) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 trr-int 0\r\n"); + ASSERT_EQ(sdp_attr_get_rtcp_fb_trr_int(sdp_ptr_, 1, 120, 1), 0U); +} + +TEST_F(SdpTest, parseRtcpFbTrrInt123) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 trr-int 123\r\n"); + ASSERT_EQ(sdp_attr_get_rtcp_fb_trr_int(sdp_ptr_, 1, 120, 1), 123U); +} + +TEST_F(SdpTest, parseRtcpFbCcmFir) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 ccm fir\r\n"); + ASSERT_EQ(sdp_attr_get_rtcp_fb_ccm(sdp_ptr_, 1, 120, 1), SDP_RTCP_FB_CCM_FIR); +} + +TEST_F(SdpTest, parseRtcpFbCcmTmmbr) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 ccm tmmbr\r\n"); + ASSERT_EQ(sdp_attr_get_rtcp_fb_ccm(sdp_ptr_, 1, 120, 1), + SDP_RTCP_FB_CCM_TMMBR); +} + +TEST_F(SdpTest, parseRtcpFbCcmTmmbrSmaxpr) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 ccm tmmbr smaxpr=456\r\n"); + ASSERT_EQ(sdp_attr_get_rtcp_fb_ccm(sdp_ptr_, 1, 120, 1), + SDP_RTCP_FB_CCM_TMMBR); +} + +TEST_F(SdpTest, parseRtcpFbCcmTstr) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 ccm tstr\r\n"); + ASSERT_EQ(sdp_attr_get_rtcp_fb_ccm(sdp_ptr_, 1, 120, 1), + SDP_RTCP_FB_CCM_TSTR); +} + +TEST_F(SdpTest, parseRtcpFbCcmVbcm) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 ccm vbcm 123 456 789\r\n"); + ASSERT_EQ(sdp_attr_get_rtcp_fb_ccm(sdp_ptr_, 1, 120, 1), + SDP_RTCP_FB_CCM_VBCM); + // We don't currently parse out VBCM submessage types, since we don't have + // any use for them. +} + +TEST_F(SdpTest, parseRtcpFbCcmFoo) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 ccm foo\r\n"); + ASSERT_EQ(sdp_attr_get_rtcp_fb_ccm(sdp_ptr_, 1, 120, 1), + SDP_RTCP_FB_CCM_UNKNOWN); +} + +TEST_F(SdpTest, parseRtcpFbCcmFooBarBaz) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 ccm foo bar baz\r\n"); + ASSERT_EQ(sdp_attr_get_rtcp_fb_ccm(sdp_ptr_, 1, 120, 1), + SDP_RTCP_FB_CCM_UNKNOWN); +} + +TEST_F(SdpTest, parseRtcpFbFoo) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 foo\r\n"); +} + +TEST_F(SdpTest, parseRtcpFbFooBar) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 foo bar\r\n"); +} + +TEST_F(SdpTest, parseRtcpFbFooBarBaz) { + ParseSdp(kVideoSdp + "a=rtcp-fb:120 foo bar baz\r\n"); +} + +static const std::string kVideoSdpWithUnknonwBrokenFtmp = + "v=0\r\n" + "o=- 4294967296 2 IN IP4 127.0.0.1\r\n" + "s=SIP Call\r\n" + "c=IN IP4 198.51.100.7\r\n" + "t=0 0\r\n" + "m=video 56436 RTP/SAVPF 120\r\n" + "a=rtpmap:120 VP8/90000\r\n" + "a=fmtp:122 unknown=10\n" + "a=rtpmap:122 red/90000\r\n"; + +TEST_F(SdpTest, parseUnknownBrokenFtmp) { + ParseSdp(kVideoSdpWithUnknonwBrokenFtmp); +} + +TEST_F(SdpTest, parseRtcpFbKitchenSink) { + ParseSdp(kVideoSdp + + "a=rtcp-fb:120 ack rpsi\r\n" + "a=rtcp-fb:120 ack app\r\n" + "a=rtcp-fb:120 ack app foo\r\n" + "a=rtcp-fb:120 ack foo bar\r\n" + "a=rtcp-fb:120 ack foo bar baz\r\n" + "a=rtcp-fb:120 nack\r\n" + "a=rtcp-fb:120 nack pli\r\n" + "a=rtcp-fb:120 nack sli\r\n" + "a=rtcp-fb:120 nack rpsi\r\n" + "a=rtcp-fb:120 nack app\r\n" + "a=rtcp-fb:120 nack app foo\r\n" + "a=rtcp-fb:120 nack app foo bar\r\n" + "a=rtcp-fb:120 nack foo bar baz\r\n" + "a=rtcp-fb:120 trr-int 0\r\n" + "a=rtcp-fb:120 trr-int 123\r\n" + "a=rtcp-fb:120 goog-remb\r\n" + "a=rtcp-fb:120 ccm fir\r\n" + "a=rtcp-fb:120 ccm tmmbr\r\n" + "a=rtcp-fb:120 ccm tmmbr smaxpr=456\r\n" + "a=rtcp-fb:120 ccm tstr\r\n" + "a=rtcp-fb:120 ccm vbcm 123 456 789\r\n" + "a=rtcp-fb:120 ccm foo\r\n" + "a=rtcp-fb:120 ccm foo bar baz\r\n" + "a=rtcp-fb:120 foo\r\n" + "a=rtcp-fb:120 foo bar\r\n" + "a=rtcp-fb:120 foo bar baz\r\n"); + + ASSERT_EQ(sdp_attr_get_rtcp_fb_ack(sdp_ptr_, 1, 120, 1), + SDP_RTCP_FB_ACK_RPSI); + ASSERT_EQ(sdp_attr_get_rtcp_fb_ack(sdp_ptr_, 1, 120, 2), SDP_RTCP_FB_ACK_APP); + ASSERT_EQ(sdp_attr_get_rtcp_fb_ack(sdp_ptr_, 1, 120, 3), SDP_RTCP_FB_ACK_APP); + ASSERT_EQ(sdp_attr_get_rtcp_fb_ack(sdp_ptr_, 1, 120, 4), + SDP_RTCP_FB_ACK_UNKNOWN); + ASSERT_EQ(sdp_attr_get_rtcp_fb_ack(sdp_ptr_, 1, 120, 5), + SDP_RTCP_FB_ACK_UNKNOWN); + ASSERT_EQ(sdp_attr_get_rtcp_fb_ack(sdp_ptr_, 1, 120, 6), + SDP_RTCP_FB_ACK_NOT_FOUND); + + ASSERT_EQ(sdp_attr_get_rtcp_fb_nack(sdp_ptr_, 1, 120, 1), + SDP_RTCP_FB_NACK_BASIC); + ASSERT_EQ(sdp_attr_get_rtcp_fb_nack(sdp_ptr_, 1, 120, 2), + SDP_RTCP_FB_NACK_PLI); + ASSERT_EQ(sdp_attr_get_rtcp_fb_nack(sdp_ptr_, 1, 120, 3), + SDP_RTCP_FB_NACK_SLI); + ASSERT_EQ(sdp_attr_get_rtcp_fb_nack(sdp_ptr_, 1, 120, 4), + SDP_RTCP_FB_NACK_RPSI); + ASSERT_EQ(sdp_attr_get_rtcp_fb_nack(sdp_ptr_, 1, 120, 5), + SDP_RTCP_FB_NACK_APP); + ASSERT_EQ(sdp_attr_get_rtcp_fb_nack(sdp_ptr_, 1, 120, 6), + SDP_RTCP_FB_NACK_APP); + ASSERT_EQ(sdp_attr_get_rtcp_fb_nack(sdp_ptr_, 1, 120, 7), + SDP_RTCP_FB_NACK_APP); + ASSERT_EQ(sdp_attr_get_rtcp_fb_nack(sdp_ptr_, 1, 120, 8), + SDP_RTCP_FB_NACK_UNKNOWN); + ASSERT_EQ(sdp_attr_get_rtcp_fb_nack(sdp_ptr_, 1, 120, 9), + SDP_RTCP_FB_NACK_NOT_FOUND); + + ASSERT_EQ(sdp_attr_get_rtcp_fb_trr_int(sdp_ptr_, 1, 120, 1), 0U); + ASSERT_EQ(sdp_attr_get_rtcp_fb_trr_int(sdp_ptr_, 1, 120, 2), 123U); + ASSERT_EQ(sdp_attr_get_rtcp_fb_trr_int(sdp_ptr_, 1, 120, 3), 0xFFFFFFFF); + + ASSERT_EQ((bool)sdp_attr_get_rtcp_fb_remb_enabled(sdp_ptr_, 1, 120), true); + ASSERT_EQ((bool)sdp_attr_get_rtcp_fb_remb_enabled(sdp_ptr_, 2, 120), false); + + ASSERT_EQ(sdp_attr_get_rtcp_fb_ccm(sdp_ptr_, 1, 120, 1), SDP_RTCP_FB_CCM_FIR); + ASSERT_EQ(sdp_attr_get_rtcp_fb_ccm(sdp_ptr_, 1, 120, 2), + SDP_RTCP_FB_CCM_TMMBR); + ASSERT_EQ(sdp_attr_get_rtcp_fb_ccm(sdp_ptr_, 1, 120, 3), + SDP_RTCP_FB_CCM_TMMBR); + ASSERT_EQ(sdp_attr_get_rtcp_fb_ccm(sdp_ptr_, 1, 120, 4), + SDP_RTCP_FB_CCM_TSTR); + ASSERT_EQ(sdp_attr_get_rtcp_fb_ccm(sdp_ptr_, 1, 120, 5), + SDP_RTCP_FB_CCM_VBCM); + // We don't currently parse out VBCM submessage types, since we don't have + // any use for them. + ASSERT_EQ(sdp_attr_get_rtcp_fb_ccm(sdp_ptr_, 1, 120, 6), + SDP_RTCP_FB_CCM_UNKNOWN); + ASSERT_EQ(sdp_attr_get_rtcp_fb_ccm(sdp_ptr_, 1, 120, 7), + SDP_RTCP_FB_CCM_UNKNOWN); + ASSERT_EQ(sdp_attr_get_rtcp_fb_ccm(sdp_ptr_, 1, 120, 8), + SDP_RTCP_FB_CCM_NOT_FOUND); +} + +TEST_F(SdpTest, addRtcpFbAckRpsi) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbAck(level, SDP_RTCP_FB_ACK_RPSI, 120); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:120 ack rpsi\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbAckRpsiAllPt) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbAck(level, SDP_RTCP_FB_ACK_RPSI); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:* ack rpsi\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbAckApp) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbAck(level, SDP_RTCP_FB_ACK_APP, 120); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:120 ack app\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbAckAppAllPt) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbAck(level, SDP_RTCP_FB_ACK_APP); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:* ack app\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbNack) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbNack(level, SDP_RTCP_FB_NACK_BASIC, 120); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:120 nack\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbNackAllPt) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbNack(level, SDP_RTCP_FB_NACK_BASIC); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:* nack\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbNackSli) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbNack(level, SDP_RTCP_FB_NACK_SLI, 120); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:120 nack sli\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbNackSliAllPt) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbNack(level, SDP_RTCP_FB_NACK_SLI); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:* nack sli\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbNackPli) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbNack(level, SDP_RTCP_FB_NACK_PLI, 120); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:120 nack pli\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbNackPliAllPt) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbNack(level, SDP_RTCP_FB_NACK_PLI); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:* nack pli\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbNackRpsi) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbNack(level, SDP_RTCP_FB_NACK_RPSI, 120); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:120 nack rpsi\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbNackRpsiAllPt) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbNack(level, SDP_RTCP_FB_NACK_RPSI); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:* nack rpsi\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbNackApp) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbNack(level, SDP_RTCP_FB_NACK_APP, 120); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:120 nack app\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbNackAppAllPt) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbNack(level, SDP_RTCP_FB_NACK_APP); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:* nack app\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbNackRai) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbNack(level, SDP_RTCP_FB_NACK_RAI, 120); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:120 nack rai\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbNackRaiAllPt) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbNack(level, SDP_RTCP_FB_NACK_RAI); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:* nack rai\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbNackTllei) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbNack(level, SDP_RTCP_FB_NACK_TLLEI, 120); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:120 nack tllei\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbNackTlleiAllPt) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbNack(level, SDP_RTCP_FB_NACK_TLLEI); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:* nack tllei\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbNackPslei) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbNack(level, SDP_RTCP_FB_NACK_PSLEI, 120); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:120 nack pslei\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbNackPsleiAllPt) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbNack(level, SDP_RTCP_FB_NACK_PSLEI); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:* nack pslei\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbNackEcn) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbNack(level, SDP_RTCP_FB_NACK_ECN, 120); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:120 nack ecn\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbNackEcnAllPt) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbNack(level, SDP_RTCP_FB_NACK_ECN); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:* nack ecn\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbRemb) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbRemb(level, 120); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:120 goog-remb\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbRembAllPt) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbRemb(level); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:* goog-remb\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbTrrInt) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbTrrInt(level, 12345, 120); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:120 trr-int 12345\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbNackTrrIntAllPt) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbTrrInt(level, 0); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:* trr-int 0\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbCcmFir) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbCcm(level, SDP_RTCP_FB_CCM_FIR, 120); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:120 ccm fir\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbCcmFirAllPt) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbCcm(level, SDP_RTCP_FB_CCM_FIR); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:* ccm fir\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbCcmTmmbr) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbCcm(level, SDP_RTCP_FB_CCM_TMMBR, 120); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:120 ccm tmmbr\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbCcmTmmbrAllPt) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbCcm(level, SDP_RTCP_FB_CCM_TMMBR); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:* ccm tmmbr\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbCcmTstr) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbCcm(level, SDP_RTCP_FB_CCM_TSTR, 120); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:120 ccm tstr\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbCcmTstrAllPt) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbCcm(level, SDP_RTCP_FB_CCM_TSTR); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:* ccm tstr\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbCcmVbcm) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbCcm(level, SDP_RTCP_FB_CCM_VBCM, 120); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:120 ccm vbcm\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addRtcpFbCcmVbcmAllPt) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewRtcpFbCcm(level, SDP_RTCP_FB_CCM_VBCM); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=rtcp-fb:* ccm vbcm\r\n"), std::string::npos); +} + +TEST_F(SdpTest, parseRtcpFbAllPayloads) { + ParseSdp(kVideoSdp + "a=rtcp-fb:* ack rpsi\r\n"); + for (int i = 0; i < 128; i++) { + ASSERT_EQ(sdp_attr_get_rtcp_fb_ack(sdp_ptr_, 1, i, 1), + SDP_RTCP_FB_ACK_RPSI); + } +} +TEST_F(SdpTest, addExtMap) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewExtMap(level, SDP_EXTMAP_AUDIO_LEVEL); + std::string body = SerializeSdp(); + ASSERT_NE( + body.find("a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n"), + std::string::npos); +} + +TEST_F(SdpTest, parseExtMap) { + ParseSdp(kVideoSdp + + "a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n"); + ASSERT_STREQ(sdp_attr_get_extmap_uri(sdp_ptr_, 1, 1), SDP_EXTMAP_AUDIO_LEVEL); + ASSERT_EQ(sdp_attr_get_extmap_id(sdp_ptr_, 1, 1), 1); +} + +TEST_F(SdpTest, parseFmtpBitrate) { + ParseSdp(kVideoSdp + "a=fmtp:120 bitrate=400\r\n"); + ASSERT_EQ(400, sdp_attr_get_fmtp_bitrate_type(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpBitrateWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 bitrate=0\r\n"); + ASSERT_EQ(SDP_INVALID_VALUE, + sdp_attr_get_fmtp_bitrate_type(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpBitrateWith32001) { + ParseSdp(kVideoSdp + "a=fmtp:120 bitrate=32001\r\n"); + ASSERT_EQ(32001, sdp_attr_get_fmtp_bitrate_type(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpBitrateWith4294967296) { + ParseSdp(kVideoSdp + "a=fmtp:120 bitrate=4294967296\r\n"); + ASSERT_EQ(SDP_INVALID_VALUE, + sdp_attr_get_fmtp_bitrate_type(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpMode) { + ParseSdp(kVideoSdp + "a=fmtp:120 mode=200\r\n"); + ASSERT_EQ(200U, sdp_attr_get_fmtp_mode_for_payload_type(sdp_ptr_, 1, 0, 120)); +} + +TEST_F(SdpTest, parseFmtpModeWith4294967295) { + ParseSdp(kVideoSdp + "a=fmtp:120 mode=4294967295\r\n"); + ASSERT_EQ(4294967295, + sdp_attr_get_fmtp_mode_for_payload_type(sdp_ptr_, 1, 0, 120)); +} + +TEST_F(SdpTest, parseFmtpModeWith4294967296) { + ParseSdp(kVideoSdp + "a=fmtp:120 mode=4294967296\r\n"); + // returns 0 if not found + ASSERT_EQ(0U, sdp_attr_get_fmtp_mode_for_payload_type(sdp_ptr_, 1, 0, 120)); +} + +TEST_F(SdpTest, parseFmtpQcif) { + ParseSdp(kVideoSdp + "a=fmtp:120 qcif=20\r\n"); + ASSERT_EQ(20, sdp_attr_get_fmtp_qcif(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpQcifWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 qcif=0\r\n"); + ASSERT_EQ(SDP_INVALID_VALUE, sdp_attr_get_fmtp_qcif(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpQcifWith33) { + ParseSdp(kVideoSdp + "a=fmtp:120 qcif=33\r\n"); + ASSERT_EQ(SDP_INVALID_VALUE, sdp_attr_get_fmtp_qcif(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpCif) { + ParseSdp(kVideoSdp + "a=fmtp:120 cif=11\r\n"); + ASSERT_EQ(11, sdp_attr_get_fmtp_cif(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpCifWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 cif=0\r\n"); + ASSERT_EQ(SDP_INVALID_VALUE, sdp_attr_get_fmtp_cif(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpCifWith33) { + ParseSdp(kVideoSdp + "a=fmtp:120 cif=33\r\n"); + ASSERT_EQ(SDP_INVALID_VALUE, sdp_attr_get_fmtp_cif(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpMaxbr) { + ParseSdp(kVideoSdp + "a=fmtp:120 maxbr=21\r\n"); + ASSERT_EQ(21, sdp_attr_get_fmtp_maxbr(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpMaxbrWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 maxbr=0\r\n"); + ASSERT_EQ(SDP_INVALID_VALUE, sdp_attr_get_fmtp_maxbr(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpMaxbrWith65536) { + ParseSdp(kVideoSdp + "a=fmtp:120 maxbr=65536\r\n"); + ASSERT_EQ(SDP_INVALID_VALUE, sdp_attr_get_fmtp_maxbr(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpSqcif) { + ParseSdp(kVideoSdp + "a=fmtp:120 sqcif=6\r\n"); + ASSERT_EQ(6, sdp_attr_get_fmtp_sqcif(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpSqcifWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 sqcif=0\r\n"); + ASSERT_EQ(SDP_INVALID_VALUE, sdp_attr_get_fmtp_sqcif(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpSqcifWith33) { + ParseSdp(kVideoSdp + "a=fmtp:120 sqcif=33\r\n"); + ASSERT_EQ(SDP_INVALID_VALUE, sdp_attr_get_fmtp_sqcif(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpCif4) { + ParseSdp(kVideoSdp + "a=fmtp:120 cif4=11\r\n"); + ASSERT_EQ(11, sdp_attr_get_fmtp_cif4(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpCif4With0) { + ParseSdp(kVideoSdp + "a=fmtp:120 cif4=0\r\n"); + ASSERT_EQ(SDP_INVALID_VALUE, sdp_attr_get_fmtp_cif4(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpCif4With33) { + ParseSdp(kVideoSdp + "a=fmtp:120 cif4=33\r\n"); + ASSERT_EQ(SDP_INVALID_VALUE, sdp_attr_get_fmtp_cif4(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpCif16) { + ParseSdp(kVideoSdp + "a=fmtp:120 cif16=11\r\n"); + ASSERT_EQ(11, sdp_attr_get_fmtp_cif16(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpCif16With0) { + ParseSdp(kVideoSdp + "a=fmtp:120 cif16=0\r\n"); + ASSERT_EQ(SDP_INVALID_VALUE, sdp_attr_get_fmtp_cif16(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpCif16With33) { + ParseSdp(kVideoSdp + "a=fmtp:120 cif16=33\r\n"); + ASSERT_EQ(SDP_INVALID_VALUE, sdp_attr_get_fmtp_cif16(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpBpp) { + ParseSdp(kVideoSdp + "a=fmtp:120 bpp=7\r\n"); + ASSERT_EQ(7, sdp_attr_get_fmtp_bpp(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpBppWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 bpp=0\r\n"); + ASSERT_EQ(SDP_INVALID_VALUE, sdp_attr_get_fmtp_bpp(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpBppWith65536) { + ParseSdp(kVideoSdp + "a=fmtp:120 bpp=65536\r\n"); + ASSERT_EQ(SDP_INVALID_VALUE, sdp_attr_get_fmtp_bpp(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpHrd) { + ParseSdp(kVideoSdp + "a=fmtp:120 hrd=800\r\n"); + ASSERT_EQ(800, sdp_attr_get_fmtp_hrd(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpHrdWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 hrd=0\r\n"); + ASSERT_EQ(SDP_INVALID_VALUE, sdp_attr_get_fmtp_hrd(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpHrdWith65536) { + ParseSdp(kVideoSdp + "a=fmtp:120 hrd=65536\r\n"); + ASSERT_EQ(SDP_INVALID_VALUE, sdp_attr_get_fmtp_hrd(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpProfile) { + ParseSdp(kVideoSdp + "a=fmtp:120 profile=4\r\n"); + ASSERT_EQ(4, sdp_attr_get_fmtp_profile(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpProfileWith11) { + ParseSdp(kVideoSdp + "a=fmtp:120 profile=11\r\n"); + ASSERT_EQ(SDP_INVALID_VALUE, sdp_attr_get_fmtp_profile(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpLevel) { + ParseSdp(kVideoSdp + "a=fmtp:120 level=56\r\n"); + ASSERT_EQ(56, sdp_attr_get_fmtp_level(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpLevelWith101) { + ParseSdp(kVideoSdp + "a=fmtp:120 level=101\r\n"); + ASSERT_EQ(SDP_INVALID_VALUE, sdp_attr_get_fmtp_level(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpPacketizationMode) { + ParseSdp(kVideoSdp + "a=fmtp:120 packetization-mode=1\r\n"); + uint16_t packetizationMode; + ASSERT_EQ(SDP_SUCCESS, + sdp_attr_get_fmtp_pack_mode(sdp_ptr_, 1, 0, 1, &packetizationMode)); + ASSERT_EQ(1, packetizationMode); +} + +TEST_F(SdpTest, parseFmtpPacketizationModeWith3) { + ParseSdp(kVideoSdp + "a=fmtp:120 packetization-mode=3\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_pack_mode(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpInterleavingDepth) { + ParseSdp(kVideoSdp + "a=fmtp:120 sprop-interleaving-depth=566\r\n"); + uint16_t interleavingDepth; + ASSERT_EQ(SDP_SUCCESS, sdp_attr_get_fmtp_interleaving_depth( + sdp_ptr_, 1, 0, 1, &interleavingDepth)); + ASSERT_EQ(566, interleavingDepth); +} + +TEST_F(SdpTest, parseFmtpInterleavingDepthWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 sprop-interleaving-depth=0\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_interleaving_depth(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpInterleavingDepthWith65536) { + ParseSdp(kVideoSdp + "a=fmtp:120 sprop-interleaving-depth=65536\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_interleaving_depth(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpDeintBuf) { + ParseSdp(kVideoSdp + "a=fmtp:120 sprop-deint-buf-req=4294967295\r\n"); + uint32_t deintBuf; + ASSERT_EQ(SDP_SUCCESS, + sdp_attr_get_fmtp_deint_buf_req(sdp_ptr_, 1, 0, 1, &deintBuf)); + ASSERT_EQ(4294967295, deintBuf); +} + +TEST_F(SdpTest, parseFmtpDeintBufWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 sprop-deint-buf-req=0\r\n"); + uint32_t deintBuf; + ASSERT_EQ(SDP_SUCCESS, + sdp_attr_get_fmtp_deint_buf_req(sdp_ptr_, 1, 0, 1, &deintBuf)); + ASSERT_EQ(0U, deintBuf); +} + +TEST_F(SdpTest, parseFmtpDeintBufWith4294967296) { + ParseSdp(kVideoSdp + "a=fmtp:120 sprop-deint-buf-req=4294967296\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_deint_buf_req(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpMaxDonDiff) { + ParseSdp(kVideoSdp + "a=fmtp:120 sprop-max-don-diff=5678\r\n"); + uint32_t maxDonDiff; + ASSERT_EQ(SDP_SUCCESS, + sdp_attr_get_fmtp_max_don_diff(sdp_ptr_, 1, 0, 1, &maxDonDiff)); + ASSERT_EQ(5678U, maxDonDiff); +} + +TEST_F(SdpTest, parseFmtpMaxDonDiffWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 sprop-max-don-diff=0\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_max_don_diff(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpMaxDonDiffWith4294967296) { + ParseSdp(kVideoSdp + "a=fmtp:120 sprop-max-don-diff=4294967296\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_max_don_diff(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpInitBufTime) { + ParseSdp(kVideoSdp + "a=fmtp:120 sprop-init-buf-time=4294967295\r\n"); + uint32_t initBufTime; + ASSERT_EQ(SDP_SUCCESS, + sdp_attr_get_fmtp_init_buf_time(sdp_ptr_, 1, 0, 1, &initBufTime)); + ASSERT_EQ(4294967295, initBufTime); +} + +TEST_F(SdpTest, parseFmtpInitBufTimeWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 sprop-init-buf-time=0\r\n"); + uint32_t initBufTime; + ASSERT_EQ(SDP_SUCCESS, + sdp_attr_get_fmtp_init_buf_time(sdp_ptr_, 1, 0, 1, &initBufTime)); + ASSERT_EQ(0U, initBufTime); +} + +TEST_F(SdpTest, parseFmtpInitBufTimeWith4294967296) { + ParseSdp(kVideoSdp + "a=fmtp:120 sprop-init-buf-time=4294967296\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_init_buf_time(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpMaxMbps) { + ParseSdp(kVideoSdp + "a=fmtp:120 max-mbps=46789\r\n"); + uint32_t maxMpbs; + ASSERT_EQ(SDP_SUCCESS, + sdp_attr_get_fmtp_max_mbps(sdp_ptr_, 1, 0, 1, &maxMpbs)); + ASSERT_EQ(46789U, maxMpbs); +} + +TEST_F(SdpTest, parseFmtpMaxMbpsWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 max-mbps=0\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_max_mbps(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpMaxMbpsWith4294967296) { + ParseSdp(kVideoSdp + "a=fmtp:120 max-mbps=4294967296\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_max_mbps(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpMaxCpb) { + ParseSdp(kVideoSdp + "a=fmtp:120 max-cpb=47891\r\n"); + uint32_t maxCpb; + ASSERT_EQ(SDP_SUCCESS, sdp_attr_get_fmtp_max_cpb(sdp_ptr_, 1, 0, 1, &maxCpb)); + ASSERT_EQ(47891U, maxCpb); +} + +TEST_F(SdpTest, parseFmtpMaxCpbWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 max-cpb=0\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_max_cpb(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpMaxCpbWith4294967296) { + ParseSdp(kVideoSdp + "a=fmtp:120 max-cpb=4294967296\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_max_cpb(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpMaxDpb) { + ParseSdp(kVideoSdp + "a=fmtp:120 max-dpb=47892\r\n"); + uint32_t maxDpb; + ASSERT_EQ(SDP_SUCCESS, sdp_attr_get_fmtp_max_dpb(sdp_ptr_, 1, 0, 1, &maxDpb)); + ASSERT_EQ(47892U, maxDpb); +} + +TEST_F(SdpTest, parseFmtpMaxDpbWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 max-dpb=0\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_max_dpb(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpMaxDpbWith4294967296) { + ParseSdp(kVideoSdp + "a=fmtp:120 max-dpb=4294967296\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_max_dpb(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpMaxBr) { + ParseSdp(kVideoSdp + "a=fmtp:120 max-br=47893\r\n"); + uint32_t maxBr; + ASSERT_EQ(SDP_SUCCESS, sdp_attr_get_fmtp_max_br(sdp_ptr_, 1, 0, 1, &maxBr)); + ASSERT_EQ(47893U, maxBr); +} + +TEST_F(SdpTest, parseFmtpMaxBrWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 max-br=0\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_max_br(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpMaxBrWith4294967296) { + ParseSdp(kVideoSdp + "a=fmtp:120 max-br=4294967296\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_max_br(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpRedundantPicCap) { + ParseSdp(kVideoSdp + "a=fmtp:120 redundant-pic-cap=1\r\n"); + ASSERT_EQ(1, sdp_attr_fmtp_is_redundant_pic_cap(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpRedundantPicCapWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 redundant-pic-cap=0\r\n"); + ASSERT_EQ(0, sdp_attr_fmtp_is_redundant_pic_cap(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpRedundantPicCapWith2) { + ParseSdp(kVideoSdp + "a=fmtp:120 redundant-pic-cap=2\r\n"); + ASSERT_EQ(0, sdp_attr_fmtp_is_redundant_pic_cap(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpDeintBufCap) { + ParseSdp(kVideoSdp + "a=fmtp:120 deint-buf-cap=4294967295\r\n"); + uint32_t deintBufCap; + ASSERT_EQ(SDP_SUCCESS, + sdp_attr_get_fmtp_deint_buf_cap(sdp_ptr_, 1, 0, 1, &deintBufCap)); + ASSERT_EQ(4294967295, deintBufCap); +} + +TEST_F(SdpTest, parseFmtpDeintBufCapWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 deint-buf-cap=0\r\n"); + uint32_t deintBufCap; + ASSERT_EQ(SDP_SUCCESS, + sdp_attr_get_fmtp_deint_buf_cap(sdp_ptr_, 1, 0, 1, &deintBufCap)); + ASSERT_EQ(0U, deintBufCap); +} + +TEST_F(SdpTest, parseFmtpDeintBufCapWith4294967296) { + ParseSdp(kVideoSdp + "a=fmtp:120 deint-buf-cap=4294967296\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_deint_buf_cap(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpMaxRcmdNaluSize) { + ParseSdp(kVideoSdp + "a=fmtp:120 max-rcmd-nalu-size=4294967295\r\n"); + uint32_t maxRcmdNaluSize; + ASSERT_EQ(SDP_SUCCESS, sdp_attr_get_fmtp_max_rcmd_nalu_size( + sdp_ptr_, 1, 0, 1, &maxRcmdNaluSize)); + ASSERT_EQ(4294967295, maxRcmdNaluSize); +} + +TEST_F(SdpTest, parseFmtpMaxRcmdNaluSizeWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 max-rcmd-nalu-size=0\r\n"); + uint32_t maxRcmdNaluSize; + ASSERT_EQ(SDP_SUCCESS, sdp_attr_get_fmtp_max_rcmd_nalu_size( + sdp_ptr_, 1, 0, 1, &maxRcmdNaluSize)); + ASSERT_EQ(0U, maxRcmdNaluSize); +} + +TEST_F(SdpTest, parseFmtpMaxRcmdNaluSizeWith4294967296) { + ParseSdp(kVideoSdp + "a=fmtp:120 max-rcmd-nalu-size=4294967296\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_max_rcmd_nalu_size(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpParameterAdd) { + ParseSdp(kVideoSdp + "a=fmtp:120 parameter-add=1\r\n"); + ASSERT_EQ(1, sdp_attr_fmtp_is_parameter_add(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpParameterAddWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 parameter-add=0\r\n"); + ASSERT_EQ(0, sdp_attr_fmtp_is_parameter_add(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpParameterAddWith2) { + ParseSdp(kVideoSdp + "a=fmtp:120 parameter-add=2\r\n"); + ASSERT_EQ(0, sdp_attr_fmtp_is_parameter_add(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpAnnexK) { + ParseSdp(kVideoSdp + "a=fmtp:120 K=566\r\n"); + ASSERT_EQ(566, sdp_attr_get_fmtp_annex_k_val(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpAnnexKWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 K=0\r\n"); + ASSERT_EQ(SDP_INVALID_VALUE, + sdp_attr_get_fmtp_annex_k_val(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpAnnexKWith65536) { + ParseSdp(kVideoSdp + "a=fmtp:120 K=65536\r\n"); + ASSERT_EQ(SDP_INVALID_VALUE, + sdp_attr_get_fmtp_annex_k_val(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpAnnexN) { + ParseSdp(kVideoSdp + "a=fmtp:120 N=4567\r\n"); + ASSERT_EQ(4567, sdp_attr_get_fmtp_annex_n_val(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpAnnexNWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 N=0\r\n"); + ASSERT_EQ(SDP_INVALID_VALUE, + sdp_attr_get_fmtp_annex_n_val(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpAnnexNWith65536) { + ParseSdp(kVideoSdp + "a=fmtp:120 N=65536\r\n"); + ASSERT_EQ(SDP_INVALID_VALUE, + sdp_attr_get_fmtp_annex_n_val(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpAnnexP) { + ParseSdp(kVideoSdp + "a=fmtp:120 P=5678,2\r\n"); + ASSERT_EQ(5678, sdp_attr_get_fmtp_annex_p_picture_resize(sdp_ptr_, 1, 0, 1)); + ASSERT_EQ(2, sdp_attr_get_fmtp_annex_p_warp(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpAnnexPWithResize0) { + ParseSdp(kVideoSdp + "a=fmtp:120 P=0,3\r\n"); + ASSERT_EQ(0, sdp_attr_get_fmtp_annex_p_picture_resize(sdp_ptr_, 1, 0, 1)); + ASSERT_EQ(3, sdp_attr_get_fmtp_annex_p_warp(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpAnnexPWithResize65536) { + ParseSdp(kVideoSdp + "a=fmtp:120 P=65536,4\r\n"); + ASSERT_EQ(0, sdp_attr_get_fmtp_annex_p_picture_resize(sdp_ptr_, 1, 0, 1)); + // if the first fails, the second will too. Both default to 0 on failure. + ASSERT_EQ(0, sdp_attr_get_fmtp_annex_p_warp(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpAnnexPWithWarp65536) { + ParseSdp(kVideoSdp + "a=fmtp:120 P=346,65536\r\n"); + ASSERT_EQ(346, sdp_attr_get_fmtp_annex_p_picture_resize(sdp_ptr_, 1, 0, 1)); + ASSERT_EQ(0, sdp_attr_get_fmtp_annex_p_warp(sdp_ptr_, 1, 0, 1)); +} + +TEST_F(SdpTest, parseFmtpLevelAsymmetryAllowed) { + ParseSdp(kVideoSdp + "a=fmtp:120 level-asymmetry-allowed=1\r\n"); + + uint16_t levelAsymmetryAllowed; + ASSERT_EQ(SDP_SUCCESS, sdp_attr_get_fmtp_level_asymmetry_allowed( + sdp_ptr_, 1, 0, 1, &levelAsymmetryAllowed)); + ASSERT_EQ(1U, levelAsymmetryAllowed); +} + +TEST_F(SdpTest, parseFmtpLevelAsymmetryAllowedWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 level-asymmetry-allowed=0\r\n"); + uint16_t levelAsymmetryAllowed; + ASSERT_EQ(SDP_SUCCESS, sdp_attr_get_fmtp_level_asymmetry_allowed( + sdp_ptr_, 1, 0, 1, &levelAsymmetryAllowed)); + ASSERT_EQ(0U, levelAsymmetryAllowed); +} + +TEST_F(SdpTest, parseFmtpLevelAsymmetryAllowedWith2) { + ParseSdp(kVideoSdp + "a=fmtp:120 level-asymmetry-allowed=2\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, sdp_attr_get_fmtp_level_asymmetry_allowed( + sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpMaxAverageBitrate) { + ParseSdp(kVideoSdp + "a=fmtp:120 maxaveragebitrate=47893\r\n"); + uint32_t maxAverageBitrate; + ASSERT_EQ(SDP_SUCCESS, sdp_attr_get_fmtp_max_average_bitrate( + sdp_ptr_, 1, 0, 1, &maxAverageBitrate)); + ASSERT_EQ(47893U, maxAverageBitrate); +} + +TEST_F(SdpTest, parseFmtpMaxAverageBitrateWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 maxaveragebitrate=0\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_max_average_bitrate(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpMaxAverageBitrateWith4294967296) { + ParseSdp(kVideoSdp + "a=fmtp:120 maxaveragebitrate=4294967296\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_max_average_bitrate(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpUsedTx) { + ParseSdp(kVideoSdp + "a=fmtp:120 usedtx=1\r\n"); + tinybool usedTx; + ASSERT_EQ(SDP_SUCCESS, sdp_attr_get_fmtp_usedtx(sdp_ptr_, 1, 0, 1, &usedTx)); + ASSERT_EQ(1, usedTx); +} + +TEST_F(SdpTest, parseFmtpUsedTxWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 usedtx=0\r\n"); + tinybool usedTx; + ASSERT_EQ(SDP_SUCCESS, sdp_attr_get_fmtp_usedtx(sdp_ptr_, 1, 0, 1, &usedTx)); + ASSERT_EQ(0, usedTx); +} + +TEST_F(SdpTest, parseFmtpUsedTxWith2) { + ParseSdp(kVideoSdp + "a=fmtp:120 usedtx=2\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_usedtx(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpStereo) { + ParseSdp(kVideoSdp + "a=fmtp:120 stereo=1\r\n"); + tinybool stereo; + ASSERT_EQ(SDP_SUCCESS, sdp_attr_get_fmtp_stereo(sdp_ptr_, 1, 0, 1, &stereo)); + ASSERT_EQ(1, stereo); +} + +TEST_F(SdpTest, parseFmtpStereoWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 stereo=0\r\n"); + tinybool stereo; + ASSERT_EQ(SDP_SUCCESS, sdp_attr_get_fmtp_stereo(sdp_ptr_, 1, 0, 1, &stereo)); + ASSERT_EQ(0, stereo); +} + +TEST_F(SdpTest, parseFmtpStereoWith2) { + ParseSdp(kVideoSdp + "a=fmtp:120 stereo=2\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_stereo(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpUseInBandFec) { + ParseSdp(kVideoSdp + "a=fmtp:120 useinbandfec=1\r\n"); + tinybool useInbandFec; + ASSERT_EQ(SDP_SUCCESS, + sdp_attr_get_fmtp_useinbandfec(sdp_ptr_, 1, 0, 1, &useInbandFec)); + ASSERT_EQ(1, useInbandFec); +} + +TEST_F(SdpTest, parseFmtpUseInBandWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 useinbandfec=0\r\n"); + tinybool useInbandFec; + ASSERT_EQ(SDP_SUCCESS, + sdp_attr_get_fmtp_useinbandfec(sdp_ptr_, 1, 0, 1, &useInbandFec)); + ASSERT_EQ(0, useInbandFec); +} + +TEST_F(SdpTest, parseFmtpUseInBandWith2) { + ParseSdp(kVideoSdp + "a=fmtp:120 useinbandfec=2\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_useinbandfec(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpMaxCodedAudioBandwidth) { + ParseSdp(kVideoSdp + "a=fmtp:120 maxcodedaudiobandwidth=abcdefg\r\n"); + char* maxCodedAudioBandwith = + sdp_attr_get_fmtp_maxcodedaudiobandwidth(sdp_ptr_, 1, 0, 1); + ASSERT_EQ(0, strcmp("abcdefg", maxCodedAudioBandwith)); +} + +TEST_F(SdpTest, parseFmtpMaxCodedAudioBandwidthBad) { + ParseSdp(kVideoSdp + "a=fmtp:120 maxcodedaudiobandwidth=\r\n"); + char* maxCodedAudioBandwith = + sdp_attr_get_fmtp_maxcodedaudiobandwidth(sdp_ptr_, 1, 0, 1); + ASSERT_EQ(0, *maxCodedAudioBandwith); +} + +TEST_F(SdpTest, parseFmtpCbr) { + ParseSdp(kVideoSdp + "a=fmtp:120 cbr=1\r\n"); + tinybool cbr; + ASSERT_EQ(SDP_SUCCESS, sdp_attr_get_fmtp_cbr(sdp_ptr_, 1, 0, 1, &cbr)); + ASSERT_EQ(1, cbr); +} + +TEST_F(SdpTest, parseFmtpCbrWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 cbr=0\r\n"); + tinybool cbr; + ASSERT_EQ(SDP_SUCCESS, sdp_attr_get_fmtp_cbr(sdp_ptr_, 1, 0, 1, &cbr)); + ASSERT_EQ(0, cbr); +} + +TEST_F(SdpTest, parseFmtpCbrWith2) { + ParseSdp(kVideoSdp + "a=fmtp:120 cbr=2\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_cbr(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpMaxPlaybackRate) { + ParseSdp(kVideoSdp + "a=fmtp:120 maxplaybackrate=47900\r\n"); + sdp_attr_t* attr_p = sdp_find_attr(sdp_ptr_, 1, 0, SDP_ATTR_FMTP, 1); + ASSERT_NE(nullptr, attr_p); + ASSERT_EQ(47900U, attr_p->attr.fmtp.maxplaybackrate); +} + +TEST_F(SdpTest, parseFmtpMaxPlaybackRateWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 maxplaybackrate=0\r\n"); + sdp_attr_t* attr_p = sdp_find_attr(sdp_ptr_, 1, 0, SDP_ATTR_FMTP, 1); + ASSERT_EQ(NULL, attr_p); +} + +TEST_F(SdpTest, parseFmtpMaxPlaybackRateWith4294967296) { + ParseSdp(kVideoSdp + "a=fmtp:120 maxplaybackrate=4294967296\r\n"); + sdp_attr_t* attr_p = sdp_find_attr(sdp_ptr_, 1, 0, SDP_ATTR_FMTP, 1); + ASSERT_EQ(NULL, attr_p); +} + +TEST_F(SdpTest, parseFmtpMaxFs) { + uint32_t val = 0; + ParseSdp(kVideoSdp + "a=fmtp:120 max-fs=300;max-fr=30\r\n"); + ASSERT_EQ(sdp_attr_get_fmtp_max_fs(sdp_ptr_, 1, 0, 1, &val), SDP_SUCCESS); + ASSERT_EQ(val, 300U); +} +TEST_F(SdpTest, parseFmtpMaxFsWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 max-fs=0\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_max_fs(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpMaxFsWith4294967296) { + ParseSdp(kVideoSdp + "a=fmtp:120 max-fs=4294967296\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_max_fs(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpMaxFr) { + uint32_t val = 0; + ParseSdp(kVideoSdp + "a=fmtp:120 max-fs=300;max-fr=30\r\n"); + ASSERT_EQ(sdp_attr_get_fmtp_max_fr(sdp_ptr_, 1, 0, 1, &val), SDP_SUCCESS); + ASSERT_EQ(val, 30U); +} + +TEST_F(SdpTest, parseFmtpMaxFrWith0) { + ParseSdp(kVideoSdp + "a=fmtp:120 max-fr=0\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_max_fr(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, parseFmtpMaxFrWith4294967296) { + ParseSdp(kVideoSdp + "a=fmtp:120 max-fr=4294967296\r\n"); + ASSERT_EQ(SDP_INVALID_PARAMETER, + sdp_attr_get_fmtp_max_fr(sdp_ptr_, 1, 0, 1, nullptr)); +} + +TEST_F(SdpTest, addFmtpMaxFs) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewFmtpMaxFs(level, 300); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=fmtp:120 max-fs=300\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addFmtpMaxFr) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewFmtpMaxFr(level, 30); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=fmtp:120 max-fr=30\r\n"), std::string::npos); +} + +TEST_F(SdpTest, addFmtpMaxFsFr) { + InitLocalSdp(); + int level = AddNewMedia(SDP_MEDIA_VIDEO); + AddNewFmtpMaxFsFr(level, 300, 30); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=fmtp:120 max-fs=300;max-fr=30\r\n"), + std::string::npos); +} + +static const std::string kBrokenFmtp = + "v=0\r\n" + "o=- 4294967296 2 IN IP4 127.0.0.1\r\n" + "s=SIP Call\r\n" + "t=0 0\r\n" + "m=video 56436 RTP/SAVPF 120\r\n" + "c=IN IP4 198.51.100.7\r\n" + "a=rtpmap:120 VP8/90000\r\n" + /* Note: the \0 in this string triggered bz://1089207 + */ + "a=fmtp:120 max-fs=300;max\0fr=30"; + +TEST_F(SdpTest, parseBrokenFmtp) { + uint32_t val = 0; + const char* buf = kBrokenFmtp.data(); + ResetSdp(); + /* We need to manually invoke the parser here to be able to specify the length + * of the string beyond the \0 in last line of the string. + */ + ASSERT_EQ(sdp_parse(sdp_ptr_, buf, 165), SDP_SUCCESS); + ASSERT_EQ(sdp_attr_get_fmtp_max_fs(sdp_ptr_, 1, 0, 1, &val), + SDP_INVALID_PARAMETER); +} + +TEST_F(SdpTest, addIceLite) { + InitLocalSdp(); + uint16_t inst_num = 0; + EXPECT_EQ(sdp_add_new_attr(sdp_ptr_, SDP_SESSION_LEVEL, 0, SDP_ATTR_ICE_LITE, + &inst_num), + SDP_SUCCESS); + std::string body = SerializeSdp(); + ASSERT_NE(body.find("a=ice-lite\r\n"), std::string::npos); +} + +TEST_F(SdpTest, parseIceLite) { + std::string sdp = + "v=0\r\n" + "o=- 4294967296 2 IN IP4 127.0.0.1\r\n" + "s=SIP Call\r\n" + "t=0 0\r\n" + "a=ice-lite\r\n"; + ParseSdp(sdp); + ASSERT_TRUE( + sdp_attr_is_present(sdp_ptr_, SDP_ATTR_ICE_LITE, SDP_SESSION_LEVEL, 0)); +} + +class NewSdpTest + : public ::testing::Test, + public ::testing::WithParamInterface< ::testing::tuple<bool, bool> > { + public: + NewSdpTest() = default; + + void ParseSdp(const std::string& sdp, bool expectSuccess = true, + bool expectEqual = true) { + UniquePtr<SdpParser> firstParser(new RsdparsaSdpParser()); + UniquePtr<SdpParser> secondParser(new SipccSdpParser()); + if (::testing::get<1>(GetParam())) { + firstParser.swap(secondParser); + } + mResults = firstParser->Parse(sdp); +#ifdef DEBUG_DISPLAY_SDP + std::cout << firstParser->Name() << " Parsing SDP:" << std::endl; + std::stringstream sdpStream(sdp); + std::string line; + size_t lineNumber = 0; + while (std::getline(sdpStream, line, '\n')) { + if (line.length() && line.back() == '\r') { + line.pop_back(); + } + lineNumber++; + std::cout << std::setw(4) << lineNumber << " " << line << std::endl; + } +#endif + // Are we configured to do a parse and serialize before actually + // running the test? + if (::testing::get<0>(GetParam())) { + if (expectSuccess) { + ASSERT_TRUE(!!mResults->Sdp()) + << "Parse failed on first pass: " << SerializeParseErrors(); + } + + if (mResults->Sdp()) { + std::stringstream os, os2; + // Serialize and re-parse + mResults->Sdp()->Serialize(os); + const auto secondResults = secondParser->Parse(os.str()); + // Whether we expected the parse to work or not, it should + // succeed the second time if it succeeded the first. + ASSERT_TRUE(!!Sdp()) + << "Parse failed on second pass, SDP was: " << std::endl + << os.str() << std::endl + << "Errors were: " << IntSerializeParseErrors(secondResults); + // Serialize again and compare + secondResults->Sdp()->Serialize(os2); + if (expectEqual) { + ASSERT_EQ(os.str(), os2.str()) + << "FIRST IS " << mResults->ParserName() << ", SECOND IS " + << secondResults->ParserName(); + } else { + ASSERT_NE(os.str(), os2.str()) + << "FIRST IS " << mResults->ParserName() << ", SECOND IS " + << secondResults->ParserName(); + } + } + } + + if (expectSuccess) { + ASSERT_TRUE(!!mResults->Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(0U, ParseErrorCount()) + << "Got unexpected parse errors/warnings: " << SerializeParseErrors(); + } + } + const UniquePtr<class Sdp>& Sdp() const { + static const UniquePtr<class Sdp> NO_SDP(nullptr); + return mResults ? mResults->Sdp() : NO_SDP; + } + + size_t ParseErrorCount() const { return mResults->Errors().size(); } + size_t ParseWarningCount() const { return mResults->Warnings().size(); } + + std::string IntSerializeParseErrors( + const UniquePtr<SdpParser::Results>& aResults) const { + std::stringstream output; + for (const auto& e : aResults->Errors()) { + output << e.first << ": " << e.second << std::endl; + } + return output.str(); + } + // For streaming parse errors + std::string SerializeParseErrors() const { + return IntSerializeParseErrors(mResults); + } + + std::string IntSerializeParseWarnings( + const UniquePtr<SdpParser::Results>& aResults) const { + std::stringstream output; + for (const auto& e : aResults->Warnings()) { + output << e.first << ": " << e.second << std::endl; + } + return output.str(); + } + + std::string SerializeParseWarnings() const { + return IntSerializeParseWarnings(mResults); + } + + void CheckRtpmap(const std::string& expected_pt, + SdpRtpmapAttributeList::CodecType codec, + const std::string& name, uint32_t clock, uint16_t channels, + const std::string& search_pt, + const SdpRtpmapAttributeList& rtpmaps) const { + ASSERT_TRUE(rtpmaps.HasEntry(search_pt)); + auto attr = rtpmaps.GetEntry(search_pt); + ASSERT_EQ(expected_pt, attr.pt); + ASSERT_EQ(codec, attr.codec); + std::cout << "Codec = " << name << std::endl; + ASSERT_EQ(name, attr.name); + ASSERT_EQ(clock, attr.clock); + ASSERT_EQ(channels, attr.channels); + } + + void CheckSctpmap(const std::string& expected_pt, const std::string& name, + uint16_t streams, const std::string& search_pt, + const SdpSctpmapAttributeList& sctpmaps) const { + ASSERT_TRUE(sctpmaps.HasEntry(search_pt)); + auto attr = sctpmaps.GetFirstEntry(); + ASSERT_EQ(expected_pt, search_pt); + ASSERT_EQ(expected_pt, attr.pt); + ASSERT_EQ(name, attr.name); + ASSERT_EQ(streams, attr.streams); + } + + void CheckRtcpFb(const SdpRtcpFbAttributeList::Feedback& feedback, + const std::string& pt, SdpRtcpFbAttributeList::Type type, + const std::string& first_parameter, + const std::string& extra = "") const { + ASSERT_EQ(pt, feedback.pt); + ASSERT_EQ(type, feedback.type); + ASSERT_EQ(first_parameter, feedback.parameter); + ASSERT_EQ(extra, feedback.extra); + } + + void CheckDtmfFmtp(const std::string& expectedDtmfTones) const { + ASSERT_TRUE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kFmtpAttribute)); + auto audio_format_params = + Sdp()->GetMediaSection(0).GetAttributeList().GetFmtp().mFmtps; + ASSERT_EQ(2U, audio_format_params.size()); + + ASSERT_EQ("101", audio_format_params[1].format); + ASSERT_TRUE(!!audio_format_params[1].parameters); + const SdpFmtpAttributeList::TelephoneEventParameters* te_parameters = + static_cast<SdpFmtpAttributeList::TelephoneEventParameters*>( + audio_format_params[1].parameters.get()); + ASSERT_NE(0U, te_parameters->dtmfTones.size()); + ASSERT_EQ(expectedDtmfTones, te_parameters->dtmfTones); + } + + void CheckSerialize(const std::string& expected, + const SdpAttribute& attr) const { + std::stringstream str; + attr.Serialize(str); + ASSERT_EQ(expected, str.str()); + } + + bool ResultsAreFromSipcc() const { + return mResults && SipccSdpParser::IsNamed(mResults->ParserName()); + } + + mozilla::UniquePtr<SdpParser::Results> mResults; +}; // class NewSdpTest + +TEST_P(NewSdpTest, CreateDestroy) {} + +TEST_P(NewSdpTest, ParseEmpty) { + ParseSdp("", false); + ASSERT_FALSE(Sdp()); + ASSERT_NE(0U, ParseErrorCount()) << "Expected at least one parse error."; +} + +const std::string kBadSdp = "This is SDPARTA!!!!"; + +TEST_P(NewSdpTest, ParseGarbage) { + ParseSdp(kBadSdp, false); + ASSERT_FALSE(Sdp()); + ASSERT_NE(0U, ParseErrorCount()) << "Expected at least one parse error."; +} + +TEST_P(NewSdpTest, ParseGarbageTwice) { + ParseSdp(kBadSdp, false); + ASSERT_FALSE(Sdp()); + size_t errorCount = ParseErrorCount(); + ASSERT_NE(0U, errorCount) << "Expected at least one parse error."; + ParseSdp(kBadSdp, false); + ASSERT_FALSE(Sdp()); + ASSERT_EQ(errorCount, ParseErrorCount()) + << "Expected same error count for same SDP."; +} + +TEST_P(NewSdpTest, ParseMinimal) { + ParseSdp(kVideoSdp); + ASSERT_EQ(0U, ParseErrorCount()) + << "Got parse errors: " << SerializeParseErrors(); +} + +TEST_P(NewSdpTest, CheckOriginGetUsername) { + ParseSdp(kVideoSdp); + ASSERT_EQ("-", Sdp()->GetOrigin().GetUsername()) + << "Wrong username in origin"; +} + +TEST_P(NewSdpTest, CheckOriginGetSessionId) { + ParseSdp(kVideoSdp); + ASSERT_EQ(4294967296U, Sdp()->GetOrigin().GetSessionId()) + << "Wrong session id in origin"; +} + +TEST_P(NewSdpTest, CheckOriginGetSessionVersion) { + ParseSdp(kVideoSdp); + ASSERT_EQ(2U, Sdp()->GetOrigin().GetSessionVersion()) + << "Wrong version in origin"; +} + +TEST_P(NewSdpTest, CheckOriginGetAddrType) { + ParseSdp(kVideoSdp); + ASSERT_EQ(sdp::kIPv4, Sdp()->GetOrigin().GetAddrType()) + << "Wrong address type in origin"; +} + +TEST_P(NewSdpTest, CheckOriginGetAddress) { + ParseSdp(kVideoSdp); + ASSERT_EQ("127.0.0.1", Sdp()->GetOrigin().GetAddress()) + << "Wrong address in origin"; +} + +TEST_P(NewSdpTest, CheckGetMissingBandwidth) { + ParseSdp(kVideoSdp); + ASSERT_EQ(0U, Sdp()->GetBandwidth("CT")) << "Wrong bandwidth in session"; +} + +TEST_P(NewSdpTest, CheckGetBandwidth) { + ParseSdp("v=0" CRLF "o=- 4294967296 2 IN IP4 127.0.0.1" CRLF "s=SIP Call" CRLF + "c=IN IP4 198.51.100.7" CRLF "b=CT:5000" CRLF "b=FOOBAR:10" CRLF + "b=AS:4" CRLF "t=0 0" CRLF "m=video 56436 RTP/SAVPF 120" CRLF + "a=rtpmap:120 VP8/90000" CRLF, + true, ::testing::get<1>(GetParam())); + ASSERT_EQ(5000U, Sdp()->GetBandwidth("CT")) + << "Wrong CT bandwidth in session"; + ASSERT_EQ(0U, Sdp()->GetBandwidth("FOOBAR")) + << "Wrong FOOBAR bandwidth in session"; + ASSERT_EQ(4U, Sdp()->GetBandwidth("AS")) << "Wrong AS bandwidth in session"; +} + +TEST_P(NewSdpTest, CheckGetMediaSectionsCount) { + ParseSdp(kVideoSdp); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; +} + +TEST_P(NewSdpTest, CheckMediaSectionGetMediaType) { + ParseSdp(kVideoSdp); + ASSERT_EQ(SdpMediaSection::kVideo, Sdp()->GetMediaSection(0).GetMediaType()) + << "Wrong type for first media section"; +} + +TEST_P(NewSdpTest, CheckMediaSectionGetProtocol) { + ParseSdp(kVideoSdp); + ASSERT_EQ(SdpMediaSection::kRtpSavpf, Sdp()->GetMediaSection(0).GetProtocol()) + << "Wrong protocol for video"; +} + +TEST_P(NewSdpTest, CheckMediaSectionGetFormats) { + ParseSdp(kVideoSdp); + auto video_formats = Sdp()->GetMediaSection(0).GetFormats(); + ASSERT_EQ(1U, video_formats.size()) << "Wrong number of formats for video"; + ASSERT_EQ("120", video_formats[0]); +} + +TEST_P(NewSdpTest, CheckMediaSectionGetPort) { + ParseSdp(kVideoSdp); + ASSERT_EQ(56436U, Sdp()->GetMediaSection(0).GetPort()) + << "Wrong port number in media section"; +} + +TEST_P(NewSdpTest, CheckMediaSectionGetMissingPortCount) { + ParseSdp(kVideoSdp); + ASSERT_EQ(0U, Sdp()->GetMediaSection(0).GetPortCount()) + << "Wrong port count in media section"; +} + +TEST_P(NewSdpTest, CheckMediaSectionGetPortCount) { + ParseSdp(kVideoSdp + "m=audio 12345/2 RTP/SAVPF 0" CRLF + "a=rtpmap:0 PCMU/8000" CRLF); + ASSERT_EQ(2U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + ASSERT_EQ(2U, Sdp()->GetMediaSection(1).GetPortCount()) + << "Wrong port count in media section"; +} + +TEST_P(NewSdpTest, CheckMediaSectionGetMissingBandwidth) { + ParseSdp(kVideoSdp); + ASSERT_EQ(0U, Sdp()->GetMediaSection(0).GetBandwidth("CT")) + << "Wrong bandwidth in media section"; +} + +TEST_P(NewSdpTest, CheckMediaSectionGetBandwidth) { + ParseSdp( + "v=0\r\n" + "o=- 4294967296 2 IN IP4 127.0.0.1\r\n" + "s=SIP Call\r\n" + "c=IN IP4 198.51.100.7\r\n" + "t=0 0\r\n" + "m=video 56436 RTP/SAVPF 120\r\n" + "b=CT:1000\r\n" + "a=rtpmap:120 VP8/90000\r\n"); + ASSERT_EQ(1000U, Sdp()->GetMediaSection(0).GetBandwidth("CT")) + << "Wrong bandwidth in media section"; +} + +// Define a string that is 258 characters long. We use a long string here so +// that we can test that we are able to parse and handle a string longer than +// the default maximum length of 256 in sipcc. +#define ID_A "1234567890abcdef" +#define ID_B ID_A ID_A ID_A ID_A +#define LONG_IDENTITY ID_B ID_B ID_B ID_B "xx" + +#define BASE64_DTLS_HELLO \ + "FgEAAAAAAAAAAAAAagEAAF4AAAAAAAAAXgEARI11KHx3QB6Ky" \ + "CKgoBj/kwjKrApkL8kiZLwIqBaJGT8AAAA2ADkAOAA1ABYAEwAKADMAMgAvAAcAZgAFAAQAYw" \ + "BiAGEAFQASAAkAZQBkAGAAFAARAAgABgADAQA=" + +// SDP from a basic A/V apprtc call FFX/FFX +const std::string kBasicAudioVideoOffer = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "a=dtls-message:client " BASE64_DTLS_HELLO CRLF "a=ice-ufrag:4a799b2e" CRLF + "a=ice-pwd:e4cc12a910f106a0a744719425510e17" CRLF "a=ice-lite" CRLF + "a=ice-options:trickle foo" CRLF "a=msid-semantic:WMS stream streama" CRLF + "a=msid-semantic:foo stream" CRLF + "a=fingerprint:sha-256 " + "DF:2E:AC:8A:FD:0A:8E:99:BF:5D:E8:3C:E7:FA:FB:08:3B:3C:54:1D:D7:D4:05:77:" + "A0:72:9B:14:08:6D:0F:4C" CRLF "a=identity:" LONG_IDENTITY CRLF + "a=group:BUNDLE first second" CRLF "a=group:BUNDLE third" CRLF + "a=group:LS first third" CRLF "m=audio 9 RTP/SAVPF 109 9 0 8 101" CRLF + "c=IN IP4 0.0.0.0" CRLF "a=mid:first" CRLF "a=rtpmap:109 opus/48000/2" CRLF + "a=fmtp:109 maxplaybackrate=32000;stereo=1" CRLF "a=ptime:20" CRLF + "a=maxptime:20" CRLF "a=rtpmap:9 G722/8000" CRLF "a=rtpmap:0 PCMU/8000" CRLF + "a=rtpmap:8 PCMA/8000" CRLF "a=rtpmap:101 telephone-event/8000" CRLF + "a=fmtp:101 0-15,66,32-34,67" CRLF "a=ice-ufrag:00000000" CRLF + "a=ice-pwd:0000000000000000000000000000000" CRLF "a=sendonly" CRLF + "a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level" CRLF + "a=setup:actpass" CRLF "a=rtcp-mux" CRLF "a=msid:stream track" CRLF + "a=candidate:0 1 UDP 2130379007 10.0.0.36 62453 typ host" CRLF + "a=candidate:2 1 UDP 1694236671 24.6.134.204 62453 typ srflx raddr " + "10.0.0.36 rport 62453" CRLF + "a=candidate:3 1 UDP 100401151 162.222.183.171 49761 typ relay raddr " + "162.222.183.171 rport 49761" CRLF + "a=candidate:6 1 UDP 16515071 162.222.183.171 51858 typ relay raddr " + "162.222.183.171 rport 51858" CRLF + "a=candidate:3 2 UDP 100401150 162.222.183.171 62454 typ relay raddr " + "162.222.183.171 rport 62454" CRLF + "a=candidate:2 2 UDP 1694236670 24.6.134.204 55428 typ srflx raddr " + "10.0.0.36 rport 55428" CRLF + "a=candidate:6 2 UDP 16515070 162.222.183.171 50340 typ relay raddr " + "162.222.183.171 rport 50340" CRLF + "a=candidate:0 2 UDP 2130379006 10.0.0.36 55428 typ host" CRLF + "a=rtcp:62454 IN IP4 162.222.183.171" CRLF "a=end-of-candidates" CRLF + "a=ssrc:5150" CRLF "m=video 9 RTP/SAVPF 120 121 122 123" CRLF + "c=IN IP6 ::1" CRLF + "a=fingerprint:sha-1 " + "DF:FA:FB:08:3B:3C:54:1D:D7:D4:05:77:A0:72:9B:14:08:6D:0F:4C" CRLF + "a=mid:second" CRLF "a=rtpmap:120 VP8/90000" CRLF + "a=fmtp:120 max-fs=3600;max-fr=30" CRLF "a=rtpmap:121 VP9/90000" CRLF + "a=fmtp:121 max-fs=3600;max-fr=30" CRLF "a=rtpmap:122 red/90000" CRLF + "a=rtpmap:123 ulpfec/90000" CRLF "a=recvonly" CRLF "a=rtcp-fb:120 nack" CRLF + "a=rtcp-fb:120 nack pli" CRLF "a=rtcp-fb:120 ccm fir" CRLF + "a=rtcp-fb:121 nack" CRLF "a=rtcp-fb:121 nack pli" CRLF + "a=rtcp-fb:121 ccm fir" CRLF "a=setup:active" CRLF "a=rtcp-mux" CRLF + "a=msid:streama tracka" CRLF "a=msid:streamb trackb" CRLF + "a=candidate:0 1 UDP 2130379007 10.0.0.36 59530 typ host" CRLF + "a=candidate:0 2 UDP 2130379006 10.0.0.36 64378 typ host" CRLF + "a=candidate:2 2 UDP 1694236670 24.6.134.204 64378 typ srflx raddr " + "10.0.0.36 rport 64378" CRLF + "a=candidate:6 2 UDP 16515070 162.222.183.171 64941 typ relay raddr " + "162.222.183.171 rport 64941" CRLF + "a=candidate:6 1 UDP 16515071 162.222.183.171 64800 typ relay raddr " + "162.222.183.171 rport 64800" CRLF + "a=candidate:2 1 UDP 1694236671 24.6.134.204 59530 typ srflx raddr " + "10.0.0.36 rport 59530" CRLF + "a=candidate:3 1 UDP 100401151 162.222.183.171 62935 typ relay raddr " + "162.222.183.171 rport 62935" CRLF + "a=candidate:3 2 UDP 100401150 162.222.183.171 61026 typ relay raddr " + "162.222.183.171 rport 61026" CRLF "a=rtcp:61026" CRLF + "a=end-of-candidates" CRLF "a=ssrc:1111 foo" CRLF "a=ssrc:1111 foo:bar" CRLF + "a=ssrc:1111 msid:1d0cdb4e-5934-4f0f-9f88-40392cb60d31 " + "315b086a-5cb6-4221-89de-caf0b038c79d" CRLF + "a=imageattr:120 send * recv *" CRLF + "a=imageattr:121 send [x=640,y=480] recv [x=640,y=480]" CRLF + "a=rid:bar recv pt=120;max-width=800;max-height=600" CRLF + "a=rid:bar123 recv max-width=1920;max-height=1080" CRLF + "a=simulcast:recv bar;bar123" CRLF "m=audio 9 RTP/SAVPF 0" CRLF + "a=mid:third" CRLF "a=rtpmap:0 PCMU/8000" CRLF "a=ice-options:foo bar" CRLF + "a=msid:noappdata" CRLF "a=bundle-only" CRLF; + +TEST_P(NewSdpTest, BasicAudioVideoSdpParse) { ParseSdp(kBasicAudioVideoOffer); } + +TEST_P(NewSdpTest, CheckRemoveFmtp) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(3U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + SdpAttributeList& audioAttrList = + Sdp()->GetMediaSection(0).GetAttributeList(); + + ASSERT_TRUE(audioAttrList.HasAttribute(SdpAttribute::kFmtpAttribute)); + ASSERT_EQ(2U, audioAttrList.GetFmtp().mFmtps.size()); + ASSERT_TRUE(Sdp()->GetMediaSection(0).FindFmtp("109")); + ASSERT_TRUE(Sdp()->GetMediaSection(0).FindFmtp("101")); + + Sdp()->GetMediaSection(0).RemoveFmtp("101"); + + ASSERT_TRUE(audioAttrList.HasAttribute(SdpAttribute::kFmtpAttribute)); + ASSERT_EQ(1U, audioAttrList.GetFmtp().mFmtps.size()); + ASSERT_TRUE(Sdp()->GetMediaSection(0).FindFmtp("109")); + ASSERT_FALSE(Sdp()->GetMediaSection(0).FindFmtp("101")); + + Sdp()->GetMediaSection(0).RemoveFmtp("109"); + + ASSERT_TRUE(audioAttrList.HasAttribute(SdpAttribute::kFmtpAttribute)); + ASSERT_EQ(0U, audioAttrList.GetFmtp().mFmtps.size()); + ASSERT_FALSE(Sdp()->GetMediaSection(0).FindFmtp("109")); + ASSERT_FALSE(Sdp()->GetMediaSection(0).FindFmtp("101")); + + // make sure we haven't disturbed the video fmtps + SdpAttributeList& videoAttrList = + Sdp()->GetMediaSection(1).GetAttributeList(); + ASSERT_TRUE(videoAttrList.HasAttribute(SdpAttribute::kFmtpAttribute)); + ASSERT_EQ(2U, videoAttrList.GetFmtp().mFmtps.size()); + ASSERT_TRUE(Sdp()->GetMediaSection(1).FindFmtp("120")); + ASSERT_TRUE(Sdp()->GetMediaSection(1).FindFmtp("121")); +} + +TEST_P(NewSdpTest, CheckIceUfrag) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_TRUE( + Sdp()->GetAttributeList().HasAttribute(SdpAttribute::kIceUfragAttribute)); + auto ice_ufrag = Sdp()->GetAttributeList().GetIceUfrag(); + ASSERT_EQ("4a799b2e", ice_ufrag) << "Wrong ice-ufrag value"; + + ice_ufrag = Sdp()->GetMediaSection(0).GetAttributeList().GetIceUfrag(); + ASSERT_EQ("00000000", ice_ufrag) << "ice-ufrag isn't overridden"; + + ice_ufrag = Sdp()->GetMediaSection(1).GetAttributeList().GetIceUfrag(); + ASSERT_EQ("4a799b2e", ice_ufrag) << "ice-ufrag isn't carried to m-section"; +} + +TEST_P(NewSdpTest, CheckIcePwd) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_TRUE( + Sdp()->GetAttributeList().HasAttribute(SdpAttribute::kIcePwdAttribute)); + auto ice_pwd = Sdp()->GetAttributeList().GetIcePwd(); + ASSERT_EQ("e4cc12a910f106a0a744719425510e17", ice_pwd) + << "Wrong ice-pwd value"; + + ice_pwd = Sdp()->GetMediaSection(0).GetAttributeList().GetIcePwd(); + ASSERT_EQ("0000000000000000000000000000000", ice_pwd) + << "ice-pwd isn't overridden"; + + ice_pwd = Sdp()->GetMediaSection(1).GetAttributeList().GetIcePwd(); + ASSERT_EQ("e4cc12a910f106a0a744719425510e17", ice_pwd) + << "ice-pwd isn't carried to m-section"; +} + +TEST_P(NewSdpTest, CheckIceOptions) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_TRUE(Sdp()->GetAttributeList().HasAttribute( + SdpAttribute::kIceOptionsAttribute)); + auto ice_options = Sdp()->GetAttributeList().GetIceOptions(); + ASSERT_EQ(2U, ice_options.mValues.size()) << "Wrong ice-options size"; + ASSERT_EQ("trickle", ice_options.mValues[0]) << "Wrong ice-options value"; + ASSERT_EQ("foo", ice_options.mValues[1]) << "Wrong ice-options value"; + + ASSERT_TRUE(Sdp()->GetMediaSection(2).GetAttributeList().HasAttribute( + SdpAttribute::kIceOptionsAttribute)); + auto ice_options_media_level = + Sdp()->GetMediaSection(2).GetAttributeList().GetIceOptions(); + ASSERT_EQ(2U, ice_options_media_level.mValues.size()) + << "Wrong ice-options size"; + ASSERT_EQ("foo", ice_options_media_level.mValues[0]) + << "Wrong ice-options value"; + ASSERT_EQ("bar", ice_options_media_level.mValues[1]) + << "Wrong ice-options value"; +} + +TEST_P(NewSdpTest, CheckFingerprint) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_TRUE(Sdp()->GetAttributeList().HasAttribute( + SdpAttribute::kFingerprintAttribute)); + auto fingerprints = Sdp()->GetAttributeList().GetFingerprint(); + ASSERT_EQ(1U, fingerprints.mFingerprints.size()); + ASSERT_EQ(SdpFingerprintAttributeList::kSha256, + fingerprints.mFingerprints[0].hashFunc) + << "Wrong hash function"; + ASSERT_EQ( + "DF:2E:AC:8A:FD:0A:8E:99:BF:5D:E8:3C:E7:FA:FB:08:" + "3B:3C:54:1D:D7:D4:05:77:A0:72:9B:14:08:6D:0F:4C", + SdpFingerprintAttributeList::FormatFingerprint( + fingerprints.mFingerprints[0].fingerprint)) + << "Wrong fingerprint"; + ASSERT_EQ(0xdfU, fingerprints.mFingerprints[0].fingerprint[0]) + << "first fingerprint element is iffy"; + + ASSERT_EQ(3U, Sdp()->GetMediaSectionCount()); + + // Fallback to session level + ASSERT_TRUE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kFingerprintAttribute)); + fingerprints = Sdp()->GetMediaSection(0).GetAttributeList().GetFingerprint(); + ASSERT_EQ(1U, fingerprints.mFingerprints.size()); + ASSERT_EQ(SdpFingerprintAttributeList::kSha256, + fingerprints.mFingerprints[0].hashFunc) + << "Wrong hash function"; + ASSERT_EQ( + "DF:2E:AC:8A:FD:0A:8E:99:BF:5D:E8:3C:E7:FA:FB:08:" + "3B:3C:54:1D:D7:D4:05:77:A0:72:9B:14:08:6D:0F:4C", + SdpFingerprintAttributeList::FormatFingerprint( + fingerprints.mFingerprints[0].fingerprint)) + << "Wrong fingerprint"; + ASSERT_EQ(0xdfU, fingerprints.mFingerprints[0].fingerprint[0]) + << "first fingerprint element is iffy"; + + // Media level + ASSERT_TRUE(Sdp()->GetMediaSection(1).GetAttributeList().HasAttribute( + SdpAttribute::kFingerprintAttribute)); + fingerprints = Sdp()->GetMediaSection(1).GetAttributeList().GetFingerprint(); + ASSERT_EQ(1U, fingerprints.mFingerprints.size()); + ASSERT_EQ(SdpFingerprintAttributeList::kSha1, + fingerprints.mFingerprints[0].hashFunc) + << "Wrong hash function"; + ASSERT_EQ( + "DF:FA:FB:08:3B:3C:54:1D:D7:D4:05:77:A0:72:9B:14:" + "08:6D:0F:4C", + SdpFingerprintAttributeList::FormatFingerprint( + fingerprints.mFingerprints[0].fingerprint)) + << "Wrong fingerprint"; + ASSERT_EQ(0xdfU, fingerprints.mFingerprints[0].fingerprint[0]) + << "first fingerprint element is iffy"; +} + +TEST_P(NewSdpTest, CheckIdentity) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_TRUE( + Sdp()->GetAttributeList().HasAttribute(SdpAttribute::kIdentityAttribute)); + auto identity = Sdp()->GetAttributeList().GetIdentity(); + ASSERT_EQ(LONG_IDENTITY, identity) << "Wrong identity assertion"; +} + +TEST_P(NewSdpTest, CheckDtlsMessage) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_TRUE(Sdp()->GetAttributeList().HasAttribute( + SdpAttribute::kDtlsMessageAttribute)); + auto dtls_message = Sdp()->GetAttributeList().GetDtlsMessage(); + ASSERT_EQ(SdpDtlsMessageAttribute::kClient, dtls_message.mRole) + << "Wrong dtls-message role"; + ASSERT_EQ(BASE64_DTLS_HELLO, dtls_message.mValue) + << "Wrong dtls-message value"; +} + +TEST_P(NewSdpTest, CheckNumberOfMediaSections) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(3U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; +} + +TEST_P(NewSdpTest, CheckMlines) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(3U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + ASSERT_EQ(SdpMediaSection::kAudio, Sdp()->GetMediaSection(0).GetMediaType()) + << "Wrong type for first media section"; + ASSERT_EQ(SdpMediaSection::kRtpSavpf, Sdp()->GetMediaSection(0).GetProtocol()) + << "Wrong protocol for audio"; + auto audio_formats = Sdp()->GetMediaSection(0).GetFormats(); + ASSERT_EQ(5U, audio_formats.size()) << "Wrong number of formats for audio"; + ASSERT_EQ("109", audio_formats[0]); + ASSERT_EQ("9", audio_formats[1]); + ASSERT_EQ("0", audio_formats[2]); + ASSERT_EQ("8", audio_formats[3]); + ASSERT_EQ("101", audio_formats[4]); + + ASSERT_EQ(SdpMediaSection::kVideo, Sdp()->GetMediaSection(1).GetMediaType()) + << "Wrong type for second media section"; + ASSERT_EQ(SdpMediaSection::kRtpSavpf, Sdp()->GetMediaSection(1).GetProtocol()) + << "Wrong protocol for video"; + auto video_formats = Sdp()->GetMediaSection(1).GetFormats(); + ASSERT_EQ(4U, video_formats.size()) << "Wrong number of formats for video"; + ASSERT_EQ("120", video_formats[0]); + ASSERT_EQ("121", video_formats[1]); + ASSERT_EQ("122", video_formats[2]); + ASSERT_EQ("123", video_formats[3]); + + ASSERT_EQ(SdpMediaSection::kAudio, Sdp()->GetMediaSection(2).GetMediaType()) + << "Wrong type for third media section"; +} + +TEST_P(NewSdpTest, CheckSetup) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(3U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + ASSERT_TRUE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kSetupAttribute)); + ASSERT_EQ(SdpSetupAttribute::kActpass, + Sdp()->GetMediaSection(0).GetAttributeList().GetSetup().mRole); + ASSERT_TRUE(Sdp()->GetMediaSection(1).GetAttributeList().HasAttribute( + SdpAttribute::kSetupAttribute)); + ASSERT_EQ(SdpSetupAttribute::kActive, + Sdp()->GetMediaSection(1).GetAttributeList().GetSetup().mRole); + ASSERT_FALSE(Sdp()->GetMediaSection(2).GetAttributeList().HasAttribute( + SdpAttribute::kSetupAttribute)); +} + +TEST_P(NewSdpTest, CheckSsrc) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(3U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + ASSERT_TRUE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kSsrcAttribute)); + auto ssrcs = Sdp()->GetMediaSection(0).GetAttributeList().GetSsrc().mSsrcs; + ASSERT_EQ(1U, ssrcs.size()); + ASSERT_EQ(5150U, ssrcs[0].ssrc); + ASSERT_EQ("", ssrcs[0].attribute); + + ASSERT_TRUE(Sdp()->GetMediaSection(1).GetAttributeList().HasAttribute( + SdpAttribute::kSsrcAttribute)); + ssrcs = Sdp()->GetMediaSection(1).GetAttributeList().GetSsrc().mSsrcs; + ASSERT_EQ(3U, ssrcs.size()); + ASSERT_EQ(1111U, ssrcs[0].ssrc); + ASSERT_EQ("foo", ssrcs[0].attribute); + ASSERT_EQ(1111U, ssrcs[1].ssrc); + ASSERT_EQ("foo:bar", ssrcs[1].attribute); + ASSERT_EQ(1111U, ssrcs[2].ssrc); + ASSERT_EQ( + "msid:1d0cdb4e-5934-4f0f-9f88-40392cb60d31 " + "315b086a-5cb6-4221-89de-caf0b038c79d", + ssrcs[2].attribute); +} + +TEST_P(NewSdpTest, CheckRtpmap) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(3U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + const SdpMediaSection& audiosec = Sdp()->GetMediaSection(0); + const SdpRtpmapAttributeList& rtpmap = + audiosec.GetAttributeList().GetRtpmap(); + ASSERT_EQ(5U, rtpmap.mRtpmaps.size()) + << "Wrong number of rtpmap attributes for audio"; + + // Need to know name of type + CheckRtpmap("109", SdpRtpmapAttributeList::kOpus, "opus", 48000, 2, + audiosec.GetFormats()[0], rtpmap); + + CheckRtpmap("9", SdpRtpmapAttributeList::kG722, "G722", 8000, 1, + audiosec.GetFormats()[1], rtpmap); + + CheckRtpmap("0", SdpRtpmapAttributeList::kPCMU, "PCMU", 8000, 1, + audiosec.GetFormats()[2], rtpmap); + + CheckRtpmap("8", SdpRtpmapAttributeList::kPCMA, "PCMA", 8000, 1, + audiosec.GetFormats()[3], rtpmap); + + CheckRtpmap("101", SdpRtpmapAttributeList::kTelephoneEvent, "telephone-event", + 8000, 1, audiosec.GetFormats()[4], rtpmap); + + const SdpMediaSection& videosec = Sdp()->GetMediaSection(1); + const SdpRtpmapAttributeList videoRtpmap = + videosec.GetAttributeList().GetRtpmap(); + ASSERT_EQ(4U, videoRtpmap.mRtpmaps.size()) + << "Wrong number of rtpmap attributes for video"; + + CheckRtpmap("120", SdpRtpmapAttributeList::kVP8, "VP8", 90000, 0, + videosec.GetFormats()[0], videoRtpmap); + + CheckRtpmap("121", SdpRtpmapAttributeList::kVP9, "VP9", 90000, 0, + videosec.GetFormats()[1], videoRtpmap); + + CheckRtpmap("122", SdpRtpmapAttributeList::kRed, "red", 90000, 0, + videosec.GetFormats()[2], videoRtpmap); + + CheckRtpmap("123", SdpRtpmapAttributeList::kUlpfec, "ulpfec", 90000, 0, + videosec.GetFormats()[3], videoRtpmap); +} + +static const std::string kAudioWithTelephoneEvent = + "v=0" CRLF "o=- 4294967296 2 IN IP4 127.0.0.1" CRLF "s=SIP Call" CRLF + "c=IN IP4 198.51.100.7" CRLF "t=0 0" CRLF + "m=audio 9 RTP/SAVPF 109 9 0 8 101" CRLF "c=IN IP4 0.0.0.0" CRLF + "a=mid:first" CRLF "a=rtpmap:109 opus/48000/2" CRLF + "a=fmtp:109 maxplaybackrate=32000;stereo=1" CRLF "a=ptime:20" CRLF + "a=maxptime:20" CRLF "a=rtpmap:9 G722/8000" CRLF "a=rtpmap:0 PCMU/8000" CRLF + "a=rtpmap:8 PCMA/8000" CRLF "a=rtpmap:101 telephone-event/8000" CRLF; + +TEST_P(NewSdpTest, CheckTelephoneEventNoFmtp) { + ParseSdp(kAudioWithTelephoneEvent); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + ASSERT_TRUE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kFmtpAttribute)); + auto audio_format_params = + Sdp()->GetMediaSection(0).GetAttributeList().GetFmtp().mFmtps; + ASSERT_EQ(1U, audio_format_params.size()); + + // make sure we don't get a fmtp for codec 101 + for (size_t i = 0; i < audio_format_params.size(); ++i) { + ASSERT_NE("101", audio_format_params[i].format); + } +} + +TEST_P(NewSdpTest, CheckTelephoneEventWithDefaultEvents) { + ParseSdp(kAudioWithTelephoneEvent + "a=fmtp:101 0-15" CRLF); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + CheckDtmfFmtp("0-15"); +} + +TEST_P(NewSdpTest, CheckTelephoneEventWithBadCharacter) { + ParseSdp(kAudioWithTelephoneEvent + "a=fmtp:101 0-5." CRLF); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + CheckDtmfFmtp("0-15"); +} + +TEST_P(NewSdpTest, CheckTelephoneEventIncludingCommas) { + ParseSdp(kAudioWithTelephoneEvent + "a=fmtp:101 0-15,66,67" CRLF); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + CheckDtmfFmtp("0-15,66,67"); +} + +TEST_P(NewSdpTest, CheckTelephoneEventComplexEvents) { + ParseSdp(kAudioWithTelephoneEvent + "a=fmtp:101 0,1,2-4,5-15,66,67" CRLF); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + CheckDtmfFmtp("0,1,2-4,5-15,66,67"); +} + +TEST_P(NewSdpTest, CheckTelephoneEventNoHyphen) { + ParseSdp(kAudioWithTelephoneEvent + "a=fmtp:101 5,6,7" CRLF); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + CheckDtmfFmtp("5,6,7"); +} + +TEST_P(NewSdpTest, CheckTelephoneEventOnlyZero) { + ParseSdp(kAudioWithTelephoneEvent + "a=fmtp:101 0" CRLF); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + CheckDtmfFmtp("0"); +} + +TEST_P(NewSdpTest, CheckTelephoneEventOnlyOne) { + ParseSdp(kAudioWithTelephoneEvent + "a=fmtp:101 1" CRLF); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + CheckDtmfFmtp("1"); +} + +TEST_P(NewSdpTest, CheckTelephoneEventBadThreeDigit) { + ParseSdp(kAudioWithTelephoneEvent + "a=fmtp:101 123" CRLF); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + // check for the default dtmf tones + CheckDtmfFmtp("0-15"); +} + +TEST_P(NewSdpTest, CheckTelephoneEventBadThreeDigitWithHyphen) { + ParseSdp(kAudioWithTelephoneEvent + "a=fmtp:101 0-123" CRLF); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + // check for the default dtmf tones + CheckDtmfFmtp("0-15"); +} + +TEST_P(NewSdpTest, CheckTelephoneEventBadLeadingHyphen) { + ParseSdp(kAudioWithTelephoneEvent + "a=fmtp:101 -12" CRLF); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + // check for the default dtmf tones + CheckDtmfFmtp("0-15"); +} + +TEST_P(NewSdpTest, CheckTelephoneEventBadTrailingHyphen) { + ParseSdp(kAudioWithTelephoneEvent + "a=fmtp:101 12-" CRLF, false); +} + +TEST_P(NewSdpTest, CheckTelephoneEventBadTrailingHyphenInMiddle) { + ParseSdp(kAudioWithTelephoneEvent + "a=fmtp:101 1,12-,4" CRLF, false); +} + +TEST_P(NewSdpTest, CheckTelephoneEventBadLeadingComma) { + ParseSdp(kAudioWithTelephoneEvent + "a=fmtp:101 ,2,3" CRLF); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + // check for the default dtmf tones + CheckDtmfFmtp("0-15"); +} + +TEST_P(NewSdpTest, CheckTelephoneEventBadMultipleLeadingComma) { + ParseSdp(kAudioWithTelephoneEvent + "a=fmtp:101 ,,,2,3" CRLF); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + // check for the default dtmf tones + CheckDtmfFmtp("0-15"); +} + +TEST_P(NewSdpTest, CheckTelephoneEventBadConsecutiveCommas) { + ParseSdp(kAudioWithTelephoneEvent + "a=fmtp:101 1,,,,,,,,3" CRLF); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + // check for the default dtmf tones + CheckDtmfFmtp("0-15"); +} + +TEST_P(NewSdpTest, CheckTelephoneEventBadTrailingComma) { + ParseSdp(kAudioWithTelephoneEvent + "a=fmtp:101 1,2,3," CRLF); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + // check for the default dtmf tones + CheckDtmfFmtp("0-15"); +} + +TEST_P(NewSdpTest, CheckTelephoneEventBadTwoHyphens) { + ParseSdp(kAudioWithTelephoneEvent + "a=fmtp:101 1-2-3" CRLF); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + // check for the default dtmf tones + CheckDtmfFmtp("0-15"); +} + +TEST_P(NewSdpTest, CheckTelephoneEventBadSixDigit) { + ParseSdp(kAudioWithTelephoneEvent + "a=fmtp:101 112233" CRLF); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + // check for the default dtmf tones + CheckDtmfFmtp("0-15"); +} + +TEST_P(NewSdpTest, CheckTelephoneEventBadRangeReversed) { + ParseSdp(kAudioWithTelephoneEvent + "a=fmtp:101 33-2" CRLF); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + // check for the default dtmf tones + CheckDtmfFmtp("0-15"); +} + +static const std::string kVideoWithRedAndUlpfecSdp = + "v=0" CRLF "o=- 4294967296 2 IN IP4 127.0.0.1" CRLF "s=SIP Call" CRLF + "c=IN IP4 198.51.100.7" CRLF "t=0 0" CRLF + "m=video 9 RTP/SAVPF 97 120 121 122 123" CRLF "c=IN IP6 ::1" CRLF + "a=fingerprint:sha-1 " + "DF:FA:FB:08:3B:3C:54:1D:D7:D4:05:77:A0:72:9B:14:08:6D:0F:4C" CRLF + "a=rtpmap:97 H264/90000" CRLF "a=fmtp:97 profile-level-id=42a01e" CRLF + "a=rtpmap:120 VP8/90000" CRLF "a=fmtp:120 max-fs=3600;max-fr=30" CRLF + "a=rtpmap:121 VP9/90000" CRLF "a=fmtp:121 max-fs=3600;max-fr=30" CRLF + "a=rtpmap:122 red/90000" CRLF "a=rtpmap:123 ulpfec/90000" CRLF; + +TEST_P(NewSdpTest, CheckRedNoFmtp) { + ParseSdp(kVideoWithRedAndUlpfecSdp); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + ASSERT_TRUE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kFmtpAttribute)); + auto video_format_params = + Sdp()->GetMediaSection(0).GetAttributeList().GetFmtp().mFmtps; + ASSERT_EQ(3U, video_format_params.size()); + + // make sure we don't get a fmtp for codec 122 + for (size_t i = 0; i < video_format_params.size(); ++i) { + ASSERT_NE("122", video_format_params[i].format); + } +} + +TEST_P(NewSdpTest, CheckRedEmptyFmtp) { + // if serializing and re-parsing, we expect errors + if (::testing::get<0>(GetParam())) { + ParseSdp(kVideoWithRedAndUlpfecSdp + "a=fmtp:122" CRLF, false); + } else { + ParseSdp(kVideoWithRedAndUlpfecSdp + "a=fmtp:122" CRLF, false); + + if (ResultsAreFromSipcc()) { + ASSERT_NE(0U, ParseErrorCount()); + } else { + // This is the branch for the rust parser to check for warnings, as the + // rust parser will give a warning at that point instead of an error. + ASSERT_NE(0U, ParseWarningCount()); + } + } + + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + ASSERT_TRUE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kFmtpAttribute)); + auto video_format_params = + Sdp()->GetMediaSection(0).GetAttributeList().GetFmtp().mFmtps; + ASSERT_EQ(3U, video_format_params.size()); + + // make sure we don't get a fmtp for codec 122 + for (size_t i = 0; i < video_format_params.size(); ++i) { + ASSERT_NE("122", video_format_params[i].format); + } +} + +TEST_P(NewSdpTest, CheckRedFmtpWith2Codecs) { + ParseSdp(kVideoWithRedAndUlpfecSdp + "a=fmtp:122 120/121" CRLF); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + ASSERT_TRUE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kFmtpAttribute)); + auto video_format_params = + Sdp()->GetMediaSection(0).GetAttributeList().GetFmtp().mFmtps; + ASSERT_EQ(4U, video_format_params.size()); + + ASSERT_EQ("122", video_format_params[3].format); + ASSERT_TRUE(!!video_format_params[3].parameters); + ASSERT_EQ(SdpRtpmapAttributeList::kRed, + video_format_params[3].parameters->codec_type); + const SdpFmtpAttributeList::RedParameters* red_parameters( + static_cast<SdpFmtpAttributeList::RedParameters*>( + video_format_params[3].parameters.get())); + ASSERT_EQ(2U, red_parameters->encodings.size()); + ASSERT_EQ(120U, red_parameters->encodings[0]); + ASSERT_EQ(121U, red_parameters->encodings[1]); +} + +TEST_P(NewSdpTest, CheckRedFmtpWith3Codecs) { + ParseSdp(kVideoWithRedAndUlpfecSdp + "a=fmtp:122 120/121/123" CRLF); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + ASSERT_TRUE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kFmtpAttribute)); + auto video_format_params = + Sdp()->GetMediaSection(0).GetAttributeList().GetFmtp().mFmtps; + ASSERT_EQ(4U, video_format_params.size()); + + ASSERT_EQ("122", video_format_params[3].format); + ASSERT_TRUE(!!video_format_params[3].parameters); + ASSERT_EQ(SdpRtpmapAttributeList::kRed, + video_format_params[3].parameters->codec_type); + const SdpFmtpAttributeList::RedParameters* red_parameters( + static_cast<SdpFmtpAttributeList::RedParameters*>( + video_format_params[3].parameters.get())); + ASSERT_EQ(3U, red_parameters->encodings.size()); + ASSERT_EQ(120U, red_parameters->encodings[0]); + ASSERT_EQ(121U, red_parameters->encodings[1]); + ASSERT_EQ(123U, red_parameters->encodings[2]); +} + +const std::string kH264AudioVideoOffer = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "a=ice-ufrag:4a799b2e" CRLF + "a=ice-pwd:e4cc12a910f106a0a744719425510e17" CRLF "a=ice-lite" CRLF + "a=msid-semantic:WMS stream streama" CRLF + "a=fingerprint:sha-256 " + "DF:2E:AC:8A:FD:0A:8E:99:BF:5D:E8:3C:E7:FA:FB:08:3B:3C:54:1D:D7:D4:05:77:" + "A0:72:9B:14:08:6D:0F:4C" CRLF "a=group:BUNDLE first second" CRLF + "a=group:BUNDLE third" CRLF "a=group:LS first third" CRLF + "m=audio 9 RTP/SAVPF 109 9 0 8 101" CRLF "c=IN IP4 0.0.0.0" CRLF + "a=mid:first" CRLF "a=rtpmap:109 opus/48000/2" CRLF "a=ptime:20" CRLF + "a=maxptime:20" CRLF "a=rtpmap:9 G722/8000" CRLF "a=rtpmap:0 PCMU/8000" CRLF + "a=rtpmap:8 PCMA/8000" CRLF "a=rtpmap:101 telephone-event/8000" CRLF + "a=fmtp:109 maxplaybackrate=32000;stereo=1;useinbandfec=1" CRLF + "a=fmtp:101 0-15,66,32-34,67" CRLF "a=ice-ufrag:00000000" CRLF + "a=ice-pwd:0000000000000000000000000000000" CRLF "a=sendonly" CRLF + "a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level" CRLF + "a=setup:actpass" CRLF "a=rtcp-mux" CRLF "a=msid:stream track" CRLF + "a=candidate:0 1 UDP 2130379007 10.0.0.36 62453 typ host" CRLF + "a=candidate:2 1 UDP 1694236671 24.6.134.204 62453 typ srflx raddr " + "10.0.0.36 rport 62453" CRLF + "a=candidate:3 1 UDP 100401151 162.222.183.171 49761 typ relay raddr " + "162.222.183.171 rport 49761" CRLF + "a=candidate:6 1 UDP 16515071 162.222.183.171 51858 typ relay raddr " + "162.222.183.171 rport 51858" CRLF + "a=candidate:3 2 UDP 100401150 162.222.183.171 62454 typ relay raddr " + "162.222.183.171 rport 62454" CRLF + "a=candidate:2 2 UDP 1694236670 24.6.134.204 55428 typ srflx raddr " + "10.0.0.36 rport 55428" CRLF + "a=candidate:6 2 UDP 16515070 162.222.183.171 50340 typ relay raddr " + "162.222.183.171 rport 50340" CRLF + "a=candidate:0 2 UDP 2130379006 10.0.0.36 55428 typ host" CRLF + "m=video 9 RTP/SAVPF 97 98 120" CRLF "c=IN IP6 ::1" CRLF "a=mid:second" CRLF + "a=rtpmap:97 H264/90000" CRLF "a=fmtp:97 profile-level-id=42a01e" CRLF + "a=rtpmap:98 H264/90000" CRLF + "a=fmtp:98 " + "PROFILE=0;LEVEL=0;parameter-add=1;profile-level-id=42a00d;packetization-" + "mode=1;level-asymmetry-allowed=1;max-mbps=42000;max-fs=1400;max-cpb=1000;" + "max-dpb=1000;max-br=180000;usedtx=0;stereo=0;useinbandfec=0;cbr=0" CRLF + "a=rtpmap:120 VP8/90000" CRLF "a=fmtp:120 max-fs=3601;max-fr=31" CRLF + "a=recvonly" CRLF "a=setup:active" CRLF "a=rtcp-mux" CRLF + "a=msid:streama tracka" CRLF "a=msid:streamb trackb" CRLF + "a=candidate:0 1 UDP 2130379007 10.0.0.36 59530 typ host" CRLF + "a=candidate:0 2 UDP 2130379006 10.0.0.36 64378 typ host" CRLF + "a=candidate:2 2 UDP 1694236670 24.6.134.204 64378 typ srflx raddr " + "10.0.0.36 rport 64378" CRLF + "a=candidate:6 2 UDP 16515070 162.222.183.171 64941 typ relay raddr " + "162.222.183.171 rport 64941" CRLF + "a=candidate:6 1 UDP 16515071 162.222.183.171 64800 typ relay raddr " + "162.222.183.171 rport 64800" CRLF + "a=candidate:2 1 UDP 1694236671 24.6.134.204 59530 typ srflx raddr " + "10.0.0.36 rport 59530" CRLF + "a=candidate:3 1 UDP 100401151 162.222.183.171 62935 typ relay raddr " + "162.222.183.171 rport 62935" CRLF + "a=candidate:3 2 UDP 100401150 162.222.183.171 61026 typ relay raddr " + "162.222.183.171 rport 61026" CRLF "m=audio 9 RTP/SAVPF 0" CRLF + "a=mid:third" CRLF "a=rtpmap:0 PCMU/8000" CRLF "a=msid:noappdata" CRLF; + +TEST_P(NewSdpTest, CheckFormatParameters) { + ParseSdp(kH264AudioVideoOffer); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(3U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + ASSERT_TRUE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kFmtpAttribute)); + auto audio_format_params = + Sdp()->GetMediaSection(0).GetAttributeList().GetFmtp().mFmtps; + ASSERT_EQ(2U, audio_format_params.size()); + ASSERT_EQ("109", audio_format_params[0].format); + ASSERT_TRUE(!!audio_format_params[0].parameters); + const SdpFmtpAttributeList::OpusParameters* opus_parameters = + static_cast<SdpFmtpAttributeList::OpusParameters*>( + audio_format_params[0].parameters.get()); + ASSERT_EQ(32000U, opus_parameters->maxplaybackrate); + ASSERT_EQ(1U, opus_parameters->stereo); + ASSERT_EQ(1U, opus_parameters->useInBandFec); + ASSERT_EQ(0U, opus_parameters->maxAverageBitrate); + ASSERT_EQ(0U, opus_parameters->useDTX); + ASSERT_EQ(0U, opus_parameters->useCbr); + ASSERT_EQ(0U, opus_parameters->frameSizeMs); + ASSERT_EQ(0U, opus_parameters->minFrameSizeMs); + ASSERT_EQ(0U, opus_parameters->maxFrameSizeMs); + ASSERT_EQ("101", audio_format_params[1].format); + ASSERT_TRUE(!!audio_format_params[1].parameters); + const SdpFmtpAttributeList::TelephoneEventParameters* te_parameters = + static_cast<SdpFmtpAttributeList::TelephoneEventParameters*>( + audio_format_params[1].parameters.get()); + ASSERT_NE(0U, te_parameters->dtmfTones.size()); + ASSERT_EQ("0-15,66,32-34,67", te_parameters->dtmfTones); + + ASSERT_TRUE(Sdp()->GetMediaSection(1).GetAttributeList().HasAttribute( + SdpAttribute::kFmtpAttribute)); + auto video_format_params = + Sdp()->GetMediaSection(1).GetAttributeList().GetFmtp().mFmtps; + ASSERT_EQ(3U, video_format_params.size()); + ASSERT_EQ("97", video_format_params[0].format); + ASSERT_TRUE(!!video_format_params[0].parameters); + ASSERT_EQ(SdpRtpmapAttributeList::kH264, + video_format_params[0].parameters->codec_type); + const SdpFmtpAttributeList::H264Parameters* h264_parameters( + static_cast<SdpFmtpAttributeList::H264Parameters*>( + video_format_params[0].parameters.get())); + ASSERT_EQ((uint32_t)0x42a01e, h264_parameters->profile_level_id); + ASSERT_EQ(0U, h264_parameters->packetization_mode); + ASSERT_FALSE(static_cast<bool>(h264_parameters->level_asymmetry_allowed)); + ASSERT_EQ(0U, h264_parameters->max_mbps); + ASSERT_EQ(0U, h264_parameters->max_fs); + ASSERT_EQ(0U, h264_parameters->max_cpb); + ASSERT_EQ(0U, h264_parameters->max_dpb); + ASSERT_EQ(0U, h264_parameters->max_br); + + ASSERT_EQ("98", video_format_params[1].format); + ASSERT_TRUE(!!video_format_params[1].parameters); + ASSERT_EQ(SdpRtpmapAttributeList::kH264, + video_format_params[1].parameters->codec_type); + h264_parameters = static_cast<SdpFmtpAttributeList::H264Parameters*>( + video_format_params[1].parameters.get()); + ASSERT_EQ((uint32_t)0x42a00d, h264_parameters->profile_level_id); + ASSERT_EQ(1U, h264_parameters->packetization_mode); + ASSERT_TRUE(static_cast<bool>(h264_parameters->level_asymmetry_allowed)); + ASSERT_EQ(42000U, h264_parameters->max_mbps); + ASSERT_EQ(1400U, h264_parameters->max_fs); + ASSERT_EQ(1000U, h264_parameters->max_cpb); + ASSERT_EQ(1000U, h264_parameters->max_dpb); + ASSERT_EQ(180000U, h264_parameters->max_br); + + ASSERT_EQ("120", video_format_params[2].format); + ASSERT_TRUE(!!video_format_params[2].parameters); + ASSERT_EQ(SdpRtpmapAttributeList::kVP8, + video_format_params[2].parameters->codec_type); + const SdpFmtpAttributeList::VP8Parameters* vp8_parameters = + static_cast<SdpFmtpAttributeList::VP8Parameters*>( + video_format_params[2].parameters.get()); + ASSERT_EQ(3601U, vp8_parameters->max_fs); + ASSERT_EQ(31U, vp8_parameters->max_fr); + + ASSERT_FALSE(Sdp()->GetMediaSection(2).GetAttributeList().HasAttribute( + SdpAttribute::kFmtpAttribute)); +} + +TEST_P(NewSdpTest, CheckPtime) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_EQ(20U, Sdp()->GetMediaSection(0).GetAttributeList().GetPtime()); + ASSERT_FALSE(Sdp()->GetMediaSection(1).GetAttributeList().HasAttribute( + SdpAttribute::kPtimeAttribute)); +} + +TEST_P(NewSdpTest, CheckFlags) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_TRUE( + Sdp()->GetAttributeList().HasAttribute(SdpAttribute::kIceLiteAttribute)); + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kIceLiteAttribute)); + ASSERT_FALSE(Sdp()->GetMediaSection(1).GetAttributeList().HasAttribute( + SdpAttribute::kIceLiteAttribute)); + ASSERT_FALSE(Sdp()->GetMediaSection(2).GetAttributeList().HasAttribute( + SdpAttribute::kIceLiteAttribute)); + + ASSERT_TRUE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kRtcpMuxAttribute)); + ASSERT_FALSE(Sdp()->GetMediaSection(2).GetAttributeList().HasAttribute( + SdpAttribute::kRtcpMuxAttribute)); + + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kBundleOnlyAttribute)); + ASSERT_TRUE(Sdp()->GetMediaSection(2).GetAttributeList().HasAttribute( + SdpAttribute::kBundleOnlyAttribute)); + + ASSERT_TRUE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kEndOfCandidatesAttribute)); + ASSERT_TRUE(Sdp()->GetMediaSection(1).GetAttributeList().HasAttribute( + SdpAttribute::kEndOfCandidatesAttribute)); + ASSERT_FALSE(Sdp()->GetMediaSection(2).GetAttributeList().HasAttribute( + SdpAttribute::kEndOfCandidatesAttribute)); +} + +TEST_P(NewSdpTest, CheckConnectionLines) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(3U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + const SdpConnection& conn1 = Sdp()->GetMediaSection(0).GetConnection(); + ASSERT_EQ(sdp::kIPv4, conn1.GetAddrType()); + ASSERT_EQ("0.0.0.0", conn1.GetAddress()); + ASSERT_EQ(0U, conn1.GetTtl()); + ASSERT_EQ(0U, conn1.GetCount()); + + const SdpConnection& conn2 = Sdp()->GetMediaSection(1).GetConnection(); + ASSERT_EQ(sdp::kIPv6, conn2.GetAddrType()); + ASSERT_EQ("::1", conn2.GetAddress()); + ASSERT_EQ(0U, conn2.GetTtl()); + ASSERT_EQ(0U, conn2.GetCount()); + + // tests that we can fall through to session level as appropriate + const SdpConnection& conn3 = Sdp()->GetMediaSection(2).GetConnection(); + ASSERT_EQ(sdp::kIPv4, conn3.GetAddrType()); + ASSERT_EQ("224.0.0.1", conn3.GetAddress()); + ASSERT_EQ(100U, conn3.GetTtl()); + ASSERT_EQ(12U, conn3.GetCount()); +} + +TEST_P(NewSdpTest, CheckDirections) { + ParseSdp(kBasicAudioVideoOffer); + + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(SdpDirectionAttribute::kSendonly, + Sdp()->GetMediaSection(0).GetAttributeList().GetDirection()); + ASSERT_EQ(SdpDirectionAttribute::kRecvonly, + Sdp()->GetMediaSection(1).GetAttributeList().GetDirection()); + ASSERT_EQ(SdpDirectionAttribute::kSendrecv, + Sdp()->GetMediaSection(2).GetAttributeList().GetDirection()); +} + +TEST_P(NewSdpTest, CheckCandidates) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(3U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + ASSERT_TRUE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kCandidateAttribute)); + auto audio_candidates = + Sdp()->GetMediaSection(0).GetAttributeList().GetCandidate(); + ASSERT_EQ(8U, audio_candidates.size()); + ASSERT_EQ("0 1 UDP 2130379007 10.0.0.36 62453 typ host", audio_candidates[0]); + ASSERT_EQ( + "2 1 UDP 1694236671 24.6.134.204 62453 typ srflx raddr 10.0.0.36 rport " + "62453", + audio_candidates[1]); + ASSERT_EQ( + "3 1 UDP 100401151 162.222.183.171 49761 typ relay raddr 162.222.183.171 " + "rport 49761", + audio_candidates[2]); + ASSERT_EQ( + "6 1 UDP 16515071 162.222.183.171 51858 typ relay raddr 162.222.183.171 " + "rport 51858", + audio_candidates[3]); + ASSERT_EQ( + "3 2 UDP 100401150 162.222.183.171 62454 typ relay raddr 162.222.183.171 " + "rport 62454", + audio_candidates[4]); + ASSERT_EQ( + "2 2 UDP 1694236670 24.6.134.204 55428 typ srflx raddr 10.0.0.36 rport " + "55428", + audio_candidates[5]); + ASSERT_EQ( + "6 2 UDP 16515070 162.222.183.171 50340 typ relay raddr 162.222.183.171 " + "rport 50340", + audio_candidates[6]); + ASSERT_EQ("0 2 UDP 2130379006 10.0.0.36 55428 typ host", audio_candidates[7]); + + ASSERT_TRUE(Sdp()->GetMediaSection(1).GetAttributeList().HasAttribute( + SdpAttribute::kCandidateAttribute)); + auto video_candidates = + Sdp()->GetMediaSection(1).GetAttributeList().GetCandidate(); + ASSERT_EQ(8U, video_candidates.size()); + ASSERT_EQ("0 1 UDP 2130379007 10.0.0.36 59530 typ host", video_candidates[0]); + ASSERT_EQ("0 2 UDP 2130379006 10.0.0.36 64378 typ host", video_candidates[1]); + ASSERT_EQ( + "2 2 UDP 1694236670 24.6.134.204 64378 typ srflx raddr 10.0.0.36 rport " + "64378", + video_candidates[2]); + ASSERT_EQ( + "6 2 UDP 16515070 162.222.183.171 64941 typ relay raddr 162.222.183.171 " + "rport 64941", + video_candidates[3]); + ASSERT_EQ( + "6 1 UDP 16515071 162.222.183.171 64800 typ relay raddr 162.222.183.171 " + "rport 64800", + video_candidates[4]); + ASSERT_EQ( + "2 1 UDP 1694236671 24.6.134.204 59530 typ srflx raddr 10.0.0.36 rport " + "59530", + video_candidates[5]); + ASSERT_EQ( + "3 1 UDP 100401151 162.222.183.171 62935 typ relay raddr 162.222.183.171 " + "rport 62935", + video_candidates[6]); + ASSERT_EQ( + "3 2 UDP 100401150 162.222.183.171 61026 typ relay raddr 162.222.183.171 " + "rport 61026", + video_candidates[7]); + + ASSERT_FALSE(Sdp()->GetMediaSection(2).GetAttributeList().HasAttribute( + SdpAttribute::kCandidateAttribute)); +} + +TEST_P(NewSdpTest, CheckMid) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_EQ("first", Sdp()->GetMediaSection(0).GetAttributeList().GetMid()); + ASSERT_EQ("second", Sdp()->GetMediaSection(1).GetAttributeList().GetMid()); + ASSERT_EQ("third", Sdp()->GetMediaSection(2).GetAttributeList().GetMid()); +} + +TEST_P(NewSdpTest, CheckMsid) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_TRUE(Sdp()->GetAttributeList().HasAttribute( + SdpAttribute::kMsidSemanticAttribute)); + auto semantics = Sdp()->GetAttributeList().GetMsidSemantic().mMsidSemantics; + ASSERT_EQ(2U, semantics.size()); + ASSERT_EQ("WMS", semantics[0].semantic); + ASSERT_EQ(2U, semantics[0].msids.size()); + ASSERT_EQ("stream", semantics[0].msids[0]); + ASSERT_EQ("streama", semantics[0].msids[1]); + ASSERT_EQ("foo", semantics[1].semantic); + ASSERT_EQ(1U, semantics[1].msids.size()); + ASSERT_EQ("stream", semantics[1].msids[0]); + + const SdpMsidAttributeList& msids1 = + Sdp()->GetMediaSection(0).GetAttributeList().GetMsid(); + ASSERT_EQ(1U, msids1.mMsids.size()); + ASSERT_EQ("stream", msids1.mMsids[0].identifier); + ASSERT_EQ("track", msids1.mMsids[0].appdata); + const SdpMsidAttributeList& msids2 = + Sdp()->GetMediaSection(1).GetAttributeList().GetMsid(); + ASSERT_EQ(2U, msids2.mMsids.size()); + ASSERT_EQ("streama", msids2.mMsids[0].identifier); + ASSERT_EQ("tracka", msids2.mMsids[0].appdata); + ASSERT_EQ("streamb", msids2.mMsids[1].identifier); + ASSERT_EQ("trackb", msids2.mMsids[1].appdata); + const SdpMsidAttributeList& msids3 = + Sdp()->GetMediaSection(2).GetAttributeList().GetMsid(); + ASSERT_EQ(1U, msids3.mMsids.size()); + ASSERT_EQ("noappdata", msids3.mMsids[0].identifier); + ASSERT_EQ("", msids3.mMsids[0].appdata); +} + +TEST_P(NewSdpTest, CheckManyMsidSemantics) { + if (::testing::get<0>(GetParam())) { + return; + } + std::ostringstream msid; + msid << "a=msid-semantic: foo"; + for (size_t i = 0; i < SDP_MAX_MEDIA_STREAMS + 1; ++i) { + msid << " " << i; + } + msid << CRLF; + + std::string offer = + "v=0" CRLF "o=- 4294967296 2 IN IP4 127.0.0.1" CRLF "s=SIP Call" CRLF + "c=IN IP4 198.51.100.7" CRLF "t=0 0" CRLF; + offer += msid.str(); + offer += "m=video 56436 RTP/SAVPF 120" CRLF "b=CT:1000" CRLF + "a=rtpmap:120 VP8/90000" CRLF; + + ParseSdp(offer); + auto semantics = Sdp()->GetAttributeList().GetMsidSemantic().mMsidSemantics; + ASSERT_EQ(1U, semantics.size()); + + // Only sipcc is limited by SDP_MAX_MEDIA_STREAMS, the Rust parser + // will parse all of the msids. + ASSERT_GE(semantics[0].msids.size(), + static_cast<size_t>(SDP_MAX_MEDIA_STREAMS)); +} + +TEST_P(NewSdpTest, CheckRid) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_TRUE(!!Sdp()); + ASSERT_EQ(3U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + ASSERT_FALSE( + Sdp()->GetAttributeList().HasAttribute(SdpAttribute::kRidAttribute)); + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kRidAttribute)); + ASSERT_TRUE(Sdp()->GetMediaSection(1).GetAttributeList().HasAttribute( + SdpAttribute::kRidAttribute)); + ASSERT_FALSE(Sdp()->GetMediaSection(2).GetAttributeList().HasAttribute( + SdpAttribute::kRidAttribute)); + + const SdpRidAttributeList& rids = + Sdp()->GetMediaSection(1).GetAttributeList().GetRid(); + + ASSERT_EQ(2U, rids.mRids.size()); + + ASSERT_EQ("bar", rids.mRids[0].id); + ASSERT_EQ(sdp::kRecv, rids.mRids[0].direction); + ASSERT_EQ(1U, rids.mRids[0].formats.size()); + ASSERT_EQ(120U, rids.mRids[0].formats[0]); + ASSERT_EQ(800U, rids.mRids[0].constraints.maxWidth); + ASSERT_EQ(600U, rids.mRids[0].constraints.maxHeight); + + ASSERT_EQ("bar123", rids.mRids[1].id); + ASSERT_EQ(sdp::kRecv, rids.mRids[1].direction); + ASSERT_EQ(0U, rids.mRids[1].formats.size()); + ASSERT_EQ(1920U, rids.mRids[1].constraints.maxWidth); + ASSERT_EQ(1080U, rids.mRids[1].constraints.maxHeight); +} + +TEST_P(NewSdpTest, CheckMediaLevelIceUfrag) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(3U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + ASSERT_TRUE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kIceUfragAttribute, true)); + ASSERT_EQ("00000000", + Sdp()->GetMediaSection(0).GetAttributeList().GetIceUfrag()); + + ASSERT_TRUE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kIceUfragAttribute, false)); + + ASSERT_TRUE(Sdp()->GetMediaSection(1).GetAttributeList().HasAttribute( + SdpAttribute::kIceUfragAttribute, true)); + ASSERT_EQ("4a799b2e", + Sdp()->GetMediaSection(1).GetAttributeList().GetIceUfrag()); +} + +TEST_P(NewSdpTest, CheckMediaLevelIcePwd) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(3U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + ASSERT_TRUE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kIcePwdAttribute)); + ASSERT_EQ("0000000000000000000000000000000", + Sdp()->GetMediaSection(0).GetAttributeList().GetIcePwd()); + + ASSERT_TRUE(Sdp()->GetMediaSection(1).GetAttributeList().HasAttribute( + SdpAttribute::kIcePwdAttribute)); + ASSERT_EQ("e4cc12a910f106a0a744719425510e17", + Sdp()->GetMediaSection(1).GetAttributeList().GetIcePwd()); +} + +TEST_P(NewSdpTest, CheckGroups) { + ParseSdp(kBasicAudioVideoOffer); + const SdpGroupAttributeList& group = Sdp()->GetAttributeList().GetGroup(); + const SdpGroupAttributeList::Group& group1 = group.mGroups[0]; + ASSERT_EQ(SdpGroupAttributeList::kBundle, group1.semantics); + ASSERT_EQ(2U, group1.tags.size()); + ASSERT_EQ("first", group1.tags[0]); + ASSERT_EQ("second", group1.tags[1]); + + const SdpGroupAttributeList::Group& group2 = group.mGroups[1]; + ASSERT_EQ(SdpGroupAttributeList::kBundle, group2.semantics); + ASSERT_EQ(1U, group2.tags.size()); + ASSERT_EQ("third", group2.tags[0]); + + const SdpGroupAttributeList::Group& group3 = group.mGroups[2]; + ASSERT_EQ(SdpGroupAttributeList::kLs, group3.semantics); + ASSERT_EQ(2U, group3.tags.size()); + ASSERT_EQ("first", group3.tags[0]); + ASSERT_EQ("third", group3.tags[1]); +} + +TEST_P(NewSdpTest, CheckManyGroups) { + std::ostringstream bundle; + bundle << "a=group:BUNDLE"; + for (size_t i = 0; i < SDP_MAX_MEDIA_STREAMS + 1; ++i) { + bundle << " " << i; + } + bundle << CRLF; + + std::string offer = + "v=0" CRLF "o=- 4294967296 2 IN IP4 127.0.0.1" CRLF "s=SIP Call" CRLF + "c=IN IP4 198.51.100.7" CRLF "t=0 0" CRLF; + offer += bundle.str(); + offer += "m=video 56436 RTP/SAVPF 120" CRLF "b=CT:1000" CRLF + "a=rtpmap:120 VP8/90000" CRLF; + // Rust will reserialize what Sipcc serializes to, but otherwise they will + // differ. + ParseSdp(offer, true, + ::testing::get<0>(GetParam()) && ::testing::get<1>(GetParam())); + const SdpGroupAttributeList& group = Sdp()->GetAttributeList().GetGroup(); + const SdpGroupAttributeList::Group& group1 = group.mGroups[0]; + ASSERT_EQ(SdpGroupAttributeList::kBundle, group1.semantics); + + // Only sipcc is limited by SDP_MAX_MEDIA_STREAMS, the Rust parser + // will parse all of the groups. + ASSERT_GE(group1.tags.size(), static_cast<size_t>(SDP_MAX_MEDIA_STREAMS)); +} + +// SDP from a basic A/V call with data channel FFX/FFX +const std::string kBasicAudioVideoDataOffer = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 27987 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "t=0 0" CRLF "a=ice-ufrag:8a39d2ae" CRLF + "a=ice-pwd:601d53aba51a318351b3ecf5ee00048f" CRLF + "a=fingerprint:sha-256 " + "30:FF:8E:2B:AC:9D:ED:70:18:10:67:C8:AE:9E:68:F3:86:53:51:B0:AC:31:B7:BE:" + "6D:CF:A4:2E:D3:6E:B4:28" CRLF "m=audio 9 RTP/SAVPF 109 9 0 8 101" CRLF + "c=IN IP4 0.0.0.0" CRLF "a=rtpmap:109 opus/48000/2" CRLF "a=ptime:20" CRLF + "a=rtpmap:9 G722/8000" CRLF "a=rtpmap:0 PCMU/8000" CRLF + "a=rtpmap:8 PCMA/8000" CRLF "a=rtpmap:101 telephone-event/8000" CRLF + "a=fmtp:101 0-15" CRLF "a=sendrecv" CRLF + "a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level" CRLF + "a=extmap:2/sendonly some_extension" CRLF + "a=extmap:3 some_other_extension some_params some more params" CRLF + "a=setup:actpass" CRLF "a=rtcp-mux" CRLF + "m=video 9 RTP/SAVPF 120 126 97" CRLF "c=IN IP4 0.0.0.0" CRLF + "a=rtpmap:120 VP8/90000" CRLF "a=rtpmap:126 H264/90000" CRLF + "a=fmtp:126 profile-level-id=42e01f;packetization-mode=1" CRLF + "a=rtpmap:97 H264/90000" CRLF "a=fmtp:97 profile-level-id=42e01f" CRLF + "a=sendrecv" CRLF + // sipcc barfs on this, despite that it is valid syntax + // Do we care about fixing? + //"a=rtcp-fb:120 ack" CRLF // Should be ignored by sipcc + "a=rtcp-fb:120 ack rpsi" CRLF "a=rtcp-fb:120 ack app foo" CRLF + "a=rtcp-fb:120 ack foo" CRLF // Should be ignored + "a=rtcp-fb:120 nack" CRLF "a=rtcp-fb:120 nack sli" CRLF + "a=rtcp-fb:120 nack pli" CRLF "a=rtcp-fb:120 nack rpsi" CRLF + "a=rtcp-fb:120 nack app foo" CRLF + "a=rtcp-fb:120 nack foo" CRLF // Should be ignored + "a=rtcp-fb:120 ccm fir" CRLF "a=rtcp-fb:120 ccm tmmbr" CRLF + "a=rtcp-fb:120 ccm tstr" CRLF "a=rtcp-fb:120 ccm vbcm" CRLF + "a=rtcp-fb:120 ccm foo" CRLF // Should be ignored + "a=rtcp-fb:120 trr-int 10" CRLF "a=rtcp-fb:120 goog-remb" CRLF + "a=rtcp-fb:120 foo" CRLF // Should be ignored + "a=rtcp-fb:126 nack" CRLF "a=rtcp-fb:126 nack pli" CRLF + "a=rtcp-fb:126 ccm fir" CRLF "a=rtcp-fb:97 nack" CRLF + "a=rtcp-fb:97 nack pli" CRLF "a=rtcp-fb:97 ccm fir" CRLF + "a=rtcp-fb:* ccm tmmbr" CRLF "a=rtcp-fb:120 transport-cc" CRLF + "a=setup:actpass" CRLF "a=rtcp-mux" CRLF + "m=application 9 DTLS/SCTP 5000" CRLF "c=IN IP4 0.0.0.0" CRLF + "a=sctpmap:5000 webrtc-datachannel 16" CRLF "a=setup:actpass" CRLF; + +TEST_P(NewSdpTest, BasicAudioVideoDataSdpParse) { + ParseSdp(kBasicAudioVideoDataOffer, true); + ASSERT_EQ(0U, ParseErrorCount()) + << "Got parse errors: " << SerializeParseErrors(); +} + +TEST_P(NewSdpTest, CheckApplicationParameters) { + ParseSdp(kBasicAudioVideoDataOffer, true); + ASSERT_TRUE(!!Sdp()); + ASSERT_EQ(3U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + ASSERT_EQ(SdpMediaSection::kAudio, Sdp()->GetMediaSection(0).GetMediaType()) + << "Wrong type for first media section"; + ASSERT_EQ(SdpMediaSection::kVideo, Sdp()->GetMediaSection(1).GetMediaType()) + << "Wrong type for second media section"; + ASSERT_EQ(SdpMediaSection::kApplication, + Sdp()->GetMediaSection(2).GetMediaType()) + << "Wrong type for third media section"; + + ASSERT_EQ(SdpMediaSection::kDtlsSctp, Sdp()->GetMediaSection(2).GetProtocol()) + << "Wrong protocol for application"; + auto app_formats = Sdp()->GetMediaSection(2).GetFormats(); + ASSERT_EQ(1U, app_formats.size()) << "Wrong number of formats for audio"; + ASSERT_EQ("5000", app_formats[0]); + + const SdpConnection& conn3 = Sdp()->GetMediaSection(2).GetConnection(); + ASSERT_EQ(sdp::kIPv4, conn3.GetAddrType()); + ASSERT_EQ("0.0.0.0", conn3.GetAddress()); + ASSERT_EQ(0U, conn3.GetTtl()); + ASSERT_EQ(0U, conn3.GetCount()); + + ASSERT_TRUE(Sdp()->GetMediaSection(2).GetAttributeList().HasAttribute( + SdpAttribute::kSetupAttribute)); + ASSERT_EQ(SdpSetupAttribute::kActpass, + Sdp()->GetMediaSection(2).GetAttributeList().GetSetup().mRole); +} + +TEST_P(NewSdpTest, CheckExtmap) { + ParseSdp(kBasicAudioVideoDataOffer, true); + ASSERT_TRUE(!!Sdp()); + ASSERT_EQ(3U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + ASSERT_TRUE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kExtmapAttribute)); + + auto extmaps = + Sdp()->GetMediaSection(0).GetAttributeList().GetExtmap().mExtmaps; + ASSERT_EQ(3U, extmaps.size()); + + ASSERT_EQ(1U, extmaps[0].entry); + ASSERT_FALSE(extmaps[0].direction_specified); + ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level", + extmaps[0].extensionname); + ASSERT_EQ("", extmaps[0].extensionattributes); + + ASSERT_EQ(2U, extmaps[1].entry); + ASSERT_TRUE(extmaps[1].direction_specified); + ASSERT_EQ(SdpDirectionAttribute::kSendonly, extmaps[1].direction); + ASSERT_EQ("some_extension", extmaps[1].extensionname); + ASSERT_EQ("", extmaps[1].extensionattributes); + + ASSERT_EQ(3U, extmaps[2].entry); + ASSERT_FALSE(extmaps[2].direction_specified); + ASSERT_EQ("some_other_extension", extmaps[2].extensionname); + ASSERT_EQ("some_params some more params", extmaps[2].extensionattributes); +} + +TEST_P(NewSdpTest, CheckRtcpFb) { + ParseSdp(kBasicAudioVideoDataOffer, true); + ASSERT_TRUE(!!Sdp()); + ASSERT_EQ(3U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + auto& video_attrs = Sdp()->GetMediaSection(1).GetAttributeList(); + ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kRtcpFbAttribute)); + auto& rtcpfbs = video_attrs.GetRtcpFb().mFeedbacks; + + if (ResultsAreFromSipcc()) { + ASSERT_EQ(21U, rtcpfbs.size()); + } else { + ASSERT_EQ("WEBRTCSDP", mResults->ParserName()); + std::stringstream os; + Sdp()->Serialize(os); + // Reserialized results should be the same + if (::testing::get<1>(GetParam())) { + ASSERT_EQ(21U, rtcpfbs.size()); + } else { + ASSERT_EQ(21U, rtcpfbs.size()); + } + } + + CheckRtcpFb(rtcpfbs[0], "120", SdpRtcpFbAttributeList::kAck, "rpsi"); + CheckRtcpFb(rtcpfbs[1], "120", SdpRtcpFbAttributeList::kAck, "app", "foo"); + CheckRtcpFb(rtcpfbs[2], "120", SdpRtcpFbAttributeList::kNack, ""); + CheckRtcpFb(rtcpfbs[3], "120", SdpRtcpFbAttributeList::kNack, "sli"); + CheckRtcpFb(rtcpfbs[4], "120", SdpRtcpFbAttributeList::kNack, "pli"); + CheckRtcpFb(rtcpfbs[5], "120", SdpRtcpFbAttributeList::kNack, "rpsi"); + CheckRtcpFb(rtcpfbs[6], "120", SdpRtcpFbAttributeList::kNack, "app", "foo"); + CheckRtcpFb(rtcpfbs[7], "120", SdpRtcpFbAttributeList::kCcm, "fir"); + CheckRtcpFb(rtcpfbs[8], "120", SdpRtcpFbAttributeList::kCcm, "tmmbr"); + CheckRtcpFb(rtcpfbs[9], "120", SdpRtcpFbAttributeList::kCcm, "tstr"); + CheckRtcpFb(rtcpfbs[10], "120", SdpRtcpFbAttributeList::kCcm, "vbcm"); + CheckRtcpFb(rtcpfbs[11], "120", SdpRtcpFbAttributeList::kTrrInt, "10"); + CheckRtcpFb(rtcpfbs[12], "120", SdpRtcpFbAttributeList::kRemb, ""); + CheckRtcpFb(rtcpfbs[13], "126", SdpRtcpFbAttributeList::kNack, ""); + CheckRtcpFb(rtcpfbs[14], "126", SdpRtcpFbAttributeList::kNack, "pli"); + CheckRtcpFb(rtcpfbs[15], "126", SdpRtcpFbAttributeList::kCcm, "fir"); + CheckRtcpFb(rtcpfbs[16], "97", SdpRtcpFbAttributeList::kNack, ""); + CheckRtcpFb(rtcpfbs[17], "97", SdpRtcpFbAttributeList::kNack, "pli"); + CheckRtcpFb(rtcpfbs[18], "97", SdpRtcpFbAttributeList::kCcm, "fir"); + CheckRtcpFb(rtcpfbs[19], "*", SdpRtcpFbAttributeList::kCcm, "tmmbr"); + CheckRtcpFb(rtcpfbs[20], "120", SdpRtcpFbAttributeList::kTransportCC, ""); +} + +TEST_P(NewSdpTest, CheckRtcp) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_TRUE(!!Sdp()); + ASSERT_EQ(3U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + ASSERT_FALSE( + Sdp()->GetAttributeList().HasAttribute(SdpAttribute::kRtcpAttribute)); + ASSERT_TRUE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kRtcpAttribute)); + ASSERT_TRUE(Sdp()->GetMediaSection(1).GetAttributeList().HasAttribute( + SdpAttribute::kRtcpAttribute)); + ASSERT_FALSE(Sdp()->GetMediaSection(2).GetAttributeList().HasAttribute( + SdpAttribute::kRtcpAttribute)); + + auto& rtcpAttr_0 = Sdp()->GetMediaSection(0).GetAttributeList().GetRtcp(); + ASSERT_EQ(62454U, rtcpAttr_0.mPort); + ASSERT_EQ(sdp::kInternet, rtcpAttr_0.mNetType); + ASSERT_EQ(sdp::kIPv4, rtcpAttr_0.mAddrType); + ASSERT_EQ("162.222.183.171", rtcpAttr_0.mAddress); + + auto& rtcpAttr_1 = Sdp()->GetMediaSection(1).GetAttributeList().GetRtcp(); + ASSERT_EQ(61026U, rtcpAttr_1.mPort); + ASSERT_EQ("", rtcpAttr_1.mAddress); +} + +TEST_P(NewSdpTest, CheckImageattr) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_TRUE(!!Sdp()); + ASSERT_EQ(3U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + ASSERT_FALSE(Sdp()->GetAttributeList().HasAttribute( + SdpAttribute::kImageattrAttribute)); + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kImageattrAttribute)); + ASSERT_TRUE(Sdp()->GetMediaSection(1).GetAttributeList().HasAttribute( + SdpAttribute::kImageattrAttribute)); + ASSERT_FALSE(Sdp()->GetMediaSection(2).GetAttributeList().HasAttribute( + SdpAttribute::kImageattrAttribute)); + + const SdpImageattrAttributeList& imageattrs = + Sdp()->GetMediaSection(1).GetAttributeList().GetImageattr(); + + ASSERT_EQ(2U, imageattrs.mImageattrs.size()); + const SdpImageattrAttributeList::Imageattr& imageattr_0( + imageattrs.mImageattrs[0]); + ASSERT_TRUE(imageattr_0.pt.isSome()); + ASSERT_EQ(120U, *imageattr_0.pt); + ASSERT_TRUE(imageattr_0.sendAll); + ASSERT_TRUE(imageattr_0.recvAll); + + const SdpImageattrAttributeList::Imageattr& imageattr_1( + imageattrs.mImageattrs[1]); + ASSERT_TRUE(imageattr_1.pt.isSome()); + ASSERT_EQ(121U, *imageattr_1.pt); + ASSERT_FALSE(imageattr_1.sendAll); + ASSERT_FALSE(imageattr_1.recvAll); + ASSERT_EQ(1U, imageattr_1.sendSets.size()); + ASSERT_EQ(1U, imageattr_1.sendSets[0].xRange.discreteValues.size()); + ASSERT_EQ(640U, imageattr_1.sendSets[0].xRange.discreteValues.front()); + ASSERT_EQ(1U, imageattr_1.sendSets[0].yRange.discreteValues.size()); + ASSERT_EQ(480U, imageattr_1.sendSets[0].yRange.discreteValues.front()); + ASSERT_EQ(1U, imageattr_1.recvSets.size()); + ASSERT_EQ(1U, imageattr_1.recvSets[0].xRange.discreteValues.size()); + ASSERT_EQ(640U, imageattr_1.recvSets[0].xRange.discreteValues.front()); + ASSERT_EQ(1U, imageattr_1.recvSets[0].yRange.discreteValues.size()); + ASSERT_EQ(480U, imageattr_1.recvSets[0].yRange.discreteValues.front()); +} + +TEST_P(NewSdpTest, CheckSimulcast) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_TRUE(!!Sdp()); + ASSERT_EQ(3U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + ASSERT_FALSE(Sdp()->GetAttributeList().HasAttribute( + SdpAttribute::kSimulcastAttribute)); + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kSimulcastAttribute)); + ASSERT_TRUE(Sdp()->GetMediaSection(1).GetAttributeList().HasAttribute( + SdpAttribute::kSimulcastAttribute)); + ASSERT_FALSE(Sdp()->GetMediaSection(2).GetAttributeList().HasAttribute( + SdpAttribute::kSimulcastAttribute)); + + const SdpSimulcastAttribute& simulcast = + Sdp()->GetMediaSection(1).GetAttributeList().GetSimulcast(); + + ASSERT_EQ(2U, simulcast.recvVersions.size()); + ASSERT_EQ(0U, simulcast.sendVersions.size()); + ASSERT_EQ(1U, simulcast.recvVersions[0].choices.size()); + ASSERT_EQ("bar", simulcast.recvVersions[0].choices[0].rid); + ASSERT_EQ(1U, simulcast.recvVersions[1].choices.size()); + ASSERT_EQ("bar123", simulcast.recvVersions[1].choices[0].rid); +} + +TEST_P(NewSdpTest, CheckSctpmap) { + ParseSdp(kBasicAudioVideoDataOffer); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(3U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + const SdpMediaSection& appsec = Sdp()->GetMediaSection(2); + ASSERT_TRUE( + appsec.GetAttributeList().HasAttribute(SdpAttribute::kSctpmapAttribute)); + const SdpSctpmapAttributeList& sctpmap = + appsec.GetAttributeList().GetSctpmap(); + + ASSERT_EQ(1U, sctpmap.mSctpmaps.size()) + << "Wrong number of sctpmap attributes"; + ASSERT_EQ(1U, appsec.GetFormats().size()); + + // Need to know name of type + CheckSctpmap("5000", "webrtc-datachannel", 16, appsec.GetFormats()[0], + sctpmap); +} + +TEST_P(NewSdpTest, CheckMaxPtime) { + ParseSdp(kBasicAudioVideoOffer); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(3U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + ASSERT_TRUE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kMaxptimeAttribute)); + ASSERT_EQ(Sdp()->GetMediaSection(0).GetAttributeList().GetMaxptime(), 20U); +} + +const std::string kNewSctpportOfferDraft21 = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 27987 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "t=0 0" CRLF "a=ice-ufrag:8a39d2ae" CRLF + "a=ice-pwd:601d53aba51a318351b3ecf5ee00048f" CRLF + "a=fingerprint:sha-256 " + "30:FF:8E:2B:AC:9D:ED:70:18:10:67:C8:AE:9E:68:F3:86:53:51:B0:AC:31:B7:BE:" + "6D:CF:A4:2E:D3:6E:B4:28" CRLF + "m=application 9 UDP/DTLS/SCTP webrtc-datachannel" CRLF + "c=IN IP4 0.0.0.0" CRLF "a=sctp-port:5000" CRLF + "a=max-message-size:10000" CRLF "a=setup:actpass" CRLF; + +TEST_P(NewSdpTest, NewSctpportSdpParse) { + ParseSdp(kNewSctpportOfferDraft21, false); +} + +INSTANTIATE_TEST_SUITE_P(RoundTripSerialize, NewSdpTest, + ::testing::Combine(::testing::Bool(), + ::testing::Bool())); + +const std::string kCandidateInSessionSDP = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "a=candidate:0 1 UDP 2130379007 10.0.0.36 62453 typ host" CRLF + "m=audio 9 RTP/SAVPF 109 9 0 8 101" CRLF "c=IN IP4 0.0.0.0" CRLF + "a=rtpmap:109 opus/48000/2" CRLF; + +// This may or may not parse, but if it does, the errant candidate attribute +// should be ignored. +TEST_P(NewSdpTest, CheckCandidateInSessionLevel) { + ParseSdp(kCandidateInSessionSDP, false); + if (Sdp()) { + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kCandidateAttribute)); + ASSERT_FALSE(Sdp()->GetAttributeList().HasAttribute( + SdpAttribute::kCandidateAttribute)); + } +} + +const std::string kBundleOnlyInSessionSDP = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "a=bundle-only" CRLF "m=audio 9 RTP/SAVPF 109 9 0 8 101" CRLF + "c=IN IP4 0.0.0.0" CRLF "a=rtpmap:109 opus/48000/2" CRLF; + +// This may or may not parse, but if it does, the errant attribute +// should be ignored. +TEST_P(NewSdpTest, CheckBundleOnlyInSessionLevel) { + ParseSdp(kBundleOnlyInSessionSDP, false); + if (Sdp()) { + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kBundleOnlyAttribute)); + ASSERT_FALSE(Sdp()->GetAttributeList().HasAttribute( + SdpAttribute::kBundleOnlyAttribute)); + } +} + +const std::string kFmtpInSessionSDP = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "a=fmtp:109 0-15" CRLF "m=audio 9 RTP/SAVPF 109 9 0 8 101" CRLF + "c=IN IP4 0.0.0.0" CRLF "a=rtpmap:109 opus/48000/2" CRLF; + +// This may or may not parse, but if it does, the errant attribute +// should be ignored. +TEST_P(NewSdpTest, CheckFmtpInSessionLevel) { + ParseSdp(kFmtpInSessionSDP, false); + if (Sdp()) { + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kFmtpAttribute)); + ASSERT_FALSE( + Sdp()->GetAttributeList().HasAttribute(SdpAttribute::kFmtpAttribute)); + } +} + +const std::string kIceMismatchInSessionSDP = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "a=ice-mismatch" CRLF "m=audio 9 RTP/SAVPF 109 9 0 8 101" CRLF + "c=IN IP4 0.0.0.0" CRLF "a=rtpmap:109 opus/48000/2" CRLF; + +// This may or may not parse, but if it does, the errant attribute +// should be ignored. +TEST_P(NewSdpTest, CheckIceMismatchInSessionLevel) { + ParseSdp(kIceMismatchInSessionSDP, false); + if (Sdp()) { + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kIceMismatchAttribute)); + ASSERT_FALSE(Sdp()->GetAttributeList().HasAttribute( + SdpAttribute::kIceMismatchAttribute)); + } +} + +const std::string kImageattrInSessionSDP = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "a=imageattr:120 send * recv *" CRLF "m=video 9 RTP/SAVPF 120" CRLF + "c=IN IP4 0.0.0.0" CRLF "a=rtpmap:120 VP8/90000" CRLF; + +// This may or may not parse, but if it does, the errant attribute +// should be ignored. +TEST_P(NewSdpTest, CheckImageattrInSessionLevel) { + ParseSdp(kImageattrInSessionSDP, false); + if (Sdp()) { + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kImageattrAttribute)); + ASSERT_FALSE(Sdp()->GetAttributeList().HasAttribute( + SdpAttribute::kImageattrAttribute)); + } +} + +const std::string kLabelInSessionSDP = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "a=label:foobar" CRLF "m=video 9 RTP/SAVPF 120" CRLF "c=IN IP4 0.0.0.0" CRLF + "a=rtpmap:120 VP8/90000" CRLF; + +// This may or may not parse, but if it does, the errant attribute +// should be ignored. +TEST_P(NewSdpTest, CheckLabelInSessionLevel) { + ParseSdp(kLabelInSessionSDP, false); + if (Sdp()) { + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kLabelAttribute)); + ASSERT_FALSE( + Sdp()->GetAttributeList().HasAttribute(SdpAttribute::kLabelAttribute)); + } +} + +const std::string kMaxptimeInSessionSDP = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "a=maxptime:100" CRLF "m=video 9 RTP/SAVPF 120" CRLF "c=IN IP4 0.0.0.0" CRLF + "a=rtpmap:120 VP8/90000" CRLF; + +// This may or may not parse, but if it does, the errant attribute +// should be ignored. +TEST_P(NewSdpTest, CheckMaxptimeInSessionLevel) { + ParseSdp(kMaxptimeInSessionSDP, false); + if (Sdp()) { + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kMaxptimeAttribute)); + ASSERT_FALSE(Sdp()->GetAttributeList().HasAttribute( + SdpAttribute::kMaxptimeAttribute)); + } +} + +const std::string kMidInSessionSDP = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "a=mid:foobar" CRLF "m=video 9 RTP/SAVPF 120" CRLF "c=IN IP4 0.0.0.0" CRLF + "a=rtpmap:120 VP8/90000" CRLF; + +// This may or may not parse, but if it does, the errant attribute +// should be ignored. +TEST_P(NewSdpTest, CheckMidInSessionLevel) { + ParseSdp(kMidInSessionSDP, false); + if (Sdp()) { + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kMidAttribute)); + ASSERT_FALSE( + Sdp()->GetAttributeList().HasAttribute(SdpAttribute::kMidAttribute)); + } +} + +const std::string kMsidInSessionSDP = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "a=msid:foobar" CRLF "m=video 9 RTP/SAVPF 120" CRLF "c=IN IP4 0.0.0.0" CRLF + "a=rtpmap:120 VP8/90000" CRLF; + +// This may or may not parse, but if it does, the errant attribute +// should be ignored. +TEST_P(NewSdpTest, CheckMsidInSessionLevel) { + ParseSdp(kMsidInSessionSDP, false); + if (Sdp()) { + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kMsidAttribute)); + ASSERT_FALSE( + Sdp()->GetAttributeList().HasAttribute(SdpAttribute::kMsidAttribute)); + } +} + +const std::string kPtimeInSessionSDP = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "a=ptime:50" CRLF "m=video 9 RTP/SAVPF 120" CRLF "c=IN IP4 0.0.0.0" CRLF + "a=rtpmap:120 VP8/90000" CRLF; + +// This may or may not parse, but if it does, the errant attribute +// should be ignored. +TEST_P(NewSdpTest, CheckPtimeInSessionLevel) { + ParseSdp(kPtimeInSessionSDP, false); + if (Sdp()) { + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kPtimeAttribute)); + ASSERT_FALSE( + Sdp()->GetAttributeList().HasAttribute(SdpAttribute::kPtimeAttribute)); + } +} + +const std::string kRemoteCandidatesInSessionSDP = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "a=remote-candidates:0 10.0.0.1 5555" CRLF "m=video 9 RTP/SAVPF 120" CRLF + "c=IN IP4 0.0.0.0" CRLF "a=rtpmap:120 VP8/90000" CRLF; + +// This may or may not parse, but if it does, the errant attribute +// should be ignored. +TEST_P(NewSdpTest, CheckRemoteCandidatesInSessionLevel) { + ParseSdp(kRemoteCandidatesInSessionSDP, false); + if (Sdp()) { + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kRemoteCandidatesAttribute)); + ASSERT_FALSE(Sdp()->GetAttributeList().HasAttribute( + SdpAttribute::kRemoteCandidatesAttribute)); + } +} + +const std::string kRtcpInSessionSDP = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "a=rtcp:5555" CRLF "m=video 9 RTP/SAVPF 120" CRLF "c=IN IP4 0.0.0.0" CRLF + "a=rtpmap:120 VP8/90000" CRLF; + +// This may or may not parse, but if it does, the errant attribute +// should be ignored. +TEST_P(NewSdpTest, CheckRtcpInSessionLevel) { + ParseSdp(kRtcpInSessionSDP, false); + if (Sdp()) { + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kRtcpAttribute)); + ASSERT_FALSE( + Sdp()->GetAttributeList().HasAttribute(SdpAttribute::kRtcpAttribute)); + } +} + +const std::string kRtcpFbInSessionSDP = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "a=rtcp-fb:120 nack" CRLF "m=video 9 RTP/SAVPF 120" CRLF + "c=IN IP4 0.0.0.0" CRLF "a=rtpmap:120 VP8/90000" CRLF; + +// This may or may not parse, but if it does, the errant attribute +// should be ignored. +TEST_P(NewSdpTest, CheckRtcpFbInSessionLevel) { + ParseSdp(kRtcpFbInSessionSDP, false, true); + if (Sdp()) { + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kRtcpFbAttribute)); + ASSERT_FALSE( + Sdp()->GetAttributeList().HasAttribute(SdpAttribute::kRtcpFbAttribute)); + } +} + +const std::string kRtcpMuxInSessionSDP = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "a=rtcp-mux" CRLF "m=video 9 RTP/SAVPF 120" CRLF "c=IN IP4 0.0.0.0" CRLF + "a=rtpmap:120 VP8/90000" CRLF; + +// This may or may not parse, but if it does, the errant attribute +// should be ignored. +TEST_P(NewSdpTest, CheckRtcpMuxInSessionLevel) { + ParseSdp(kRtcpMuxInSessionSDP, false); + if (Sdp()) { + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kRtcpMuxAttribute)); + ASSERT_FALSE(Sdp()->GetAttributeList().HasAttribute( + SdpAttribute::kRtcpMuxAttribute)); + } +} + +const std::string kRtcpRsizeInSessionSDP = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "a=rtcp-rsize" CRLF "m=video 9 RTP/SAVPF 120" CRLF "c=IN IP4 0.0.0.0" CRLF + "a=rtpmap:120 VP8/90000" CRLF; + +// This may or may not parse, but if it does, the errant attribute +// should be ignored. +TEST_P(NewSdpTest, CheckRtcpRsizeInSessionLevel) { + ParseSdp(kRtcpRsizeInSessionSDP, false); + if (Sdp()) { + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kRtcpRsizeAttribute)); + ASSERT_FALSE(Sdp()->GetAttributeList().HasAttribute( + SdpAttribute::kRtcpRsizeAttribute)); + } +} + +const std::string kRtpmapInSessionSDP = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "a=rtpmap:120 VP8/90000" CRLF "m=video 9 RTP/SAVPF 120" CRLF + "c=IN IP4 0.0.0.0" CRLF; + +// This may or may not parse, but if it does, the errant attribute +// should be ignored. +TEST_P(NewSdpTest, CheckRtpmapInSessionLevel) { + ParseSdp(kRtpmapInSessionSDP, false); + if (Sdp()) { + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kRtpmapAttribute)); + ASSERT_FALSE( + Sdp()->GetAttributeList().HasAttribute(SdpAttribute::kRtpmapAttribute)); + } +} + +const std::string kSctpmapInSessionSDP = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "a=sctpmap:5000" CRLF "m=video 9 RTP/SAVPF 120" CRLF "c=IN IP4 0.0.0.0" CRLF + "a=rtpmap:120 VP8/90000" CRLF; + +// This may or may not parse, but if it does, the errant attribute +// should be ignored. +TEST_P(NewSdpTest, CheckSctpmapInSessionLevel) { + ParseSdp(kSctpmapInSessionSDP, false); + if (Sdp()) { + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kSctpmapAttribute)); + ASSERT_FALSE(Sdp()->GetAttributeList().HasAttribute( + SdpAttribute::kSctpmapAttribute)); + } +} + +const std::string kSsrcInSessionSDP = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "a=ssrc:5000" CRLF "m=video 9 RTP/SAVPF 120" CRLF "c=IN IP4 0.0.0.0" CRLF + "a=rtpmap:120 VP8/90000" CRLF; + +// This may or may not parse, but if it does, the errant attribute +// should be ignored. +TEST_P(NewSdpTest, CheckSsrcInSessionLevel) { + ParseSdp(kSsrcInSessionSDP, false); + if (Sdp()) { + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kSsrcAttribute)); + ASSERT_FALSE( + Sdp()->GetAttributeList().HasAttribute(SdpAttribute::kSsrcAttribute)); + } +} + +const std::string kMalformedImageattr = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "m=video 9 RTP/SAVPF 120" CRLF "c=IN IP4 0.0.0.0" CRLF + "a=rtpmap:120 VP8/90000" CRLF "a=imageattr:flob" CRLF; + +TEST_P(NewSdpTest, CheckMalformedImageattr) { + if (::testing::get<0>(GetParam())) { + // Don't do a parse/serialize before running this test + return; + } + + ParseSdp(kMalformedImageattr, false); + ASSERT_NE(0U, ParseErrorCount()); +} + +TEST_P(NewSdpTest, ParseInvalidSimulcastNoSuchSendRid) { + ParseSdp("v=0" CRLF "o=- 4294967296 2 IN IP4 127.0.0.1" CRLF "s=SIP Call" CRLF + "c=IN IP4 198.51.100.7" CRLF "b=CT:5000" CRLF "t=0 0" CRLF + "m=video 56436 RTP/SAVPF 120" CRLF "a=rtpmap:120 VP8/90000" CRLF + "a=sendrecv" CRLF "a=simulcast:send 9" CRLF + "a=rid:9 recv max-width=800;max-height=600" CRLF, + false); + ASSERT_NE(0U, ParseErrorCount()); +} + +TEST_P(NewSdpTest, ParseInvalidSimulcastNoSuchRecvRid) { + ParseSdp("v=0" CRLF "o=- 4294967296 2 IN IP4 127.0.0.1" CRLF "s=SIP Call" CRLF + "c=IN IP4 198.51.100.7" CRLF "b=CT:5000" CRLF "t=0 0" CRLF + "m=video 56436 RTP/SAVPF 120" CRLF "a=rtpmap:120 VP8/90000" CRLF + "a=sendrecv" CRLF "a=simulcast:recv 9" CRLF + "a=rid:9 send max-width=800;max-height=600" CRLF, + false); + ASSERT_NE(0U, ParseErrorCount()); +} + +TEST_P(NewSdpTest, ParseSimulcastNotSending) { + ParseSdp("v=0" CRLF "o=- 4294967296 2 IN IP4 127.0.0.1" CRLF "s=SIP Call" CRLF + "c=IN IP4 198.51.100.7" CRLF "b=CT:5000" CRLF "t=0 0" CRLF + "m=video 56436 RTP/SAVPF 120" CRLF "a=rtpmap:120 VP8/90000" CRLF + "a=recvonly" CRLF "a=simulcast:send 120" CRLF "a=rid:120 send" CRLF, + false); + ASSERT_EQ(0U, ParseErrorCount()); +} + +TEST_P(NewSdpTest, ParseSimulcastNotReceiving) { + ParseSdp("v=0" CRLF "o=- 4294967296 2 IN IP4 127.0.0.1" CRLF "s=SIP Call" CRLF + "c=IN IP4 198.51.100.7" CRLF "b=CT:5000" CRLF "t=0 0" CRLF + "m=video 56436 RTP/SAVPF 120" CRLF "a=rtpmap:120 VP8/90000" CRLF + "a=sendonly" CRLF "a=simulcast:recv 120" CRLF "a=rid:120 recv" CRLF, + false); + ASSERT_EQ(0U, ParseErrorCount()); +} + +TEST_P(NewSdpTest, ParseInvalidRidNoSuchPt) { + SKIP_TEST_WITH_SIPCC_PARSER + ParseSdp("v=0" CRLF "o=- 4294967296 2 IN IP4 127.0.0.1" CRLF "s=SIP Call" CRLF + "c=IN IP4 198.51.100.7" CRLF "b=CT:5000" CRLF "t=0 0" CRLF + "m=video 56436 RTP/SAVPF 120" CRLF "a=rtpmap:120 VP8/90000" CRLF + "a=sendrecv" CRLF "a=simulcast:recv rid=9" CRLF + "a=rid:9 recv pt=101;max-width=800;max-height=600" CRLF, + false); + ASSERT_NE(0U, ParseErrorCount()); +} + +const std::string kNoAttributes = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "m=video 9 RTP/SAVPF 120" CRLF "c=IN IP4 0.0.0.0" CRLF + "a=rtpmap:120 VP8/90000" CRLF; + +TEST_P(NewSdpTest, CheckNoAttributes) { + ParseSdp(kNoAttributes); + + for (auto a = static_cast<size_t>(SdpAttribute::kFirstAttribute); + a <= static_cast<size_t>(SdpAttribute::kLastAttribute); ++a) { + SdpAttribute::AttributeType type = + static_cast<SdpAttribute::AttributeType>(a); + + // rtpmap is a special case right now, we throw parse errors if it is + // missing, and then insert one. + // direction is another special case that gets a default if not present + if (type != SdpAttribute::kRtpmapAttribute && + type != SdpAttribute::kDirectionAttribute) { + ASSERT_FALSE( + Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute(type)) + << "Attribute " << a << " should not have been present at media level"; + ASSERT_FALSE(Sdp()->GetAttributeList().HasAttribute(type)) + << "Attribute " << a << " should not have been present at session level"; + } + } + + ASSERT_FALSE( + Sdp()->GetAttributeList().HasAttribute(SdpAttribute::kRtpmapAttribute)); + + ASSERT_TRUE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kDirectionAttribute)); + ASSERT_EQ(SdpDirectionAttribute::kSendrecv, + Sdp()->GetMediaSection(0).GetAttributeList().GetDirection()); + ASSERT_TRUE(Sdp()->GetAttributeList().HasAttribute( + SdpAttribute::kDirectionAttribute)); + ASSERT_EQ(SdpDirectionAttribute::kSendrecv, + Sdp()->GetAttributeList().GetDirection()); +} + +const std::string kMediaLevelDtlsMessage = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "m=video 9 RTP/SAVPF 120" CRLF "c=IN IP4 0.0.0.0" CRLF + "a=dtls-message:client " BASE64_DTLS_HELLO CRLF + "a=rtpmap:120 VP8/90000" CRLF; + +TEST_P(NewSdpTest, CheckMediaLevelDtlsMessage) { + ParseSdp(kMediaLevelDtlsMessage); + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + + // dtls-message is not defined for use at the media level; we don't + // parse it + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kDtlsMessageAttribute)); +} + +TEST_P(NewSdpTest, CheckSetPort) { + // Parse any valid sdp with a media section + ParseSdp("v=0" CRLF "o=- 4294967296 2 IN IP4 127.0.0.1" CRLF "s=SIP Call" CRLF + "c=IN IP4 198.51.100.7" CRLF "b=CT:5000" CRLF "t=0 0" CRLF + "m=video 56436 RTP/SAVPF 120" CRLF "a=rtpmap:120 VP8/90000" CRLF + "a=sendonly" CRLF, + false); + + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + + constexpr unsigned int expectedParesPort = 56436; + unsigned int currentPort = Sdp()->GetMediaSection(0).GetPort(); + ASSERT_EQ(expectedParesPort, currentPort); + + Sdp()->GetMediaSection(0).SetPort(currentPort + 1); + ASSERT_EQ(currentPort + 1, Sdp()->GetMediaSection(0).GetPort()); +} + +TEST_P(NewSdpTest, CheckAddCodec) { + // Parse any valid sdp with a media section + ParseSdp("v=0" CRLF "o=- 4294967296 2 IN IP4 127.0.0.1" CRLF "s=SIP Call" CRLF + "c=IN IP4 198.51.100.7" CRLF "b=CT:5000" CRLF "t=0 0" CRLF + "m=video 56436 RTP/SAVPF 120" CRLF "a=rtpmap:120 VP8/90000" CRLF + "a=sendonly" CRLF); + + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()); + + ASSERT_EQ(1U, Sdp()->GetMediaSection(0).GetFormats().size()); + ASSERT_EQ( + 1U, + Sdp()->GetMediaSection(0).GetAttributeList().GetRtpmap().mRtpmaps.size()); + + Sdp()->GetMediaSection(0).AddCodec("110", "opus", 48000, 2); + + ASSERT_EQ(2U, Sdp()->GetMediaSection(0).GetFormats().size()); + const auto& rtpmaps = + Sdp()->GetMediaSection(0).GetAttributeList().GetRtpmap(); + ASSERT_EQ(2U, rtpmaps.mRtpmaps.size()); + + ASSERT_TRUE(rtpmaps.HasEntry("120")); + ASSERT_TRUE(rtpmaps.HasEntry("110")); + const auto aRtpmap = rtpmaps.GetEntry("110"); + ASSERT_EQ(aRtpmap.pt, "110"); + ASSERT_EQ(aRtpmap.codec, SdpRtpmapAttributeList::kOpus); + ASSERT_EQ(aRtpmap.name, "opus"); + ASSERT_EQ(aRtpmap.clock, 48000U); + ASSERT_EQ(aRtpmap.channels, 2U); +} + +TEST_P(NewSdpTest, CheckClearCodecs) { + // Parse any valid sdp with a media section + ParseSdp("v=0" CRLF "o=- 4294967296 2 IN IP4 127.0.0.1" CRLF "s=SIP Call" CRLF + "c=IN IP4 198.51.100.7" CRLF "b=CT:5000" CRLF "t=0 0" CRLF + "m=video 56436 RTP/SAVPF 120 110" CRLF "a=rtpmap:120 VP8/90000" CRLF + "a=sendonly" CRLF "a=rtpmap:110 opus/48000" CRLF); + // SIPCC strips the channels on opus so the /2 was omitted + + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()); + + ASSERT_EQ(2U, Sdp()->GetMediaSection(0).GetFormats().size()); + ASSERT_EQ( + 2U, + Sdp()->GetMediaSection(0).GetAttributeList().GetRtpmap().mRtpmaps.size()); + + Sdp()->GetMediaSection(0).ClearCodecs(); + + ASSERT_EQ(0U, Sdp()->GetMediaSection(0).GetFormats().size()); + ASSERT_FALSE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kRtpmapAttribute)); +} + +TEST_P(NewSdpTest, CheckAddMediaSection) { + ParseSdp(kBasicAudioVideoOffer); + + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(3U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections"; + + Sdp()->AddMediaSection( + SdpMediaSection::kVideo, SdpDirectionAttribute::Direction::kSendrecv, + 58000, SdpMediaSection::kUdpDtlsSctp, sdp::kIPv4, "127.0.0.1"); + + ASSERT_EQ(4U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections after adding media section"; + + const SdpMediaSection& newMediaSection = Sdp()->GetMediaSection(3); + + ASSERT_EQ(SdpMediaSection::kVideo, newMediaSection.GetMediaType()); + ASSERT_EQ(SdpDirectionAttribute::Direction::kSendrecv, + newMediaSection.GetDirectionAttribute().mValue); + ASSERT_EQ(58000U, newMediaSection.GetPort()); + ASSERT_EQ(SdpMediaSection::kUdpDtlsSctp, newMediaSection.GetProtocol()); + ASSERT_EQ(sdp::kIPv4, newMediaSection.GetConnection().GetAddrType()); + ASSERT_EQ("127.0.0.1", newMediaSection.GetConnection().GetAddress()); + + Sdp()->AddMediaSection(SdpMediaSection::kAudio, + SdpDirectionAttribute::Direction::kSendonly, 14006, + SdpMediaSection::kTcpDtlsRtpSavpf, sdp::kIPv6, + "2607:f8b0:4004:801::2013"); + + ASSERT_EQ(5U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections after adding media section"; + + const SdpMediaSection& nextNewMediaSection = Sdp()->GetMediaSection(4); + + ASSERT_EQ(SdpMediaSection::kAudio, nextNewMediaSection.GetMediaType()); + ASSERT_EQ(SdpDirectionAttribute::Direction::kSendonly, + nextNewMediaSection.GetDirectionAttribute().mValue); + ASSERT_EQ(14006U, nextNewMediaSection.GetPort()); + ASSERT_EQ(SdpMediaSection::kTcpDtlsRtpSavpf, + nextNewMediaSection.GetProtocol()); + ASSERT_EQ(sdp::kIPv6, nextNewMediaSection.GetConnection().GetAddrType()); + ASSERT_EQ("2607:f8b0:4004:801::2013", + nextNewMediaSection.GetConnection().GetAddress()); + + if (!ResultsAreFromSipcc()) { + // All following AddMediaSection calls are expected to fail + // SdpMediaSection::kDccpRtpAvp is expected to cause a failure + Sdp()->AddMediaSection(SdpMediaSection::kAudio, + SdpDirectionAttribute::Direction::kSendonly, 14006, + SdpMediaSection::kDccpRtpAvp, sdp::kIPv6, + "2607:f8b0:4004:801::2013"); + ASSERT_EQ(5U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections after adding media section"; + + // sdp::kAddrTypeNone is expected to cause a failure + Sdp()->AddMediaSection(SdpMediaSection::kAudio, + SdpDirectionAttribute::Direction::kSendonly, 14006, + SdpMediaSection::kDtlsSctp, sdp::kAddrTypeNone, + "2607:f8b0:4004:801::2013"); + ASSERT_EQ(5U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections after adding media section"; + + // "NOT:AN.IP.ADDRESS" is expected to cause a failure + Sdp()->AddMediaSection(SdpMediaSection::kAudio, + SdpDirectionAttribute::Direction::kSendonly, 14006, + SdpMediaSection::kTcpDtlsRtpSavpf, sdp::kIPv6, + "NOT:AN.IP.ADDRESS"); + ASSERT_EQ(5U, Sdp()->GetMediaSectionCount()) + << "Wrong number of media sections after adding media section"; + } +} + +TEST_P(NewSdpTest, CheckAddDataChannel_Draft05) { + // Parse any valid sdp with a media section + ParseSdp("v=0" CRLF "o=- 4294967296 2 IN IP4 127.0.0.1" CRLF "s=SIP Call" CRLF + "c=IN IP4 198.51.100.7" CRLF "b=CT:5000" CRLF "t=0 0" CRLF + "m=application 56436 DTLS/SCTP 5000" CRLF); + + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()); + + auto& mediaSection = Sdp()->GetMediaSection(0); + mediaSection.AddDataChannel("webrtc-datachannel", 6000, 16, 0); + + ASSERT_FALSE(mediaSection.GetAttributeList().HasAttribute( + SdpAttribute::kMaxMessageSizeAttribute)); + ASSERT_TRUE(mediaSection.GetAttributeList().HasAttribute( + SdpAttribute::kSctpmapAttribute)); + ASSERT_TRUE(mediaSection.GetAttributeList().GetSctpmap().HasEntry("6000")); + ASSERT_EQ( + 16U, + mediaSection.GetAttributeList().GetSctpmap().GetFirstEntry().streams); + ASSERT_EQ("webrtc-datachannel", + mediaSection.GetAttributeList().GetSctpmap().GetFirstEntry().name); + + mediaSection.AddDataChannel("webrtc-datachannel", 15000, 8, 1800); + + ASSERT_TRUE(mediaSection.GetAttributeList().HasAttribute( + SdpAttribute::kMaxMessageSizeAttribute)); + ASSERT_EQ(1800U, mediaSection.GetAttributeList().GetMaxMessageSize()); + ASSERT_TRUE(mediaSection.GetAttributeList().HasAttribute( + SdpAttribute::kSctpmapAttribute)); + ASSERT_TRUE(mediaSection.GetAttributeList().GetSctpmap().HasEntry("15000")); + ASSERT_EQ( + 8U, mediaSection.GetAttributeList().GetSctpmap().GetFirstEntry().streams); +} + +TEST_P(NewSdpTest, CheckAddDataChannel) { + ParseSdp("v=0" CRLF "o=- 4294967296 2 IN IP4 127.0.0.1" CRLF "s=SIP Call" CRLF + "c=IN IP4 198.51.100.7" CRLF "b=CT:5000" CRLF "t=0 0" CRLF + "m=application 56436 UDP/DTLS/SCTP webrtc-datachannel" CRLF); + + ASSERT_TRUE(!!Sdp()) + << "Parse failed: " << SerializeParseErrors(); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()); + + auto& mediaSection = Sdp()->GetMediaSection(0); + mediaSection.AddDataChannel("webrtc-datachannel", 6000, 16, 0); + + ASSERT_FALSE(mediaSection.GetAttributeList().HasAttribute( + SdpAttribute::kMaxMessageSizeAttribute)); + ASSERT_TRUE(mediaSection.GetAttributeList().HasAttribute( + SdpAttribute::kSctpPortAttribute)); + ASSERT_EQ(6000U, mediaSection.GetAttributeList().GetSctpPort()); + + mediaSection.AddDataChannel("webrtc-datachannel", 15000, 8, 1800); + + ASSERT_TRUE(mediaSection.GetAttributeList().HasAttribute( + SdpAttribute::kMaxMessageSizeAttribute)); + ASSERT_EQ(1800U, mediaSection.GetAttributeList().GetMaxMessageSize()); + ASSERT_TRUE(mediaSection.GetAttributeList().HasAttribute( + SdpAttribute::kSctpPortAttribute)); + ASSERT_EQ(15000U, mediaSection.GetAttributeList().GetSctpPort()); +} + +TEST(NewSdpTestNoFixture, CheckParsingResultComparer) +{ + auto check_comparison = [](const std::string sdp_string) { + SipccSdpParser sipccParser; + RsdparsaSdpParser rustParser; + auto print_errors = + [](const mozilla::UniquePtr<SdpParser::Results>& results, + const char* name) { + for (const auto& e : results->Errors()) { + std::cerr << name << " Line " << e.first << ": " << e.second; + } + }; + auto sipccResults = sipccParser.Parse(sdp_string); + auto sipccSdp = std::move(sipccResults->Sdp()); + print_errors(sipccResults, "sipcc"); + + auto rustResults = rustParser.Parse(sdp_string); + auto rustSdp = std::move(rustResults->Sdp()); + print_errors(rustResults, "webrtc-sdp"); + + ParsingResultComparer comparer; + return sipccSdp && rustSdp + ? comparer.Compare(*rustSdp, *sipccSdp, sdp_string) + : false; + }; + + ASSERT_TRUE(check_comparison(kBasicAudioVideoOffer)); + ASSERT_TRUE(check_comparison(kH264AudioVideoOffer)); + + // Check the Fmtp comprison + const std::string kBasicOpusFmtp1 = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "m=video 9 RTP/SAVPF 120" CRLF "a=rtpmap:120 opus/48000/1" CRLF + "a=fmtp:120 stereo=1;useinbandfec=1" CRLF; + ASSERT_TRUE(check_comparison(kBasicOpusFmtp1)); + + const std::string kBasicOpusFmtp2 = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "m=video 9 RTP/SAVPF 120" CRLF "a=rtpmap:120 opus/48000" CRLF + "a=fmtp:120 useinbandfec=1;stereo=1;maxplaybackrate=32000" CRLF; + ASSERT_TRUE(check_comparison(kBasicOpusFmtp2)); + + const std::string kBasicVP8Fmtp1 = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "m=video 9 RTP/SAVPF 120" CRLF "a=rtpmap:120 VP8/90000" CRLF + "a=fmtp:120 max-fs=3600;max-fr=60" CRLF; + ASSERT_TRUE(check_comparison(kBasicVP8Fmtp1)); + // + const std::string kBasicVP8Fmtp2 = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "m=video 9 RTP/SAVPF 120" CRLF "a=rtpmap:120 VP8/90000" CRLF + "a=fmtp:120 max-fr=60;max-fs=3600" CRLF; + ASSERT_TRUE(check_comparison(kBasicVP8Fmtp2)); + + const std::string kBasicH264Fmtp1 = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "m=video 9 RTP/SAVPF 120" CRLF "a=rtpmap:120 H264/90000" CRLF + "a=fmtp:120 profile-level-id=42a01e;level_asymmetry_allowed=1" CRLF; + ASSERT_TRUE(check_comparison(kBasicH264Fmtp1)); + + const std::string kBasicH264Fmtp2 = + "v=0" CRLF "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF + "s=SIP Call" CRLF "c=IN IP4 224.0.0.1/100/12" CRLF "t=0 0" CRLF + "m=video 9 RTP/SAVPF 120" CRLF "a=rtpmap:120 H264/90000" CRLF + "a=fmtp:120 " + "level_asymmetry_allowed=1;profile-level-id=42a01e;max_fs=3600" CRLF; + ASSERT_TRUE(check_comparison(kBasicH264Fmtp2)); +} + +TEST(NewSdpTestNoFixture, CheckAttributeTypeSerialize) +{ + for (auto a = static_cast<size_t>(SdpAttribute::kFirstAttribute); + a <= static_cast<size_t>(SdpAttribute::kLastAttribute); ++a) { + SdpAttribute::AttributeType type = + static_cast<SdpAttribute::AttributeType>(a); + + // Direction attributes are handled a little differently + if (type != SdpAttribute::kDirectionAttribute) { + std::ostringstream os; + os << type; + ASSERT_NE("", os.str()); + } + } +} + +static SdpImageattrAttributeList::XYRange ParseXYRange( + const std::string& input) { + std::istringstream is(input + ","); + std::string error; + SdpImageattrAttributeList::XYRange range; + EXPECT_TRUE(range.Parse(is, &error)) << error; + EXPECT_EQ(',', is.get()); + EXPECT_EQ(EOF, is.get()); + return range; +} + +TEST(NewSdpTestNoFixture, CheckImageattrXYRangeParseValid) +{ + { + SdpImageattrAttributeList::XYRange range(ParseXYRange("640")); + ASSERT_EQ(1U, range.discreteValues.size()); + ASSERT_EQ(640U, range.discreteValues[0]); + } + + { + SdpImageattrAttributeList::XYRange range(ParseXYRange("[320,640]")); + ASSERT_EQ(2U, range.discreteValues.size()); + ASSERT_EQ(320U, range.discreteValues[0]); + ASSERT_EQ(640U, range.discreteValues[1]); + } + + { + SdpImageattrAttributeList::XYRange range(ParseXYRange("[320,640,1024]")); + ASSERT_EQ(3U, range.discreteValues.size()); + ASSERT_EQ(320U, range.discreteValues[0]); + ASSERT_EQ(640U, range.discreteValues[1]); + ASSERT_EQ(1024U, range.discreteValues[2]); + } + + { + SdpImageattrAttributeList::XYRange range(ParseXYRange("[320:640]")); + ASSERT_EQ(0U, range.discreteValues.size()); + ASSERT_EQ(320U, range.min); + ASSERT_EQ(1U, range.step); + ASSERT_EQ(640U, range.max); + } + + { + SdpImageattrAttributeList::XYRange range(ParseXYRange("[320:16:640]")); + ASSERT_EQ(0U, range.discreteValues.size()); + ASSERT_EQ(320U, range.min); + ASSERT_EQ(16U, range.step); + ASSERT_EQ(640U, range.max); + } +} + +template <typename T> +void ParseInvalid(const std::string& input, size_t last) { + std::istringstream is(input); + T parsed; + std::string error; + ASSERT_FALSE(parsed.Parse(is, &error)) + << "\'" << input << "\' should not have parsed successfully"; + is.clear(); + ASSERT_EQ(last, static_cast<size_t>(is.tellg())) + << "Parse failed at unexpected location:" << std::endl + << input << std::endl + << std::string(is.tellg(), ' ') << "^" << std::endl; + // For a human to eyeball to make sure the error strings look sane + std::cout << "\"" << input << "\" - " << error << std::endl; +} + +TEST(NewSdpTestNoFixture, CheckImageattrXYRangeParseInvalid) +{ + ParseInvalid<SdpImageattrAttributeList::XYRange>("[-1", 1); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[-", 1); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[-v", 1); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[640:-1", 5); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[640:16:-1", 8); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[640,-1", 5); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[640,-]", 5); + ParseInvalid<SdpImageattrAttributeList::XYRange>("-v", 0); + ParseInvalid<SdpImageattrAttributeList::XYRange>("-1", 0); + ParseInvalid<SdpImageattrAttributeList::XYRange>("", 0); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[", 1); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[v", 1); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[", 1); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[ 640", 1); + // It looks like the overflow detection only happens once the whole number + // is scanned... + ParseInvalid<SdpImageattrAttributeList::XYRange>("[99999999999999999:", 18); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[640", 4); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[640:", 5); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[640:v", 5); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[640:16", 7); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[640:16:", 8); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[640:16:v", 8); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[640:16:320]", 11); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[640:16:320", 11); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[640:16:320v", 11); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[640:1024", 9); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[640:320]", 8); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[640:1024v", 9); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[640,", 5); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[640,v", 5); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[640]", 4); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[640x", 4); + ParseInvalid<SdpImageattrAttributeList::XYRange>("[640,]", 5); + ParseInvalid<SdpImageattrAttributeList::XYRange>(" ", 0); + ParseInvalid<SdpImageattrAttributeList::XYRange>("v", 0); +} + +static SdpImageattrAttributeList::SRange ParseSRange(const std::string& input) { + std::istringstream is(input + ","); + std::string error; + SdpImageattrAttributeList::SRange range; + EXPECT_TRUE(range.Parse(is, &error)) << error; + EXPECT_EQ(',', is.get()); + EXPECT_EQ(EOF, is.get()); + return range; +} + +TEST(NewSdpTestNoFixture, CheckImageattrSRangeParseValid) +{ + { + SdpImageattrAttributeList::SRange range(ParseSRange("0.1")); + ASSERT_EQ(1U, range.discreteValues.size()); + ASSERT_FLOAT_EQ(0.1f, range.discreteValues[0]); + } + + { + SdpImageattrAttributeList::SRange range(ParseSRange("[0.1,0.2]")); + ASSERT_EQ(2U, range.discreteValues.size()); + ASSERT_FLOAT_EQ(0.1f, range.discreteValues[0]); + ASSERT_FLOAT_EQ(0.2f, range.discreteValues[1]); + } + + { + SdpImageattrAttributeList::SRange range(ParseSRange("[0.1,0.2,0.3]")); + ASSERT_EQ(3U, range.discreteValues.size()); + ASSERT_FLOAT_EQ(0.1f, range.discreteValues[0]); + ASSERT_FLOAT_EQ(0.2f, range.discreteValues[1]); + ASSERT_FLOAT_EQ(0.3f, range.discreteValues[2]); + } + + { + SdpImageattrAttributeList::SRange range(ParseSRange("[0.1-0.2]")); + ASSERT_EQ(0U, range.discreteValues.size()); + ASSERT_FLOAT_EQ(0.1f, range.min); + ASSERT_FLOAT_EQ(0.2f, range.max); + } +} + +TEST(NewSdpTestNoFixture, CheckImageattrSRangeParseInvalid) +{ + ParseInvalid<SdpImageattrAttributeList::SRange>("", 0); + ParseInvalid<SdpImageattrAttributeList::SRange>("[", 1); + ParseInvalid<SdpImageattrAttributeList::SRange>("[v", 1); + ParseInvalid<SdpImageattrAttributeList::SRange>("[-1", 1); + ParseInvalid<SdpImageattrAttributeList::SRange>("[", 1); + ParseInvalid<SdpImageattrAttributeList::SRange>("[-", 1); + ParseInvalid<SdpImageattrAttributeList::SRange>("[v", 1); + ParseInvalid<SdpImageattrAttributeList::SRange>("[ 0.2", 1); + ParseInvalid<SdpImageattrAttributeList::SRange>("[10.1-", 5); + ParseInvalid<SdpImageattrAttributeList::SRange>("[0.08-", 5); + ParseInvalid<SdpImageattrAttributeList::SRange>("[0.2", 4); + ParseInvalid<SdpImageattrAttributeList::SRange>("[0.2-", 5); + ParseInvalid<SdpImageattrAttributeList::SRange>("[0.2-v", 5); + ParseInvalid<SdpImageattrAttributeList::SRange>("[0.2--1", 5); + ParseInvalid<SdpImageattrAttributeList::SRange>("[0.2-0.3", 8); + ParseInvalid<SdpImageattrAttributeList::SRange>("[0.2-0.1]", 8); + ParseInvalid<SdpImageattrAttributeList::SRange>("[0.2-0.3v", 8); + ParseInvalid<SdpImageattrAttributeList::SRange>("[0.2,", 5); + ParseInvalid<SdpImageattrAttributeList::SRange>("[0.2,v", 5); + ParseInvalid<SdpImageattrAttributeList::SRange>("[0.2,-1", 5); + ParseInvalid<SdpImageattrAttributeList::SRange>("[0.2]", 4); + ParseInvalid<SdpImageattrAttributeList::SRange>("[0.2v", 4); + ParseInvalid<SdpImageattrAttributeList::SRange>("[0.2,]", 5); + ParseInvalid<SdpImageattrAttributeList::SRange>("[0.2,-]", 5); + ParseInvalid<SdpImageattrAttributeList::SRange>(" ", 0); + ParseInvalid<SdpImageattrAttributeList::SRange>("v", 0); + ParseInvalid<SdpImageattrAttributeList::SRange>("-v", 0); + ParseInvalid<SdpImageattrAttributeList::SRange>("-1", 0); +} + +static SdpImageattrAttributeList::PRange ParsePRange(const std::string& input) { + std::istringstream is(input + ","); + std::string error; + SdpImageattrAttributeList::PRange range; + EXPECT_TRUE(range.Parse(is, &error)) << error; + EXPECT_EQ(',', is.get()); + EXPECT_EQ(EOF, is.get()); + return range; +} + +TEST(NewSdpTestNoFixture, CheckImageattrPRangeParseValid) +{ + SdpImageattrAttributeList::PRange range(ParsePRange("[0.1000-9.9999]")); + ASSERT_FLOAT_EQ(0.1f, range.min); + ASSERT_FLOAT_EQ(9.9999f, range.max); +} + +TEST(NewSdpTestNoFixture, CheckImageattrPRangeParseInvalid) +{ + ParseInvalid<SdpImageattrAttributeList::PRange>("", 0); + ParseInvalid<SdpImageattrAttributeList::PRange>("[", 1); + ParseInvalid<SdpImageattrAttributeList::PRange>("[v", 1); + ParseInvalid<SdpImageattrAttributeList::PRange>("[-1", 1); + ParseInvalid<SdpImageattrAttributeList::PRange>("[", 1); + ParseInvalid<SdpImageattrAttributeList::PRange>("[-", 1); + ParseInvalid<SdpImageattrAttributeList::PRange>("[v", 1); + ParseInvalid<SdpImageattrAttributeList::PRange>("[ 0.2", 1); + ParseInvalid<SdpImageattrAttributeList::PRange>("[10.1-", 5); + ParseInvalid<SdpImageattrAttributeList::PRange>("[0.08-", 5); + ParseInvalid<SdpImageattrAttributeList::PRange>("[0.2", 4); + ParseInvalid<SdpImageattrAttributeList::PRange>("[0.2-", 5); + ParseInvalid<SdpImageattrAttributeList::PRange>("[0.2-v", 5); + ParseInvalid<SdpImageattrAttributeList::PRange>("[0.2--1", 5); + ParseInvalid<SdpImageattrAttributeList::PRange>("[0.2-0.3", 8); + ParseInvalid<SdpImageattrAttributeList::PRange>("[0.2-0.1]", 8); + ParseInvalid<SdpImageattrAttributeList::PRange>("[0.2-0.3v", 8); + ParseInvalid<SdpImageattrAttributeList::PRange>("[0.2,", 4); + ParseInvalid<SdpImageattrAttributeList::PRange>("[0.2:", 4); + ParseInvalid<SdpImageattrAttributeList::PRange>("[0.2]", 4); + ParseInvalid<SdpImageattrAttributeList::PRange>("[0.2v", 4); + ParseInvalid<SdpImageattrAttributeList::PRange>(" ", 0); + ParseInvalid<SdpImageattrAttributeList::PRange>("v", 0); + ParseInvalid<SdpImageattrAttributeList::PRange>("-x", 0); + ParseInvalid<SdpImageattrAttributeList::PRange>("-1", 0); +} + +static SdpImageattrAttributeList::Set ParseSet(const std::string& input) { + std::istringstream is(input + " "); + std::string error; + SdpImageattrAttributeList::Set set; + EXPECT_TRUE(set.Parse(is, &error)) << error; + EXPECT_EQ(' ', is.get()); + EXPECT_EQ(EOF, is.get()); + return set; +} + +TEST(NewSdpTestNoFixture, CheckImageattrSetParseValid) +{ + { + SdpImageattrAttributeList::Set set(ParseSet("[x=320,y=240]")); + ASSERT_EQ(1U, set.xRange.discreteValues.size()); + ASSERT_EQ(320U, set.xRange.discreteValues[0]); + ASSERT_EQ(1U, set.yRange.discreteValues.size()); + ASSERT_EQ(240U, set.yRange.discreteValues[0]); + ASSERT_FALSE(set.sRange.IsSet()); + ASSERT_FALSE(set.pRange.IsSet()); + ASSERT_FLOAT_EQ(0.5f, set.qValue); + } + + { + SdpImageattrAttributeList::Set set(ParseSet("[X=320,Y=240]")); + ASSERT_EQ(1U, set.xRange.discreteValues.size()); + ASSERT_EQ(320U, set.xRange.discreteValues[0]); + ASSERT_EQ(1U, set.yRange.discreteValues.size()); + ASSERT_EQ(240U, set.yRange.discreteValues[0]); + ASSERT_FALSE(set.sRange.IsSet()); + ASSERT_FALSE(set.pRange.IsSet()); + ASSERT_FLOAT_EQ(0.5f, set.qValue); + } + + { + SdpImageattrAttributeList::Set set(ParseSet("[x=320,y=240,par=[0.1-0.2]]")); + ASSERT_EQ(1U, set.xRange.discreteValues.size()); + ASSERT_EQ(320U, set.xRange.discreteValues[0]); + ASSERT_EQ(1U, set.yRange.discreteValues.size()); + ASSERT_EQ(240U, set.yRange.discreteValues[0]); + ASSERT_FALSE(set.sRange.IsSet()); + ASSERT_TRUE(set.pRange.IsSet()); + ASSERT_FLOAT_EQ(0.1f, set.pRange.min); + ASSERT_FLOAT_EQ(0.2f, set.pRange.max); + ASSERT_FLOAT_EQ(0.5f, set.qValue); + } + + { + SdpImageattrAttributeList::Set set(ParseSet("[x=320,y=240,sar=[0.1-0.2]]")); + ASSERT_EQ(1U, set.xRange.discreteValues.size()); + ASSERT_EQ(320U, set.xRange.discreteValues[0]); + ASSERT_EQ(1U, set.yRange.discreteValues.size()); + ASSERT_EQ(240U, set.yRange.discreteValues[0]); + ASSERT_TRUE(set.sRange.IsSet()); + ASSERT_FLOAT_EQ(0.1f, set.sRange.min); + ASSERT_FLOAT_EQ(0.2f, set.sRange.max); + ASSERT_FALSE(set.pRange.IsSet()); + ASSERT_FLOAT_EQ(0.5f, set.qValue); + } + + { + SdpImageattrAttributeList::Set set(ParseSet("[x=320,y=240,q=0.1]")); + ASSERT_EQ(1U, set.xRange.discreteValues.size()); + ASSERT_EQ(320U, set.xRange.discreteValues[0]); + ASSERT_EQ(1U, set.yRange.discreteValues.size()); + ASSERT_EQ(240U, set.yRange.discreteValues[0]); + ASSERT_FALSE(set.sRange.IsSet()); + ASSERT_FALSE(set.pRange.IsSet()); + ASSERT_FLOAT_EQ(0.1f, set.qValue); + } + + { + SdpImageattrAttributeList::Set set( + ParseSet("[x=320,y=240,par=[0.1-0.2],sar=[0.3-0.4],q=0.6]")); + ASSERT_EQ(1U, set.xRange.discreteValues.size()); + ASSERT_EQ(320U, set.xRange.discreteValues[0]); + ASSERT_EQ(1U, set.yRange.discreteValues.size()); + ASSERT_EQ(240U, set.yRange.discreteValues[0]); + ASSERT_TRUE(set.sRange.IsSet()); + ASSERT_FLOAT_EQ(0.3f, set.sRange.min); + ASSERT_FLOAT_EQ(0.4f, set.sRange.max); + ASSERT_TRUE(set.pRange.IsSet()); + ASSERT_FLOAT_EQ(0.1f, set.pRange.min); + ASSERT_FLOAT_EQ(0.2f, set.pRange.max); + ASSERT_FLOAT_EQ(0.6f, set.qValue); + } + + { + SdpImageattrAttributeList::Set set(ParseSet("[x=320,y=240,foo=bar,q=0.1]")); + ASSERT_EQ(1U, set.xRange.discreteValues.size()); + ASSERT_EQ(320U, set.xRange.discreteValues[0]); + ASSERT_EQ(1U, set.yRange.discreteValues.size()); + ASSERT_EQ(240U, set.yRange.discreteValues[0]); + ASSERT_FALSE(set.sRange.IsSet()); + ASSERT_FALSE(set.pRange.IsSet()); + ASSERT_FLOAT_EQ(0.1f, set.qValue); + } + + { + SdpImageattrAttributeList::Set set( + ParseSet("[x=320,y=240,foo=bar,q=0.1,bar=baz]")); + ASSERT_EQ(1U, set.xRange.discreteValues.size()); + ASSERT_EQ(320U, set.xRange.discreteValues[0]); + ASSERT_EQ(1U, set.yRange.discreteValues.size()); + ASSERT_EQ(240U, set.yRange.discreteValues[0]); + ASSERT_FALSE(set.sRange.IsSet()); + ASSERT_FALSE(set.pRange.IsSet()); + ASSERT_FLOAT_EQ(0.1f, set.qValue); + } + + { + SdpImageattrAttributeList::Set set( + ParseSet("[x=320,y=240,foo=[bar],q=0.1,bar=[baz]]")); + ASSERT_EQ(1U, set.xRange.discreteValues.size()); + ASSERT_EQ(320U, set.xRange.discreteValues[0]); + ASSERT_EQ(1U, set.yRange.discreteValues.size()); + ASSERT_EQ(240U, set.yRange.discreteValues[0]); + ASSERT_FALSE(set.sRange.IsSet()); + ASSERT_FALSE(set.pRange.IsSet()); + ASSERT_FLOAT_EQ(0.1f, set.qValue); + } + + { + SdpImageattrAttributeList::Set set( + ParseSet("[x=320,y=240,foo=[par=foo,sar=bar],q=0.1,bar=[baz]]")); + ASSERT_EQ(1U, set.xRange.discreteValues.size()); + ASSERT_EQ(320U, set.xRange.discreteValues[0]); + ASSERT_EQ(1U, set.yRange.discreteValues.size()); + ASSERT_EQ(240U, set.yRange.discreteValues[0]); + ASSERT_FALSE(set.sRange.IsSet()); + ASSERT_FALSE(set.pRange.IsSet()); + ASSERT_FLOAT_EQ(0.1f, set.qValue); + } +} + +TEST(NewSdpTestNoFixture, CheckImageattrSetParseInvalid) +{ + ParseInvalid<SdpImageattrAttributeList::Set>("", 0); + ParseInvalid<SdpImageattrAttributeList::Set>("x", 0); + ParseInvalid<SdpImageattrAttributeList::Set>("[", 1); + ParseInvalid<SdpImageattrAttributeList::Set>("[=", 2); + ParseInvalid<SdpImageattrAttributeList::Set>("[x", 2); + ParseInvalid<SdpImageattrAttributeList::Set>("[y=", 3); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=[", 4); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=320", 6); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=320v", 6); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=320,", 7); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=320,=", 8); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=320,x", 8); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=320,x=", 9); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=320,y=[", 10); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=320,y=240", 12); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=320,y=240x", 12); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=320,y=240,", 13); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=320,y=240,q=", 15); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=320,y=240,q=v", 15); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=320,y=240,q=0.5", 18); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=320,y=240,q=0.5,", 19); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=320,y=240,q=0.5,]", 20); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=320,y=240,q=0.5,=]", 20); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=320,y=240,q=0.5,sar=v]", 23); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=320,y=240,q=0.5,q=0.4", 21); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=320,y=240,sar=", 17); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=320,y=240,sar=v", 17); + ParseInvalid<SdpImageattrAttributeList::Set>( + "[x=320,y=240,sar=[0.5-0.6],sar=[0.7-0.8]", 31); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=320,y=240,par=", 17); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=320,y=240,par=x", 17); + ParseInvalid<SdpImageattrAttributeList::Set>( + "[x=320,y=240,par=[0.5-0.6],par=[0.7-0.8]", 31); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=320,y=240,foo=", 17); + ParseInvalid<SdpImageattrAttributeList::Set>("[x=320,y=240,foo=x", 18); +} + +static SdpImageattrAttributeList::Imageattr ParseImageattr( + const std::string& input) { + std::istringstream is(input); + std::string error; + SdpImageattrAttributeList::Imageattr imageattr; + EXPECT_TRUE(imageattr.Parse(is, &error)) << error; + EXPECT_TRUE(is.eof()); + return imageattr; +} + +TEST(NewSdpTestNoFixture, CheckImageattrParseValid) +{ + { + SdpImageattrAttributeList::Imageattr imageattr(ParseImageattr("* send *")); + ASSERT_FALSE(imageattr.pt.isSome()); + ASSERT_TRUE(imageattr.sendAll); + ASSERT_TRUE(imageattr.sendSets.empty()); + ASSERT_FALSE(imageattr.recvAll); + ASSERT_TRUE(imageattr.recvSets.empty()); + } + + { + SdpImageattrAttributeList::Imageattr imageattr(ParseImageattr("* SEND *")); + ASSERT_FALSE(imageattr.pt.isSome()); + ASSERT_TRUE(imageattr.sendAll); + ASSERT_TRUE(imageattr.sendSets.empty()); + ASSERT_FALSE(imageattr.recvAll); + ASSERT_TRUE(imageattr.recvSets.empty()); + } + + { + SdpImageattrAttributeList::Imageattr imageattr(ParseImageattr("* recv *")); + ASSERT_FALSE(imageattr.pt.isSome()); + ASSERT_FALSE(imageattr.sendAll); + ASSERT_TRUE(imageattr.sendSets.empty()); + ASSERT_TRUE(imageattr.recvAll); + ASSERT_TRUE(imageattr.recvSets.empty()); + } + + { + SdpImageattrAttributeList::Imageattr imageattr(ParseImageattr("* RECV *")); + ASSERT_FALSE(imageattr.pt.isSome()); + ASSERT_FALSE(imageattr.sendAll); + ASSERT_TRUE(imageattr.sendSets.empty()); + ASSERT_TRUE(imageattr.recvAll); + ASSERT_TRUE(imageattr.recvSets.empty()); + } + + { + SdpImageattrAttributeList::Imageattr imageattr( + ParseImageattr("* recv * send *")); + ASSERT_FALSE(imageattr.pt.isSome()); + ASSERT_TRUE(imageattr.sendAll); + ASSERT_TRUE(imageattr.sendSets.empty()); + ASSERT_TRUE(imageattr.recvAll); + ASSERT_TRUE(imageattr.recvSets.empty()); + } + + { + SdpImageattrAttributeList::Imageattr imageattr( + ParseImageattr("* send * recv *")); + ASSERT_FALSE(imageattr.pt.isSome()); + ASSERT_TRUE(imageattr.sendAll); + ASSERT_TRUE(imageattr.sendSets.empty()); + ASSERT_TRUE(imageattr.recvAll); + ASSERT_TRUE(imageattr.recvSets.empty()); + } + + { + SdpImageattrAttributeList::Imageattr imageattr( + ParseImageattr("8 send * recv *")); + ASSERT_EQ(8U, *imageattr.pt); + ASSERT_TRUE(imageattr.sendAll); + ASSERT_TRUE(imageattr.sendSets.empty()); + ASSERT_TRUE(imageattr.recvAll); + ASSERT_TRUE(imageattr.recvSets.empty()); + } + + { + SdpImageattrAttributeList::Imageattr imageattr( + ParseImageattr("8 send [x=320,y=240] recv *")); + ASSERT_EQ(8U, *imageattr.pt); + ASSERT_FALSE(imageattr.sendAll); + ASSERT_EQ(1U, imageattr.sendSets.size()); + ASSERT_EQ(1U, imageattr.sendSets[0].xRange.discreteValues.size()); + ASSERT_EQ(320U, imageattr.sendSets[0].xRange.discreteValues[0]); + ASSERT_EQ(1U, imageattr.sendSets[0].yRange.discreteValues.size()); + ASSERT_EQ(240U, imageattr.sendSets[0].yRange.discreteValues[0]); + ASSERT_TRUE(imageattr.recvAll); + ASSERT_TRUE(imageattr.recvSets.empty()); + } + + { + SdpImageattrAttributeList::Imageattr imageattr( + ParseImageattr("8 send [x=320,y=240] [x=640,y=480] recv *")); + ASSERT_EQ(8U, *imageattr.pt); + ASSERT_FALSE(imageattr.sendAll); + ASSERT_EQ(2U, imageattr.sendSets.size()); + ASSERT_EQ(1U, imageattr.sendSets[0].xRange.discreteValues.size()); + ASSERT_EQ(320U, imageattr.sendSets[0].xRange.discreteValues[0]); + ASSERT_EQ(1U, imageattr.sendSets[0].yRange.discreteValues.size()); + ASSERT_EQ(240U, imageattr.sendSets[0].yRange.discreteValues[0]); + ASSERT_EQ(1U, imageattr.sendSets[1].xRange.discreteValues.size()); + ASSERT_EQ(640U, imageattr.sendSets[1].xRange.discreteValues[0]); + ASSERT_EQ(1U, imageattr.sendSets[1].yRange.discreteValues.size()); + ASSERT_EQ(480U, imageattr.sendSets[1].yRange.discreteValues[0]); + ASSERT_TRUE(imageattr.recvAll); + ASSERT_TRUE(imageattr.recvSets.empty()); + } + + { + SdpImageattrAttributeList::Imageattr imageattr( + ParseImageattr("8 send * recv [x=320,y=240]")); + ASSERT_EQ(8U, *imageattr.pt); + ASSERT_FALSE(imageattr.recvAll); + ASSERT_EQ(1U, imageattr.recvSets.size()); + ASSERT_EQ(1U, imageattr.recvSets[0].xRange.discreteValues.size()); + ASSERT_EQ(320U, imageattr.recvSets[0].xRange.discreteValues[0]); + ASSERT_EQ(1U, imageattr.recvSets[0].yRange.discreteValues.size()); + ASSERT_EQ(240U, imageattr.recvSets[0].yRange.discreteValues[0]); + ASSERT_TRUE(imageattr.sendAll); + ASSERT_TRUE(imageattr.sendSets.empty()); + } + + { + SdpImageattrAttributeList::Imageattr imageattr( + ParseImageattr("8 send * recv [x=320,y=240] [x=640,y=480]")); + ASSERT_EQ(8U, *imageattr.pt); + ASSERT_FALSE(imageattr.recvAll); + ASSERT_EQ(2U, imageattr.recvSets.size()); + ASSERT_EQ(1U, imageattr.recvSets[0].xRange.discreteValues.size()); + ASSERT_EQ(320U, imageattr.recvSets[0].xRange.discreteValues[0]); + ASSERT_EQ(1U, imageattr.recvSets[0].yRange.discreteValues.size()); + ASSERT_EQ(240U, imageattr.recvSets[0].yRange.discreteValues[0]); + ASSERT_EQ(1U, imageattr.recvSets[1].xRange.discreteValues.size()); + ASSERT_EQ(640U, imageattr.recvSets[1].xRange.discreteValues[0]); + ASSERT_EQ(1U, imageattr.recvSets[1].yRange.discreteValues.size()); + ASSERT_EQ(480U, imageattr.recvSets[1].yRange.discreteValues[0]); + ASSERT_TRUE(imageattr.sendAll); + ASSERT_TRUE(imageattr.sendSets.empty()); + } +} + +TEST(NewSdpTestNoFixture, CheckImageattrParseInvalid) +{ + ParseInvalid<SdpImageattrAttributeList::Imageattr>("", 0); + ParseInvalid<SdpImageattrAttributeList::Imageattr>(" ", 0); + ParseInvalid<SdpImageattrAttributeList::Imageattr>("-1", 0); + ParseInvalid<SdpImageattrAttributeList::Imageattr>("99999 ", 5); + ParseInvalid<SdpImageattrAttributeList::Imageattr>("*", 1); + ParseInvalid<SdpImageattrAttributeList::Imageattr>("* sen", 5); + ParseInvalid<SdpImageattrAttributeList::Imageattr>("* vcer *", 6); + ParseInvalid<SdpImageattrAttributeList::Imageattr>("* send x", 7); + ParseInvalid<SdpImageattrAttributeList::Imageattr>("* send [x=640,y=480] [", + 22); + ParseInvalid<SdpImageattrAttributeList::Imageattr>("* send * sen", 12); + ParseInvalid<SdpImageattrAttributeList::Imageattr>("* send * vcer *", 13); + ParseInvalid<SdpImageattrAttributeList::Imageattr>("* send * send *", 13); + ParseInvalid<SdpImageattrAttributeList::Imageattr>("* recv * recv *", 13); + ParseInvalid<SdpImageattrAttributeList::Imageattr>("* send * recv x", 14); + ParseInvalid<SdpImageattrAttributeList::Imageattr>( + "* send * recv [x=640,y=480] [", 29); + ParseInvalid<SdpImageattrAttributeList::Imageattr>( + "* send * recv [x=640,y=480] *", 28); + ParseInvalid<SdpImageattrAttributeList::Imageattr>( + "* send * recv [x=640,y=480] foobajooba", 28); +} + +TEST(NewSdpTestNoFixture, CheckImageattrXYRangeSerialization) +{ + SdpImageattrAttributeList::XYRange range; + std::stringstream os; + + range.min = 320; + range.max = 640; + range.Serialize(os); + ASSERT_EQ("[320:640]", os.str()); + os.str(""); // clear + + range.step = 16; + range.Serialize(os); + ASSERT_EQ("[320:16:640]", os.str()); + os.str(""); // clear + + range.min = 0; + range.max = 0; + range.discreteValues.push_back(320); + range.Serialize(os); + ASSERT_EQ("320", os.str()); + os.str(""); + + range.discreteValues.push_back(640); + range.Serialize(os); + ASSERT_EQ("[320,640]", os.str()); +} + +TEST(NewSdpTestNoFixture, CheckImageattrSRangeSerialization) +{ + SdpImageattrAttributeList::SRange range; + std::ostringstream os; + + range.min = 0.1f; + range.max = 0.9999f; + range.Serialize(os); + ASSERT_EQ("[0.1000-0.9999]", os.str()); + os.str(""); + + range.min = 0.0f; + range.max = 0.0f; + range.discreteValues.push_back(0.1f); + range.Serialize(os); + ASSERT_EQ("0.1000", os.str()); + os.str(""); + + range.discreteValues.push_back(0.5f); + range.Serialize(os); + ASSERT_EQ("[0.1000,0.5000]", os.str()); +} + +TEST(NewSdpTestNoFixture, CheckImageattrPRangeSerialization) +{ + SdpImageattrAttributeList::PRange range; + std::ostringstream os; + + range.min = 0.1f; + range.max = 0.9999f; + range.Serialize(os); + ASSERT_EQ("[0.1000-0.9999]", os.str()); +} + +TEST(NewSdpTestNoFixture, CheckImageattrSetSerialization) +{ + SdpImageattrAttributeList::Set set; + std::ostringstream os; + + set.xRange.discreteValues.push_back(640); + set.yRange.discreteValues.push_back(480); + set.Serialize(os); + ASSERT_EQ("[x=640,y=480]", os.str()); + os.str(""); + + set.qValue = 0.00f; + set.Serialize(os); + ASSERT_EQ("[x=640,y=480,q=0.00]", os.str()); + os.str(""); + + set.qValue = 0.10f; + set.Serialize(os); + ASSERT_EQ("[x=640,y=480,q=0.10]", os.str()); + os.str(""); + + set.qValue = 1.00f; + set.Serialize(os); + ASSERT_EQ("[x=640,y=480,q=1.00]", os.str()); + os.str(""); + + set.sRange.discreteValues.push_back(1.1f); + set.Serialize(os); + ASSERT_EQ("[x=640,y=480,sar=1.1000,q=1.00]", os.str()); + os.str(""); + + set.pRange.min = 0.9f; + set.pRange.max = 1.1f; + set.Serialize(os); + ASSERT_EQ("[x=640,y=480,sar=1.1000,par=[0.9000-1.1000],q=1.00]", os.str()); + os.str(""); +} + +TEST(NewSdpTestNoFixture, CheckImageattrSerialization) +{ + SdpImageattrAttributeList::Imageattr imageattr; + std::ostringstream os; + + imageattr.sendAll = true; + imageattr.pt = Some<uint16_t>(8U); + imageattr.Serialize(os); + ASSERT_EQ("8 send *", os.str()); + os.str(""); + + imageattr.pt.reset(); + ; + imageattr.Serialize(os); + ASSERT_EQ("* send *", os.str()); + os.str(""); + + imageattr.sendAll = false; + imageattr.recvAll = true; + imageattr.Serialize(os); + ASSERT_EQ("* recv *", os.str()); + os.str(""); + + imageattr.sendAll = true; + imageattr.Serialize(os); + ASSERT_EQ("* send * recv *", os.str()); + os.str(""); + + imageattr.sendAll = false; + imageattr.sendSets.push_back(SdpImageattrAttributeList::Set()); + imageattr.sendSets.back().xRange.discreteValues.push_back(320); + imageattr.sendSets.back().yRange.discreteValues.push_back(240); + imageattr.Serialize(os); + ASSERT_EQ("* send [x=320,y=240] recv *", os.str()); + os.str(""); + + imageattr.sendSets.push_back(SdpImageattrAttributeList::Set()); + imageattr.sendSets.back().xRange.discreteValues.push_back(640); + imageattr.sendSets.back().yRange.discreteValues.push_back(480); + imageattr.Serialize(os); + ASSERT_EQ("* send [x=320,y=240] [x=640,y=480] recv *", os.str()); + os.str(""); + + imageattr.recvAll = false; + imageattr.recvSets.push_back(SdpImageattrAttributeList::Set()); + imageattr.recvSets.back().xRange.discreteValues.push_back(320); + imageattr.recvSets.back().yRange.discreteValues.push_back(240); + imageattr.Serialize(os); + ASSERT_EQ("* send [x=320,y=240] [x=640,y=480] recv [x=320,y=240]", os.str()); + os.str(""); + + imageattr.recvSets.push_back(SdpImageattrAttributeList::Set()); + imageattr.recvSets.back().xRange.discreteValues.push_back(640); + imageattr.recvSets.back().yRange.discreteValues.push_back(480); + imageattr.Serialize(os); + ASSERT_EQ( + "* send [x=320,y=240] [x=640,y=480] recv [x=320,y=240] [x=640,y=480]", + os.str()); + os.str(""); +} + +TEST(NewSdpTestNoFixture, CheckSimulcastVersionSerialize) +{ + std::ostringstream os; + + SdpSimulcastAttribute::Version version; + version.choices.push_back(SdpSimulcastAttribute::Encoding("8", false)); + version.Serialize(os); + ASSERT_EQ("8", os.str()); + os.str(""); + + version.choices.push_back(SdpSimulcastAttribute::Encoding("9", true)); + version.Serialize(os); + ASSERT_EQ("8,~9", os.str()); + os.str(""); + + version.choices.push_back(SdpSimulcastAttribute::Encoding("0", false)); + version.Serialize(os); + ASSERT_EQ("8,~9,0", os.str()); + os.str(""); +} + +static SdpSimulcastAttribute::Version ParseSimulcastVersion( + const std::string& input) { + std::istringstream is(input + ";"); + std::string error; + SdpSimulcastAttribute::Version version; + EXPECT_TRUE(version.Parse(is, &error)) << error; + EXPECT_EQ(';', is.get()); + EXPECT_EQ(EOF, is.get()); + return version; +} + +TEST(NewSdpTestNoFixture, CheckSimulcastVersionValidParse) +{ + { + SdpSimulcastAttribute::Version version(ParseSimulcastVersion("1")); + ASSERT_EQ(1U, version.choices.size()); + ASSERT_EQ("1", version.choices[0].rid); + ASSERT_FALSE(version.choices[0].paused); + } + + { + SdpSimulcastAttribute::Version version(ParseSimulcastVersion("1,~2")); + ASSERT_EQ(2U, version.choices.size()); + ASSERT_EQ("1", version.choices[0].rid); + ASSERT_EQ("2", version.choices[1].rid); + ASSERT_FALSE(version.choices[0].paused); + ASSERT_TRUE(version.choices[1].paused); + } +} + +TEST(NewSdpTestNoFixture, CheckSimulcastVersionInvalidParse) +{ + ParseInvalid<SdpSimulcastAttribute::Version>("", 0); + ParseInvalid<SdpSimulcastAttribute::Version>(",", 0); + ParseInvalid<SdpSimulcastAttribute::Version>(";", 0); + ParseInvalid<SdpSimulcastAttribute::Version>(" ", 0); + ParseInvalid<SdpSimulcastAttribute::Version>("8,", 2); + ParseInvalid<SdpSimulcastAttribute::Version>("8, ", 2); + ParseInvalid<SdpSimulcastAttribute::Version>("8,,", 2); + ParseInvalid<SdpSimulcastAttribute::Version>("8,;", 2); +} + +TEST(NewSdpTestNoFixture, CheckSimulcastVersionsSerialize) +{ + std::ostringstream os; + + SdpSimulcastAttribute::Versions versions; + versions.push_back(SdpSimulcastAttribute::Version()); + versions.back().choices.push_back( + SdpSimulcastAttribute::Encoding("8", false)); + versions.Serialize(os); + ASSERT_EQ("8", os.str()); + os.str(""); + + versions.push_back(SdpSimulcastAttribute::Version()); + versions.Serialize(os); + ASSERT_EQ("8", os.str()); + os.str(""); + + versions.back().choices.push_back(SdpSimulcastAttribute::Encoding("9", true)); + versions.Serialize(os); + ASSERT_EQ("8;~9", os.str()); + os.str(""); + + versions.push_back(SdpSimulcastAttribute::Version()); + versions.back().choices.push_back( + SdpSimulcastAttribute::Encoding("0", false)); + versions.Serialize(os); + ASSERT_EQ("8;~9;0", os.str()); + os.str(""); +} + +static SdpSimulcastAttribute::Versions ParseSimulcastVersions( + const std::string& input) { + std::istringstream is(input + " "); + std::string error; + SdpSimulcastAttribute::Versions list; + EXPECT_TRUE(list.Parse(is, &error)) << error; + EXPECT_EQ(' ', is.get()); + EXPECT_EQ(EOF, is.get()); + return list; +} + +TEST(NewSdpTestNoFixture, CheckSimulcastVersionsValidParse) +{ + { + SdpSimulcastAttribute::Versions versions(ParseSimulcastVersions("8")); + ASSERT_EQ(1U, versions.size()); + ASSERT_EQ(1U, versions[0].choices.size()); + ASSERT_EQ("8", versions[0].choices[0].rid); + ASSERT_FALSE(versions[0].choices[0].paused); + } + + { + SdpSimulcastAttribute::Versions versions(ParseSimulcastVersions("~8,9")); + ASSERT_EQ(1U, versions.size()); + ASSERT_EQ(2U, versions[0].choices.size()); + ASSERT_EQ("8", versions[0].choices[0].rid); + ASSERT_EQ("9", versions[0].choices[1].rid); + ASSERT_TRUE(versions[0].choices[0].paused); + ASSERT_FALSE(versions[0].choices[1].paused); + } + + { + SdpSimulcastAttribute::Versions versions(ParseSimulcastVersions("8,9;~10")); + ASSERT_EQ(2U, versions.size()); + ASSERT_EQ(2U, versions[0].choices.size()); + ASSERT_EQ("8", versions[0].choices[0].rid); + ASSERT_EQ("9", versions[0].choices[1].rid); + ASSERT_FALSE(versions[0].choices[0].paused); + ASSERT_FALSE(versions[0].choices[1].paused); + ASSERT_EQ(1U, versions[1].choices.size()); + ASSERT_EQ("10", versions[1].choices[0].rid); + ASSERT_TRUE(versions[1].choices[0].paused); + } +} + +TEST(NewSdpTestNoFixture, CheckSimulcastVersionsInvalidParse) +{ + ParseInvalid<SdpSimulcastAttribute::Versions>("", 0); + ParseInvalid<SdpSimulcastAttribute::Versions>(";", 0); + ParseInvalid<SdpSimulcastAttribute::Versions>("8;", 2); + ParseInvalid<SdpSimulcastAttribute::Versions>("8;;", 2); +} + +TEST(NewSdpTestNoFixture, CheckSimulcastSerialize) +{ + std::ostringstream os; + + SdpSimulcastAttribute simulcast; + simulcast.recvVersions.push_back(SdpSimulcastAttribute::Version()); + simulcast.recvVersions.back().choices.push_back( + SdpSimulcastAttribute::Encoding("8", false)); + simulcast.Serialize(os); + ASSERT_EQ("a=simulcast:recv 8" CRLF, os.str()); + os.str(""); + + simulcast.sendVersions.push_back(SdpSimulcastAttribute::Version()); + simulcast.sendVersions.back().choices.push_back( + SdpSimulcastAttribute::Encoding("9", true)); + simulcast.Serialize(os); + ASSERT_EQ("a=simulcast:send ~9 recv 8" CRLF, os.str()); +} + +static SdpSimulcastAttribute ParseSimulcast(const std::string& input) { + std::istringstream is(input); + std::string error; + SdpSimulcastAttribute simulcast; + EXPECT_TRUE(simulcast.Parse(is, &error)) << error; + EXPECT_TRUE(is.eof()); + return simulcast; +} + +TEST(NewSdpTestNoFixture, CheckSimulcastValidParse) +{ + { + SdpSimulcastAttribute simulcast(ParseSimulcast("send 8")); + ASSERT_EQ(1U, simulcast.sendVersions.size()); + ASSERT_EQ(1U, simulcast.sendVersions[0].choices.size()); + ASSERT_EQ("8", simulcast.sendVersions[0].choices[0].rid); + ASSERT_FALSE(simulcast.sendVersions[0].choices[0].paused); + ASSERT_EQ(0U, simulcast.recvVersions.size()); + } + + { + SdpSimulcastAttribute simulcast(ParseSimulcast(" SEND 8")); + ASSERT_EQ(1U, simulcast.sendVersions.size()); + ASSERT_EQ(1U, simulcast.sendVersions[0].choices.size()); + ASSERT_EQ("8", simulcast.sendVersions[0].choices[0].rid); + ASSERT_FALSE(simulcast.sendVersions[0].choices[0].paused); + ASSERT_EQ(0U, simulcast.recvVersions.size()); + } + + { + SdpSimulcastAttribute simulcast(ParseSimulcast("recv 8")); + ASSERT_EQ(1U, simulcast.recvVersions.size()); + ASSERT_EQ(1U, simulcast.recvVersions[0].choices.size()); + ASSERT_EQ("8", simulcast.recvVersions[0].choices[0].rid); + ASSERT_FALSE(simulcast.recvVersions[0].choices[0].paused); + ASSERT_EQ(0U, simulcast.sendVersions.size()); + } + + { + SdpSimulcastAttribute simulcast( + ParseSimulcast("send 8,9;~101;97,~98 recv 101,120;97")); + ASSERT_EQ(3U, simulcast.sendVersions.size()); + ASSERT_EQ(2U, simulcast.sendVersions[0].choices.size()); + ASSERT_EQ("8", simulcast.sendVersions[0].choices[0].rid); + ASSERT_EQ("9", simulcast.sendVersions[0].choices[1].rid); + ASSERT_FALSE(simulcast.sendVersions[0].choices[0].paused); + ASSERT_FALSE(simulcast.sendVersions[0].choices[1].paused); + ASSERT_EQ(1U, simulcast.sendVersions[1].choices.size()); + ASSERT_EQ("101", simulcast.sendVersions[1].choices[0].rid); + ASSERT_TRUE(simulcast.sendVersions[1].choices[0].paused); + ASSERT_EQ(2U, simulcast.sendVersions[2].choices.size()); + ASSERT_EQ("97", simulcast.sendVersions[2].choices[0].rid); + ASSERT_EQ("98", simulcast.sendVersions[2].choices[1].rid); + ASSERT_FALSE(simulcast.sendVersions[2].choices[0].paused); + ASSERT_TRUE(simulcast.sendVersions[2].choices[1].paused); + + ASSERT_EQ(2U, simulcast.recvVersions.size()); + ASSERT_EQ(2U, simulcast.recvVersions[0].choices.size()); + ASSERT_EQ("101", simulcast.recvVersions[0].choices[0].rid); + ASSERT_EQ("120", simulcast.recvVersions[0].choices[1].rid); + ASSERT_EQ(1U, simulcast.recvVersions[1].choices.size()); + ASSERT_EQ("97", simulcast.recvVersions[1].choices[0].rid); + } +} + +TEST(NewSdpTestNoFixture, CheckSimulcastInvalidParse) +{ + ParseInvalid<SdpSimulcastAttribute>("", 0); + ParseInvalid<SdpSimulcastAttribute>(" ", 1); + ParseInvalid<SdpSimulcastAttribute>("vcer ", 4); + ParseInvalid<SdpSimulcastAttribute>(" send 8 send ", 12); + ParseInvalid<SdpSimulcastAttribute>(" recv 8 recv ", 12); +} + +static SdpRidAttributeList::Rid ParseRid(const std::string& input) { + std::istringstream is(input); + std::string error; + SdpRidAttributeList::Rid rid; + EXPECT_TRUE(rid.Parse(is, &error)) << error; + EXPECT_TRUE(is.eof()); + return rid; +} + +TEST(NewSdpTestNoFixture, CheckRidValidParse) +{ + { + SdpRidAttributeList::Rid rid(ParseRid("1 send")); + ASSERT_EQ("1", rid.id); + ASSERT_EQ(sdp::kSend, rid.direction); + ASSERT_EQ(0U, rid.formats.size()); + ASSERT_EQ(0U, rid.constraints.maxWidth); + ASSERT_EQ(0U, rid.constraints.maxHeight); + ASSERT_FALSE(rid.constraints.maxFps.isSome()); + ASSERT_EQ(0U, rid.constraints.maxFs); + ASSERT_EQ(0U, rid.constraints.maxBr); + ASSERT_EQ(0U, rid.constraints.maxPps); + ASSERT_EQ(0U, rid.dependIds.size()); + } + + { + SdpRidAttributeList::Rid rid(ParseRid("1 send pt=96;max-width=800")); + ASSERT_EQ("1", rid.id); + ASSERT_EQ(sdp::kSend, rid.direction); + ASSERT_EQ(1U, rid.formats.size()); + ASSERT_EQ(96U, rid.formats[0]); + ASSERT_EQ(800U, rid.constraints.maxWidth); + ASSERT_EQ(0U, rid.constraints.maxHeight); + ASSERT_FALSE(rid.constraints.maxFps.isSome()); + ASSERT_EQ(0U, rid.constraints.maxFs); + ASSERT_EQ(0U, rid.constraints.maxBr); + ASSERT_EQ(0U, rid.constraints.maxPps); + ASSERT_EQ(0U, rid.dependIds.size()); + } + + { + SdpRidAttributeList::Rid rid(ParseRid("1 send pt=96,97,98;max-width=800")); + ASSERT_EQ("1", rid.id); + ASSERT_EQ(sdp::kSend, rid.direction); + ASSERT_EQ(3U, rid.formats.size()); + ASSERT_EQ(96U, rid.formats[0]); + ASSERT_EQ(97U, rid.formats[1]); + ASSERT_EQ(98U, rid.formats[2]); + ASSERT_EQ(800U, rid.constraints.maxWidth); + ASSERT_EQ(0U, rid.constraints.maxHeight); + ASSERT_FALSE(rid.constraints.maxFps.isSome()); + ASSERT_FALSE(rid.constraints.maxFps.isSome()); + ASSERT_EQ(0U, rid.constraints.maxFs); + ASSERT_EQ(0U, rid.constraints.maxBr); + ASSERT_EQ(0U, rid.constraints.maxPps); + ASSERT_EQ(0U, rid.dependIds.size()); + } + + { + SdpRidAttributeList::Rid rid(ParseRid("foo send")); + ASSERT_EQ(0U, rid.formats.size()); + ASSERT_EQ(0U, rid.constraints.maxWidth); + ASSERT_EQ(0U, rid.constraints.maxHeight); + ASSERT_FALSE(rid.constraints.maxFps.isSome()); + ASSERT_EQ(0U, rid.constraints.maxFs); + ASSERT_EQ(0U, rid.constraints.maxBr); + ASSERT_EQ(0U, rid.constraints.maxPps); + ASSERT_EQ(0U, rid.dependIds.size()); + } + + { + SdpRidAttributeList::Rid rid(ParseRid("foo send pt=96")); + ASSERT_EQ(1U, rid.formats.size()); + ASSERT_EQ(96U, rid.formats[0]); + ASSERT_EQ(0U, rid.constraints.maxWidth); + ASSERT_EQ(0U, rid.constraints.maxHeight); + ASSERT_FALSE(rid.constraints.maxFps.isSome()); + ASSERT_EQ(0U, rid.constraints.maxFs); + ASSERT_EQ(0U, rid.constraints.maxBr); + ASSERT_EQ(0U, rid.constraints.maxPps); + ASSERT_EQ(0U, rid.dependIds.size()); + } + + // This is not technically permitted by the BNF, but the parse code is simpler + // if we allow it. If we decide to stop allowing this, this will need to be + // converted to an invalid parse test-case. + { + SdpRidAttributeList::Rid rid(ParseRid("foo send max-br=30000;pt=96")); + ASSERT_EQ(1U, rid.formats.size()); + ASSERT_EQ(96U, rid.formats[0]); + ASSERT_EQ(0U, rid.constraints.maxWidth); + ASSERT_EQ(0U, rid.constraints.maxHeight); + ASSERT_FALSE(rid.constraints.maxFps.isSome()); + ASSERT_EQ(0U, rid.constraints.maxFs); + ASSERT_EQ(30000U, rid.constraints.maxBr); + ASSERT_EQ(0U, rid.constraints.maxPps); + ASSERT_EQ(0U, rid.dependIds.size()); + } + + { + SdpRidAttributeList::Rid rid(ParseRid("foo send pt=96,97,98")); + ASSERT_EQ(3U, rid.formats.size()); + ASSERT_EQ(96U, rid.formats[0]); + ASSERT_EQ(97U, rid.formats[1]); + ASSERT_EQ(98U, rid.formats[2]); + ASSERT_EQ(0U, rid.constraints.maxWidth); + ASSERT_EQ(0U, rid.constraints.maxHeight); + ASSERT_FALSE(rid.constraints.maxFps.isSome()); + ASSERT_EQ(0U, rid.constraints.maxFs); + ASSERT_EQ(0U, rid.constraints.maxBr); + ASSERT_EQ(0U, rid.constraints.maxPps); + ASSERT_EQ(0U, rid.dependIds.size()); + } + + { + SdpRidAttributeList::Rid rid(ParseRid("foo send max-width=800")); + ASSERT_EQ(0U, rid.formats.size()); + ASSERT_EQ(800U, rid.constraints.maxWidth); + ASSERT_EQ(0U, rid.constraints.maxHeight); + ASSERT_FALSE(rid.constraints.maxFps.isSome()); + ASSERT_EQ(0U, rid.constraints.maxFs); + ASSERT_EQ(0U, rid.constraints.maxBr); + ASSERT_EQ(0U, rid.constraints.maxPps); + ASSERT_EQ(0U, rid.dependIds.size()); + } + + { + SdpRidAttributeList::Rid rid(ParseRid("foo send max-height=640")); + ASSERT_EQ(0U, rid.formats.size()); + ASSERT_EQ(0U, rid.constraints.maxWidth); + ASSERT_EQ(640U, rid.constraints.maxHeight); + ASSERT_FALSE(rid.constraints.maxFps.isSome()); + ASSERT_EQ(0U, rid.constraints.maxFs); + ASSERT_EQ(0U, rid.constraints.maxBr); + ASSERT_EQ(0U, rid.constraints.maxPps); + ASSERT_EQ(0U, rid.dependIds.size()); + } + + { + SdpRidAttributeList::Rid rid(ParseRid("foo send max-fps=30")); + ASSERT_EQ(0U, rid.formats.size()); + ASSERT_EQ(0U, rid.constraints.maxWidth); + ASSERT_EQ(0U, rid.constraints.maxHeight); + ASSERT_EQ(30.0, *rid.constraints.maxFps); + ASSERT_EQ(0U, rid.constraints.maxFs); + ASSERT_EQ(0U, rid.constraints.maxBr); + ASSERT_EQ(0U, rid.constraints.maxPps); + ASSERT_EQ(0U, rid.dependIds.size()); + } + + { + SdpRidAttributeList::Rid rid(ParseRid("foo send max-fs=3600")); + ASSERT_EQ(0U, rid.formats.size()); + ASSERT_EQ(0U, rid.constraints.maxWidth); + ASSERT_EQ(0U, rid.constraints.maxHeight); + ASSERT_FALSE(rid.constraints.maxFps.isSome()); + ASSERT_EQ(3600U, rid.constraints.maxFs); + ASSERT_EQ(0U, rid.constraints.maxBr); + ASSERT_EQ(0U, rid.constraints.maxPps); + ASSERT_EQ(0U, rid.dependIds.size()); + } + + { + SdpRidAttributeList::Rid rid(ParseRid("foo send max-br=30000")); + ASSERT_EQ(0U, rid.formats.size()); + ASSERT_EQ(0U, rid.constraints.maxWidth); + ASSERT_EQ(0U, rid.constraints.maxHeight); + ASSERT_FALSE(rid.constraints.maxFps.isSome()); + ASSERT_EQ(0U, rid.constraints.maxFs); + ASSERT_EQ(30000U, rid.constraints.maxBr); + ASSERT_EQ(0U, rid.constraints.maxPps); + ASSERT_EQ(0U, rid.dependIds.size()); + } + + { + SdpRidAttributeList::Rid rid(ParseRid("foo send max-pps=9216000")); + ASSERT_EQ(0U, rid.formats.size()); + ASSERT_EQ(0U, rid.constraints.maxWidth); + ASSERT_EQ(0U, rid.constraints.maxHeight); + ASSERT_FALSE(rid.constraints.maxFps.isSome()); + ASSERT_EQ(0U, rid.constraints.maxFs); + ASSERT_EQ(0U, rid.constraints.maxBr); + ASSERT_EQ(9216000U, rid.constraints.maxPps); + ASSERT_EQ(0U, rid.dependIds.size()); + } + + { + SdpRidAttributeList::Rid rid(ParseRid("foo send depend=foo")); + ASSERT_EQ(0U, rid.formats.size()); + ASSERT_EQ(0U, rid.constraints.maxWidth); + ASSERT_EQ(0U, rid.constraints.maxHeight); + ASSERT_FALSE(rid.constraints.maxFps.isSome()); + ASSERT_EQ(0U, rid.constraints.maxFs); + ASSERT_EQ(0U, rid.constraints.maxBr); + ASSERT_EQ(0U, rid.constraints.maxPps); + ASSERT_EQ(1U, rid.dependIds.size()); + ASSERT_EQ("foo", rid.dependIds[0]); + } + + { + SdpRidAttributeList::Rid rid(ParseRid("foo send max-foo=20")); + ASSERT_EQ(0U, rid.formats.size()); + ASSERT_EQ(0U, rid.constraints.maxWidth); + ASSERT_EQ(0U, rid.constraints.maxHeight); + ASSERT_FALSE(rid.constraints.maxFps.isSome()); + ASSERT_EQ(0U, rid.constraints.maxFs); + ASSERT_EQ(0U, rid.constraints.maxBr); + ASSERT_EQ(0U, rid.constraints.maxPps); + ASSERT_EQ(0U, rid.dependIds.size()); + } + + { + SdpRidAttributeList::Rid rid(ParseRid("foo send depend=foo,bar")); + ASSERT_EQ(0U, rid.formats.size()); + ASSERT_EQ(0U, rid.constraints.maxWidth); + ASSERT_EQ(0U, rid.constraints.maxHeight); + ASSERT_FALSE(rid.constraints.maxFps.isSome()); + ASSERT_EQ(0U, rid.constraints.maxFs); + ASSERT_EQ(0U, rid.constraints.maxBr); + ASSERT_EQ(0U, rid.constraints.maxPps); + ASSERT_EQ(2U, rid.dependIds.size()); + ASSERT_EQ("foo", rid.dependIds[0]); + ASSERT_EQ("bar", rid.dependIds[1]); + } + + { + SdpRidAttributeList::Rid rid( + ParseRid("foo send max-width=800;max-height=600")); + ASSERT_EQ(0U, rid.formats.size()); + ASSERT_EQ(800U, rid.constraints.maxWidth); + ASSERT_EQ(600U, rid.constraints.maxHeight); + ASSERT_FALSE(rid.constraints.maxFps.isSome()); + ASSERT_EQ(0U, rid.constraints.maxFs); + ASSERT_EQ(0U, rid.constraints.maxBr); + ASSERT_EQ(0U, rid.constraints.maxPps); + ASSERT_EQ(0U, rid.dependIds.size()); + } + + { + SdpRidAttributeList::Rid rid( + ParseRid("foo send pt=96,97;max-width=800;max-height=600")); + ASSERT_EQ(2U, rid.formats.size()); + ASSERT_EQ(96U, rid.formats[0]); + ASSERT_EQ(97U, rid.formats[1]); + ASSERT_EQ(800U, rid.constraints.maxWidth); + ASSERT_EQ(600U, rid.constraints.maxHeight); + ASSERT_FALSE(rid.constraints.maxFps.isSome()); + ASSERT_EQ(0U, rid.constraints.maxFs); + ASSERT_EQ(0U, rid.constraints.maxBr); + ASSERT_EQ(0U, rid.constraints.maxPps); + ASSERT_EQ(0U, rid.dependIds.size()); + } + + { + SdpRidAttributeList::Rid rid( + ParseRid("foo send depend=foo,bar;max-width=800;max-height=600")); + ASSERT_EQ(0U, rid.formats.size()); + ASSERT_EQ(800U, rid.constraints.maxWidth); + ASSERT_EQ(600U, rid.constraints.maxHeight); + ASSERT_FALSE(rid.constraints.maxFps.isSome()); + ASSERT_EQ(0U, rid.constraints.maxFs); + ASSERT_EQ(0U, rid.constraints.maxBr); + ASSERT_EQ(0U, rid.constraints.maxPps); + ASSERT_EQ(2U, rid.dependIds.size()); + ASSERT_EQ("foo", rid.dependIds[0]); + ASSERT_EQ("bar", rid.dependIds[1]); + } + + { + SdpRidAttributeList::Rid rid( + ParseRid("foo send max-foo=20;max-width=800;max-height=600")); + ASSERT_EQ(0U, rid.formats.size()); + ASSERT_EQ(800U, rid.constraints.maxWidth); + ASSERT_EQ(600U, rid.constraints.maxHeight); + ASSERT_FALSE(rid.constraints.maxFps.isSome()); + ASSERT_EQ(0U, rid.constraints.maxFs); + ASSERT_EQ(0U, rid.constraints.maxBr); + ASSERT_EQ(0U, rid.constraints.maxPps); + ASSERT_EQ(0U, rid.dependIds.size()); + } +} + +TEST(NewSdpTestNoFixture, CheckRidInvalidParse) +{ + ParseInvalid<SdpRidAttributeList::Rid>("", 0); + ParseInvalid<SdpRidAttributeList::Rid>(" ", 0); + ParseInvalid<SdpRidAttributeList::Rid>("foo", 3); + ParseInvalid<SdpRidAttributeList::Rid>("foo ", 4); + ParseInvalid<SdpRidAttributeList::Rid>("foo ", 5); + ParseInvalid<SdpRidAttributeList::Rid>("foo bar", 7); + ParseInvalid<SdpRidAttributeList::Rid>("foo recv ", 9); + ParseInvalid<SdpRidAttributeList::Rid>("foo recv pt=", 12); + ParseInvalid<SdpRidAttributeList::Rid>(" ", 0); + ParseInvalid<SdpRidAttributeList::Rid>("foo send pt", 11); + ParseInvalid<SdpRidAttributeList::Rid>("foo send pt=", 12); + ParseInvalid<SdpRidAttributeList::Rid>("foo send pt=x", 12); + ParseInvalid<SdpRidAttributeList::Rid>("foo send pt=-1", 12); + ParseInvalid<SdpRidAttributeList::Rid>("foo send pt=96,", 15); + ParseInvalid<SdpRidAttributeList::Rid>("foo send pt=196", 15); + ParseInvalid<SdpRidAttributeList::Rid>("foo send max-width", 18); + ParseInvalid<SdpRidAttributeList::Rid>("foo send max-width=", 19); + ParseInvalid<SdpRidAttributeList::Rid>("foo send max-width=x", 19); + ParseInvalid<SdpRidAttributeList::Rid>("foo send max-width=-1", 19); + ParseInvalid<SdpRidAttributeList::Rid>("foo send max-width=800;", 23); + ParseInvalid<SdpRidAttributeList::Rid>("foo send max-width=800; ", 24); + ParseInvalid<SdpRidAttributeList::Rid>("foo send depend=", 16); + ParseInvalid<SdpRidAttributeList::Rid>("foo send depend=,", 16); + ParseInvalid<SdpRidAttributeList::Rid>("foo send depend=1,", 18); + ParseInvalid<SdpRidAttributeList::Rid>("0123456789az-_", 14); +} + +TEST(NewSdpTestNoFixture, CheckRidSerialize) +{ + { + SdpRidAttributeList::Rid rid; + rid.id = "foo"; + rid.direction = sdp::kSend; + std::ostringstream os; + rid.Serialize(os); + ASSERT_EQ("foo send", os.str()); + } + + { + SdpRidAttributeList::Rid rid; + rid.id = "foo"; + rid.direction = sdp::kSend; + std::ostringstream os; + rid.Serialize(os); + ASSERT_EQ("foo send", os.str()); + } + + { + SdpRidAttributeList::Rid rid; + rid.id = "foo"; + rid.direction = sdp::kSend; + rid.formats.push_back(96); + std::ostringstream os; + rid.Serialize(os); + ASSERT_EQ("foo send pt=96", os.str()); + } + + { + SdpRidAttributeList::Rid rid; + rid.id = "foo"; + rid.direction = sdp::kSend; + rid.formats.push_back(96); + rid.formats.push_back(97); + std::ostringstream os; + rid.Serialize(os); + ASSERT_EQ("foo send pt=96,97", os.str()); + } + + { + SdpRidAttributeList::Rid rid; + rid.id = "foo"; + rid.direction = sdp::kSend; + rid.constraints.maxWidth = 800; + std::ostringstream os; + rid.Serialize(os); + ASSERT_EQ("foo send max-width=800", os.str()); + } + + { + SdpRidAttributeList::Rid rid; + rid.id = "foo"; + rid.direction = sdp::kSend; + rid.constraints.maxHeight = 600; + std::ostringstream os; + rid.Serialize(os); + ASSERT_EQ("foo send max-height=600", os.str()); + } + + { + SdpRidAttributeList::Rid rid; + rid.id = "foo"; + rid.direction = sdp::kSend; + rid.constraints.maxFps = Some(30); + std::ostringstream os; + rid.Serialize(os); + ASSERT_EQ("foo send max-fps=30", os.str()); + } + + { + SdpRidAttributeList::Rid rid; + rid.id = "foo"; + rid.direction = sdp::kSend; + rid.constraints.maxFs = 3600; + std::ostringstream os; + rid.Serialize(os); + ASSERT_EQ("foo send max-fs=3600", os.str()); + } + + { + SdpRidAttributeList::Rid rid; + rid.id = "foo"; + rid.direction = sdp::kSend; + rid.constraints.maxBr = 30000; + std::ostringstream os; + rid.Serialize(os); + ASSERT_EQ("foo send max-br=30000", os.str()); + } + + { + SdpRidAttributeList::Rid rid; + rid.id = "foo"; + rid.direction = sdp::kSend; + rid.constraints.maxPps = 9216000; + std::ostringstream os; + rid.Serialize(os); + ASSERT_EQ("foo send max-pps=9216000", os.str()); + } + + { + SdpRidAttributeList::Rid rid; + rid.id = "foo"; + rid.direction = sdp::kSend; + rid.dependIds.push_back("foo"); + std::ostringstream os; + rid.Serialize(os); + ASSERT_EQ("foo send depend=foo", os.str()); + } + + { + SdpRidAttributeList::Rid rid; + rid.id = "foo"; + rid.direction = sdp::kSend; + rid.dependIds.push_back("foo"); + rid.dependIds.push_back("bar"); + std::ostringstream os; + rid.Serialize(os); + ASSERT_EQ("foo send depend=foo,bar", os.str()); + } + + { + SdpRidAttributeList::Rid rid; + rid.id = "foo"; + rid.direction = sdp::kSend; + rid.formats.push_back(96); + rid.constraints.maxBr = 30000; + std::ostringstream os; + rid.Serialize(os); + ASSERT_EQ("foo send pt=96;max-br=30000", os.str()); + } +} + +TEST_F(SdpTest, hugeSdp) { + std::string offer = + "v=0\r\n" + "o=- 1109973417102828257 2 IN IP4 127.0.0.1\r\n" + "s=-\r\n" + "t=0 0\r\n" + "a=group:BUNDLE audio video\r\n" + "a=msid-semantic: WMS 1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIP\r\n" + "m=audio 32952 UDP/TLS/RTP/SAVPF 111 103 104 0 8 107 106 105 13 126\r\n" + "c=IN IP4 128.64.32.16\r\n" + "a=rtcp:32952 IN IP4 128.64.32.16\r\n" + "a=candidate:77142221 1 udp 2113937151 192.168.137.1 54081 typ host " + "generation 0\r\n" + "a=candidate:77142221 2 udp 2113937151 192.168.137.1 54081 typ host " + "generation 0\r\n" + "a=candidate:983072742 1 udp 2113937151 172.22.0.56 54082 typ host " + "generation 0\r\n" + "a=candidate:983072742 2 udp 2113937151 172.22.0.56 54082 typ host " + "generation 0\r\n" + "a=candidate:2245074553 1 udp 1845501695 32.64.128.1 62397 typ srflx " + "raddr 192.168.137.1 rport 54081 generation 0\r\n" + "a=candidate:2245074553 2 udp 1845501695 32.64.128.1 62397 typ srflx " + "raddr 192.168.137.1 rport 54081 generation 0\r\n" + "a=candidate:2479353907 1 udp 1845501695 32.64.128.1 54082 typ srflx " + "raddr 172.22.0.56 rport 54082 generation 0\r\n" + "a=candidate:2479353907 2 udp 1845501695 32.64.128.1 54082 typ srflx " + "raddr 172.22.0.56 rport 54082 generation 0\r\n" + "a=candidate:1243276349 1 tcp 1509957375 192.168.137.1 0 typ host " + "generation 0\r\n" + "a=candidate:1243276349 2 tcp 1509957375 192.168.137.1 0 typ host " + "generation 0\r\n" + "a=candidate:1947960086 1 tcp 1509957375 172.22.0.56 0 typ host " + "generation 0\r\n" + "a=candidate:1947960086 2 tcp 1509957375 172.22.0.56 0 typ host " + "generation 0\r\n" + "a=candidate:1808221584 1 udp 33562367 128.64.32.16 32952 typ relay " + "raddr 32.64.128.1 rport 62398 generation 0\r\n" + "a=candidate:1808221584 2 udp 33562367 128.64.32.16 32952 typ relay " + "raddr 32.64.128.1 rport 62398 generation 0\r\n" + "a=candidate:507872740 1 udp 33562367 128.64.32.16 40975 typ relay raddr " + "32.64.128.1 rport 54085 generation 0\r\n" + "a=candidate:507872740 2 udp 33562367 128.64.32.16 40975 typ relay raddr " + "32.64.128.1 rport 54085 generation 0\r\n" + "a=ice-ufrag:xQuJwjX3V3eMA81k\r\n" + "a=ice-pwd:ZUiRmjS2GDhG140p73dAsSVP\r\n" + "a=ice-options:google-ice\r\n" + "a=fingerprint:sha-256 " + "59:4A:8B:73:A7:73:53:71:88:D7:4D:58:28:0C:79:72:31:29:9B:05:37:DD:58:43:" + "C2:D4:85:A2:B3:66:38:7A\r\n" + "a=setup:active\r\n" + "a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n" + "a=sendrecv\r\n" + "a=mid:audio\r\n" + "a=rtcp-mux\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_80 " + "inline:/U44g3ULdtapeiSg+T3n6dDLBKIjpOhb/NXAL/2b\r\n" + "a=rtpmap:111 opus/48000/2\r\n" + "a=fmtp:111 minptime=10\r\n" + "a=rtpmap:103 ISAC/16000\r\n" + "a=rtpmap:104 ISAC/32000\r\n" + "a=rtpmap:0 PCMU/8000\r\n" + "a=rtpmap:8 PCMA/8000\r\n" + "a=rtpmap:107 CN/48000\r\n" + "a=rtpmap:106 CN/32000\r\n" + "a=rtpmap:105 CN/16000\r\n" + "a=rtpmap:13 CN/8000\r\n" + "a=rtpmap:126 telephone-event/8000\r\n" + "a=maxptime:60\r\n" + "a=ssrc:2271517329 cname:mKDNt7SQf6pwDlIn\r\n" + "a=ssrc:2271517329 msid:1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIP " + "1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIPa0\r\n" + "a=ssrc:2271517329 mslabel:1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIP\r\n" + "a=ssrc:2271517329 label:1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIPa0\r\n" + "m=video 32952 UDP/TLS/RTP/SAVPF 100 116 117\r\n" + "c=IN IP4 128.64.32.16\r\n" + "a=rtcp:32952 IN IP4 128.64.32.16\r\n" + "a=candidate:77142221 1 udp 2113937151 192.168.137.1 54081 typ host " + "generation 0\r\n" + "a=candidate:77142221 2 udp 2113937151 192.168.137.1 54081 typ host " + "generation 0\r\n" + "a=candidate:983072742 1 udp 2113937151 172.22.0.56 54082 typ host " + "generation 0\r\n" + "a=candidate:983072742 2 udp 2113937151 172.22.0.56 54082 typ host " + "generation 0\r\n" + "a=candidate:2245074553 1 udp 1845501695 32.64.128.1 62397 typ srflx " + "raddr 192.168.137.1 rport 54081 generation 0\r\n" + "a=candidate:2245074553 2 udp 1845501695 32.64.128.1 62397 typ srflx " + "raddr 192.168.137.1 rport 54081 generation 0\r\n" + "a=candidate:2479353907 1 udp 1845501695 32.64.128.1 54082 typ srflx " + "raddr 172.22.0.56 rport 54082 generation 0\r\n" + "a=candidate:2479353907 2 udp 1845501695 32.64.128.1 54082 typ srflx " + "raddr 172.22.0.56 rport 54082 generation 0\r\n" + "a=candidate:1243276349 1 tcp 1509957375 192.168.137.1 0 typ host " + "generation 0\r\n" + "a=candidate:1243276349 2 tcp 1509957375 192.168.137.1 0 typ host " + "generation 0\r\n" + "a=candidate:1947960086 1 tcp 1509957375 172.22.0.56 0 typ host " + "generation 0\r\n" + "a=candidate:1947960086 2 tcp 1509957375 172.22.0.56 0 typ host " + "generation 0\r\n" + "a=candidate:1808221584 1 udp 33562367 128.64.32.16 32952 typ relay " + "raddr 32.64.128.1 rport 62398 generation 0\r\n" + "a=candidate:1808221584 2 udp 33562367 128.64.32.16 32952 typ relay " + "raddr 32.64.128.1 rport 62398 generation 0\r\n" + "a=candidate:507872740 1 udp 33562367 128.64.32.16 40975 typ relay raddr " + "32.64.128.1 rport 54085 generation 0\r\n" + "a=candidate:507872740 2 udp 33562367 128.64.32.16 40975 typ relay raddr " + "32.64.128.1 rport 54085 generation 0\r\n" + "a=ice-ufrag:xQuJwjX3V3eMA81k\r\n" + "a=ice-pwd:ZUiRmjS2GDhG140p73dAsSVP\r\n" + "a=ice-options:google-ice\r\n" + "a=fingerprint:sha-256 " + "59:4A:8B:73:A7:73:53:71:88:D7:4D:58:28:0C:79:72:31:29:9B:05:37:DD:58:43:" + "C2:D4:85:A2:B3:66:38:7A\r\n" + "a=setup:active\r\n" + "a=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\n" + "a=extmap:3 " + "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n" + "a=extmap:6 " + "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\n" + "a=sendrecv\r\n" + "a=mid:video\r\n" + "a=rtcp-mux\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_80 " + "inline:/U44g3ULdtapeiSg+T3n6dDLBKIjpOhb/NXAL/2b\r\n" + "a=rtpmap:100 VP8/90000\r\n" + "a=rtcp-fb:100 ccm fir\r\n" + "a=rtcp-fb:100 nack\r\n" + "a=rtcp-fb:100 goog-remb\r\n" + "a=rtpmap:116 red/90000\r\n" + "a=rtpmap:117 ulpfec/90000\r\n" + "a=ssrc:54724160 cname:mKDNt7SQf6pwDlIn\r\n" + "a=ssrc:54724160 msid:1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIP " + "1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIPv0\r\n" + "a=ssrc:54724160 mslabel:1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIP\r\n" + "a=ssrc:54724160 label:1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIPv0\r\n"; + + ParseSdp(offer); +} + +TEST_P(NewSdpTest, CheckSsrcGroup) { + ParseSdp(kVideoSdp + "a=ssrc-group:\r\n", false); + ParseSdp(kVideoSdp + "a=ssrc-group:BLAH\r\n", false); + ParseSdp(kVideoSdp + "a=ssrc-group:FID\r\n", false); + ParseSdp(kVideoSdp + "a=ssrc-group:FID 1 2 3 four\r\n", false); + ParseSdp(kVideoSdp + "a=ssrc-group:FID 1\r\n"); + + std::array<char, 8192> gigantic = {{0}}; + gigantic.fill('A'); + gigantic[gigantic.size() - 1] = 0; + ParseSdp(kVideoSdp + "a=ssrc-group:" + gigantic.data() + "\r\n", false); + + ParseSdp(kVideoSdp + "a=ssrc-group:fid 1\r\n"); + ParseSdp(kVideoSdp + "a=ssrc-group:FID \r\n", false); + ParseSdp(kVideoSdp + "a=ssrc-group:FID 0\r\n"); + ParseSdp(kVideoSdp + "a=ssrc-group:FID 9999999999\r\n", false); + ParseSdp(kVideoSdp + "a=ssrc-group:FID 99999999999999999999\r\n", false); + ParseSdp(kVideoSdp + "a=ssrc-group:FID 1twothree\r\n", false); + ParseSdp(kVideoSdp + "a=ssrc-group:FID -1\r\n", false); + + ParseSdp(kVideoSdp + "a=ssrc-group:FID 3156517279 2673335628\r\n"); + ASSERT_EQ(1U, Sdp()->GetMediaSectionCount()); + + const SdpSsrcGroupAttributeList& group = + Sdp()->GetMediaSection(0).GetAttributeList().GetSsrcGroup(); + + ASSERT_EQ(1U, group.mSsrcGroups.size()); + ASSERT_EQ(SdpSsrcGroupAttributeList::Semantics::kFid, + group.mSsrcGroups[0].semantics); + ASSERT_EQ(2U, group.mSsrcGroups[0].ssrcs.size()); + ASSERT_EQ(3156517279U, group.mSsrcGroups[0].ssrcs[0]); + ASSERT_EQ(2673335628U, group.mSsrcGroups[0].ssrcs[1]); +} + +TEST_P(NewSdpTest, CheckSsrcGroupSerialization) { + std::vector ssrcs = {3156517279U, 2673335628U}; + { + SdpSsrcGroupAttributeList list; + list.PushEntry(SdpSsrcGroupAttributeList::Semantics::kFec, ssrcs); + CheckSerialize("a=ssrc-group:FEC 3156517279 2673335628\r\n", list); + } + { + SdpSsrcGroupAttributeList list; + list.PushEntry(SdpSsrcGroupAttributeList::Semantics::kFid, ssrcs); + CheckSerialize("a=ssrc-group:FID 3156517279 2673335628\r\n", list); + } + { + SdpSsrcGroupAttributeList list; + list.PushEntry(SdpSsrcGroupAttributeList::Semantics::kFecFr, ssrcs); + CheckSerialize("a=ssrc-group:FEC-FR 3156517279 2673335628\r\n", list); + } + { + SdpSsrcGroupAttributeList list; + list.PushEntry(SdpSsrcGroupAttributeList::Semantics::kDup, ssrcs); + CheckSerialize("a=ssrc-group:DUP 3156517279 2673335628\r\n", list); + } + { + SdpSsrcGroupAttributeList list; + list.PushEntry(SdpSsrcGroupAttributeList::Semantics::kSim, ssrcs); + CheckSerialize("a=ssrc-group:SIM 3156517279 2673335628\r\n", list); + } +} + +TEST_P(NewSdpTest, CheckRtxApt) { + ParseSdp(kVideoSdp + "a=rtpmap:96 rtx/90000\r\na=fmtp:96", false); + ParseSdp(kVideoSdp + + "a=rtpmap:96 rtx/90000\r\na=fmtp:96 apt=120; apt=124\r\n"); + ParseSdp(kVideoSdp + "a=rtpmap:96 rtx/90000\r\na=fmtp:96 apt=", false); + ParseSdp(kVideoSdp + "a=rtpmap:96 rtx/90000\r\na=fmtp:96 apt=blah", false); + ParseSdp(kVideoSdp + + "a=rtpmap:96 rtx/90000\r\na=fmtp:96 " + "apt=999999999999999999999999999999", + false); + ParseSdp(kVideoSdp + "a=rtpmap:96 rtx/90000\r\na=fmtp:96 apt=-120\r\n", + false); + ParseSdp(kVideoSdp + "a=rtpmap:96 rtx/90000\r\na=fmtp:96 apt=1twenty\r\n", + false); + ParseSdp(kVideoSdp + "a=rtpmap:96 rtx/90000\r\na=fmtp:96 APT=120\r\n"); + + ParseSdp(kVideoSdp + "a=rtpmap:96 rtx/90000\r\na=fmtp:96 apt=120\r\n"); + + const SdpMediaSection& msec = Sdp()->GetMediaSection(0); + ASSERT_EQ(1U, msec.GetFormats().size()); + ASSERT_EQ(2U, msec.GetAttributeList().GetRtpmap().mRtpmaps.size()); + const SdpRtpmapAttributeList::Rtpmap& rtpmap = + msec.GetAttributeList().GetRtpmap().GetEntry("96"); + ASSERT_EQ(rtpmap.pt, "96"); + ASSERT_EQ(rtpmap.codec, SdpRtpmapAttributeList::CodecType::kRtx); + ASSERT_EQ(rtpmap.name, "rtx"); + ASSERT_EQ(rtpmap.clock, 90000U); + + ASSERT_TRUE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kFmtpAttribute)); + const auto& format_params = msec.GetAttributeList().GetFmtp().mFmtps; + ASSERT_EQ(1U, format_params.size()); + ASSERT_EQ("96", format_params[0].format); + ASSERT_EQ(SdpRtpmapAttributeList::CodecType::kRtx, + format_params[0].parameters->codec_type); + ASSERT_EQ(static_cast<uint8_t>(120), + static_cast<SdpFmtpAttributeList::RtxParameters*>( + format_params[0].parameters.get()) + ->apt); + ASSERT_EQ(Nothing(), static_cast<SdpFmtpAttributeList::RtxParameters*>( + format_params[0].parameters.get()) + ->rtx_time); +} + +TEST_P(NewSdpTest, CheckRtxAptRtxTime) { + ParseSdp(kVideoSdp + "a=rtpmap:96 rtx/90000\r\na=fmtp:96 apt=120;rtx-time=", + false); + ParseSdp( + kVideoSdp + "a=rtpmap:96 rtx/90000\r\na=fmtp:96 apt=120;rtx-time=blah", + false); + ParseSdp(kVideoSdp + + "a=rtpmap:96 rtx/90000\r\na=fmtp:96 " + "apt=120;rtx-time=9999999999999999999999999999999", + false); + ParseSdp( + kVideoSdp + "a=rtpmap:96 rtx/90000\r\na=fmtp:96 apt=120;rtx-time=-3000", + false); + ParseSdp(kVideoSdp + + "a=rtpmap:96 rtx/90000\r\na=fmtp:96 apt=120;rtx-time=3thousand", + false); + + { + ParseSdp(kVideoSdp + + "a=rtpmap:96 rtx/90000\r\na=fmtp:96 apt=120;RTX-TIME=3000\r\n"); + + const SdpMediaSection& msec = Sdp()->GetMediaSection(0); + ASSERT_EQ(1U, msec.GetFormats().size()); + ASSERT_EQ(2U, msec.GetAttributeList().GetRtpmap().mRtpmaps.size()); + const SdpRtpmapAttributeList::Rtpmap& rtpmap = + msec.GetAttributeList().GetRtpmap().GetEntry("96"); + ASSERT_EQ(rtpmap.pt, "96"); + ASSERT_EQ(rtpmap.codec, SdpRtpmapAttributeList::CodecType::kRtx); + ASSERT_EQ(rtpmap.name, "rtx"); + ASSERT_EQ(rtpmap.clock, 90000U); + + ASSERT_TRUE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kFmtpAttribute)); + const auto& format_params = msec.GetAttributeList().GetFmtp().mFmtps; + ASSERT_EQ(1U, format_params.size()); + ASSERT_EQ("96", format_params[0].format); + ASSERT_EQ(SdpRtpmapAttributeList::CodecType::kRtx, + format_params[0].parameters->codec_type); + ASSERT_EQ(static_cast<uint8_t>(120), + static_cast<SdpFmtpAttributeList::RtxParameters*>( + format_params[0].parameters.get()) + ->apt); + ASSERT_EQ(Some(3000U), static_cast<SdpFmtpAttributeList::RtxParameters*>( + format_params[0].parameters.get()) + ->rtx_time); + } + + { + ParseSdp(kVideoSdp + + "a=rtpmap:96 rtx/90000\r\na=fmtp:96 " + "apt=120;rtx-time=3000;rtx-time=3300\r\n"); + + const SdpMediaSection& msec = Sdp()->GetMediaSection(0); + ASSERT_EQ(1U, msec.GetFormats().size()); + ASSERT_EQ(2U, msec.GetAttributeList().GetRtpmap().mRtpmaps.size()); + const SdpRtpmapAttributeList::Rtpmap& rtpmap = + msec.GetAttributeList().GetRtpmap().GetEntry("96"); + ASSERT_EQ(rtpmap.pt, "96"); + ASSERT_EQ(rtpmap.codec, SdpRtpmapAttributeList::CodecType::kRtx); + ASSERT_EQ(rtpmap.name, "rtx"); + ASSERT_EQ(rtpmap.clock, 90000U); + + ASSERT_TRUE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kFmtpAttribute)); + const auto& format_params = msec.GetAttributeList().GetFmtp().mFmtps; + ASSERT_EQ(1U, format_params.size()); + ASSERT_EQ("96", format_params[0].format); + ASSERT_EQ(SdpRtpmapAttributeList::CodecType::kRtx, + format_params[0].parameters->codec_type); + ASSERT_EQ(static_cast<uint8_t>(120), + static_cast<SdpFmtpAttributeList::RtxParameters*>( + format_params[0].parameters.get()) + ->apt); + + // Last rtx-time wins. This parameter is unused by our implementation + // anyway. + ASSERT_EQ(Some(3300U), static_cast<SdpFmtpAttributeList::RtxParameters*>( + format_params[0].parameters.get()) + ->rtx_time); + } + + { + ParseSdp(kVideoSdp + + "a=rtpmap:96 rtx/90000\r\na=fmtp:96 apt=120;rtx-time=3000\r\n"); + + const SdpMediaSection& msec = Sdp()->GetMediaSection(0); + ASSERT_EQ(1U, msec.GetFormats().size()); + ASSERT_EQ(2U, msec.GetAttributeList().GetRtpmap().mRtpmaps.size()); + const SdpRtpmapAttributeList::Rtpmap& rtpmap = + msec.GetAttributeList().GetRtpmap().GetEntry("96"); + ASSERT_EQ(rtpmap.pt, "96"); + ASSERT_EQ(rtpmap.codec, SdpRtpmapAttributeList::CodecType::kRtx); + ASSERT_EQ(rtpmap.name, "rtx"); + ASSERT_EQ(rtpmap.clock, 90000U); + + ASSERT_TRUE(Sdp()->GetMediaSection(0).GetAttributeList().HasAttribute( + SdpAttribute::kFmtpAttribute)); + const auto& format_params = msec.GetAttributeList().GetFmtp().mFmtps; + ASSERT_EQ(1U, format_params.size()); + ASSERT_EQ("96", format_params[0].format); + ASSERT_EQ(SdpRtpmapAttributeList::CodecType::kRtx, + format_params[0].parameters->codec_type); + ASSERT_EQ(static_cast<uint8_t>(120), + static_cast<SdpFmtpAttributeList::RtxParameters*>( + format_params[0].parameters.get()) + ->apt); + ASSERT_EQ(Some(3000U), static_cast<SdpFmtpAttributeList::RtxParameters*>( + format_params[0].parameters.get()) + ->rtx_time); + } +} + +} // End namespace test. diff --git a/media/webrtc/signaling/gtest/videoconduit_unittests.cpp b/media/webrtc/signaling/gtest/videoconduit_unittests.cpp new file mode 100644 index 0000000000..040d8d9ee0 --- /dev/null +++ b/media/webrtc/signaling/gtest/videoconduit_unittests.cpp @@ -0,0 +1,2336 @@ + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#define GTEST_HAS_RTTI 0 +#include "gtest/gtest.h" + +#include "nspr.h" +#include "nss.h" +#include "ssl.h" + +#include "Canonicals.h" +#include "VideoConduit.h" +#include "RtpRtcpConfig.h" +#include "WebrtcCallWrapper.h" +#include "WebrtcGmpVideoCodec.h" + +#include "api/video/video_sink_interface.h" +#include "media/base/media_constants.h" +#include "media/base/video_adapter.h" + +#include "MockCall.h" +#include "MockConduit.h" + +using namespace mozilla; +using namespace testing; +using namespace webrtc; + +namespace test { + +class MockVideoSink : public rtc::VideoSinkInterface<webrtc::VideoFrame> { + public: + MockVideoSink() : mVideoFrame(nullptr, kVideoRotation_0, 0) {} + + ~MockVideoSink() override = default; + + void OnFrame(const webrtc::VideoFrame& frame) override { + mVideoFrame = frame; + ++mOnFrameCount; + } + + size_t mOnFrameCount = 0; + webrtc::VideoFrame mVideoFrame; +}; + +class VideoConduitTest : public Test { + public: + VideoConduitTest( + VideoSessionConduit::Options aOptions = VideoSessionConduit::Options()) + : mCallWrapper(MockCallWrapper::Create()), + mVideoConduit(MakeRefPtr<WebrtcVideoConduit>( + mCallWrapper, GetCurrentSerialEventTarget(), std::move(aOptions), + "", TrackingId(TrackingId::Source::Unimplemented, 0))), + mControl(GetCurrentSerialEventTarget()) { + NSS_NoDB_Init(nullptr); + + EXPECT_EQ(mVideoConduit->Init(), kMediaConduitNoError); + mVideoConduit->InitControl(&mControl); + mControl.Update([](auto& aControl) { + aControl.mLocalSsrcs = {42}; + aControl.mLocalVideoRtxSsrcs = {43}; + }); + } + + ~VideoConduitTest() override { + mozilla::Unused << WaitFor(mVideoConduit->Shutdown()); + mCallWrapper->Destroy(); + } + + MockCall* Call() { return mCallWrapper->GetMockCall(); } + + MediaConduitErrorCode SendVideoFrame(unsigned short width, + unsigned short height, + uint64_t capture_time_ms) { + rtc::scoped_refptr<webrtc::I420Buffer> buffer = + webrtc::I420Buffer::Create(width, height); + memset(buffer->MutableDataY(), 0x10, buffer->StrideY() * buffer->height()); + memset(buffer->MutableDataU(), 0x80, + buffer->StrideU() * ((buffer->height() + 1) / 2)); + memset(buffer->MutableDataV(), 0x80, + buffer->StrideV() * ((buffer->height() + 1) / 2)); + + webrtc::VideoFrame frame(buffer, capture_time_ms, capture_time_ms, + webrtc::kVideoRotation_0); + return mVideoConduit->SendVideoFrame(frame); + } + + const RefPtr<MockCallWrapper> mCallWrapper; + const RefPtr<mozilla::WebrtcVideoConduit> mVideoConduit; + ConcreteControl mControl; +}; + +TEST_F(VideoConduitTest, TestConfigureReceiveMediaCodecs) { + // No codecs + mControl.Update([&](auto& aControl) { + aControl.mReceiving = true; + aControl.mVideoRecvCodecs = {}; + }); + ASSERT_TRUE(Call()->mVideoReceiveConfig); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders.size(), 0U); + + // empty codec name + mControl.Update([&](auto& aControl) { + VideoCodecConfig codec(120, "", EncodingConstraints()); + aControl.mVideoRecvCodecs = {codec}; + }); + ASSERT_TRUE(Call()->mVideoReceiveConfig); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders.size(), 0U); + + // Defaults + mControl.Update([&](auto& aControl) { + VideoCodecConfig codec(120, "VP8", EncodingConstraints()); + aControl.mVideoRecvCodecs = {codec}; + aControl.mVideoRecvRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_TRUE(Call()->mVideoReceiveConfig); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders.size(), 1U); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders[0].payload_type, 120); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders[0].video_format.name, "VP8"); + ASSERT_NE(Call()->mVideoReceiveConfig->rtp.local_ssrc, 0U); + ASSERT_NE(Call()->mVideoReceiveConfig->rtp.remote_ssrc, 0U); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.rtcp_mode, + webrtc::RtcpMode::kCompound); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.nack.rtp_history_ms, 0); + ASSERT_FALSE(Call()->mVideoReceiveConfig->rtp.remb); + ASSERT_FALSE(Call()->mVideoReceiveConfig->rtp.tmmbr); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.keyframe_method, + webrtc::KeyFrameReqMethod::kNone); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.ulpfec_payload_type, -1); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.red_payload_type, -1); + ASSERT_EQ( + Call()->mVideoReceiveConfig->rtp.rtx_associated_payload_types.size(), 0U); +} + +TEST_F(VideoConduitTest, TestConfigureReceiveMediaCodecsFEC) { + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mFECFbSet = true; + aControl.mVideoRecvCodecs = { + codecConfig, VideoCodecConfig(1, "ulpfec", EncodingConstraints()), + VideoCodecConfig(2, "red", EncodingConstraints())}; + aControl.mVideoRecvRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + aControl.mReceiving = true; + }); + ASSERT_TRUE(Call()->mVideoReceiveConfig); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders.size(), 1U); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders[0].payload_type, 120); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders[0].video_format.name, "VP8"); + ASSERT_NE(Call()->mVideoReceiveConfig->rtp.local_ssrc, 0U); + ASSERT_NE(Call()->mVideoReceiveConfig->rtp.remote_ssrc, 0U); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.rtcp_mode, + webrtc::RtcpMode::kCompound); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.nack.rtp_history_ms, 0); + ASSERT_FALSE(Call()->mVideoReceiveConfig->rtp.remb); + ASSERT_FALSE(Call()->mVideoReceiveConfig->rtp.tmmbr); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.keyframe_method, + webrtc::KeyFrameReqMethod::kNone); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.ulpfec_payload_type, 1); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.red_payload_type, 2); + ASSERT_EQ( + Call()->mVideoReceiveConfig->rtp.rtx_associated_payload_types.size(), 0U); +} + +TEST_F(VideoConduitTest, TestConfigureReceiveMediaCodecsH264) { + mControl.Update([&](auto& aControl) { + // Insert twice to test that only one H264 codec is used at a time + aControl.mReceiving = true; + aControl.mVideoRecvCodecs = { + VideoCodecConfig(120, "H264", EncodingConstraints()), + VideoCodecConfig(120, "H264", EncodingConstraints())}; + aControl.mVideoRecvRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_TRUE(Call()->mVideoReceiveConfig); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders.size(), 1U); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders[0].payload_type, 120); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders[0].video_format.name, "H264"); + ASSERT_NE(Call()->mVideoReceiveConfig->rtp.local_ssrc, 0U); + ASSERT_NE(Call()->mVideoReceiveConfig->rtp.remote_ssrc, 0U); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.rtcp_mode, + webrtc::RtcpMode::kCompound); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.nack.rtp_history_ms, 0); + ASSERT_FALSE(Call()->mVideoReceiveConfig->rtp.remb); + ASSERT_FALSE(Call()->mVideoReceiveConfig->rtp.tmmbr); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.keyframe_method, + webrtc::KeyFrameReqMethod::kNone); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.ulpfec_payload_type, -1); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.red_payload_type, -1); + ASSERT_EQ( + Call()->mVideoReceiveConfig->rtp.rtx_associated_payload_types.size(), 0U); +} + +TEST_F(VideoConduitTest, TestConfigureReceiveMediaCodecsKeyframeRequestType) { + // PLI should be preferred to FIR, same codec. + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mNackFbTypes.push_back("pli"); + codecConfig.mCcmFbTypes.push_back("fir"); + aControl.mReceiving = true; + aControl.mVideoRecvCodecs = {codecConfig}; + aControl.mVideoRecvRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_TRUE(Call()->mVideoReceiveConfig); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders.size(), 1U); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.keyframe_method, + webrtc::KeyFrameReqMethod::kPliRtcp); + + // Just FIR + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mCcmFbTypes.push_back("fir"); + aControl.mVideoRecvCodecs = {codecConfig}; + }); + ASSERT_TRUE(Call()->mVideoReceiveConfig); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders.size(), 1U); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.keyframe_method, + webrtc::KeyFrameReqMethod::kFirRtcp); + + // PLI should be preferred to FIR, multiple codecs. + mControl.Update([&](auto& aControl) { + VideoCodecConfig pliCodec(120, "VP8", EncodingConstraints()); + pliCodec.mNackFbTypes.push_back("pli"); + VideoCodecConfig firCodec(120, "VP8", EncodingConstraints()); + firCodec.mCcmFbTypes.push_back("fir"); + aControl.mVideoRecvCodecs = {pliCodec, firCodec}; + }); + ASSERT_TRUE(Call()->mVideoReceiveConfig); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders.size(), 2U); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.keyframe_method, + webrtc::KeyFrameReqMethod::kPliRtcp); +} + +TEST_F(VideoConduitTest, TestConfigureReceiveMediaCodecsNack) { + mControl.Update([&](auto& aControl) { + aControl.mReceiving = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mNackFbTypes.push_back(""); + aControl.mVideoRecvCodecs = {codecConfig}; + aControl.mVideoRecvRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_TRUE(Call()->mVideoReceiveConfig); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders.size(), 1U); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders[0].payload_type, 120); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders[0].video_format.name, "VP8"); + ASSERT_NE(Call()->mVideoReceiveConfig->rtp.local_ssrc, 0U); + ASSERT_NE(Call()->mVideoReceiveConfig->rtp.remote_ssrc, 0U); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.rtcp_mode, + webrtc::RtcpMode::kCompound); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.nack.rtp_history_ms, 1000); + ASSERT_FALSE(Call()->mVideoReceiveConfig->rtp.remb); + ASSERT_FALSE(Call()->mVideoReceiveConfig->rtp.tmmbr); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.keyframe_method, + webrtc::KeyFrameReqMethod::kNone); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.ulpfec_payload_type, -1); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.red_payload_type, -1); + ASSERT_EQ( + Call()->mVideoReceiveConfig->rtp.rtx_associated_payload_types.size(), 0U); +} + +TEST_F(VideoConduitTest, TestConfigureReceiveMediaCodecsRemb) { + mControl.Update([&](auto& aControl) { + aControl.mReceiving = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mRembFbSet = true; + aControl.mVideoRecvCodecs = {codecConfig}; + aControl.mVideoRecvRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_TRUE(Call()->mVideoReceiveConfig); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders.size(), 1U); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders[0].payload_type, 120); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders[0].video_format.name, "VP8"); + ASSERT_NE(Call()->mVideoReceiveConfig->rtp.local_ssrc, 0U); + ASSERT_NE(Call()->mVideoReceiveConfig->rtp.remote_ssrc, 0U); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.rtcp_mode, + webrtc::RtcpMode::kCompound); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.nack.rtp_history_ms, 0); + ASSERT_TRUE(Call()->mVideoReceiveConfig->rtp.remb); + ASSERT_FALSE(Call()->mVideoReceiveConfig->rtp.tmmbr); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.keyframe_method, + webrtc::KeyFrameReqMethod::kNone); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.ulpfec_payload_type, -1); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.red_payload_type, -1); + ASSERT_EQ( + Call()->mVideoReceiveConfig->rtp.rtx_associated_payload_types.size(), 0U); +} + +TEST_F(VideoConduitTest, TestConfigureReceiveMediaCodecsTmmbr) { + mControl.Update([&](auto& aControl) { + aControl.mReceiving = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mCcmFbTypes.push_back("tmmbr"); + aControl.mVideoRecvCodecs = {codecConfig}; + aControl.mVideoRecvRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_TRUE(Call()->mVideoReceiveConfig); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders.size(), 1U); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders[0].payload_type, 120); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders[0].video_format.name, "VP8"); + ASSERT_NE(Call()->mVideoReceiveConfig->rtp.local_ssrc, 0U); + ASSERT_NE(Call()->mVideoReceiveConfig->rtp.remote_ssrc, 0U); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.rtcp_mode, + webrtc::RtcpMode::kCompound); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.nack.rtp_history_ms, 0); + ASSERT_FALSE(Call()->mVideoReceiveConfig->rtp.remb); + ASSERT_TRUE(Call()->mVideoReceiveConfig->rtp.tmmbr); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.keyframe_method, + webrtc::KeyFrameReqMethod::kNone); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.ulpfec_payload_type, -1); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.red_payload_type, -1); + ASSERT_EQ( + Call()->mVideoReceiveConfig->rtp.rtx_associated_payload_types.size(), 0U); +} + +TEST_F(VideoConduitTest, TestConfigureSendMediaCodec) { + // defaults + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_TRUE(Call()->mVideoSendConfig); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.payload_name, "VP8"); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.payload_type, 120); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.rtcp_mode, + webrtc::RtcpMode::kCompound); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.max_packet_size, kVideoMtu); + ASSERT_EQ(Call()->mVideoSendEncoderConfig->content_type, + VideoEncoderConfig::ContentType::kRealtimeVideo); + ASSERT_EQ(Call()->mVideoSendEncoderConfig->min_transmit_bitrate_bps, 0); + ASSERT_EQ(Call()->mVideoSendEncoderConfig->max_bitrate_bps, KBPS(10000)); + ASSERT_EQ(Call()->mVideoSendEncoderConfig->number_of_streams, 1U); + + // empty codec name + mControl.Update([&](auto& aControl) { + aControl.mVideoSendCodec = + Some(VideoCodecConfig(120, "", EncodingConstraints())); + }); + // Bad codec gets ignored + ASSERT_EQ(Call()->mVideoSendConfig->rtp.payload_name, "VP8"); +} + +TEST_F(VideoConduitTest, TestConfigureSendMediaCodecMaxFps) { + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + EncodingConstraints constraints; + VideoCodecConfig codecConfig(120, "VP8", constraints); + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + std::vector<webrtc::VideoStream> videoStreams; + videoStreams = Call()->CreateEncoderStreams(640, 480); + ASSERT_EQ(videoStreams.size(), 1U); + ASSERT_EQ(videoStreams[0].max_framerate, 30); // DEFAULT_VIDEO_MAX_FRAMERATE + + mControl.Update([&](auto& aControl) { + EncodingConstraints constraints; + constraints.maxFps = Some(42); + VideoCodecConfig codecConfig(120, "VP8", constraints); + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + videoStreams = Call()->CreateEncoderStreams(640, 480); + ASSERT_EQ(videoStreams.size(), 1U); + ASSERT_EQ(videoStreams[0].max_framerate, 42); +} + +TEST_F(VideoConduitTest, TestConfigureSendMediaCodecMaxMbps) { + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + EncodingConstraints constraints; + constraints.maxMbps = 0; + VideoCodecConfig codecConfig(120, "VP8", constraints); + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + SendVideoFrame(640, 480, 1); + std::vector<webrtc::VideoStream> videoStreams; + videoStreams = Call()->CreateEncoderStreams(640, 480); + ASSERT_EQ(videoStreams.size(), 1U); + ASSERT_EQ(videoStreams[0].max_framerate, 30); // DEFAULT_VIDEO_MAX_FRAMERATE + + mControl.Update([&](auto& aControl) { + EncodingConstraints constraints; + constraints.maxMbps = 10000; + VideoCodecConfig codecConfig(120, "VP8", constraints); + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + SendVideoFrame(640, 480, 1); + videoStreams = Call()->CreateEncoderStreams(640, 480); + ASSERT_EQ(videoStreams.size(), 1U); + ASSERT_EQ(videoStreams[0].max_framerate, 8); +} + +TEST_F(VideoConduitTest, TestConfigureSendMediaCodecDefaults) { + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + + { + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(640, 480); + EXPECT_EQ(videoStreams.size(), 1U); + EXPECT_EQ(videoStreams[0].min_bitrate_bps, 150000); + EXPECT_EQ(videoStreams[0].target_bitrate_bps, 500000); + EXPECT_EQ(videoStreams[0].max_bitrate_bps, 2000000); + } + + { + // SelectBitrates not called until we send a frame + SendVideoFrame(1280, 720, 1); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(1280, 720); + EXPECT_EQ(videoStreams.size(), 1U); + EXPECT_EQ(videoStreams[0].min_bitrate_bps, 200000); + EXPECT_EQ(videoStreams[0].target_bitrate_bps, 800000); + EXPECT_EQ(videoStreams[0].max_bitrate_bps, 2500000); + } +} + +TEST_F(VideoConduitTest, TestConfigureSendMediaCodecTias) { + // TIAS + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfigTias(120, "VP8", EncodingConstraints()); + codecConfigTias.mEncodings.emplace_back(); + codecConfigTias.mTias = 1000000; + aControl.mVideoSendCodec = Some(codecConfigTias); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + { + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + SendVideoFrame(1280, 720, 1); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(1280, 720); + ASSERT_EQ(videoStreams.size(), 1U); + ASSERT_EQ(videoStreams[0].min_bitrate_bps, 200000); + ASSERT_EQ(videoStreams[0].target_bitrate_bps, 800000); + ASSERT_EQ(videoStreams[0].max_bitrate_bps, 1000000); + } + + // TIAS (too low) + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfigTiasLow(120, "VP8", EncodingConstraints()); + codecConfigTiasLow.mEncodings.emplace_back(); + codecConfigTiasLow.mTias = 1000; + aControl.mVideoSendCodec = Some(codecConfigTiasLow); + }); + { + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + SendVideoFrame(1280, 720, 1); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(1280, 720); + ASSERT_EQ(videoStreams.size(), 1U); + ASSERT_EQ(videoStreams[0].min_bitrate_bps, 30000); + ASSERT_EQ(videoStreams[0].target_bitrate_bps, 30000); + ASSERT_EQ(videoStreams[0].max_bitrate_bps, 30000); + } +} + +TEST_F(VideoConduitTest, TestConfigureSendMediaCodecMaxBr) { + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + auto& encoding = codecConfig.mEncodings.emplace_back(); + encoding.constraints.maxBr = 50000; + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + SendVideoFrame(1280, 720, 1); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(1280, 720); + ASSERT_EQ(videoStreams.size(), 1U); + ASSERT_LE(videoStreams[0].min_bitrate_bps, 50000); + ASSERT_LE(videoStreams[0].target_bitrate_bps, 50000); + ASSERT_EQ(videoStreams[0].max_bitrate_bps, 50000); +} + +TEST_F(VideoConduitTest, TestConfigureSendMediaCodecScaleResolutionBy) { + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + { + auto& encoding = codecConfig.mEncodings.emplace_back(); + encoding.constraints.scaleDownBy = 2; + } + { + auto& encoding = codecConfig.mEncodings.emplace_back(); + encoding.constraints.scaleDownBy = 4; + } + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + aControl.mLocalSsrcs = {42, 1729}; + aControl.mLocalVideoRtxSsrcs = {43, 1730}; + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + + UniquePtr<MockVideoSink> sink(new MockVideoSink()); + rtc::VideoSinkWants wants; + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + + SendVideoFrame(640, 360, 1); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams.size(), 2U); + ASSERT_EQ(videoStreams[0].width, 160U); + ASSERT_EQ(videoStreams[0].height, 90U); + ASSERT_EQ(videoStreams[1].width, 320U); + ASSERT_EQ(videoStreams[1].height, 180U); +} + +TEST_F(VideoConduitTest, TestConfigureSendMediaCodecCodecMode) { + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + aControl.mVideoCodecMode = webrtc::VideoCodecMode::kScreensharing; + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + ASSERT_EQ(Call()->mVideoSendEncoderConfig->content_type, + VideoEncoderConfig::ContentType::kScreen); +} + +TEST_F(VideoConduitTest, TestConfigureSendMediaCodecFEC) { + { + // H264 + FEC + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfig(120, "H264", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + codecConfig.mFECFbSet = true; + codecConfig.mULPFECPayloadType = 1; + codecConfig.mREDPayloadType = 2; + codecConfig.mREDRTXPayloadType = 3; + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_TRUE(Call()->mVideoSendConfig); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.ulpfec.ulpfec_payload_type, 1); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.ulpfec.red_payload_type, 2); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.ulpfec.red_rtx_payload_type, 3); + } + + { + // H264 + FEC + Nack + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig(120, "H264", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + codecConfig.mFECFbSet = true; + codecConfig.mNackFbTypes.push_back(""); + codecConfig.mULPFECPayloadType = 1; + codecConfig.mREDPayloadType = 2; + codecConfig.mREDRTXPayloadType = 3; + aControl.mVideoSendCodec = Some(codecConfig); + }); + ASSERT_TRUE(Call()->mVideoSendConfig); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.ulpfec.ulpfec_payload_type, -1); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.ulpfec.red_payload_type, -1); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.ulpfec.red_rtx_payload_type, -1); + } + + { + // VP8 + FEC + Nack + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + codecConfig.mFECFbSet = true; + codecConfig.mNackFbTypes.push_back(""); + codecConfig.mULPFECPayloadType = 1; + codecConfig.mREDPayloadType = 2; + codecConfig.mREDRTXPayloadType = 3; + aControl.mVideoSendCodec = Some(codecConfig); + }); + ASSERT_TRUE(Call()->mVideoSendConfig); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.ulpfec.ulpfec_payload_type, 1); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.ulpfec.red_payload_type, 2); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.ulpfec.red_rtx_payload_type, 3); + } +} + +TEST_F(VideoConduitTest, TestConfigureSendMediaCodecNack) { + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_TRUE(Call()->mVideoSendConfig); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.nack.rtp_history_ms, 0); + + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig = *aControl.mVideoSendCodec.Ref(); + codecConfig.mNackFbTypes.push_back(""); + aControl.mVideoSendCodec = Some(codecConfig); + }); + ASSERT_TRUE(Call()->mVideoSendConfig); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.nack.rtp_history_ms, 1000); +} + +TEST_F(VideoConduitTest, TestConfigureSendMediaCodecRids) { + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_TRUE(Call()->mVideoSendConfig); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.rids.size(), 0U); + + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + { + auto& encoding = codecConfig.mEncodings.emplace_back(); + encoding.rid = "1"; + } + { + auto& encoding = codecConfig.mEncodings.emplace_back(); + encoding.rid = "2"; + } + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mLocalSsrcs = {42, 1729}; + aControl.mLocalVideoRtxSsrcs = {43, 1730}; + }); + ASSERT_TRUE(Call()->mVideoSendConfig); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.rids.size(), 2U); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.rids[0], "2"); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.rids[1], "1"); +} + +TEST_F(VideoConduitTest, TestOnSinkWantsChanged) { + UniquePtr<MockVideoSink> sink(new MockVideoSink()); + rtc::VideoSinkWants wants; + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + codecConfig.mEncodingConstraints.maxFs = 0; + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + wants.max_pixel_count = 256000; + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + SendVideoFrame(1920, 1080, 1); + { + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(1920, 1080); + EXPECT_LE(videoStreams[0].width * videoStreams[0].height, 256000U); + ASSERT_EQ(videoStreams.size(), 1U); + EXPECT_EQ(videoStreams[0].width, 673U); + EXPECT_EQ(videoStreams[0].height, 379U); + } + + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig = *aControl.mVideoSendCodec.Ref(); + codecConfig.mEncodingConstraints.maxFs = 500; + aControl.mVideoSendCodec = Some(codecConfig); + }); + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + SendVideoFrame(1920, 1080, 2); + { + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + EXPECT_LE(videoStreams[0].width * videoStreams[0].height, 500U * 16U * 16U); + ASSERT_EQ(videoStreams.size(), 1U); + EXPECT_EQ(videoStreams[0].width, 476U); + EXPECT_EQ(videoStreams[0].height, 268U); + } + + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig = *aControl.mVideoSendCodec.Ref(); + codecConfig.mEncodingConstraints.maxFs = 1000; + aControl.mVideoSendCodec = Some(codecConfig); + }); + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + SendVideoFrame(1920, 1080, 3); + { + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + EXPECT_LE(videoStreams[0].width * videoStreams[0].height, + 1000U * 16U * 16U); + ASSERT_EQ(videoStreams.size(), 1U); + EXPECT_EQ(videoStreams[0].width, 673U); + EXPECT_EQ(videoStreams[0].height, 379U); + } + + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig = *aControl.mVideoSendCodec.Ref(); + codecConfig.mEncodingConstraints.maxFs = 500; + aControl.mVideoSendCodec = Some(codecConfig); + }); + wants.max_pixel_count = 64000; + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + SendVideoFrame(1920, 1080, 4); + { + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + EXPECT_LE(videoStreams[0].width * videoStreams[0].height, 64000U); + ASSERT_EQ(videoStreams.size(), 1U); + EXPECT_EQ(videoStreams[0].width, 336U); + EXPECT_EQ(videoStreams[0].height, 189U); + } +} + +class VideoConduitTestScalingLocked : public VideoConduitTest { + public: + static VideoSessionConduit::Options CreateOptions() { + VideoSessionConduit::Options options; + options.mLockScaling = true; + return options; + } + VideoConduitTestScalingLocked() : VideoConduitTest(CreateOptions()) {} +}; + +TEST_F(VideoConduitTestScalingLocked, TestOnSinkWantsChanged) { + UniquePtr<MockVideoSink> sink(new MockVideoSink()); + rtc::VideoSinkWants wants; + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodingConstraints.maxFs = 0; + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + wants.max_pixel_count = 256000; + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + SendVideoFrame(1920, 1080, 1); + EXPECT_EQ(sink->mVideoFrame.width(), 1920); + EXPECT_EQ(sink->mVideoFrame.height(), 1080); + { + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams.size(), 1U); + EXPECT_EQ(videoStreams[0].width, 1920U); + EXPECT_EQ(videoStreams[0].height, 1080U); + } + + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig = *aControl.mVideoSendCodec.Ref(); + codecConfig.mEncodingConstraints.maxFs = 500; + aControl.mVideoSendCodec = Some(codecConfig); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + SendVideoFrame(1920, 1080, 2); + { + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + EXPECT_LE(videoStreams[0].width * videoStreams[0].height, 500U * 16U * 16U); + ASSERT_EQ(videoStreams.size(), 1U); + EXPECT_EQ(videoStreams[0].width, 476U); + EXPECT_EQ(videoStreams[0].height, 268U); + } +} + +TEST_F(VideoConduitTest, TestConfigureSendMediaCodecSimulcastOddScreen) { + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + { + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + { + auto& encoding = codecConfig.mEncodings.emplace_back(); + encoding.constraints.scaleDownBy = 2; + } + { + auto& encoding = codecConfig.mEncodings.emplace_back(); + encoding.constraints.scaleDownBy = 4; + } + aControl.mVideoSendCodec = Some(codecConfig); + } + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + aControl.mLocalSsrcs = {42, 43, 44}; + aControl.mLocalVideoRtxSsrcs = {45, 46, 47}; + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + + UniquePtr<MockVideoSink> sink(new MockVideoSink()); + rtc::VideoSinkWants wants; + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + + SendVideoFrame(26, 24, 1); + { + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams.size(), 3U); + EXPECT_EQ(videoStreams[2].width, 26U); + EXPECT_EQ(videoStreams[2].height, 24U); + EXPECT_EQ(videoStreams[1].width, 13U); + EXPECT_EQ(videoStreams[1].height, 12U); + EXPECT_EQ(videoStreams[0].width, 6U); + EXPECT_EQ(videoStreams[0].height, 6U); + } + + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig = *aControl.mVideoSendCodec.Ref(); + codecConfig.mEncodings.clear(); + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mLocalSsrcs = {42}; + aControl.mLocalVideoRtxSsrcs = {43}; + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + SendVideoFrame(26, 24, 2); + { + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams.size(), 1U); + EXPECT_EQ(videoStreams[0].width, 26U); + EXPECT_EQ(videoStreams[0].height, 24U); + } +} + +TEST_F(VideoConduitTest, TestConfigureSendMediaCodecSimulcastAllScaling) { + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + { + auto& encoding = codecConfig.mEncodings.emplace_back(); + encoding.constraints.scaleDownBy = 2; + } + { + auto& encoding = codecConfig.mEncodings.emplace_back(); + encoding.constraints.scaleDownBy = 4; + } + { + auto& encoding = codecConfig.mEncodings.emplace_back(); + encoding.constraints.scaleDownBy = 6; + } + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + aControl.mLocalSsrcs = {42, 43, 44}; + aControl.mLocalVideoRtxSsrcs = {45, 46, 47}; + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + + UniquePtr<MockVideoSink> sink(new MockVideoSink()); + rtc::VideoSinkWants wants; + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + + SendVideoFrame(1281, 721, 1); + { + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams.size(), 3U); + EXPECT_EQ(videoStreams[2].width, 640U); + EXPECT_EQ(videoStreams[2].height, 360U); + EXPECT_EQ(videoStreams[1].width, 320U); + EXPECT_EQ(videoStreams[1].height, 180U); + EXPECT_EQ(videoStreams[0].width, 213U); + EXPECT_EQ(videoStreams[0].height, 120U); + } + + SendVideoFrame(1281, 721, 2); + { + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams.size(), 3U); + EXPECT_EQ(videoStreams[2].width, 640U); + EXPECT_EQ(videoStreams[2].height, 360U); + EXPECT_EQ(videoStreams[1].width, 320U); + EXPECT_EQ(videoStreams[1].height, 180U); + EXPECT_EQ(videoStreams[0].width, 213U); + EXPECT_EQ(videoStreams[0].height, 120U); + } + + SendVideoFrame(1280, 720, 3); + { + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams.size(), 3U); + EXPECT_EQ(videoStreams[2].width, 640U); + EXPECT_EQ(videoStreams[2].height, 360U); + EXPECT_EQ(videoStreams[1].width, 320U); + EXPECT_EQ(videoStreams[1].height, 180U); + EXPECT_EQ(videoStreams[0].width, 213U); + EXPECT_EQ(videoStreams[0].height, 120U); + } + + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig = *aControl.mVideoSendCodec.Ref(); + codecConfig.mEncodings[0].constraints.scaleDownBy = 1; + codecConfig.mEncodings[1].constraints.scaleDownBy = 2; + codecConfig.mEncodings[2].constraints.scaleDownBy = 4; + aControl.mVideoSendCodec = Some(codecConfig); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + SendVideoFrame(1280, 720, 4); + { + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams.size(), 3U); + EXPECT_EQ(videoStreams[2].width, 1280U); + EXPECT_EQ(videoStreams[2].height, 720U); + EXPECT_EQ(videoStreams[1].width, 640U); + EXPECT_EQ(videoStreams[1].height, 360U); + EXPECT_EQ(videoStreams[0].width, 320U); + EXPECT_EQ(videoStreams[0].height, 180U); + } +} + +TEST_F(VideoConduitTest, TestConfigureSendMediaCodecSimulcastScreenshare) { + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + { + auto& encoding = codecConfig.mEncodings.emplace_back(); + encoding.constraints.scaleDownBy = 2; + } + { + auto& encoding = codecConfig.mEncodings.emplace_back(); + encoding.constraints.scaleDownBy = 4; + } + + aControl.mTransmitting = true; + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + aControl.mLocalSsrcs = {42, 43, 44}; + aControl.mLocalVideoRtxSsrcs = {45, 46, 47}; + aControl.mVideoCodecMode = webrtc::VideoCodecMode::kScreensharing; + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(640, 480); + ASSERT_EQ(videoStreams.size(), 1U); +} + +TEST_F(VideoConduitTest, TestReconfigureReceiveMediaCodecs) { + // Defaults + mControl.Update([&](auto& aControl) { + aControl.mReceiving = true; + aControl.mVideoRecvCodecs = { + VideoCodecConfig(120, "VP8", EncodingConstraints())}; + aControl.mVideoRecvRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_TRUE(Call()->mVideoReceiveConfig); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders.size(), 1U); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders[0].payload_type, 120); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders[0].video_format.name, "VP8"); + ASSERT_NE(Call()->mVideoReceiveConfig->rtp.local_ssrc, 0U); + ASSERT_NE(Call()->mVideoReceiveConfig->rtp.remote_ssrc, 0U); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.rtcp_mode, + webrtc::RtcpMode::kCompound); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.nack.rtp_history_ms, 0); + ASSERT_FALSE(Call()->mVideoReceiveConfig->rtp.remb); + ASSERT_FALSE(Call()->mVideoReceiveConfig->rtp.tmmbr); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.keyframe_method, + webrtc::KeyFrameReqMethod::kNone); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.ulpfec_payload_type, -1); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.red_payload_type, -1); + ASSERT_EQ( + Call()->mVideoReceiveConfig->rtp.rtx_associated_payload_types.size(), 0U); + + // FEC + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfigFecFb(120, "VP8", EncodingConstraints()); + codecConfigFecFb.mFECFbSet = true; + VideoCodecConfig codecConfigFEC(1, "ulpfec", EncodingConstraints()); + VideoCodecConfig codecConfigRED(2, "red", EncodingConstraints()); + aControl.mVideoRecvCodecs = {codecConfigFecFb, codecConfigFEC, + codecConfigRED}; + }); + ASSERT_TRUE(Call()->mVideoReceiveConfig); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders.size(), 1U); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders[0].payload_type, 120); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders[0].video_format.name, "VP8"); + ASSERT_NE(Call()->mVideoReceiveConfig->rtp.local_ssrc, 0U); + ASSERT_NE(Call()->mVideoReceiveConfig->rtp.remote_ssrc, 0U); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.rtcp_mode, + webrtc::RtcpMode::kCompound); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.nack.rtp_history_ms, 0); + ASSERT_FALSE(Call()->mVideoReceiveConfig->rtp.remb); + ASSERT_FALSE(Call()->mVideoReceiveConfig->rtp.tmmbr); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.keyframe_method, + webrtc::KeyFrameReqMethod::kNone); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.ulpfec_payload_type, 1); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.red_payload_type, 2); + ASSERT_EQ( + Call()->mVideoReceiveConfig->rtp.rtx_associated_payload_types.size(), 0U); + + // H264 + mControl.Update([&](auto& aControl) { + aControl.mVideoRecvCodecs = { + VideoCodecConfig(120, "H264", EncodingConstraints())}; + }); + ASSERT_TRUE(Call()->mVideoReceiveConfig); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders.size(), 1U); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders[0].payload_type, 120); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders[0].video_format.name, "H264"); + ASSERT_NE(Call()->mVideoReceiveConfig->rtp.local_ssrc, 0U); + ASSERT_NE(Call()->mVideoReceiveConfig->rtp.remote_ssrc, 0U); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.rtcp_mode, + webrtc::RtcpMode::kCompound); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.nack.rtp_history_ms, 0); + ASSERT_FALSE(Call()->mVideoReceiveConfig->rtp.remb); + ASSERT_FALSE(Call()->mVideoReceiveConfig->rtp.tmmbr); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.keyframe_method, + webrtc::KeyFrameReqMethod::kNone); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.ulpfec_payload_type, -1); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.red_payload_type, -1); + ASSERT_EQ( + Call()->mVideoReceiveConfig->rtp.rtx_associated_payload_types.size(), 0U); + + // Nack + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfigNack(120, "VP8", EncodingConstraints()); + codecConfigNack.mNackFbTypes.push_back(""); + aControl.mVideoRecvCodecs = {codecConfigNack}; + }); + ASSERT_TRUE(Call()->mVideoReceiveConfig); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders.size(), 1U); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders[0].payload_type, 120); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders[0].video_format.name, "VP8"); + ASSERT_NE(Call()->mVideoReceiveConfig->rtp.local_ssrc, 0U); + ASSERT_NE(Call()->mVideoReceiveConfig->rtp.remote_ssrc, 0U); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.rtcp_mode, + webrtc::RtcpMode::kCompound); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.nack.rtp_history_ms, 1000); + ASSERT_FALSE(Call()->mVideoReceiveConfig->rtp.remb); + ASSERT_FALSE(Call()->mVideoReceiveConfig->rtp.tmmbr); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.keyframe_method, + webrtc::KeyFrameReqMethod::kNone); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.ulpfec_payload_type, -1); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.red_payload_type, -1); + ASSERT_EQ( + Call()->mVideoReceiveConfig->rtp.rtx_associated_payload_types.size(), 0U); + + // Remb + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfigRemb(120, "VP8", EncodingConstraints()); + codecConfigRemb.mRembFbSet = true; + aControl.mVideoRecvCodecs = {codecConfigRemb}; + }); + ASSERT_TRUE(Call()->mVideoReceiveConfig); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders.size(), 1U); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders[0].payload_type, 120); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders[0].video_format.name, "VP8"); + ASSERT_NE(Call()->mVideoReceiveConfig->rtp.local_ssrc, 0U); + ASSERT_NE(Call()->mVideoReceiveConfig->rtp.remote_ssrc, 0U); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.rtcp_mode, + webrtc::RtcpMode::kCompound); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.nack.rtp_history_ms, 0); + ASSERT_TRUE(Call()->mVideoReceiveConfig->rtp.remb); + ASSERT_FALSE(Call()->mVideoReceiveConfig->rtp.tmmbr); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.keyframe_method, + webrtc::KeyFrameReqMethod::kNone); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.ulpfec_payload_type, -1); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.red_payload_type, -1); + ASSERT_EQ( + Call()->mVideoReceiveConfig->rtp.rtx_associated_payload_types.size(), 0U); + + // Tmmbr + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfigTmmbr(120, "VP8", EncodingConstraints()); + codecConfigTmmbr.mCcmFbTypes.push_back("tmmbr"); + aControl.mVideoRecvCodecs = {codecConfigTmmbr}; + }); + ASSERT_TRUE(Call()->mVideoReceiveConfig); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders.size(), 1U); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders[0].payload_type, 120); + ASSERT_EQ(Call()->mVideoReceiveConfig->decoders[0].video_format.name, "VP8"); + ASSERT_NE(Call()->mVideoReceiveConfig->rtp.local_ssrc, 0U); + ASSERT_NE(Call()->mVideoReceiveConfig->rtp.remote_ssrc, 0U); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.rtcp_mode, + webrtc::RtcpMode::kCompound); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.nack.rtp_history_ms, 0); + ASSERT_FALSE(Call()->mVideoReceiveConfig->rtp.remb); + ASSERT_TRUE(Call()->mVideoReceiveConfig->rtp.tmmbr); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.keyframe_method, + webrtc::KeyFrameReqMethod::kNone); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.ulpfec_payload_type, -1); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.red_payload_type, -1); + ASSERT_EQ( + Call()->mVideoReceiveConfig->rtp.rtx_associated_payload_types.size(), 0U); +} + +TEST_F(VideoConduitTest, TestReconfigureSendMediaCodec) { + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_FALSE(Call()->mVideoSendConfig); + + // Defaults + mControl.Update([&](auto& aControl) { aControl.mTransmitting = true; }); + ASSERT_TRUE(Call()->mVideoSendConfig); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.payload_name, "VP8"); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.payload_type, 120); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.rtcp_mode, + webrtc::RtcpMode::kCompound); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.max_packet_size, kVideoMtu); + ASSERT_EQ(Call()->mVideoSendEncoderConfig->content_type, + VideoEncoderConfig::ContentType::kRealtimeVideo); + ASSERT_EQ(Call()->mVideoSendEncoderConfig->min_transmit_bitrate_bps, 0); + ASSERT_EQ(Call()->mVideoSendEncoderConfig->max_bitrate_bps, KBPS(10000)); + ASSERT_EQ(Call()->mVideoSendEncoderConfig->number_of_streams, 1U); + mControl.Update([&](auto& aControl) { aControl.mTransmitting = false; }); + + // FEC + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfigFEC(120, "VP8", EncodingConstraints()); + codecConfigFEC.mEncodings.emplace_back(); + codecConfigFEC.mFECFbSet = true; + codecConfigFEC.mNackFbTypes.push_back(""); + codecConfigFEC.mULPFECPayloadType = 1; + codecConfigFEC.mREDPayloadType = 2; + codecConfigFEC.mREDRTXPayloadType = 3; + aControl.mVideoSendCodec = Some(codecConfigFEC); + }); + ASSERT_TRUE(Call()->mVideoSendConfig); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.ulpfec.ulpfec_payload_type, 1); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.ulpfec.red_payload_type, 2); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.ulpfec.red_rtx_payload_type, 3); + mControl.Update([&](auto& aControl) { aControl.mTransmitting = false; }); + + // H264 + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfigH264(120, "H264", EncodingConstraints()); + codecConfigH264.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfigH264); + }); + ASSERT_TRUE(Call()->mVideoSendConfig); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.payload_name, "H264"); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.payload_type, 120); + mControl.Update([&](auto& aControl) { aControl.mTransmitting = false; }); + + // TIAS + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfigTias(120, "VP8", EncodingConstraints()); + codecConfigTias.mEncodings.emplace_back(); + codecConfigTias.mTias = 1000000; + aControl.mVideoSendCodec = Some(codecConfigTias); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + SendVideoFrame(1280, 720, 1); + + { + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(1280, 720); + ASSERT_EQ(videoStreams.size(), 1U); + ASSERT_EQ(videoStreams[0].min_bitrate_bps, 200000); + ASSERT_EQ(videoStreams[0].target_bitrate_bps, 800000); + ASSERT_EQ(videoStreams[0].max_bitrate_bps, 1000000); + } + mControl.Update([&](auto& aControl) { aControl.mTransmitting = false; }); + + // MaxBr + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + VideoCodecConfig::Encoding encoding; + { + auto& encoding = codecConfig.mEncodings.emplace_back(); + encoding.constraints.maxBr = 50000; + } + aControl.mVideoSendCodec = Some(codecConfig); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + SendVideoFrame(1280, 720, 1); + { + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(1280, 720); + ASSERT_EQ(videoStreams.size(), 1U); + ASSERT_LE(videoStreams[0].min_bitrate_bps, 50000); + ASSERT_LE(videoStreams[0].target_bitrate_bps, 50000); + ASSERT_EQ(videoStreams[0].max_bitrate_bps, 50000); + } + mControl.Update([&](auto& aControl) { aControl.mTransmitting = false; }); + + // MaxFs + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfigMaxFs(120, "VP8", EncodingConstraints()); + codecConfigMaxFs.mEncodingConstraints.maxFs = 3600; + VideoCodecConfig::Encoding encoding; + encoding.constraints.maxBr = 0; + codecConfigMaxFs.mEncodings.push_back(encoding); + aControl.mVideoSendCodec = Some(codecConfigMaxFs); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + + UniquePtr<MockVideoSink> sink(new MockVideoSink()); + rtc::VideoSinkWants wants; + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + + { + SendVideoFrame(1280, 720, 1); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[0].width, 1280U); + ASSERT_EQ(videoStreams[0].height, 720U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), 1000U); + ASSERT_EQ(sink->mOnFrameCount, 1U); + } + + { + SendVideoFrame(640, 360, 2); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[0].width, 640U); + ASSERT_EQ(videoStreams[0].height, 360U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), 2000U); + ASSERT_EQ(sink->mOnFrameCount, 2U); + } + + { + SendVideoFrame(1920, 1280, 3); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[0].width, 1174U); + ASSERT_EQ(videoStreams[0].height, 783U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), 3000U); + ASSERT_EQ(sink->mOnFrameCount, 3U); + } +} + +TEST_F(VideoConduitTest, TestReconfigureSendMediaCodecWhileTransmitting) { + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_FALSE(Call()->mVideoSendConfig); + + // Defaults + mControl.Update([&](auto& aControl) { aControl.mTransmitting = true; }); + ASSERT_TRUE(Call()->mVideoSendConfig); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.payload_name, "VP8"); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.payload_type, 120); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.rtcp_mode, + webrtc::RtcpMode::kCompound); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.max_packet_size, kVideoMtu); + ASSERT_EQ(Call()->mVideoSendEncoderConfig->content_type, + VideoEncoderConfig::ContentType::kRealtimeVideo); + ASSERT_EQ(Call()->mVideoSendEncoderConfig->min_transmit_bitrate_bps, 0); + ASSERT_EQ(Call()->mVideoSendEncoderConfig->max_bitrate_bps, KBPS(10000)); + ASSERT_EQ(Call()->mVideoSendEncoderConfig->number_of_streams, 1U); + + // Changing these parameters should not require flipping mTransmitting for the + // changes to take effect. + + // TIAS + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfigTias(120, "VP8", EncodingConstraints()); + codecConfigTias.mEncodings.emplace_back(); + codecConfigTias.mTias = 1000000; + aControl.mVideoSendCodec = Some(codecConfigTias); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + SendVideoFrame(1280, 720, 1); + + { + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(1280, 720); + ASSERT_EQ(videoStreams.size(), 1U); + ASSERT_EQ(videoStreams[0].min_bitrate_bps, 200000); + ASSERT_EQ(videoStreams[0].target_bitrate_bps, 800000); + ASSERT_EQ(videoStreams[0].max_bitrate_bps, 1000000); + } + + // MaxBr + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + { + auto& encoding = codecConfig.mEncodings.emplace_back(); + encoding.constraints.maxBr = 50000; + } + aControl.mVideoSendCodec = Some(codecConfig); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + SendVideoFrame(1280, 720, 1); + { + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(1280, 720); + ASSERT_EQ(videoStreams.size(), 1U); + ASSERT_LE(videoStreams[0].min_bitrate_bps, 50000); + ASSERT_LE(videoStreams[0].target_bitrate_bps, 50000); + ASSERT_EQ(videoStreams[0].max_bitrate_bps, 50000); + } + + // MaxFs + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodingConstraints.maxFs = 3600; + { + auto& encoding = codecConfig.mEncodings.emplace_back(); + encoding.constraints.maxBr = 0; + } + aControl.mVideoSendCodec = Some(codecConfig); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + + UniquePtr<MockVideoSink> sink(new MockVideoSink()); + rtc::VideoSinkWants wants; + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + + { + SendVideoFrame(1280, 720, 1); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[0].width, 1280U); + ASSERT_EQ(videoStreams[0].height, 720U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), 1000U); + ASSERT_EQ(sink->mOnFrameCount, 1U); + } + + { + SendVideoFrame(640, 360, 2); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[0].width, 640U); + ASSERT_EQ(videoStreams[0].height, 360U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), 2000U); + ASSERT_EQ(sink->mOnFrameCount, 2U); + } + + { + SendVideoFrame(1920, 1280, 3); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[0].width, 1174U); + ASSERT_EQ(videoStreams[0].height, 783U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), 3000U); + ASSERT_EQ(sink->mOnFrameCount, 3U); + } + + // ScaleResolutionDownBy + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + { + auto& encoding = codecConfig.mEncodings.emplace_back(); + encoding.constraints.maxFs = 0; + encoding.constraints.scaleDownBy = 3.7; + } + aControl.mVideoSendCodec = Some(codecConfig); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + + { + SendVideoFrame(1280, 720, 4); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[0].width, 345U); + ASSERT_EQ(videoStreams[0].height, 194U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), 4000U); + ASSERT_EQ(sink->mOnFrameCount, 4U); + } + + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfigScaleDownBy = *aControl.mVideoSendCodec.Ref(); + codecConfigScaleDownBy.mEncodings[0].constraints.scaleDownBy = 1.3; + aControl.mVideoSendCodec = Some(codecConfigScaleDownBy); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + + { + SendVideoFrame(641, 359, 5); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[0].width, 493U); + ASSERT_EQ(videoStreams[0].height, 276U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), 5000U); + ASSERT_EQ(sink->mOnFrameCount, 5U); + } +} + +TEST_F(VideoConduitTest, TestVideoEncode) { + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + + UniquePtr<MockVideoSink> sink(new MockVideoSink()); + rtc::VideoSinkWants wants; + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + + SendVideoFrame(1280, 720, 1); + ASSERT_EQ(sink->mVideoFrame.width(), 1280); + ASSERT_EQ(sink->mVideoFrame.height(), 720); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), 1000U); + ASSERT_EQ(sink->mOnFrameCount, 1U); + + SendVideoFrame(640, 360, 2); + ASSERT_EQ(sink->mVideoFrame.width(), 640); + ASSERT_EQ(sink->mVideoFrame.height(), 360); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), 2000U); + ASSERT_EQ(sink->mOnFrameCount, 2U); + + SendVideoFrame(1920, 1280, 3); + ASSERT_EQ(sink->mVideoFrame.width(), 1920); + ASSERT_EQ(sink->mVideoFrame.height(), 1280); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), 3000U); + ASSERT_EQ(sink->mOnFrameCount, 3U); + + mVideoConduit->RemoveSink(sink.get()); +} + +TEST_F(VideoConduitTest, TestVideoEncodeMaxFs) { + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodingConstraints.maxFs = 3600; + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + + UniquePtr<MockVideoSink> sink(new MockVideoSink()); + rtc::VideoSinkWants wants; + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + + { + SendVideoFrame(1280, 720, 1); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[0].width, 1280U); + ASSERT_EQ(videoStreams[0].height, 720U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), 1000U); + ASSERT_EQ(sink->mOnFrameCount, 1U); + } + + { + SendVideoFrame(640, 360, 2); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[0].width, 640U); + ASSERT_EQ(videoStreams[0].height, 360U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), 2000U); + ASSERT_EQ(sink->mOnFrameCount, 2U); + } + + { + SendVideoFrame(1920, 1280, 3); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[0].width, 1174U); + ASSERT_EQ(videoStreams[0].height, 783U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), 3000U); + ASSERT_EQ(sink->mOnFrameCount, 3U); + } + + // maxFs should not force pixel count above what a sink has requested. + // We set 3600 macroblocks (16x16 pixels), so we request 3500 here. + wants.max_pixel_count = 3500 * 16 * 16; + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + + { + SendVideoFrame(1280, 720, 4); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[0].width, 1260U); + ASSERT_EQ(videoStreams[0].height, 709U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), 4000U); + ASSERT_EQ(sink->mOnFrameCount, 4U); + } + + { + SendVideoFrame(640, 360, 5); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[0].width, 640U); + ASSERT_EQ(videoStreams[0].height, 360U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), 5000U); + ASSERT_EQ(sink->mOnFrameCount, 5U); + } + + { + SendVideoFrame(1920, 1280, 6); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[0].width, 1158U); + ASSERT_EQ(videoStreams[0].height, 772U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), 6000U); + ASSERT_EQ(sink->mOnFrameCount, 6U); + } + + mVideoConduit->RemoveSink(sink.get()); +} + +TEST_F(VideoConduitTest, TestVideoEncodeMaxFsNegotiatedThenSinkWants) { + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + codecConfig.mEncodingConstraints.maxFs = 3500; + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + + UniquePtr<MockVideoSink> sink(new MockVideoSink()); + rtc::VideoSinkWants wants; + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + + unsigned int frame = 0; + + { + SendVideoFrame(1280, 720, frame++); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[0].width, 1260U); + ASSERT_EQ(videoStreams[0].height, 709U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), (frame - 1) * 1000); + ASSERT_EQ(sink->mOnFrameCount, frame); + } + + wants.max_pixel_count = 3600 * 16 * 16; + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + + { + SendVideoFrame(1280, 720, frame++); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[0].width, 1260U); + ASSERT_EQ(videoStreams[0].height, 709U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), (frame - 1) * 1000); + ASSERT_EQ(sink->mOnFrameCount, frame); + } + + mVideoConduit->RemoveSink(sink.get()); +} + +TEST_F(VideoConduitTest, TestVideoEncodeMaxFsCodecChange) { + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + codecConfig.mEncodingConstraints.maxFs = 3500; + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + + UniquePtr<MockVideoSink> sink(new MockVideoSink()); + rtc::VideoSinkWants wants; + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + + unsigned int frame = 0; + + { + SendVideoFrame(1280, 720, frame++); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[0].width, 1260U); + ASSERT_EQ(videoStreams[0].height, 709U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), (frame - 1) * 1000); + ASSERT_EQ(sink->mOnFrameCount, frame); + } + + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig(121, "VP9", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + codecConfig.mEncodingConstraints.maxFs = 3500; + aControl.mVideoSendCodec = Some(codecConfig); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + + { + SendVideoFrame(1280, 720, frame++); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[0].width, 1260U); + ASSERT_EQ(videoStreams[0].height, 709U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), (frame - 1) * 1000); + ASSERT_EQ(sink->mOnFrameCount, frame); + } + + mVideoConduit->RemoveSink(sink.get()); +} + +TEST_F(VideoConduitTest, TestVideoEncodeMaxFsSinkWantsThenCodecChange) { + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + + UniquePtr<MockVideoSink> sink(new MockVideoSink()); + rtc::VideoSinkWants wants; + wants.max_pixel_count = 3500 * 16 * 16; + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + + unsigned int frame = 0; + + SendVideoFrame(1280, 720, frame++); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[0].width, 1260U); + ASSERT_EQ(videoStreams[0].height, 709U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), (frame - 1) * 1000); + ASSERT_EQ(sink->mOnFrameCount, frame); + + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig(121, "VP9", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + + { + SendVideoFrame(1280, 720, frame++); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[0].width, 1260U); + ASSERT_EQ(videoStreams[0].height, 709U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), (frame - 1) * 1000); + ASSERT_EQ(sink->mOnFrameCount, frame); + } + + mVideoConduit->RemoveSink(sink.get()); +} + +TEST_F(VideoConduitTest, TestVideoEncodeMaxFsNegotiated) { + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + + UniquePtr<MockVideoSink> sink(new MockVideoSink()); + rtc::VideoSinkWants wants; + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + + unsigned int frame = 0; + SendVideoFrame(1280, 720, frame++); + ASSERT_EQ(sink->mVideoFrame.width(), 1280); + ASSERT_EQ(sink->mVideoFrame.height(), 720); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), (frame - 1) * 1000); + ASSERT_EQ(sink->mOnFrameCount, frame); + + // Ensure that negotiating a new max-fs works + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig = *aControl.mVideoSendCodec.Ref(); + codecConfig.mEncodingConstraints.maxFs = 3500; + aControl.mVideoSendCodec = Some(codecConfig); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + + { + SendVideoFrame(1280, 720, frame++); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[0].width, 1260U); + ASSERT_EQ(videoStreams[0].height, 709U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), (frame - 1) * 1000); + ASSERT_EQ(sink->mOnFrameCount, frame); + } + + // Ensure that negotiating max-fs away works + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig = *aControl.mVideoSendCodec.Ref(); + codecConfig.mEncodingConstraints.maxFs = 0; + aControl.mVideoSendCodec = Some(codecConfig); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + + SendVideoFrame(1280, 720, frame++); + ASSERT_EQ(sink->mVideoFrame.width(), 1280); + ASSERT_EQ(sink->mVideoFrame.height(), 720); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), (frame - 1) * 1000); + ASSERT_EQ(sink->mOnFrameCount, frame); + + mVideoConduit->RemoveSink(sink.get()); +} + +TEST_F(VideoConduitTest, TestVideoEncodeMaxWidthAndHeight) { + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodingConstraints.maxWidth = 1280; + codecConfig.mEncodingConstraints.maxHeight = 720; + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + + UniquePtr<MockVideoSink> sink(new MockVideoSink()); + rtc::VideoSinkWants wants; + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + + SendVideoFrame(1280, 720, 1); + ASSERT_EQ(sink->mVideoFrame.width(), 1280); + ASSERT_EQ(sink->mVideoFrame.height(), 720); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), 1000U); + ASSERT_EQ(sink->mOnFrameCount, 1U); + + SendVideoFrame(640, 360, 2); + ASSERT_EQ(sink->mVideoFrame.width(), 640); + ASSERT_EQ(sink->mVideoFrame.height(), 360); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), 2000U); + ASSERT_EQ(sink->mOnFrameCount, 2U); + + { + SendVideoFrame(1920, 1280, 3); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[0].width, 1080U); + ASSERT_EQ(videoStreams[0].height, 720U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), 3000U); + ASSERT_EQ(sink->mOnFrameCount, 3U); + } + + mVideoConduit->RemoveSink(sink.get()); +} + +TEST_F(VideoConduitTest, TestVideoEncodeScaleResolutionBy) { + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodingConstraints.maxFs = 3600; + auto& encoding = codecConfig.mEncodings.emplace_back(); + encoding.constraints.scaleDownBy = 2; + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + + UniquePtr<MockVideoSink> sink(new MockVideoSink()); + rtc::VideoSinkWants wants; + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + + { + SendVideoFrame(1280, 720, 1); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[0].width, 640U); + ASSERT_EQ(videoStreams[0].height, 360U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), 1000U); + ASSERT_EQ(sink->mOnFrameCount, 1U); + } + + { + SendVideoFrame(640, 360, 2); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[0].width, 320U); + ASSERT_EQ(videoStreams[0].height, 180U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), 2000U); + ASSERT_EQ(sink->mOnFrameCount, 2U); + mVideoConduit->RemoveSink(sink.get()); + } +} + +TEST_F(VideoConduitTest, TestVideoEncodeSimulcastScaleResolutionBy) { + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + { + auto& encoding = codecConfig.mEncodings.emplace_back(); + encoding.constraints.scaleDownBy = 2; + } + { + auto& encoding = codecConfig.mEncodings.emplace_back(); + encoding.constraints.scaleDownBy = 3; + } + { + auto& encoding = codecConfig.mEncodings.emplace_back(); + encoding.constraints.scaleDownBy = 4; + } + + aControl.mTransmitting = true; + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + aControl.mLocalSsrcs = {42, 43, 44}; + aControl.mLocalVideoRtxSsrcs = {45, 46, 47}; + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + + UniquePtr<MockVideoSink> sink(new MockVideoSink()); + rtc::VideoSinkWants wants; + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + + { + SendVideoFrame(640, 480, 1); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[2].width, 320U); + ASSERT_EQ(videoStreams[2].height, 240U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), 1000U); + ASSERT_EQ(sink->mOnFrameCount, 1U); + } + + { + SendVideoFrame(1280, 720, 2); + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(sink->mVideoFrame.width(), + sink->mVideoFrame.height()); + ASSERT_EQ(videoStreams[2].width, 640U); + ASSERT_EQ(videoStreams[2].height, 360U); + ASSERT_EQ(sink->mVideoFrame.timestamp_us(), 2000U); + ASSERT_EQ(sink->mOnFrameCount, 2U); + mVideoConduit->RemoveSink(sink.get()); + } +} + +TEST_F(VideoConduitTest, TestVideoEncodeLargeScaleResolutionByFrameDropping) { + for (const auto& scales : + {std::vector{200U}, std::vector{200U, 300U}, std::vector{300U, 200U}}) { + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + for (const auto& scale : scales) { + auto& encoding = codecConfig.mEncodings.emplace_back(); + encoding.constraints.scaleDownBy = scale; + } + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + aControl.mLocalSsrcs = scales; + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + + UniquePtr<MockVideoSink> sink(new MockVideoSink()); + rtc::VideoSinkWants wants; + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + + { + // If all layers' scaleDownBy is larger than any input dimension, that + // dimension becomes zero and we drop it. + // NB: libwebrtc doesn't CreateEncoderStreams() unless there's a real + // frame, so no reason to call it here. + SendVideoFrame(199, 199, 1); + EXPECT_EQ(sink->mOnFrameCount, 0U); + } + + { + // If only width becomes zero, we drop. + SendVideoFrame(199, 200, 2); + EXPECT_EQ(sink->mOnFrameCount, 0U); + } + + { + // If only height becomes zero, we drop. + SendVideoFrame(200, 199, 3); + EXPECT_EQ(sink->mOnFrameCount, 0U); + } + + { + // If dimensions are non-zero, we pass through. + SendVideoFrame(200, 200, 4); + EXPECT_EQ(sink->mOnFrameCount, 1U); + } + + mVideoConduit->RemoveSink(sink.get()); + } +} + +TEST_F(VideoConduitTest, TestVideoEncodeLargeScaleResolutionByStreamCreation) { + for (const auto& scales : + {std::vector{200U}, std::vector{200U, 300U}, std::vector{300U, 200U}}) { + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + for (const auto& scale : scales) { + auto& encoding = codecConfig.mEncodings.emplace_back(); + encoding.constraints.scaleDownBy = scale; + } + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + aControl.mLocalSsrcs = scales; + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + + { + // If dimensions scale to <1, we create a 1x1 stream. + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(199, 199); + ASSERT_EQ(videoStreams.size(), scales.size()); + for (const auto& stream : videoStreams) { + EXPECT_EQ(stream.width, 1U); + EXPECT_EQ(stream.height, 1U); + } + } + + { + // If width scales to <1, we create a 1x1 stream. + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(199, 200); + ASSERT_EQ(videoStreams.size(), scales.size()); + for (const auto& stream : videoStreams) { + EXPECT_EQ(stream.width, 1U); + EXPECT_EQ(stream.height, 1U); + } + } + + { + // If height scales to <1, we create a 1x1 stream. + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(200, 199); + ASSERT_EQ(videoStreams.size(), scales.size()); + for (const auto& stream : videoStreams) { + EXPECT_EQ(stream.width, 1U); + EXPECT_EQ(stream.height, 1U); + } + } + + { + // If dimensions scale to 1, we create a 1x1 stream. + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(200, 200); + ASSERT_EQ(videoStreams.size(), scales.size()); + for (const auto& stream : videoStreams) { + EXPECT_EQ(stream.width, 1U); + EXPECT_EQ(stream.height, 1U); + } + } + + { + // If one dimension scales to 0 and the other >1, we create a 1x1 stream. + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(400, 199); + ASSERT_EQ(videoStreams.size(), scales.size()); + for (const auto& stream : videoStreams) { + EXPECT_EQ(stream.width, 1U); + EXPECT_EQ(stream.height, 1U); + } + } + + { + // Legit case scaling down to more than 1x1. + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(600, 400); + ASSERT_EQ(videoStreams.size(), scales.size()); + for (size_t i = 0; i < scales.size(); ++i) { + // Streams are backwards for some reason + const auto& stream = videoStreams[scales.size() - i - 1]; + const auto& scale = scales[i]; + if (scale == 200U) { + EXPECT_EQ(stream.width, 3U); + EXPECT_EQ(stream.height, 2U); + } else { + EXPECT_EQ(stream.width, 2U); + EXPECT_EQ(stream.height, 1U); + } + } + } + } +} + +TEST_F(VideoConduitTest, TestVideoEncodeResolutionAlignment) { + UniquePtr<MockVideoSink> sink(new MockVideoSink()); + + for (const auto& scales : {std::vector{1U}, std::vector{1U, 9U}}) { + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + for (const auto& scale : scales) { + auto& encoding = codecConfig.mEncodings.emplace_back(); + encoding.constraints.scaleDownBy = scale; + } + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + aControl.mLocalSsrcs = scales; + }); + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + + for (const auto& alignment : {2, 16, 39, 400, 1000}) { + // Test that requesting specific alignment always results in the expected + // number of layers and valid alignment. + rtc::VideoSinkWants wants; + wants.resolution_alignment = alignment; + mVideoConduit->AddOrUpdateSink(sink.get(), wants); + + const std::vector<webrtc::VideoStream> videoStreams = + Call()->CreateEncoderStreams(640, 480); + ASSERT_EQ(videoStreams.size(), scales.size()); + for (size_t i = 0; i < videoStreams.size(); ++i) { + // videoStreams is backwards + const auto& stream = videoStreams[videoStreams.size() - 1 - i]; + const auto& scale = scales[i]; + uint32_t expectation = + 480 / scale < static_cast<uint32_t>(alignment) ? 1 : 0; + EXPECT_EQ(stream.width % alignment, expectation) + << " for scale " << scale << " and alignment " << alignment; + EXPECT_EQ(stream.height % alignment, expectation); + } + } + } + + mVideoConduit->RemoveSink(sink.get()); +} + +TEST_F(VideoConduitTest, TestSettingRtpRtcpRsize) { + mControl.Update([&](auto& aControl) { + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + RtpRtcpConfig rtcpConf(webrtc::RtcpMode::kReducedSize); + + aControl.mReceiving = true; + aControl.mVideoRecvCodecs = {codecConfig}; + aControl.mVideoRecvRtpRtcpConfig = Some(rtcpConf); + aControl.mTransmitting = true; + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = Some(rtcpConf); + }); + ASSERT_TRUE(Call()->mVideoReceiveConfig); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.rtcp_mode, + webrtc::RtcpMode::kReducedSize); + ASSERT_TRUE(Call()->mVideoSendConfig); + ASSERT_EQ(Call()->mVideoSendConfig->rtp.rtcp_mode, + webrtc::RtcpMode::kReducedSize); +} + +TEST_F(VideoConduitTest, TestRemoteSsrcDefault) { + mControl.Update([&](auto& aControl) { + aControl.mRemoteSsrc = 0; + aControl.mLocalSsrcs = {1}; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + aControl.mReceiving = true; + aControl.mTransmitting = true; + }); + ASSERT_TRUE(Call()->mVideoReceiveConfig); + ASSERT_THAT(Call()->mVideoReceiveConfig->rtp.remote_ssrc, + Not(testing::AnyOf(0U, 1U))); + ASSERT_TRUE(Call()->mVideoSendConfig); + ASSERT_THAT(Call()->mVideoSendConfig->rtp.ssrcs, ElementsAre(1U)); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.local_ssrc, + Call()->mVideoSendConfig->rtp.ssrcs[0]); +} + +TEST_F(VideoConduitTest, TestRemoteSsrcCollision) { + mControl.Update([&](auto& aControl) { + aControl.mRemoteSsrc = 1; + aControl.mLocalSsrcs = {1}; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + aControl.mReceiving = true; + aControl.mTransmitting = true; + }); + EXPECT_TRUE(Call()->mVideoReceiveConfig); + EXPECT_EQ(Call()->mVideoReceiveConfig->rtp.remote_ssrc, 1U); + EXPECT_TRUE(Call()->mVideoSendConfig); + EXPECT_THAT(Call()->mVideoSendConfig->rtp.ssrcs, + ElementsAre(Not(testing::AnyOf(0U, 1U)))); + EXPECT_EQ(Call()->mVideoReceiveConfig->rtp.local_ssrc, + Call()->mVideoSendConfig->rtp.ssrcs[0]); +} + +TEST_F(VideoConduitTest, TestLocalSsrcDefault) { + mControl.Update([&](auto& aControl) { + aControl.mRemoteSsrc = 1; + aControl.mLocalSsrcs = {}; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + aControl.mReceiving = true; + aControl.mTransmitting = true; + }); + ASSERT_TRUE(Call()->mVideoReceiveConfig); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.remote_ssrc, 1U); + ASSERT_TRUE(Call()->mVideoSendConfig); + ASSERT_THAT(Call()->mVideoSendConfig->rtp.ssrcs, + ElementsAre(Not(testing::AnyOf(0U, 1U)))); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.local_ssrc, + Call()->mVideoSendConfig->rtp.ssrcs[0]); +} + +TEST_F(VideoConduitTest, TestLocalSsrcCollision) { + mControl.Update([&](auto& aControl) { + aControl.mRemoteSsrc = 1; + aControl.mLocalSsrcs = {2, 2}; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + codecConfig.mEncodings.emplace_back(); + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + aControl.mReceiving = true; + aControl.mTransmitting = true; + }); + ASSERT_TRUE(Call()->mVideoReceiveConfig); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.remote_ssrc, 1U); + ASSERT_TRUE(Call()->mVideoSendConfig); + ASSERT_THAT(Call()->mVideoSendConfig->rtp.ssrcs, + ElementsAre(2U, Not(testing::AnyOf(0U, 2U)))); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.local_ssrc, + Call()->mVideoSendConfig->rtp.ssrcs[0]); +} + +TEST_F(VideoConduitTest, TestLocalSsrcUnorderedCollision) { + mControl.Update([&](auto& aControl) { + aControl.mRemoteSsrc = 1; + aControl.mLocalSsrcs = {2, 3, 2}; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + for (int i = 0; i < 3; ++i) { + codecConfig.mEncodings.emplace_back(); + } + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + aControl.mReceiving = true; + aControl.mTransmitting = true; + }); + ASSERT_TRUE(Call()->mVideoReceiveConfig); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.remote_ssrc, 1U); + ASSERT_TRUE(Call()->mVideoSendConfig); + ASSERT_THAT(Call()->mVideoSendConfig->rtp.ssrcs, + ElementsAre(2U, 3U, Not(testing::AnyOf(0U, 2U)))); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.local_ssrc, + Call()->mVideoSendConfig->rtp.ssrcs[0]); +} + +TEST_F(VideoConduitTest, TestLocalAndRemoteSsrcCollision) { + mControl.Update([&](auto& aControl) { + aControl.mRemoteSsrc = 1; + aControl.mLocalSsrcs = {1, 2, 2}; + VideoCodecConfig codecConfig(120, "VP8", EncodingConstraints()); + for (int i = 0; i < 3; ++i) { + codecConfig.mEncodings.emplace_back(); + } + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + aControl.mReceiving = true; + aControl.mTransmitting = true; + }); + ASSERT_TRUE(Call()->mVideoReceiveConfig); + ASSERT_THAT(Call()->mVideoReceiveConfig->rtp.remote_ssrc, 1U); + ASSERT_TRUE(Call()->mVideoSendConfig); + ASSERT_THAT(Call()->mVideoSendConfig->rtp.ssrcs, + ElementsAre(Not(testing::AnyOf(0U, 1U, 2U)), 2U, + Not(testing::AnyOf( + 0U, 1U, 2U, + Call()->mVideoReceiveConfig->rtp.remote_ssrc)))); + ASSERT_EQ(Call()->mVideoReceiveConfig->rtp.local_ssrc, + Call()->mVideoSendConfig->rtp.ssrcs[0]); +} + +TEST_F(VideoConduitTest, TestExternalRemoteSsrcCollision) { + auto other = MakeRefPtr<MockConduit>(); + mCallWrapper->RegisterConduit(other); + + // First the mControl update should trigger an UnsetRemoteSSRC(1) from us. + // Then we simulate another conduit using that same ssrc, which should trigger + // us to generate a fresh ssrc that is not 0 and not 1. + { + InSequence s; + EXPECT_CALL(*other, UnsetRemoteSSRC(1U)).Times(2); + EXPECT_CALL(*other, UnsetRemoteSSRC(Not(testing::AnyOf(0U, 1U)))); + } + + mControl.Update([&](auto& aControl) { + aControl.mRemoteSsrc = 1; + aControl.mReceiving = true; + }); + EXPECT_TRUE(Call()->mVideoReceiveConfig); + EXPECT_EQ(Call()->mVideoReceiveConfig->rtp.remote_ssrc, 1U); + + mozilla::Unused << WaitFor(InvokeAsync( + GetCurrentSerialEventTarget(), __func__, [wrapper = mCallWrapper] { + wrapper->UnsetRemoteSSRC(1); + return GenericPromise::CreateAndResolve(true, __func__); + })); + + EXPECT_TRUE(Call()->mVideoReceiveConfig); + EXPECT_THAT(Call()->mVideoReceiveConfig->rtp.remote_ssrc, + Not(testing::AnyOf(0U, 1U))); +} + +TEST_F(VideoConduitTest, TestVideoConfigurationH264) { + const int profileLevelId1 = 0x42E00D; + const int profileLevelId2 = 0x64000C; + const char* sprop1 = "foo bar"; + const char* sprop2 = "baz"; + + // Test that VideoConduit propagates H264 configuration data properly. + // We do two tests: + // - Test valid data in packetization mode 0 (SingleNALU) + // - Test different valid data in packetization mode 1 (NonInterleaved) + + { + mControl.Update([&](auto& aControl) { + aControl.mTransmitting = true; + VideoCodecConfigH264 h264{}; + h264.packetization_mode = 0; + h264.profile_level_id = profileLevelId1; + strncpy(h264.sprop_parameter_sets, sprop1, + sizeof(h264.sprop_parameter_sets) - 1); + VideoCodecConfig codecConfig(97, "H264", EncodingConstraints(), &h264); + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + aControl.mVideoSendRtpRtcpConfig = + Some(RtpRtcpConfig(webrtc::RtcpMode::kCompound)); + }); + + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + auto& params = Call()->mVideoSendEncoderConfig->video_format.parameters; + EXPECT_EQ(params[cricket::kH264FmtpPacketizationMode], "0"); + EXPECT_EQ(params[cricket::kH264FmtpProfileLevelId], "42e00d"); + EXPECT_EQ(params[cricket::kH264FmtpSpropParameterSets], sprop1); + } + + { + mControl.Update([&](auto& aControl) { + VideoCodecConfigH264 h264{}; + h264.packetization_mode = 1; + h264.profile_level_id = profileLevelId2; + strncpy(h264.sprop_parameter_sets, sprop2, + sizeof(h264.sprop_parameter_sets) - 1); + VideoCodecConfig codecConfig(126, "H264", EncodingConstraints(), &h264); + codecConfig.mEncodings.emplace_back(); + aControl.mVideoSendCodec = Some(codecConfig); + }); + + ASSERT_TRUE(Call()->mVideoSendEncoderConfig); + auto& params = Call()->mVideoSendEncoderConfig->video_format.parameters; + EXPECT_EQ(params[cricket::kH264FmtpPacketizationMode], "1"); + EXPECT_EQ(params[cricket::kH264FmtpProfileLevelId], "64000c"); + EXPECT_EQ(params[cricket::kH264FmtpSpropParameterSets], sprop2); + } +} + +} // End namespace test. |