diff --git a/submodules/TelegramVoip/Sources/OngoingCallContext.swift b/submodules/TelegramVoip/Sources/OngoingCallContext.swift index ce8c733b26..68860c76e2 100644 --- a/submodules/TelegramVoip/Sources/OngoingCallContext.swift +++ b/submodules/TelegramVoip/Sources/OngoingCallContext.swift @@ -728,6 +728,18 @@ public final class OngoingCallContext { filteredConnections.append(contentsOf: callConnectionDescriptionsWebrtc(connection)) } + /*#if DEBUG + filteredConnections.removeAll() + filteredConnections.append(OngoingCallConnectionDescriptionWebrtc( + connectionId: 1, + hasStun: true, + hasTurn: true, ip: "178.62.7.192", + port: 1400, + username: "user", + password: "user") + ) + #endif*/ + let context = OngoingCallThreadLocalContextWebrtc(version: version, queue: OngoingCallThreadLocalContextQueueImpl(queue: queue), proxy: voipProxyServer, networkType: ongoingNetworkTypeForTypeWebrtc(initialNetworkType), dataSaving: ongoingDataSavingForTypeWebrtc(dataSaving), derivedState: derivedState.data, key: key, isOutgoing: isOutgoing, connections: filteredConnections, maxLayer: maxLayer, allowP2P: allowP2P, allowTCP: enableTCP, enableStunMarking: enableStunMarking, logPath: tempLogPath, statsLogPath: tempStatsLogPath, sendSignalingData: { [weak callSessionManager] data in callSessionManager?.sendSignalingData(internalId: internalId, data: data) }, videoCapturer: video?.impl, preferredVideoCodec: preferredVideoCodec, audioInputDeviceId: "") diff --git a/submodules/TgVoipWebrtc/BUILD b/submodules/TgVoipWebrtc/BUILD index 842991e710..8ea93a9ab6 100644 --- a/submodules/TgVoipWebrtc/BUILD +++ b/submodules/TgVoipWebrtc/BUILD @@ -1,3 +1,4 @@ +load("@build_bazel_rules_apple//apple:ios.bzl", "ios_unit_test") config_setting( name = "debug_build", @@ -92,9 +93,52 @@ objc_library( "VideoToolbox", "CoreTelephony", "CoreMedia", + "GLKit", "AVFoundation", ], visibility = [ "//visibility:public", ], ) + +objc_library( + name = "TgCallsTestsLib", + copts = [ + "-I{}/tgcalls/tgcalls".format(package_name()), + "-Ithird-party/webrtc/webrtc", + "-Ithird-party/webrtc/dependencies", + "-Ithird-party/webrtc/dependencies/third_party/abseil-cpp", + "-Ithird-party/webrtc/webrtc/sdk/objc", + "-Ithird-party/webrtc/webrtc/sdk/objc/base", + "-Ithird-party/webrtc/webrtc/sdk/objc/components/renderer/metal", + "-Ithird-party/webrtc/webrtc/sdk/objc/components/renderer/opengl", + "-Ithird-party/webrtc/webrtc/sdk/objc/components/video_codec", + "-Ithird-party/libyuv/third_party/libyuv/include", + "-Ithird-party/libyuv", + "-Ithird-party/webrtc/webrtc/sdk/objc/api/video_codec", + "-DWEBRTC_IOS", + "-DWEBRTC_MAC", + "-DWEBRTC_POSIX", + "-DRTC_ENABLE_VP9", + "-DTGVOIP_NAMESPACE=tgvoip_webrtc", + "-std=c++14", + ], + srcs = glob([ + "tests/*.m", + "tests/*.mm", + ]), + deps = [ + ":TgVoipWebrtc" + ], +) + +ios_unit_test( + name = "TgCallsTests", + minimum_os_version = "9.0", + deps = [ + ":TgCallsTestsLib", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TgVoipWebrtc/tests/TestNegotiation.mm b/submodules/TgVoipWebrtc/tests/TestNegotiation.mm new file mode 100644 index 0000000000..a3d531431d --- /dev/null +++ b/submodules/TgVoipWebrtc/tests/TestNegotiation.mm @@ -0,0 +1,705 @@ +#import + +#include "api/task_queue/default_task_queue_factory.h" +#include "media/engine/webrtc_media_engine.h" +#include "api/audio_codecs/audio_encoder_factory_template.h" +#include "api/audio_codecs/audio_decoder_factory_template.h" +#include "api/audio_codecs/opus/audio_decoder_opus.h" +#include "api/audio_codecs/opus/audio_decoder_multi_channel_opus.h" +#include "api/audio_codecs/opus/audio_encoder_opus.h" +#include "api/audio_codecs/L16/audio_decoder_L16.h" +#include "api/audio_codecs/L16/audio_encoder_L16.h" + +#include "StaticThreads.h" +#include "FakeAudioDeviceModule.h" +#include "platform/PlatformInterface.h" + +#include "v2/ContentNegotiation.h" + +namespace { + class Context { + public: + Context(bool isOutgoing) : + _isOutgoing(isOutgoing), + _threads(tgcalls::StaticThreads::getThreads()), + _taskQueueFactory(webrtc::CreateDefaultTaskQueueFactory()), + _uniqueRandomIdGenerator(std::make_unique()) { + _threads->getWorkerThread()->Invoke(RTC_FROM_HERE, [&]() { + cricket::MediaEngineDependencies mediaDeps; + mediaDeps.task_queue_factory = _taskQueueFactory.get(); + mediaDeps.audio_encoder_factory = webrtc::CreateAudioEncoderFactory(); + mediaDeps.audio_decoder_factory = webrtc::CreateAudioDecoderFactory(); + + mediaDeps.video_encoder_factory = tgcalls::PlatformInterface::SharedInstance()->makeVideoEncoderFactory(true); + mediaDeps.video_decoder_factory = tgcalls::PlatformInterface::SharedInstance()->makeVideoDecoderFactory(); + + tgcalls::FakeAudioDeviceModule::Options options; + options.num_channels = 1; + _audioDeviceModule = tgcalls::FakeAudioDeviceModule::Creator(nullptr, nullptr, options)(_taskQueueFactory.get()); + + mediaDeps.adm = _audioDeviceModule; + + _mediaEngine = cricket::CreateMediaEngine(std::move(mediaDeps)); + + _channelManager = cricket::ChannelManager::Create( + std::move(_mediaEngine), + true, + _threads->getWorkerThread(), + _threads->getNetworkThread() + ); + }); + + _contentNegotiationContext = std::make_unique(isOutgoing, _uniqueRandomIdGenerator.get()); + _contentNegotiationContext->copyCodecsFromChannelManager(_channelManager.get(), isOutgoing); + } + + ~Context() { + _contentNegotiationContext.reset(); + + _threads->getWorkerThread()->Invoke(RTC_FROM_HERE, [&]() { + _channelManager.reset(); + _mediaEngine.reset(); + _audioDeviceModule = nullptr; + }); + } + + + public: + tgcalls::ContentNegotiationContext *contentNegotiationContext() const { + return _contentNegotiationContext.get(); + } + + void assertSsrcs(std::vector const &outgoingSsrcs, std::vector const &incomingSsrcs) { + std::set incomingSsrcsSet; + for (auto ssrc : incomingSsrcs) { + incomingSsrcsSet.insert(ssrc); + } + + std::set outgoingSsrcsSet; + for (auto ssrc : outgoingSsrcs) { + outgoingSsrcsSet.insert(ssrc); + } + + std::set actualIncomingSsrcs; + std::set actualOutgoingSsrcs; + + auto coordinatedState = _contentNegotiationContext->coordinatedState(); + XCTAssert(coordinatedState != nullptr); + + for (const auto &content : coordinatedState->incomingContents) { + actualIncomingSsrcs.insert(content.ssrc); + } + for (const auto &content : coordinatedState->outgoingContents) { + actualOutgoingSsrcs.insert(content.ssrc); + } + + XCTAssert(incomingSsrcsSet == actualIncomingSsrcs); + XCTAssert(outgoingSsrcsSet == actualOutgoingSsrcs); + } + + bool isContentsEqualToRemote(Context &remoteContext) { + auto localCoordinatedState = _contentNegotiationContext->coordinatedState(); + auto remoteCoordinatedState = remoteContext.contentNegotiationContext()->coordinatedState(); + + auto mediaContentComparator = [](tgcalls::signaling::MediaContent const &lhs, tgcalls::signaling::MediaContent const &rhs) -> bool { + return lhs.ssrc < rhs.ssrc; + }; + + auto localIncomingContents = localCoordinatedState->incomingContents; + std::sort(localIncomingContents.begin(), localIncomingContents.end(), mediaContentComparator); + + auto localOutgoingContents = localCoordinatedState->outgoingContents; + std::sort(localOutgoingContents.begin(), localOutgoingContents.end(), mediaContentComparator); + + auto remoteIncomingContents = remoteCoordinatedState->incomingContents; + std::sort(remoteIncomingContents.begin(), remoteIncomingContents.end(), mediaContentComparator); + + auto remoteOutgoingContents = remoteCoordinatedState->outgoingContents; + std::sort(remoteOutgoingContents.begin(), remoteOutgoingContents.end(), mediaContentComparator); + + if (localIncomingContents != remoteOutgoingContents) { + return false; + } + if (localOutgoingContents != remoteIncomingContents) { + return false; + } + + return true; + } + + private: + __unused bool _isOutgoing = false; + std::shared_ptr _threads; + std::unique_ptr _taskQueueFactory; + std::unique_ptr _uniqueRandomIdGenerator; + rtc::scoped_refptr _audioDeviceModule; + std::unique_ptr _mediaEngine; + std::unique_ptr _channelManager; + std::unique_ptr _contentNegotiationContext; + }; + +std::unique_ptr copyNegotiationContents(tgcalls::ContentNegotiationContext::NegotiationContents *value) { + if (!value) { + return nullptr; + } + + auto result = std::make_unique(); + result->exchangeId = value->exchangeId; + result->contents = value->contents; + + return result; +} + +void runUntilStableSequential(Context &localContext, Context &remoteContext) { + for (int i = 0; i < 6; i++) { + auto localOffer = localContext.contentNegotiationContext()->getPendingOffer(); + if (localOffer) { + auto remoteAnswer = remoteContext.contentNegotiationContext()->setRemoteNegotiationContent(copyNegotiationContents(localOffer.get())); + XCTAssert(remoteAnswer != nullptr); + + auto localResponse = localContext.contentNegotiationContext()->setRemoteNegotiationContent(copyNegotiationContents(remoteAnswer.get())); + XCTAssert(localResponse == nullptr); + } else { + auto remoteOffer = remoteContext.contentNegotiationContext()->getPendingOffer(); + if (remoteOffer) { + auto localAnswer = localContext.contentNegotiationContext()->setRemoteNegotiationContent(copyNegotiationContents(remoteOffer.get())); + XCTAssert(localAnswer != nullptr); + + auto remoteResponse = remoteContext.contentNegotiationContext()->setRemoteNegotiationContent(copyNegotiationContents(localAnswer.get())); + XCTAssert(remoteResponse == nullptr); + } else { + return; + } + } + } + + XCTFail(@"Did not complete"); +} + +void runUntilStableConcurrent(Context &localContext, Context &remoteContext) { + std::vector> localNegotiationContent; + std::vector> remoteNegotiationContent; + + for (int i = 0; i < 6; i++) { + std::unique_ptr nextLocalNegotiationContent; + std::unique_ptr nextRemoteNegotiationContent; + + while (!localNegotiationContent.empty()) { + auto content = std::move(localNegotiationContent[0]); + localNegotiationContent.erase(localNegotiationContent.begin()); + + nextRemoteNegotiationContent = remoteContext.contentNegotiationContext()->setRemoteNegotiationContent(copyNegotiationContents(content.get())); + } + while (!remoteNegotiationContent.empty()) { + auto content = std::move(remoteNegotiationContent[0]); + remoteNegotiationContent.erase(remoteNegotiationContent.begin()); + + nextLocalNegotiationContent = localContext.contentNegotiationContext()->setRemoteNegotiationContent(copyNegotiationContents(content.get())); + } + + if (nextLocalNegotiationContent) { + localNegotiationContent.push_back(std::move(nextLocalNegotiationContent)); + } + if (nextRemoteNegotiationContent) { + remoteNegotiationContent.push_back(std::move(nextRemoteNegotiationContent)); + } + + auto localOffer = localContext.contentNegotiationContext()->getPendingOffer(); + if (localOffer) { + localNegotiationContent.push_back(std::move(localOffer)); + } + + auto remoteOffer = remoteContext.contentNegotiationContext()->getPendingOffer(); + if (remoteOffer) { + remoteNegotiationContent.push_back(std::move(remoteOffer)); + } + + if (localNegotiationContent.empty() && remoteNegotiationContent.empty()) { + return; + } + } + + XCTFail(@"Did not complete"); +} + +} + +@interface NegotiationTests : XCTestCase +@end + +@implementation NegotiationTests + +- (void)setUp { + [super setUp]; + + self.continueAfterFailure = false; +} + +- (void)tearDown { + [super tearDown]; +} + +- (void)testNegotiateEmpty { + Context localContext(true); + Context remoteContext(false); + + XCTAssert(localContext.contentNegotiationContext()->getPendingOffer() != nullptr); + XCTAssert(remoteContext.contentNegotiationContext()->getPendingOffer() != nullptr); +} + +- (void)testNegotiateAudioOnewayOutgoing { + Context localContext(true); + Context remoteContext(false); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + + localContext.contentNegotiationContext()->addOutgoingChannel(tgcalls::signaling::MediaContent::Type::Audio); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + + auto offer = localContext.contentNegotiationContext()->getPendingOffer(); + XCTAssert(offer != nullptr); + XCTAssert(offer->contents.size() == 1); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + + XCTAssert(localContext.contentNegotiationContext()->getPendingOffer() == nullptr); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + + auto response = remoteContext.contentNegotiationContext()->setRemoteNegotiationContent(copyNegotiationContents(offer.get())); + XCTAssert(response != nullptr); + XCTAssert(response->contents.size() == offer->contents.size()); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.size() == 1); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents[0].ssrc == offer->contents[0].ssrc); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + + auto backOffer = remoteContext.contentNegotiationContext()->getPendingOffer(); + XCTAssert(backOffer != nullptr); + XCTAssert(backOffer->contents.size() == 0); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.size() == 1); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents[0].ssrc == offer->contents[0].ssrc); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + + auto responseToAnswer = localContext.contentNegotiationContext()->setRemoteNegotiationContent(copyNegotiationContents(response.get())); + XCTAssert(responseToAnswer == nullptr); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.size() == 1); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents[0].ssrc == offer->contents[0].ssrc); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.size() == 1); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents[0].ssrc == offer->contents[0].ssrc); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + + auto nextOffer = localContext.contentNegotiationContext()->getPendingOffer(); + XCTAssert(nextOffer == nullptr); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.size() == 1); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents[0].ssrc == offer->contents[0].ssrc); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.size() == 1); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents[0].ssrc == offer->contents[0].ssrc); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); +} + +- (void)testNegotiateAudioOnewayIncoming { + Context localContext(true); + Context remoteContext(false); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + + remoteContext.contentNegotiationContext()->addOutgoingChannel(tgcalls::signaling::MediaContent::Type::Audio); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + + auto offer = localContext.contentNegotiationContext()->getPendingOffer(); + XCTAssert(offer != nullptr); + XCTAssert(offer->contents.empty()); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + + XCTAssert(localContext.contentNegotiationContext()->getPendingOffer() == nullptr); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + + auto response = remoteContext.contentNegotiationContext()->setRemoteNegotiationContent(copyNegotiationContents(offer.get())); + XCTAssert(response != nullptr); + XCTAssert(response->contents.size() == offer->contents.size()); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + + auto backOffer = remoteContext.contentNegotiationContext()->getPendingOffer(); + XCTAssert(backOffer != nullptr); + XCTAssert(backOffer->contents.size() == 1); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + + auto responseToAnswer = localContext.contentNegotiationContext()->setRemoteNegotiationContent(copyNegotiationContents(response.get())); + XCTAssert(responseToAnswer == nullptr); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + + auto responseToBackOffer = localContext.contentNegotiationContext()->setRemoteNegotiationContent(copyNegotiationContents(backOffer.get())); + XCTAssert(responseToBackOffer != nullptr); + XCTAssert(responseToBackOffer->contents.size() == 1); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.size() == 1); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents[0].ssrc == backOffer->contents[0].ssrc); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + + auto responseToBackOfferAnswer = remoteContext.contentNegotiationContext()->setRemoteNegotiationContent(copyNegotiationContents(responseToBackOffer.get())); + XCTAssert(responseToBackOfferAnswer == nullptr); + + XCTAssert(localContext.contentNegotiationContext()->getPendingOffer() == nullptr); + XCTAssert(remoteContext.contentNegotiationContext()->getPendingOffer() == nullptr); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.size() == 1); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents[0].ssrc == backOffer->contents[0].ssrc); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.size() == 1); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents[0].ssrc == backOffer->contents[0].ssrc); +} + +- (void)testNegotiateAudioTwoway { + Context localContext(true); + Context remoteContext(false); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + + localContext.contentNegotiationContext()->addOutgoingChannel(tgcalls::signaling::MediaContent::Type::Audio); + remoteContext.contentNegotiationContext()->addOutgoingChannel(tgcalls::signaling::MediaContent::Type::Audio); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + + auto offer = localContext.contentNegotiationContext()->getPendingOffer(); + XCTAssert(offer != nullptr); + XCTAssert(offer->contents.size() == 1); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + + XCTAssert(localContext.contentNegotiationContext()->getPendingOffer() == nullptr); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + + auto response = remoteContext.contentNegotiationContext()->setRemoteNegotiationContent(copyNegotiationContents(offer.get())); + XCTAssert(response != nullptr); + XCTAssert(response->contents.size() == offer->contents.size()); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.size() == 1); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents[0].ssrc == offer->contents[0].ssrc); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + + auto backOffer = remoteContext.contentNegotiationContext()->getPendingOffer(); + XCTAssert(backOffer != nullptr); + XCTAssert(backOffer->contents.size() == 1); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.size() == 1); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents[0].ssrc == offer->contents[0].ssrc); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + + auto responseToAnswer = localContext.contentNegotiationContext()->setRemoteNegotiationContent(copyNegotiationContents(response.get())); + XCTAssert(responseToAnswer == nullptr); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.empty()); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.size() == 1); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents[0].ssrc == offer->contents[0].ssrc); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.size() == 1); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents[0].ssrc == offer->contents[0].ssrc); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + + XCTAssert(localContext.contentNegotiationContext()->getPendingOffer() == nullptr); + + auto responseToBackOffer = localContext.contentNegotiationContext()->setRemoteNegotiationContent(copyNegotiationContents(backOffer.get())); + XCTAssert(responseToBackOffer != nullptr); + XCTAssert(responseToBackOffer->contents.size() == 1); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.size() == 1); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents[0].ssrc == backOffer->contents[0].ssrc); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.size() == 1); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents[0].ssrc == offer->contents[0].ssrc); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.size() == 1); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents[0].ssrc == offer->contents[0].ssrc); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.empty()); + + auto responseToBackOfferAnswer = remoteContext.contentNegotiationContext()->setRemoteNegotiationContent(copyNegotiationContents(responseToBackOffer.get())); + XCTAssert(responseToBackOfferAnswer == nullptr); + + XCTAssert(localContext.contentNegotiationContext()->getPendingOffer() == nullptr); + XCTAssert(remoteContext.contentNegotiationContext()->getPendingOffer() == nullptr); + + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents.size() == 1); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->incomingContents[0].ssrc == backOffer->contents[0].ssrc); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents.size() == 1); + XCTAssert(localContext.contentNegotiationContext()->coordinatedState()->outgoingContents[0].ssrc == offer->contents[0].ssrc); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents.size() == 1); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->incomingContents[0].ssrc == offer->contents[0].ssrc); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents.size() == 1); + XCTAssert(remoteContext.contentNegotiationContext()->coordinatedState()->outgoingContents[0].ssrc == backOffer->contents[0].ssrc); +} + +- (void)testConcurrentOffers { + Context localContext(true); + Context remoteContext(false); + + auto localOffer = localContext.contentNegotiationContext()->getPendingOffer(); + XCTAssert(localOffer != nullptr); + + auto remoteOffer = remoteContext.contentNegotiationContext()->getPendingOffer(); + XCTAssert(remoteOffer != nullptr); + + auto localAnswer = remoteContext.contentNegotiationContext()->setRemoteNegotiationContent(copyNegotiationContents(localOffer.get())); + XCTAssert(localAnswer != nullptr); + XCTAssert(localAnswer->exchangeId == localOffer->exchangeId); + + auto remoteAnswer = localContext.contentNegotiationContext()->setRemoteNegotiationContent(copyNegotiationContents(remoteOffer.get())); + XCTAssert(remoteAnswer == nullptr); +} + +- (void)service_runUntilStable1Using:(std::function)runUntilStable { + Context localContext(true); + Context remoteContext(false); + + runUntilStable(localContext, remoteContext); + + localContext.assertSsrcs({}, {}); + remoteContext.assertSsrcs({}, {}); + localContext.isContentsEqualToRemote(remoteContext); + + auto localAudioId = localContext.contentNegotiationContext()->addOutgoingChannel(tgcalls::signaling::MediaContent::Type::Audio); + + runUntilStable(localContext, remoteContext); + + auto localAudioSsrc = localContext.contentNegotiationContext()->outgoingChannelSsrc(localAudioId); + XCTAssert(localAudioSsrc); + + localContext.assertSsrcs({ localAudioSsrc.value() }, {}); + remoteContext.assertSsrcs({}, { localAudioSsrc.value() }); + localContext.isContentsEqualToRemote(remoteContext); + + auto remoteAudioId = remoteContext.contentNegotiationContext()->addOutgoingChannel(tgcalls::signaling::MediaContent::Type::Audio); + + runUntilStable(localContext, remoteContext); + + auto remoteAudioSsrc = remoteContext.contentNegotiationContext()->outgoingChannelSsrc(remoteAudioId); + XCTAssert(remoteAudioSsrc); + + localContext.assertSsrcs({ localAudioSsrc.value() }, { remoteAudioSsrc.value() }); + remoteContext.assertSsrcs({ remoteAudioSsrc.value() }, { localAudioSsrc.value() }); + localContext.isContentsEqualToRemote(remoteContext); + + auto remoteVideoId = remoteContext.contentNegotiationContext()->addOutgoingChannel(tgcalls::signaling::MediaContent::Type::Video); + + runUntilStable(localContext, remoteContext); + + auto remoteVideoSsrc = remoteContext.contentNegotiationContext()->outgoingChannelSsrc(remoteVideoId); + XCTAssert(remoteVideoSsrc); + + localContext.assertSsrcs({ localAudioSsrc.value() }, { remoteAudioSsrc.value(), remoteVideoSsrc.value() }); + remoteContext.assertSsrcs({ remoteAudioSsrc.value(), remoteVideoSsrc.value() }, { localAudioSsrc.value() }); + localContext.isContentsEqualToRemote(remoteContext); + + auto localVideoId = localContext.contentNegotiationContext()->addOutgoingChannel(tgcalls::signaling::MediaContent::Type::Video); + + runUntilStable(localContext, remoteContext); + + auto localVideoSsrc = localContext.contentNegotiationContext()->outgoingChannelSsrc(localVideoId); + XCTAssert(localVideoSsrc); + + localContext.assertSsrcs({ localAudioSsrc.value(), localVideoSsrc.value() }, { remoteAudioSsrc.value(), remoteVideoSsrc.value() }); + remoteContext.assertSsrcs({ remoteAudioSsrc.value(), remoteVideoSsrc.value() }, { localAudioSsrc.value(), localVideoSsrc.value() }); + localContext.isContentsEqualToRemote(remoteContext); + + auto remoteScreencastId = remoteContext.contentNegotiationContext()->addOutgoingChannel(tgcalls::signaling::MediaContent::Type::Video); + + runUntilStable(localContext, remoteContext); + + auto remoteScreencastSsrc = remoteContext.contentNegotiationContext()->outgoingChannelSsrc(remoteScreencastId); + XCTAssert(remoteScreencastSsrc); + + localContext.assertSsrcs({ localAudioSsrc.value(), localVideoSsrc.value() }, { remoteAudioSsrc.value(), remoteVideoSsrc.value(), remoteScreencastSsrc.value() }); + remoteContext.assertSsrcs({ remoteAudioSsrc.value(), remoteVideoSsrc.value(), remoteScreencastSsrc.value() }, { localAudioSsrc.value(), localVideoSsrc.value() }); + localContext.isContentsEqualToRemote(remoteContext); + + localContext.contentNegotiationContext()->removeOutgoingChannel(localVideoId); + + // local removal is reflected right away + localContext.assertSsrcs({ localAudioSsrc.value() }, { remoteAudioSsrc.value(), remoteVideoSsrc.value(), remoteScreencastSsrc.value() }); + remoteContext.assertSsrcs({ remoteAudioSsrc.value(), remoteVideoSsrc.value(), remoteScreencastSsrc.value() }, { localAudioSsrc.value(), localVideoSsrc.value() }); + localVideoSsrc = localContext.contentNegotiationContext()->outgoingChannelSsrc(localVideoId); + XCTAssert(!localVideoSsrc); + + runUntilStable(localContext, remoteContext); + + localVideoSsrc = localContext.contentNegotiationContext()->outgoingChannelSsrc(localVideoId); + XCTAssert(!localVideoSsrc); + + localContext.assertSsrcs({ localAudioSsrc.value() }, { remoteAudioSsrc.value(), remoteVideoSsrc.value(), remoteScreencastSsrc.value() }); + remoteContext.assertSsrcs({ remoteAudioSsrc.value(), remoteVideoSsrc.value(), remoteScreencastSsrc.value() }, { localAudioSsrc.value() }); + localContext.isContentsEqualToRemote(remoteContext); + + localVideoId = localContext.contentNegotiationContext()->addOutgoingChannel(tgcalls::signaling::MediaContent::Type::Video); + + runUntilStable(localContext, remoteContext); + + localVideoSsrc = localContext.contentNegotiationContext()->outgoingChannelSsrc(localVideoId); + XCTAssert(localVideoSsrc); + + localContext.assertSsrcs({ localAudioSsrc.value(), localVideoSsrc.value() }, { remoteAudioSsrc.value(), remoteVideoSsrc.value(), remoteScreencastSsrc.value() }); + remoteContext.assertSsrcs({ remoteAudioSsrc.value(), remoteVideoSsrc.value(), remoteScreencastSsrc.value() }, { localAudioSsrc.value(), localVideoSsrc.value() }); + localContext.isContentsEqualToRemote(remoteContext); +} + +- (void)service_runUntilStable2Using:(std::function)runUntilStable { + Context localContext(true); + Context remoteContext(false); + + runUntilStable(localContext, remoteContext); + + localContext.assertSsrcs({}, {}); + remoteContext.assertSsrcs({}, {}); + localContext.isContentsEqualToRemote(remoteContext); + + auto localAudioId = localContext.contentNegotiationContext()->addOutgoingChannel(tgcalls::signaling::MediaContent::Type::Audio); + auto remoteAudioId = remoteContext.contentNegotiationContext()->addOutgoingChannel(tgcalls::signaling::MediaContent::Type::Audio); + + runUntilStable(localContext, remoteContext); + + auto localAudioSsrc = localContext.contentNegotiationContext()->outgoingChannelSsrc(localAudioId); + XCTAssert(localAudioSsrc); + + auto remoteAudioSsrc = remoteContext.contentNegotiationContext()->outgoingChannelSsrc(remoteAudioId); + XCTAssert(remoteAudioSsrc); + + localContext.assertSsrcs({ localAudioSsrc.value() }, { remoteAudioSsrc.value() }); + remoteContext.assertSsrcs({ remoteAudioSsrc.value() }, { localAudioSsrc.value() }); + localContext.isContentsEqualToRemote(remoteContext); + + auto remoteVideoId = remoteContext.contentNegotiationContext()->addOutgoingChannel(tgcalls::signaling::MediaContent::Type::Video); + auto localVideoId = localContext.contentNegotiationContext()->addOutgoingChannel(tgcalls::signaling::MediaContent::Type::Video); + + runUntilStable(localContext, remoteContext); + + auto remoteVideoSsrc = remoteContext.contentNegotiationContext()->outgoingChannelSsrc(remoteVideoId); + XCTAssert(remoteVideoSsrc); + + auto localVideoSsrc = localContext.contentNegotiationContext()->outgoingChannelSsrc(localVideoId); + XCTAssert(localVideoSsrc); + + localContext.assertSsrcs({ localAudioSsrc.value(), localVideoSsrc.value() }, { remoteAudioSsrc.value(), remoteVideoSsrc.value() }); + remoteContext.assertSsrcs({ remoteAudioSsrc.value(), remoteVideoSsrc.value() }, { localAudioSsrc.value(), localVideoSsrc.value() }); + localContext.isContentsEqualToRemote(remoteContext); + + auto remoteScreencastId = remoteContext.contentNegotiationContext()->addOutgoingChannel(tgcalls::signaling::MediaContent::Type::Video); + + runUntilStable(localContext, remoteContext); + + auto remoteScreencastSsrc = remoteContext.contentNegotiationContext()->outgoingChannelSsrc(remoteScreencastId); + XCTAssert(remoteScreencastSsrc); + + localContext.assertSsrcs({ localAudioSsrc.value(), localVideoSsrc.value() }, { remoteAudioSsrc.value(), remoteVideoSsrc.value(), remoteScreencastSsrc.value() }); + remoteContext.assertSsrcs({ remoteAudioSsrc.value(), remoteVideoSsrc.value(), remoteScreencastSsrc.value() }, { localAudioSsrc.value(), localVideoSsrc.value() }); + localContext.isContentsEqualToRemote(remoteContext); + + localContext.contentNegotiationContext()->removeOutgoingChannel(localVideoId); + + // local removal is reflected right away + localContext.assertSsrcs({ localAudioSsrc.value() }, { remoteAudioSsrc.value(), remoteVideoSsrc.value(), remoteScreencastSsrc.value() }); + remoteContext.assertSsrcs({ remoteAudioSsrc.value(), remoteVideoSsrc.value(), remoteScreencastSsrc.value() }, { localAudioSsrc.value(), localVideoSsrc.value() }); + localVideoSsrc = localContext.contentNegotiationContext()->outgoingChannelSsrc(localVideoId); + XCTAssert(!localVideoSsrc); + + runUntilStable(localContext, remoteContext); + + localVideoSsrc = localContext.contentNegotiationContext()->outgoingChannelSsrc(localVideoId); + XCTAssert(!localVideoSsrc); + + localContext.assertSsrcs({ localAudioSsrc.value() }, { remoteAudioSsrc.value(), remoteVideoSsrc.value(), remoteScreencastSsrc.value() }); + remoteContext.assertSsrcs({ remoteAudioSsrc.value(), remoteVideoSsrc.value(), remoteScreencastSsrc.value() }, { localAudioSsrc.value() }); + localContext.isContentsEqualToRemote(remoteContext); + + localVideoId = localContext.contentNegotiationContext()->addOutgoingChannel(tgcalls::signaling::MediaContent::Type::Video); + + runUntilStable(localContext, remoteContext); + + localVideoSsrc = localContext.contentNegotiationContext()->outgoingChannelSsrc(localVideoId); + XCTAssert(localVideoSsrc); + + localContext.assertSsrcs({ localAudioSsrc.value(), localVideoSsrc.value() }, { remoteAudioSsrc.value(), remoteVideoSsrc.value(), remoteScreencastSsrc.value() }); + remoteContext.assertSsrcs({ remoteAudioSsrc.value(), remoteVideoSsrc.value(), remoteScreencastSsrc.value() }, { localAudioSsrc.value(), localVideoSsrc.value() }); + localContext.isContentsEqualToRemote(remoteContext); +} +- (void)testConvergenceSequential1 { + [self service_runUntilStable1Using:&runUntilStableSequential]; +} + +- (void)testConvergenceSequential2 { + [self service_runUntilStable2Using:&runUntilStableSequential]; +} + +- (void)testConvergenceConcurrent1 { + [self service_runUntilStable1Using:&runUntilStableConcurrent]; +} + +- (void)testConvergenceConcurrent2 { + [self service_runUntilStable2Using:&runUntilStableConcurrent]; +} + +@end diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 33af020811..95369c01df 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 33af0208111e5994e33c7fbb8872cd08bf38edd7 +Subproject commit 95369c01dfc8639b89dd91c4bc6e9da2ae2ed223