diff --git a/CMake/Dependencies/libawscpp-CMakeLists.txt b/CMake/Dependencies/libawscpp-CMakeLists.txt index 60f6bee6f8..088e34cd86 100644 --- a/CMake/Dependencies/libawscpp-CMakeLists.txt +++ b/CMake/Dependencies/libawscpp-CMakeLists.txt @@ -2,13 +2,28 @@ cmake_minimum_required(VERSION 3.6.3) project(libawscpp-download NONE) include(ExternalProject) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + message(STATUS "Configuring for Linux") + set(TARGET_ARCH LINUX) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + message(STATUS "Configuring for macOS") + set(TARGET_ARCH APPLE) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") + message(STATUS "Configuring for Windows") + set(TARGET_ARCH WINDOWS) +endif() + ExternalProject_Add(libawscpp-download GIT_REPOSITORY https://github.com/aws/aws-sdk-cpp.git GIT_TAG 1.11.217 LIST_SEPARATOR "|" - CMAKE_ARGS -DBUILD_SHARED_LIBS=OFF - -DBUILD_ONLY=kinesisvideo|kinesis-video-webrtc-storage + CMAKE_ARGS -DBUILD_SHARED_LIBS=ON + -DENABLE_TESTING=OFF + -DBUILD_ONLY=${BUILD_ONLY} -DCMAKE_INSTALL_PREFIX=${OPEN_SRC_INSTALL_PREFIX} + -DCMAKE_PREFIX_PATH=${OPEN_SRC_INSTALL_PREFIX} + -DTARGET_ARCH=${TARGET_ARCH} + -DCUSTOM_MEMORY_MANAGEMENT=OFF BUILD_ALWAYS TRUE TEST_COMMAND "" ) diff --git a/CMakeLists.txt b/CMakeLists.txt index 38d2f984dd..0430a9925d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,6 +21,7 @@ option(ENABLE_DATA_CHANNEL "Enable support for data channel" ON) option(ENABLE_KVS_THREADPOOL "Enable support for KVS thread pool in signaling" ON) option(INSTRUMENTED_ALLOCATORS "Enable memory instrumentation" OFF) option(ENABLE_AWS_SDK_IN_TESTS "Enable support for compiling AWS SDKs for tests" ON) +option(ENABLE_AWS_SDK_INTEG "Enable building samples with cloudwatch" OFF) # Developer Flags option(BUILD_TEST "Build the testing tree." OFF) @@ -233,9 +234,12 @@ if(BUILD_DEPENDENCIES) if(BUILD_TEST) build_dependency(gtest) - + set(BUILD_ARGS + -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} + -DCMAKE_C_FLAGS=${CMAKE_C_FLAGS} + -DBUILD_ONLY=kinesisvideo|kinesis-video-webrtc-storage) if(ENABLE_AWS_SDK_IN_TESTS) - build_dependency(awscpp) + build_dependency(awscpp ${BUILD_ARGS}) endif() endif() @@ -247,9 +251,18 @@ if(BUILD_DEPENDENCIES) if (LINK_PROFILER) build_dependency(gperftools) endif() + message(STATUS "Finished building dependencies.") endif() +if(ENABLE_AWS_SDK_INTEG) + set(BUILD_ARGS + -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} + -DCMAKE_C_FLAGS=${CMAKE_C_FLAGS} + -DBUILD_ONLY=monitoring|logs) + build_dependency(awscpp ${BUILD_ARGS}) +endif() + # building kvsCommonLws also builds kvspic set(BUILD_ARGS -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} @@ -481,6 +494,9 @@ if (BUILD_SAMPLE) add_subdirectory(samples) endif() +if(ENABLE_AWS_SDK_INTEG) + add_subdirectory(cloudwatch-integ) +endif() if(BUILD_TEST) # adding ZLIB because aws sdk static link seems to be broken when zlib is needed if(NOT WIN32) diff --git a/README.md b/README.md index 114891e447..d2510cc0ee 100644 --- a/README.md +++ b/README.md @@ -251,11 +251,9 @@ The SDK also tracks entry and exit of functions which increases the verbosity of `add_definitions(-DLOG_STREAMING)` Note: This log level is extremely VERBOSE and could flood the files if using file based logging strategy. -
- Time-to-first-frame breakdown metrics +### Time-to-first-frame breakdown metrics to JS viewer -There is a flag in the sample application which (pSampleConfiguration->enableSendingMetricsToViewerViaDc) can be set to TRUE to send metrics from the master to the [JS viewer](https://awslabs.github.io/amazon-kinesis-video-streams-webrtc-sdk-js/examples/index.html). This helps get a detailed breakdown of time-to-first-frame and all the processes and API calls on master and the viewer both. This is intended to be used with the KVS WebRTC C SDK running as the master and the JS SDK as the viewer. The master sends peer, ice-agent, signaling and data-channel metrics to the viewer which are plotted ~ 20 seconds after the viewer is started. Since the timeline plot is intended to understand the time-to-first-frame, the sample web page needs to be refreshed and the master needs to be restarted if a new / updated plot is needed. While using the SDK in this mode, it is expected that all datachannel messages are JSON messages. This feature is only meant to be used for a single viewer at a time. -
+There is a flag in the sample application which (`pSampleConfiguration->enableSendingMetricsToViewerViaDc`) can be set to TRUE to send metrics from the master to the [JS viewer](https://awslabs.github.io/amazon-kinesis-video-streams-webrtc-sdk-js/examples/index.html). This helps get a detailed breakdown of time-to-first-frame and all the processes and API calls on master and the viewer both. This is intended to be used with the KVS WebRTC C SDK running as the master and the JS SDK as the viewer. The master sends peer, ice-agent, signaling and data-channel metrics to the viewer which are plotted ~ 20 seconds after the viewer is started. Since the timeline plot is intended to understand the time-to-first-frame, the sample web page needs to be refreshed and the master needs to be restarted if a new / updated plot is needed. While using the SDK in this mode, it is expected that all datachannel messages are JSON messages. This feature is only meant to be used for a single viewer at a time. ### Set path to SSL CA certificate (**Optional**) @@ -450,7 +448,7 @@ The certificate generating function ([createCertificateAndKey](https://awslabs.g **Important Note: It is recommended to rotate the certificates often - preferably for every peer connection to avoid a compromised client weakening the security of the new connections.** Take `kvsWebRTCClientMaster` as sample, add `RtcCertificate certificates[CERT_COUNT];` to **SampleConfiguration** in [Samples.h](./samples/Samples.h). -Then pass in the pre-generated certificate in initializePeerConnection() in [Common.c](./samples/Common.c). +Then pass in the pre-generated certificate in initializePeerConnection() in [Common.c](samples/lib/Common.c). ```c configuration.certificates[0].pCertificate = pSampleConfiguration->certificates[0].pCertificate; diff --git a/cloudwatch-integ/CMakeLists.txt b/cloudwatch-integ/CMakeLists.txt new file mode 100644 index 0000000000..b2404c2eb3 --- /dev/null +++ b/cloudwatch-integ/CMakeLists.txt @@ -0,0 +1,56 @@ +cmake_minimum_required(VERSION 3.6.3) + +project(KinesisVideoWebRTCClientSamplesCloudwatch LANGUAGES C CXX) +set(CMAKE_CXX_STANDARD 11) + +message("OPEN_SRC_INSTALL_PREFIX=${OPEN_SRC_INSTALL_PREFIX}") + +include_directories(${OPEN_SRC_INSTALL_PREFIX}/include) +include_directories(${OPEN_SRC_INCLUDE_DIRS}) +link_directories(${OPEN_SRC_INSTALL_PREFIX}/lib) + +find_package(ZLIB REQUIRED) +find_package(AWSSDK REQUIRED COMPONENTS monitoring logs) + +set(CW_CONFIG_HEADER "configs/default_config.h" CACHE FILEPATH "Config header for the cloudwatch integrated demo") + +add_definitions(-DCW_CONFIG_HEADER="${CW_CONFIG_HEADER}") +message(STATUS "Config header set to ${CW_CONFIG_HEADER}") + +add_executable( + kvsWebrtcClientMasterCW + Cloudwatch.cpp + CloudwatchLogs.cpp + CloudwatchMonitoring.cpp + ../samples/lib/Common.c + ../samples/lib/Utility.c + ../samples/lib/MetricsHandling.c + ../samples/lib/DataChannelHandling.c + ../samples/lib/SignalingMsgHandler.c + ../samples/lib/Media.c + kvsWebRTCClientMasterCloudwatch.cpp) +target_link_libraries(kvsWebrtcClientMasterCW + kvsWebrtcClient + kvsWebrtcSignalingClient + ${EXTRA_DEPS} + kvsCommonLws kvspicUtils websockets kvssdp kvsstun + ${AWSSDK_LINK_LIBRARIES}) + +add_executable( + kvsWebrtcClientViewerCW + ../samples/lib/Common.c + ../samples/lib/Utility.c + ../samples/lib/MetricsHandling.c + ../samples/lib/DataChannelHandling.c + ../samples/lib/SignalingMsgHandler.c + ../samples/lib/Media.c + Cloudwatch.cpp + CloudwatchLogs.cpp + CloudwatchMonitoring.cpp + kvsWebRTCClientViewerCloudwatch.cpp) +target_link_libraries(kvsWebrtcClientViewerCW + kvsWebrtcClient + kvsWebrtcSignalingClient + ${EXTRA_DEPS} + kvsCommonLws kvspicUtils websockets kvssdp kvsstun + ${AWSSDK_LINK_LIBRARIES}) diff --git a/cloudwatch-integ/Cloudwatch.cpp b/cloudwatch-integ/Cloudwatch.cpp new file mode 100644 index 0000000000..9d02af316b --- /dev/null +++ b/cloudwatch-integ/Cloudwatch.cpp @@ -0,0 +1,82 @@ +#include "Include.h" +#include "Cloudwatch.h" + +namespace CppInteg { + +Cloudwatch::Cloudwatch(ClientConfiguration* pClientConfig) + : logs(pClientConfig), monitoring(pClientConfig), terminated(FALSE) +{ +} + +STATUS Cloudwatch::init(PCHAR channelName, PCHAR region, BOOL isMaster, BOOL isStorage) +{ + ENTERS(); + STATUS retStatus = STATUS_SUCCESS; + ClientConfiguration clientConfig; + CreateLogGroupRequest createLogGroupRequest; + Aws::CloudWatchLogs::Model::CreateLogStreamOutcome createLogStreamOutcome; + CreateLogStreamRequest createLogStreamRequest; + + clientConfig.region = region; + auto& instance = getInstanceImpl(&clientConfig); + + if (STATUS_FAILED(instance.logs.init(channelName, region, isMaster, isStorage))) { + DLOGW("Failed to create Cloudwatch logger"); + } else { + globalCustomLogPrintFn = logger; + } + + CHK_STATUS(instance.monitoring.init(channelName, region, isMaster, isStorage)); + +CleanUp: + + LEAVES(); + return retStatus; +} + +Cloudwatch& Cloudwatch::getInstance() +{ + return getInstanceImpl(); +} + +Cloudwatch& Cloudwatch::getInstanceImpl(ClientConfiguration* pClientConfig) +{ + static Cloudwatch instance{pClientConfig}; + return instance; +} + +VOID Cloudwatch::deinit() +{ + auto& instance = getInstance(); + instance.logs.deinit(); + instance.monitoring.deinit(); + instance.terminated = TRUE; +} + +VOID Cloudwatch::logger(UINT32 level, PCHAR tag, PCHAR fmt, ...) +{ + CHAR logFmtString[MAX_LOG_FORMAT_LENGTH + 1]; + CHAR cwLogFmtString[MAX_LOG_FORMAT_LENGTH + 1]; + UINT32 logLevel = GET_LOGGER_LOG_LEVEL(); + UNUSED_PARAM(tag); + + if (level >= logLevel) { + addLogMetadata(logFmtString, (UINT32) ARRAY_SIZE(logFmtString), fmt, level); + + // Creating a copy to store the logFmtString for cloudwatch logging purpose + va_list valist, valist_cw; + va_start(valist_cw, fmt); + vsnprintf(cwLogFmtString, (SIZE_T) SIZEOF(cwLogFmtString), logFmtString, valist_cw); + va_end(valist_cw); + va_start(valist, fmt); + vprintf(logFmtString, valist); + va_end(valist); + + auto& instance = getInstance(); + if (!instance.terminated) { + instance.logs.push(cwLogFmtString); + } + } +} + +} // namespace Canary diff --git a/cloudwatch-integ/Cloudwatch.h b/cloudwatch-integ/Cloudwatch.h new file mode 100644 index 0000000000..d91e5f3a70 --- /dev/null +++ b/cloudwatch-integ/Cloudwatch.h @@ -0,0 +1,31 @@ +#pragma once + +#include "Include.h" +#include "CloudwatchLogs.h" +#include "CloudwatchMonitoring.h" + +namespace CppInteg { + +class Cloudwatch { + public: + Cloudwatch() = delete; + Cloudwatch(Cloudwatch const&) = delete; + void operator=(Cloudwatch const&) = delete; + + CloudwatchLogs logs; + CloudwatchMonitoring monitoring; + + static Cloudwatch& getInstance(); + static STATUS init(PCHAR channelName, PCHAR region, BOOL isMaster, BOOL isStorage); + static VOID deinit(); + static VOID logger(UINT32, PCHAR, PCHAR, ...); + + private: + static Cloudwatch& getInstanceImpl(ClientConfiguration* = nullptr); + + Cloudwatch(ClientConfiguration*); + BOOL terminated; +}; +typedef Cloudwatch* PCloudwatch; + +} // namespace Canary diff --git a/cloudwatch-integ/CloudwatchLogs.cpp b/cloudwatch-integ/CloudwatchLogs.cpp new file mode 100644 index 0000000000..798cc3d678 --- /dev/null +++ b/cloudwatch-integ/CloudwatchLogs.cpp @@ -0,0 +1,123 @@ +#include "Include.h" +#include "CloudwatchLogs.h" + +namespace CppInteg { + +CloudwatchLogs::CloudwatchLogs(ClientConfiguration* pClientConfig) : client(*pClientConfig) +{ +} + +STATUS CloudwatchLogs::init(PCHAR channelName, PCHAR region, BOOL isMaster, BOOL isStorage) +{ + STATUS retStatus = STATUS_SUCCESS; + CreateLogGroupRequest createLogGroupRequest; + Aws::CloudWatchLogs::Model::CreateLogStreamOutcome createLogStreamOutcome; + CreateLogStreamRequest createLogStreamRequest; + std::stringstream defaultLogStreamName; + if(isStorage) { + defaultLogStreamName << channelName << '-' << "StorageMaster" << '-' + << GETTIME() / HUNDREDS_OF_NANOS_IN_A_MILLISECOND; + + } else { + defaultLogStreamName << channelName << '-' << (isMaster ? "master" : "viewer") << '-' + << GETTIME() / HUNDREDS_OF_NANOS_IN_A_MILLISECOND; + + } + + this->logStreamName = defaultLogStreamName.str(); + this->logGroupName = LOG_GROUP_NAME; + + DLOGI("Log stream name: %s", this->logStreamName.c_str()); + + createLogGroupRequest.SetLogGroupName(this->logGroupName); + // ignore error since if this operation fails, CreateLogStream should fail as well. + // There might be some errors that can lead to successfull CreateLogStream, e.g. log group already exists. + this->client.CreateLogGroup(createLogGroupRequest); + + createLogStreamRequest.SetLogGroupName(this->logGroupName); + createLogStreamRequest.SetLogStreamName(this->logStreamName); + createLogStreamOutcome = this->client.CreateLogStream(createLogStreamRequest); + + CHK_ERR(createLogStreamOutcome.IsSuccess(), STATUS_INVALID_OPERATION, "Failed to create \"%s\" log stream: %s", + this->logStreamName.c_str(), createLogStreamOutcome.GetError().GetMessage().c_str()); + +CleanUp: + + return retStatus; +} + +VOID CloudwatchLogs::deinit() +{ + this->flush(TRUE); +} + +VOID CloudwatchLogs::push(string log) +{ + std::lock_guard lock(this->sync.mutex); + Aws::String awsCwString(log.c_str(), log.size()); + auto logEvent = + Aws::CloudWatchLogs::Model::InputLogEvent().WithMessage(awsCwString).WithTimestamp(GETTIME() / HUNDREDS_OF_NANOS_IN_A_MILLISECOND); + this->logs.push_back(logEvent); + if (this->logs.size() >= MAX_CLOUDWATCH_LOG_COUNT) { + this->flush(); + } +} + +VOID CloudwatchLogs::flush(BOOL sync) +{ + std::unique_lock lock(this->sync.mutex); + if (this->logs.size() == 0) { + return; + } + auto pendingLogs = this->logs; + this->logs.clear(); + + // wait until previous logs have been flushed entirely + auto waitUntilFlushed = [this] { return !this->sync.pending.load(); }; + this->sync.await.wait(lock, waitUntilFlushed); + + auto request = Aws::CloudWatchLogs::Model::PutLogEventsRequest() + .WithLogGroupName(this->logGroupName) + .WithLogStreamName(this->logStreamName) + .WithLogEvents(pendingLogs); + + if (this->token != "") { + request.SetSequenceToken(this->token); + } + + if (!sync) { + auto asyncHandler = [this](const Aws::CloudWatchLogs::CloudWatchLogsClient* cwClientLog, + const Aws::CloudWatchLogs::Model::PutLogEventsRequest& request, + const Aws::CloudWatchLogs::Model::PutLogEventsOutcome& outcome, + const std::shared_ptr& context) { + UNUSED_PARAM(cwClientLog); + UNUSED_PARAM(request); + UNUSED_PARAM(context); + + if (!outcome.IsSuccess()) { + // Need to use printf so that we don't get into an infinite loop where we keep flushing + printf("Failed to push logs: %s\n", outcome.GetError().GetMessage().c_str()); + } else { + printf("Successfully pushed logs to cloudwatch\n"); + this->token = outcome.GetResult().GetNextSequenceToken(); + } + + this->sync.pending = FALSE; + this->sync.await.notify_one(); + }; + + this->sync.pending = TRUE; + this->client.PutLogEventsAsync(request, asyncHandler); + } else { + auto outcome = this->client.PutLogEvents(request); + if (!outcome.IsSuccess()) { + // Need to use printf so that we don't get into an infinite loop where we keep flushing + printf("Failed to push logs: %s\n", outcome.GetError().GetMessage().c_str()); + } else { + DLOGS("Successfully pushed logs to cloudwatch"); + this->token = outcome.GetResult().GetNextSequenceToken(); + } + } +} + +} // namespace Canary diff --git a/cloudwatch-integ/CloudwatchLogs.h b/cloudwatch-integ/CloudwatchLogs.h new file mode 100644 index 0000000000..e63b50d418 --- /dev/null +++ b/cloudwatch-integ/CloudwatchLogs.h @@ -0,0 +1,28 @@ +#pragma once + +namespace CppInteg { + +class CloudwatchLogs { + public: + CloudwatchLogs(ClientConfiguration*); + STATUS init(PCHAR channelName, PCHAR region, BOOL isMaster, BOOL isStorage); + VOID deinit(); + VOID push(string log); + VOID flush(BOOL sync = FALSE); + std::string logGroupName, logStreamName; + + private: + class Synchronization { + public: + std::atomic pending; + std::recursive_mutex mutex; + std::condition_variable_any await; + }; + + CloudWatchLogsClient client; + Synchronization sync; + Aws::Vector logs; + Aws::String token; +}; + +} // namespace Canary diff --git a/cloudwatch-integ/CloudwatchMonitoring.cpp b/cloudwatch-integ/CloudwatchMonitoring.cpp new file mode 100644 index 0000000000..693792c5ba --- /dev/null +++ b/cloudwatch-integ/CloudwatchMonitoring.cpp @@ -0,0 +1,409 @@ +#include "Include.h" +#include "CloudwatchMonitoring.h" + +namespace CppInteg { + +CloudwatchMonitoring::CloudwatchMonitoring(ClientConfiguration* pClientConfig) : client(*pClientConfig) +{ +} + +STATUS CloudwatchMonitoring::init(PCHAR channelName, PCHAR region, BOOL isMaster, BOOL isStorage) +{ + STATUS retStatus = STATUS_SUCCESS; + this->isStorage = isStorage; + isStorage ? this->channelDimension.SetName(INDIVIDUAL_STORAGE_CW_DIMENSION) : this->channelDimension.SetName(INDIVIDUAL_CW_DIMENSION); + this->channelDimension.SetValue(channelName); + + isStorage ? this->labelDimension.SetName(AGGREGATE_STORAGE_CW_DIMENSION) : this->labelDimension.SetName(AGGREGATE_CW_DIMENSION); + this->labelDimension.SetValue(SCENARIO_LABEL); + + return retStatus; +} + +VOID CloudwatchMonitoring::deinit() +{ + // need to wait all metrics to be flushed out, otherwise we'll get a segfault. + // https://docs.aws.amazon.com/sdk-for-cpp/v1/developer-guide/basic-use.html + // TODO: maybe add a timeout? But, this might cause a segfault if it hits a timeout. + while (this->pendingMetrics.load() > 0) { + THREAD_SLEEP(HUNDREDS_OF_NANOS_IN_A_MILLISECOND * 500); + } +} + +static const CHAR* unitToString(const Aws::CloudWatch::Model::StandardUnit& unit) +{ + switch (unit) { + case Aws::CloudWatch::Model::StandardUnit::Count: + return "Count"; + case Aws::CloudWatch::Model::StandardUnit::Count_Second: + return "Count_Second"; + case Aws::CloudWatch::Model::StandardUnit::Milliseconds: + return "Milliseconds"; + case Aws::CloudWatch::Model::StandardUnit::Percent: + return "Percent"; + case Aws::CloudWatch::Model::StandardUnit::None: + return "None"; + case Aws::CloudWatch::Model::StandardUnit::Kilobits_Second: + return "Kilobits_Second"; + default: + return "Unknown unit"; + } +} + +VOID CloudwatchMonitoring::push(const MetricDatum& datum) +{ + Aws::CloudWatch::Model::PutMetricDataRequest cwRequest; + MetricDatum single = datum; + MetricDatum aggregated = datum; + + single.AddDimensions(this->channelDimension); + single.AddDimensions(this->labelDimension); + aggregated.AddDimensions(this->labelDimension); + + cwRequest.SetNamespace(DEFAULT_CLOUDWATCH_NAMESPACE); + cwRequest.AddMetricData(single); + cwRequest.AddMetricData(aggregated); + + auto asyncHandler = [this](const Aws::CloudWatch::CloudWatchClient* cwClient, const Aws::CloudWatch::Model::PutMetricDataRequest& request, + const Aws::CloudWatch::Model::PutMetricDataOutcome& outcome, + const std::shared_ptr& context) { + UNUSED_PARAM(cwClient); + UNUSED_PARAM(request); + UNUSED_PARAM(context); + + if (!outcome.IsSuccess()) { + DLOGE("Failed to put sample metric data: %s", outcome.GetError().GetMessage().c_str()); + } else { + DLOGS("Successfully put sample metric data"); + } + this->pendingMetrics--; + }; + this->pendingMetrics++; + this->client.PutMetricDataAsync(cwRequest, asyncHandler); + + std::stringstream ss; + + ss << "Emitted the following metric:\n\n"; + ss << " Name : " << datum.GetMetricName() << '\n'; + ss << " Unit : " << unitToString(datum.GetUnit()) << '\n'; + + ss << " Values : "; + auto& values = datum.GetValues(); + // If the datum uses single value, GetValues will be empty and the data will be accessible + // from GetValue + if (values.empty()) { + ss << datum.GetValue(); + } else { + for (auto i = 0; i < values.size(); i++) { + ss << values[i]; + if (i != values.size() - 1) { + ss << ", "; + } + } + } + ss << '\n'; + + ss << " Dimensions : "; + auto& dimensions = datum.GetDimensions(); + if (dimensions.empty()) { + ss << "N/A"; + } else { + ss << '\n'; + for (auto& dimension : dimensions) { + ss << " - " << dimension.GetName() << "\t: " << dimension.GetValue() << '\n'; + } + } + ss << '\n'; + + DLOGD("%s", ss.str().c_str()); +} + +VOID CloudwatchMonitoring::pushExitStatus(STATUS retStatus) +{ + MetricDatum datum; + Dimension statusDimension; + CHAR status[MAX_STATUS_CODE_LENGTH]; + + statusDimension.SetName("Code"); + SPRINTF(status, "0x%08x", retStatus); + statusDimension.SetValue(status); + + datum.SetMetricName("ExitStatus"); + datum.SetValue(1.0); + datum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Count); + + datum.AddDimensions(statusDimension); + + this->push(datum); +} + +VOID CloudwatchMonitoring::pushTimeToFirstFrame(UINT64 timeToFirstFrame, Aws::CloudWatch::Model::StandardUnit unit) +{ + MetricDatum datum; + + datum.SetMetricName("TimeToFirstFrame"); + datum.SetValue(timeToFirstFrame); + datum.SetUnit(unit); + + this->push(datum); +} + + + +VOID CloudwatchMonitoring::pushStorageDisconnectToFrameSentTime(UINT64 storageDisconnectToFrameSentTime, Aws::CloudWatch::Model::StandardUnit unit) +{ + MetricDatum datum; + + datum.SetMetricName("StorageDisconnectToFrameSentTime"); + datum.SetValue(storageDisconnectToFrameSentTime); + datum.SetUnit(unit); + + this->push(datum); +} + +VOID CloudwatchMonitoring::pushJoinSessionTime(UINT64 joinSessionTime, Aws::CloudWatch::Model::StandardUnit unit) +{ + MetricDatum datum; + + datum.SetMetricName("JoinSessionTime"); + datum.SetValue(joinSessionTime); + datum.SetUnit(unit); + + this->push(datum); +} + +VOID CloudwatchMonitoring::pushOutboundRtpStats(POutgoingRTPStatsCtx pOutboundRtpStats) +{ + MetricDatum bytesDiscardedPercentageDatum, averageFramesRateDatum, nackRateDatum, retransmissionPercentDatum; + + bytesDiscardedPercentageDatum.SetMetricName("PercentageFrameDiscarded"); + bytesDiscardedPercentageDatum.SetValue(pOutboundRtpStats->framesPercentageDiscarded); + bytesDiscardedPercentageDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Percent); + this->push(bytesDiscardedPercentageDatum); + + averageFramesRateDatum.SetMetricName("FramesPerSecond"); + averageFramesRateDatum.SetValue(pOutboundRtpStats->averageFramesSentPerSecond); + averageFramesRateDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Count_Second); + this->push(averageFramesRateDatum); + + nackRateDatum.SetMetricName("NackPerSecond"); + nackRateDatum.SetValue(pOutboundRtpStats->nacksPerSecond); + nackRateDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Count_Second); + this->push(nackRateDatum); + + retransmissionPercentDatum.SetMetricName("PercentageFramesRetransmitted"); + retransmissionPercentDatum.SetValue(pOutboundRtpStats->retxBytesPercentage); + retransmissionPercentDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Percent); + this->push(retransmissionPercentDatum); +} + +VOID CloudwatchMonitoring::pushPeerConnectionMetrics(PPeerConnectionMetrics pPeerConnectionMetrics) +{ + MetricDatum pcCreationDatum, dtlsSetupDatum, iceHolePunchingDatum; + + if(pPeerConnectionMetrics == NULL) { + DLOGE("Peer connection metrics object is NULL. Cannot go further"); + return; + } + pcCreationDatum.SetMetricName("PcCreationTime"); + pcCreationDatum.SetValue(pPeerConnectionMetrics->peerConnectionStats.peerConnectionCreationTime); + pcCreationDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Milliseconds); + this->push(pcCreationDatum); + + dtlsSetupDatum.SetMetricName("DtlsSetupTime"); + dtlsSetupDatum.SetValue(pPeerConnectionMetrics->peerConnectionStats.dtlsSessionSetupTime); + dtlsSetupDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Milliseconds); + this->push(dtlsSetupDatum); + + iceHolePunchingDatum.SetMetricName("ICEHolePunchingDelay"); + iceHolePunchingDatum.SetValue(pPeerConnectionMetrics->peerConnectionStats.iceHolePunchingTime); + iceHolePunchingDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Milliseconds); + this->push(iceHolePunchingDatum); +} + +VOID CloudwatchMonitoring::pushKvsIceAgentMetrics(PKvsIceAgentMetrics pKvsIceAgentMetrics) +{ + MetricDatum localCandidateGatheringDatum, hostCandidateSetupDatum, srflxCandidateSetUpDatum, + iceAgentSetupDatum, relayCandidateSetUpDatum, iceServerParseDatum, + iceCandidatePairNominationDatum, iceCandidateGatheringDatum; + + if(pKvsIceAgentMetrics == NULL) { + DLOGE("ICE agent metrics object is NULL. Cannot go further"); + return; + } + + localCandidateGatheringDatum.SetMetricName("LocalCandidateGatheringTime"); + localCandidateGatheringDatum.SetValue(pKvsIceAgentMetrics->kvsIceAgentStats.localCandidateGatheringTime); + localCandidateGatheringDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Milliseconds); + this->push(localCandidateGatheringDatum); + + hostCandidateSetupDatum.SetMetricName("HostCandidateSetUpTime"); + hostCandidateSetupDatum.SetValue(pKvsIceAgentMetrics->kvsIceAgentStats.hostCandidateSetUpTime); + hostCandidateSetupDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Milliseconds); + this->push(hostCandidateSetupDatum); + + srflxCandidateSetUpDatum.SetMetricName("SrflxCandidateSetUpTime"); + srflxCandidateSetUpDatum.SetValue(pKvsIceAgentMetrics->kvsIceAgentStats.srflxCandidateSetUpTime); + srflxCandidateSetUpDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Milliseconds); + this->push(srflxCandidateSetUpDatum); + + relayCandidateSetUpDatum.SetMetricName("RelayCandidateSetUpTime"); + relayCandidateSetUpDatum.SetValue(pKvsIceAgentMetrics->kvsIceAgentStats.relayCandidateSetUpTime); + relayCandidateSetUpDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Milliseconds); + this->push(relayCandidateSetUpDatum); + + iceServerParseDatum.SetMetricName("IceServerResolutionTime"); + iceServerParseDatum.SetValue(pKvsIceAgentMetrics->kvsIceAgentStats.iceServerParsingTime); + iceServerParseDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Milliseconds); + this->push(iceServerParseDatum); + + iceCandidatePairNominationDatum.SetMetricName("IceCandidatePairNominationTime"); + iceCandidatePairNominationDatum.SetValue(pKvsIceAgentMetrics->kvsIceAgentStats.iceCandidatePairNominationTime); + iceCandidatePairNominationDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Milliseconds); + this->push(iceCandidatePairNominationDatum); + + iceCandidateGatheringDatum.SetMetricName("IcecandidateGatheringTime"); + iceCandidateGatheringDatum.SetValue(pKvsIceAgentMetrics->kvsIceAgentStats.candidateGatheringTime); + iceCandidateGatheringDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Milliseconds); + this->push(iceCandidateGatheringDatum); + + iceAgentSetupDatum.SetMetricName("IceAgentSetUpTime"); + iceAgentSetupDatum.SetValue(pKvsIceAgentMetrics->kvsIceAgentStats.iceAgentSetUpTime); + iceAgentSetupDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Milliseconds); + this->push(iceAgentSetupDatum); +} + +VOID CloudwatchMonitoring::pushSignalingClientMetrics(PSignalingClientMetrics pSignalingClientMetrics) +{ + MetricDatum offerToAnswerDatum, getTokenDatum, describeDatum, createDatum, endpointDatum, + iceConfigDatum, connectDatum, createClientDatum, fetchDatum, connectClientDatum, joinSessionToOfferDatum; + + UINT64 joinSessionToOffer, joinSessionCallTime; + + if(pSignalingClientMetrics == NULL) { + DLOGE("Signaling metrics object is NULL. Cannot go further"); + return; + } + + offerToAnswerDatum.SetMetricName("OfferToAnswerTime"); + offerToAnswerDatum.SetValue(pSignalingClientMetrics->signalingClientStats.offerToAnswerTime); + offerToAnswerDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Milliseconds); + this->push(offerToAnswerDatum); + + getTokenDatum.SetMetricName("GetTokenTime"); + getTokenDatum.SetValue(pSignalingClientMetrics->signalingClientStats.getTokenCallTime); + getTokenDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Milliseconds); + this->push(getTokenDatum); + + describeDatum.SetMetricName("DescribeCallTime"); + describeDatum.SetValue(pSignalingClientMetrics->signalingClientStats.describeCallTime); + describeDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Milliseconds); + this->push(describeDatum); + + createDatum.SetMetricName("CreateCallTime"); + createDatum.SetValue(pSignalingClientMetrics->signalingClientStats.createCallTime); + createDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Milliseconds); + this->push(createDatum); + + endpointDatum.SetMetricName("GetEndpointCallTime"); + endpointDatum.SetValue(pSignalingClientMetrics->signalingClientStats.getEndpointCallTime); + endpointDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Milliseconds); + this->push(endpointDatum); + + iceConfigDatum.SetMetricName("GetIceConfigCallTime"); + iceConfigDatum.SetValue(pSignalingClientMetrics->signalingClientStats.getIceConfigCallTime); + iceConfigDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Milliseconds); + this->push(iceConfigDatum); + + connectDatum.SetMetricName("ConnectCallTime"); + connectDatum.SetValue(pSignalingClientMetrics->signalingClientStats.connectCallTime); + connectDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Milliseconds); + this->push(connectDatum); + + createClientDatum.SetMetricName("CreateClientTotalTime"); + createClientDatum.SetValue(pSignalingClientMetrics->signalingClientStats.createClientTime); + createClientDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Milliseconds); + this->push(createClientDatum); + + fetchDatum.SetMetricName("FetchClientTotalTime"); + fetchDatum.SetValue(pSignalingClientMetrics->signalingClientStats.fetchClientTime); + fetchDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Milliseconds); + this->push(fetchDatum); + + connectClientDatum.SetMetricName("ConnectClientTotalTime"); + connectClientDatum.SetValue(pSignalingClientMetrics->signalingClientStats.connectClientTime); + connectClientDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Milliseconds); + this->push(connectClientDatum); + + if (this->isStorage) { + joinSessionToOffer = pSignalingClientMetrics->signalingClientStats.joinSessionToOfferRecvTime; + if (joinSessionToOffer > 0) { + joinSessionToOfferDatum.SetMetricName("JoinSessionToOfferReceived"); + joinSessionToOfferDatum.SetValue(joinSessionToOffer / HUNDREDS_OF_NANOS_IN_A_MILLISECOND); + joinSessionToOfferDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Milliseconds); + this->push(joinSessionToOfferDatum); + } + + joinSessionCallTime = (pSignalingClientMetrics->signalingClientStats.joinSessionCallTime); + if (joinSessionToOffer > 0) { + this->pushJoinSessionTime(joinSessionCallTime, Aws::CloudWatch::Model::StandardUnit::Milliseconds); + } + } +} + +VOID CloudwatchMonitoring::pushInboundRtpStats(PIncomingRTPStatsCtx pIncomingRtpStats) +{ + MetricDatum incomingBitrateDatum, incomingPacketRate, incomingFrameDropRateDatum; + + incomingBitrateDatum.SetMetricName("IncomingBitRate"); + incomingBitrateDatum.SetValue(pIncomingRtpStats->incomingBitRate); + incomingBitrateDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Kilobits_Second); + this->push(incomingBitrateDatum); + + incomingPacketRate.SetMetricName("IncomingPacketsPerSecond"); + incomingPacketRate.SetValue(pIncomingRtpStats->packetReceiveRate); + incomingPacketRate.SetUnit(Aws::CloudWatch::Model::StandardUnit::Count_Second); + this->push(incomingPacketRate); + + incomingFrameDropRateDatum.SetMetricName("IncomingFramesDroppedPerSecond"); + incomingFrameDropRateDatum.SetValue(pIncomingRtpStats->framesDroppedPerSecond); + incomingFrameDropRateDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Count_Second); + this->push(incomingFrameDropRateDatum); +} + +VOID CloudwatchMonitoring::pushRetryCount(UINT32 retryCount) +{ + MetricDatum currentRetryCountDatum; + + currentRetryCountDatum.SetMetricName("APICallRetryCount"); + currentRetryCountDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Count); + currentRetryCountDatum.SetValue(retryCount); + this->push(currentRetryCountDatum); +} + +VOID CloudwatchMonitoring::pushEndToEndMetrics(PEndToEndMetricsCtx pEndToEndMetricsCtx) +{ + MetricDatum endToEndLatencyDatum, sizeMatchDatum; + DOUBLE latency = pEndToEndMetricsCtx->frameLatencyAvg / (DOUBLE) HUNDREDS_OF_NANOS_IN_A_MILLISECOND; + + // TODO: due to https://github.com/aws-samples/amazon-kinesis-video-streams-demos/issues/96, + // it's not clear why the emitted metric shows -nan. Since -nan is a string that's outputted + // from Datum's value stringify implementation, we should try to get the original value by + // printing it ourself. + // + // If the issues doesn't exist anymore, please remove this as this is intended for debugging only. + // The generic metric logging should be sufficient. + DLOGI("Current end-to-end frame latency: %4.2lf", latency); + endToEndLatencyDatum.SetMetricName("EndToEndFrameLatency"); + endToEndLatencyDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Milliseconds); + endToEndLatencyDatum.SetValue(latency); + this->push(endToEndLatencyDatum); + + sizeMatchDatum.SetMetricName("FrameSizeMatch"); + sizeMatchDatum.SetUnit(Aws::CloudWatch::Model::StandardUnit::Count); + sizeMatchDatum.SetValue(pEndToEndMetricsCtx->sizeMatchAvg); + DLOGI("Size match? %d", pEndToEndMetricsCtx->sizeMatchAvg); + this->push(sizeMatchDatum); +} + +} // namespace Canary diff --git a/cloudwatch-integ/CloudwatchMonitoring.h b/cloudwatch-integ/CloudwatchMonitoring.h new file mode 100644 index 0000000000..ac8ffcf22a --- /dev/null +++ b/cloudwatch-integ/CloudwatchMonitoring.h @@ -0,0 +1,33 @@ +#pragma once + +#include "../samples/Samples.h" +namespace CppInteg { + +class CloudwatchMonitoring { + public: + CloudwatchMonitoring(ClientConfiguration*); + STATUS init(PCHAR channelName, PCHAR region, BOOL isMaster, BOOL isStorage); + VOID deinit(); + VOID push(const MetricDatum&); + VOID pushExitStatus(STATUS); + VOID pushTimeToFirstFrame(UINT64, Aws::CloudWatch::Model::StandardUnit); + VOID pushOutboundRtpStats(POutgoingRTPStatsCtx); + VOID pushInboundRtpStats(PIncomingRTPStatsCtx); + VOID pushPeerConnectionMetrics(PPeerConnectionMetrics); + VOID pushKvsIceAgentMetrics(PKvsIceAgentMetrics); + VOID pushSignalingClientMetrics(PSignalingClientMetrics); + VOID pushEndToEndMetrics(PEndToEndMetricsCtx); + VOID pushRetryCount(UINT32); + + VOID pushStorageDisconnectToFrameSentTime(UINT64, Aws::CloudWatch::Model::StandardUnit); + VOID pushJoinSessionTime(UINT64, Aws::CloudWatch::Model::StandardUnit); + + private: + Dimension channelDimension; + Dimension labelDimension; + CloudWatchClient client; + std::atomic pendingMetrics; + BOOL isStorage; +}; + +} // namespace CppInteg diff --git a/cloudwatch-integ/Include.h b/cloudwatch-integ/Include.h new file mode 100644 index 0000000000..c03be773d9 --- /dev/null +++ b/cloudwatch-integ/Include.h @@ -0,0 +1,52 @@ +#pragma once + +#include CW_CONFIG_HEADER + +#define DEFAULT_CLOUDWATCH_NAMESPACE "KinesisVideoSDKCanary" +// TODO: This value shouldn't matter. But, since we don't allow NULL value, we have to set to a value +#define DEFAULT_VIEWER_PEER_ID "ConsumerViewer" +#define DEFAULT_FILE_LOGGING_BUFFER_SIZE (200 * 1024) + +#define MAX_CLOUDWATCH_LOG_COUNT 128 +#define MAX_STATUS_CODE_LENGTH 16 +#define MAX_CONTROL_PLANE_URI_CHAR_LEN 256 +#define MAX_UINT64_DIGIT_COUNT 20 + +#define NUMBER_OF_H264_FRAME_FILES 1500 +#define NUMBER_OF_OPUS_FRAME_FILES 618 + +#define CHANNEL_NAME_TEMPLATE (PCHAR) "%s-%s" +#define FRAME_METADATA_SIZE (SIZEOF(UINT64) + SIZEOF(UINT32) + SIZEOF(UINT32)) +#define ANNEX_B_NALU_SIZE 4 + +#define FIRST_FRAME_TS_FILE_PATH (PCHAR) "../../" +#define STORAGE_DEFAULT_FIRST_FRAME_TS_FILE (PCHAR) "DefaultFirstFrameSentTSFileName.txt" + +#define INDIVIDUAL_STORAGE_CW_DIMENSION "StorageWebRTCSDKCanaryChannelName" +#define INDIVIDUAL_CW_DIMENSION "WebRTCSDKCanaryChannelName" +#define AGGREGATE_STORAGE_CW_DIMENSION "StorageWebRTCSDKCanaryLabel" +#define AGGREGATE_CW_DIMENSION "WebRTCSDKCanaryLabel" + +#define OUTBOUND_RTP_STATS_TIMER_INTERVAL (60 * HUNDREDS_OF_NANOS_IN_A_SECOND) +#define END_TO_END_METRICS_INVOCATION_PERIOD (30 * HUNDREDS_OF_NANOS_IN_A_SECOND) + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace Aws::Client; +using namespace Aws::CloudWatchLogs; +using namespace Aws::CloudWatchLogs::Model; +using namespace Aws::CloudWatch::Model; +using namespace Aws::CloudWatch; +using namespace std; diff --git a/cloudwatch-integ/README.md b/cloudwatch-integ/README.md new file mode 100644 index 0000000000..d0d129fb02 --- /dev/null +++ b/cloudwatch-integ/README.md @@ -0,0 +1,125 @@ +

+ Cloudwatch Integration with Amazon Kinesis Video Streams C WebRTC SDK +
+

+ +The SDK integrates with [AWS SDK CPP](https://github.com/aws/aws-sdk-cpp) to publish runtime performance metrics of the SDK and logs generated by the SDK. + +## Configuration + +The cloudwatch integration demo can be run with different configurations. The list of options that are configurable: +1. `USE_TRICKLE_ICE`: Set this to TRUE, to enable trickle ICE +2. `FORCE_TURN_ONLY`: Set this to TRUE to force TURN usage. By enabling this flag, the connection established would purely be a relay candidate based connection +3. `RUNNER_LABEL`: This label is used as a suffix in the channel name. +4. `SCENARIO_LABEL`: This label is useful in publishing aggregated performance metrics over multiple configurations. For example, if you would like to aggregate outbound frame rate across 5 different cameras, set this label to get an aggregated metric on cloudwatch dashboard +5. `USE_TURN`: Set this to TRUE if the SDK should gather TURN candidates. If set to FALSE, only host and server reflexive candidates are gathered +6. `ENABLE_TTFF_VIA_DC`: Set this to TRUE if timing components of time to first frame should be sent to the JS viewer via data channel. For more information on this feature, refer to +7. `USE_IOT`: Set this to TRUE to use IoT credential provider with the SDK. For more information on setting up an IoT thing, check [SetUp IoT thing](https://docs.aws.amazon.com/kinesisvideostreams/latest/dg/how-iot.html) +8. `ENABLE_STORAGE`: Set this to TRUE to run the master peer with storage configuration. For more information on this feature, refer to [WebRTC Storage](https://docs.aws.amazon.com/kinesisvideostreams/latest/dg/API_Operations_Amazon_Kinesis_Video_WebRTC_Storage.html) +9. `ENABLE_METRICS`: Set this to TRUE to allow the demo to collect metrics to publish to cloudwatch. +10. `SAMPLE_PRE_GENERATE_CERT`: Set this to TRUE to allow the SDK to pre-generate certs. For more information on this, refer to [Pre-gen certs](https://github.com/awslabs/amazon-kinesis-video-streams-webrtc-sdk-c?tab=readme-ov-file#use-pre-generated-certificates) +11. `RUN_TIME`: Set this value to the desired run time of the SDK. Set to 0 to collect metrics forever (till application exits by other means) +12. `LOG_GROUP_NAME`: Set this to the desired string to store the logs in cloudwatch. For more information on cloudwatch log groups and streams refer to [cloudwatch logs](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/Working-with-log-groups-and-streams.html) +13. `CHANNEL_NAME_PREFIX`: This value is used as a prefix for channel name. The channel name in this demo is of the format -. The prefix can also be passed in as a command line arg if prefered. +14. `AUDIO_CODEC`: Set the desired audio codec from the list of supported `RTC_CODEC`. The allowed values are available [here](https://awslabs.github.io/amazon-kinesis-video-streams-webrtc-sdk-c/group__PublicEnums.html#ga93240fd6a0e599dfb0c69ca76e406665) +15. `VIDEO_CODEC`: Set the desired video codec from the list of supported `RTC_CODEC`. The allowed values are available [here](https://awslabs.github.io/amazon-kinesis-video-streams-webrtc-sdk-c/group__PublicEnums.html#ga93240fd6a0e599dfb0c69ca76e406665) +16. `DEFAULT_BITRATE`: If sending mock frames, this value is useful to test the SDK under different bitrate. +17. `DEFAULT_FRAMERATE`: If sending mock frames, this value is useful to test the SDK under different frame rate conditions. + +The demo project uses a config header file approach to set these values up. The user of the demo is free to modify the applications to set it up any other way. More information on configuring the demo with a custom config file will be available in the next section. + +## Build + +To set up this project, in addition to other desired options, run: + +```shell +mkdir -p amazon-kinesis-video-streams-demos/canary/webrtc-c/build +cd amazon-kinesis-video-streams-demos/canary/webrtc-c/build +cmake .. -DENABLE_AWS_SDK_INTEG=ON +make +``` + +The demo comes with a list of different config files users can choose from. The preset config files are available in the [configs](add link here) folder. By default, the demo is configured to run with `default_config.h`. However, demo users are free to set up their own config file and pass in the absolute path to the file while configuring the demo. To do so, do the following: +```shell +cd amazon-kinesis-video-streams-demos/canary/webrtc-c/build +cmake .. -DENABLE_AWS_SDK_INTEG=ON -DCW_CONFIG_HEADER= +make +``` + +## Run + +To run the demo as master, from the build directory, run: + +```shell +./cloudwatch-integ/kvsWebrtcClientMasterCW +``` + +OR, + +```shell +./cloudwatch-integ/kvsWebrtcClientMasterCW +``` + +To run the demo as viewer, from the build directory, run: + +```shell +./cloudwatch-integ/kvsWebrtcClientViewerCW +``` + +OR, + +```shell +./cloudwatch-integ/kvsWebrtcClientViewerCW +``` + +## Cloudwatch metrics + +The Cloudwatch namespace is set to `KinesisVideoSDKCanary`. Each metric listed below will be emitted twice, one with a label dimension and the other with label and channel dimensions. By omitting the channel dimension, metrics that come from different channel names by with the same label will be aggregated. This can be really useful for a scenario where we want to logically group metrics from different runs, e.g. we have a periodic start/stop scenario and we want to run this scenario with different sets of devices. While it's useful to aggregate these metrics, it's also equally important to keep metrics with the channel dimension to keep the granular access to these metrics. + +| Category | Metric | Unit | Frequency (seconds) | Description | +|--------------------|--------------------------------|-------------------|----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Shutdown | ExitStatus | Count | - | Every time the Canary runs, it'll post exactly once. If successful, the code will be 0x00000000. | +| Initialization | SignalingInitDelay | Milliseconds | - | Measure the time it takes for Signaling from creation to connected. | +| Initialization | ICEHolePunchingDelay | Milliseconds | - | Measure the time it takes for ICE agent to successfully connect to the other peer. | +| End to End | EndToEndFrameLatency | Milliseconds | 30 | The delay from sending the frame to when the frame is received on the other end | +| End to End | FrameSizeMatch | None | 30 | The decoded canary data (header + frame data) at the receiver end is compared with the received size as part of header). If equal, 1.0 is pushed as a metric, else 0.0 is pushed | +| Outbound RTP Stats | FramesPerSecond | Count_Second | 60 | Measures the rate at which frames are sent out from the master. This is calculated using outboundRtpStats | +| Outbound RTP Stats | PercentageFrameDiscarded | Percent | 60 | This expresses the percentage of frames that dropped on the sending path within a given time interval. This is calculated using outboundRtpStats | +| Outbound RTP Stats | PercentageFramesRetransmitted | Percent | 60 | This expresses the percentage of frames that are retransmitted on the sending path within a given time interval. This is calculated using outboundRtpStats | +| Outbound RTP Stats | NackPerSecond | Count_Second | 60 | Rate at which Nacks are received by master. This is calculated using outboundRtpStats | +| Inbound RTP Stats | IncomingBitRate | Kilobits_Second | 60 | Measures the rate at which frame bits are received by master. This is calculated using inboundRtpStats | +| Inbound RTP Stats | IncomingPacketsPerSecond | Count_Second | 60 | Measures the rate at which packets are received by the master. This is calculated using inboundRtpStats | +| Inbound RTP Stats | IncomingFramesDroppedPerSecond | Count_Second | 60 | Rate at which the incoming frames are dropped. This is calculated using inboundRtpStats | +| End to End | SignalingRoundtripLatency | Milliseconds | 15 | Measure the roundtrip latency from sending an offer to receive an answer | +| Shutdown | ExitStatus | Count | - | Every time the Canary runs, it'll post exactly once. If successful, the code will be 0x00000000. | +| Signaling | CreateCallTime | Milliseconds | - | Measures time taken to create channel | +| Signaling | GetEndpointCallTime | Milliseconds | - | Measures time taken to get data endpoint | +| Signaling | GetTokenTime | Milliseconds | - | Measures time taken to get credentials (AKID or IoT) | +| Signaling | DescribeCallTime | Milliseconds | - | Measures time taken to describe the channel | +| Signaling | GetIceConfigCallTime | Milliseconds | - | Measures time taken to retrieve ICE server config | +| Signaling | ConnectCallTime | Milliseconds | - | Measures time taken to connect to the signaling channel | +| Signaling | CreateClientTotalTime | Milliseconds | - | Measures time taken to retrieve AWS credentials to get started with making signaling calls | +| Signaling | FetchClientTotalTime | Milliseconds | - | Measures time taken to describe, create channel (if applicable) followed by a describe to get the details, get endpoint and get ICE config | +| Signaling | ConnectClientTotalTime | Milliseconds | - | Measures time taken to connect to the signaling as master/viewer | +| Signaling | OfferToAnswerTime | Milliseconds | - | Measures time taken to send out an answer once the offer is received. | +| ICE | LocalCandidateGatheringTime | Milliseconds | - | Measures time taken to gather candidate from local interfaces | +| ICE | HostCandidateSetUpTime | Milliseconds | - | Measures time taken to create socket connections for local candidates and also creates candidate pairs with the available remote candidates | +| ICE | SrflxCandidateSetUpTime | Milliseconds | - | Measures time taken to create socket connections for srflx candidates | +| ICE | RelayCandidateSetUpTime | Milliseconds | - | Measures time taken to create socket connections for relay candidates. Candidate pair formation is done periodically in a timer callback as and when relay address is obtained | +| ICE | IceServerResolutionTime | Milliseconds | - | Measures time taken to DNS resolve ICE URLs | +| ICE | IceCandidatePairNominationTime | Milliseconds | - | Measures time taken to complete nomination of ICE candidate pair | +| ICE | IcecandidateGatheringTime | Milliseconds | - | Measures time taken from starting gathering to when candidate gathering is done i.e when connectivity checks for all the pairs have been sent out or gathering times out. | +| ICE | IceAgentSetUpTime | Milliseconds | - | Measures time taken to complete connectivity checks | +| Peer | PcCreationTime | Milliseconds | - | Measures time taken to create peer connection object after receiving offer | +| Peer | DtlsSetupTime | Milliseconds | - | Measures time taken to complete DTLS initialization process | +| Peer | ICEHolePunchingDelay | Milliseconds | - | Measures time taken from start of connectivity checks to when the candidate pair is nominated and peer connection established | +| Total | TimeToFirstFrame | Milliseconds | - | Measures time taken from offer to sending out first frame | + +## Cloudwatch logs + +The demo emits cloudwatch logs to the Log group `WebrtcSDK` by default. To use a different one, you can set the `LOG_GROUP_NAME` in your config file. The log stream name template is ``, where, mode values are `master`, `viewer` or `StorageMaster` and timestamp is epoch value. + + + + + diff --git a/cloudwatch-integ/configs/default_config.h b/cloudwatch-integ/configs/default_config.h new file mode 100644 index 0000000000..99c1bb9202 --- /dev/null +++ b/cloudwatch-integ/configs/default_config.h @@ -0,0 +1,32 @@ +#ifndef KVS_SDK_SAMPLE_CONFIG_H +#define KVS_SDK_SAMPLE_CONFIG_H + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#define USE_TRICKLE_ICE TRUE +#define FORCE_TURN_ONLY FALSE +#define RUNNER_LABEL (PCHAR) "testChannel" +#define SCENARIO_LABEL (PCHAR) "LongRun" +#define USE_TURN TRUE +#define ENABLE_TTFF_VIA_DC FALSE +#define USE_IOT TRUE +#define ENABLE_STORAGE FALSE +#define ENABLE_METRICS TRUE +#define SAMPLE_PRE_GENERATE_CERT TRUE +#define RUN_TIME 0 +#define LOG_GROUP_NAME (PCHAR) "WebrtcSDK" +#define CHANNEL_NAME_PREFIX (PCHAR) "sample" +#define AUDIO_CODEC RTC_CODEC_OPUS +#define VIDEO_CODEC RTC_CODEC_H264_PROFILE_42E01F_LEVEL_ASYMMETRY_ALLOWED_PACKETIZATION_MODE +#define DEFAULT_BITRATE (250 * 1024) +#define DEFAULT_FRAMERATE 30 + +#ifdef __cplusplus +} +#endif + +#endif // KVS_SDK_SAMPLE_CONFIG_H diff --git a/cloudwatch-integ/configs/lr_iot_h264_mbedtls.h b/cloudwatch-integ/configs/lr_iot_h264_mbedtls.h new file mode 100644 index 0000000000..d7d45e544d --- /dev/null +++ b/cloudwatch-integ/configs/lr_iot_h264_mbedtls.h @@ -0,0 +1,32 @@ +#ifndef KVS_SDK_SAMPLE_CONFIG_H +#define KVS_SDK_SAMPLE_CONFIG_H + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#define USE_TRICKLE_ICE TRUE +#define FORCE_TURN_ONLY FALSE +#define RUNNER_LABEL (PCHAR) "WebrtcLongRunningMBedTLS" +#define SCENARIO_LABEL (PCHAR) "WebrtcLongRunning" +#define USE_TURN TRUE +#define ENABLE_TTFF_VIA_DC FALSE +#define USE_IOT TRUE +#define ENABLE_STORAGE FALSE +#define ENABLE_METRICS TRUE +#define SAMPLE_PRE_GENERATE_CERT TRUE +#define RUN_TIME (12 * HUNDREDS_OF_NANOS_IN_AN_HOUR) +#define LOG_GROUP_NAME (PCHAR) "WebrtcSDK" +#define CHANNEL_NAME_PREFIX (PCHAR) "DEFAULT" +#define AUDIO_CODEC RTC_CODEC_OPUS +#define VIDEO_CODEC RTC_CODEC_H264_PROFILE_42E01F_LEVEL_ASYMMETRY_ALLOWED_PACKETIZATION_MODE +#define DEFAULT_BITRATE (250 * 1024) +#define DEFAULT_FRAMERATE 30 + +#ifdef __cplusplus +} +#endif + +#endif // KVS_SDK_SAMPLE_CONFIG_H diff --git a/cloudwatch-integ/configs/lr_iot_h264_openssl.h b/cloudwatch-integ/configs/lr_iot_h264_openssl.h new file mode 100644 index 0000000000..fe76c99135 --- /dev/null +++ b/cloudwatch-integ/configs/lr_iot_h264_openssl.h @@ -0,0 +1,33 @@ +#ifndef KVS_SDK_SAMPLE_CONFIG_H +#define KVS_SDK_SAMPLE_CONFIG_H + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#define USE_TRICKLE_ICE TRUE +#define FORCE_TURN_ONLY FALSE +#define RUNNER_LABEL (PCHAR) "WebrtcLongRunningOpenSSL" +#define SCENARIO_LABEL (PCHAR) "WebrtcLongRunning" +#define USE_TURN TRUE +#define ENABLE_TTFF_VIA_DC FALSE +#define USE_IOT TRUE +#define ENABLE_STORAGE FALSE +#define ENABLE_METRICS TRUE +#define SAMPLE_PRE_GENERATE_CERT TRUE +#define RUN_TIME (12 * HUNDREDS_OF_NANOS_IN_AN_HOUR) +#define LOG_GROUP_NAME (PCHAR) "WebrtcSDK" +#define CHANNEL_NAME_PREFIX (PCHAR) "DEFAULT" +#define AUDIO_CODEC RTC_CODEC_OPUS +#define VIDEO_CODEC RTC_CODEC_H264_PROFILE_42E01F_LEVEL_ASYMMETRY_ALLOWED_PACKETIZATION_MODE +#define DEFAULT_BITRATE (250 * 1024) +#define DEFAULT_FRAMERATE 30 +#ifdef __cplusplus +} +#endif + +#endif // KVS_SDK_SAMPLE_CONFIG_H + + diff --git a/cloudwatch-integ/configs/lr_static_h265_openssl.h b/cloudwatch-integ/configs/lr_static_h265_openssl.h new file mode 100644 index 0000000000..118f151473 --- /dev/null +++ b/cloudwatch-integ/configs/lr_static_h265_openssl.h @@ -0,0 +1,30 @@ +#ifndef KVS_SDK_SAMPLE_CONFIG_H +#define KVS_SDK_SAMPLE_CONFIG_H + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif +#define USE_TRICKLE_ICE TRUE +#define FORCE_TURN_ONLY FALSE +#define RUNNER_LABEL (PCHAR) "WebrtcLongRunningStaticOpenSSL-H265" +#define SCENARIO_LABEL (PCHAR) "WebrtcLongRunning" +#define USE_TURN TRUE +#define ENABLE_TTFF_VIA_DC FALSE +#define USE_IOT FALSE +#define ENABLE_STORAGE FALSE +#define ENABLE_METRICS TRUE +#define SAMPLE_PRE_GENERATE_CERT TRUE +#define RUN_TIME (12 * HUNDREDS_OF_NANOS_IN_AN_HOUR) +#define LOG_GROUP_NAME (PCHAR) "WebrtcSDK" +#define CHANNEL_NAME_PREFIX (PCHAR) "DEFAULT" +#define AUDIO_CODEC RTC_CODEC_OPUS +#define VIDEO_CODEC RTC_CODEC_H265 +#define DEFAULT_BITRATE (250 * 1024) +#define DEFAULT_FRAMERATE 30 +#ifdef __cplusplus +} +#endif + +#endif // KVS_SDK_SAMPLE_CONFIG_H diff --git a/cloudwatch-integ/configs/p_iot_h265_mbedtls.h b/cloudwatch-integ/configs/p_iot_h265_mbedtls.h new file mode 100644 index 0000000000..30f0a3aafc --- /dev/null +++ b/cloudwatch-integ/configs/p_iot_h265_mbedtls.h @@ -0,0 +1,30 @@ +#ifndef KVS_SDK_SAMPLE_CONFIG_H +#define KVS_SDK_SAMPLE_CONFIG_H + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif +#define USE_TRICKLE_ICE TRUE +#define FORCE_TURN_ONLY FALSE +#define RUNNER_LABEL (PCHAR) "WebrtcPeriodicMBedTLS-H265" +#define SCENARIO_LABEL (PCHAR) "MbedTLSPeriodic" +#define USE_TURN TRUE +#define ENABLE_TTFF_VIA_DC FALSE +#define USE_IOT TRUE +#define ENABLE_STORAGE FALSE +#define ENABLE_METRICS TRUE +#define SAMPLE_PRE_GENERATE_CERT TRUE +#define RUN_TIME (30 * HUNDREDS_OF_NANOS_IN_A_SECOND) +#define LOG_GROUP_NAME (PCHAR) "WebrtcSDK" +#define CHANNEL_NAME_PREFIX (PCHAR) "DEFAULT" +#define AUDIO_CODEC RTC_CODEC_OPUS +#define VIDEO_CODEC RTC_CODEC_H265 +#define DEFAULT_BITRATE (250 * 1024) +#define DEFAULT_FRAMERATE 30 +#ifdef __cplusplus +} +#endif + +#endif // KVS_SDK_SAMPLE_CONFIG_H diff --git a/cloudwatch-integ/configs/p_iot_h265_openssl.h b/cloudwatch-integ/configs/p_iot_h265_openssl.h new file mode 100644 index 0000000000..004407bedf --- /dev/null +++ b/cloudwatch-integ/configs/p_iot_h265_openssl.h @@ -0,0 +1,30 @@ +#ifndef KVS_SDK_SAMPLE_CONFIG_H +#define KVS_SDK_SAMPLE_CONFIG_H + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif +#define USE_TRICKLE_ICE TRUE +#define FORCE_TURN_ONLY FALSE +#define RUNNER_LABEL (PCHAR) "WebrtcPeriodicOpenSSL-H265" +#define SCENARIO_LABEL (PCHAR) "OpenSSLPeriodic" +#define USE_TURN TRUE +#define ENABLE_TTFF_VIA_DC FALSE +#define USE_IOT TRUE +#define ENABLE_STORAGE FALSE +#define ENABLE_METRICS TRUE +#define SAMPLE_PRE_GENERATE_CERT TRUE +#define RUN_TIME (30 * HUNDREDS_OF_NANOS_IN_A_SECOND) +#define LOG_GROUP_NAME (PCHAR) "WebrtcSDK" +#define CHANNEL_NAME_PREFIX (PCHAR) "DEFAULT" +#define AUDIO_CODEC RTC_CODEC_OPUS +#define VIDEO_CODEC RTC_CODEC_H265 +#define DEFAULT_BITRATE (250 * 1024) +#define DEFAULT_FRAMERATE 30 +#ifdef __cplusplus +} +#endif + +#endif // KVS_SDK_SAMPLE_CONFIG_H diff --git a/cloudwatch-integ/configs/p_static_h264_mbedtls.h b/cloudwatch-integ/configs/p_static_h264_mbedtls.h new file mode 100644 index 0000000000..e7055f73c5 --- /dev/null +++ b/cloudwatch-integ/configs/p_static_h264_mbedtls.h @@ -0,0 +1,30 @@ +#ifndef KVS_SDK_SAMPLE_CONFIG_H +#define KVS_SDK_SAMPLE_CONFIG_H + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif +#define USE_TRICKLE_ICE TRUE +#define FORCE_TURN_ONLY FALSE +#define RUNNER_LABEL (PCHAR) "WebrtcPeriodicStaticMbedTLS" +#define SCENARIO_LABEL (PCHAR) "MbedTLSPeriodic" +#define USE_TURN TRUE +#define ENABLE_TTFF_VIA_DC FALSE +#define USE_IOT FALSE +#define ENABLE_STORAGE FALSE +#define ENABLE_METRICS TRUE +#define SAMPLE_PRE_GENERATE_CERT TRUE +#define RUN_TIME (30 * HUNDREDS_OF_NANOS_IN_A_SECOND) +#define LOG_GROUP_NAME (PCHAR) "WebrtcSDK" +#define CHANNEL_NAME_PREFIX (PCHAR) "DEFAULT" +#define AUDIO_CODEC RTC_CODEC_OPUS +#define VIDEO_CODEC RTC_CODEC_H264_PROFILE_42E01F_LEVEL_ASYMMETRY_ALLOWED_PACKETIZATION_MODE +#define DEFAULT_BITRATE (250 * 1024) +#define DEFAULT_FRAMERATE 30 +#ifdef __cplusplus +} +#endif + +#endif // KVS_SDK_SAMPLE_CONFIG_H diff --git a/cloudwatch-integ/configs/storage_extended.h b/cloudwatch-integ/configs/storage_extended.h new file mode 100644 index 0000000000..63227e221f --- /dev/null +++ b/cloudwatch-integ/configs/storage_extended.h @@ -0,0 +1,30 @@ +#ifndef KVS_SDK_SAMPLE_CONFIG_H +#define KVS_SDK_SAMPLE_CONFIG_H + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif +#define USE_TRICKLE_ICE TRUE +#define FORCE_TURN_ONLY FALSE +#define RUNNER_LABEL (PCHAR) "StorageExtended" +#define SCENARIO_LABEL (PCHAR) "StorageExtended" +#define USE_TURN TRUE +#define ENABLE_TTFF_VIA_DC FALSE +#define USE_IOT FALSE +#define ENABLE_STORAGE TRUE +#define ENABLE_METRICS TRUE +#define SAMPLE_PRE_GENERATE_CERT TRUE +#define RUN_TIME (43200 * HUNDREDS_OF_NANOS_IN_A_SECOND) +#define LOG_GROUP_NAME (PCHAR) "WebrtcSDK" +#define CHANNEL_NAME_PREFIX (PCHAR) "DEFAULT" +#define AUDIO_CODEC RTC_CODEC_OPUS +#define VIDEO_CODEC RTC_CODEC_H264_PROFILE_42E01F_LEVEL_ASYMMETRY_ALLOWED_PACKETIZATION_MODE +#define DEFAULT_BITRATE (250 * 1024) +#define DEFAULT_FRAMERATE 30 +#ifdef __cplusplus +} +#endif + +#endif // KVS_SDK_SAMPLE_CONFIG_H diff --git a/cloudwatch-integ/configs/storage_periodic.h b/cloudwatch-integ/configs/storage_periodic.h new file mode 100644 index 0000000000..2a766c6db4 --- /dev/null +++ b/cloudwatch-integ/configs/storage_periodic.h @@ -0,0 +1,30 @@ +#ifndef KVS_SDK_SAMPLE_CONFIG_H +#define KVS_SDK_SAMPLE_CONFIG_H + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif +#define USE_TRICKLE_ICE TRUE +#define FORCE_TURN_ONLY FALSE +#define RUNNER_LABEL (PCHAR) "StoragePeriodic" +#define SCENARIO_LABEL (PCHAR) "StoragePeriodic" +#define USE_TURN TRUE +#define ENABLE_TTFF_VIA_DC FALSE +#define USE_IOT FALSE +#define ENABLE_STORAGE TRUE +#define ENABLE_METRICS TRUE +#define SAMPLE_PRE_GENERATE_CERT TRUE +#define RUN_TIME (300 * HUNDREDS_OF_NANOS_IN_A_SECOND) +#define LOG_GROUP_NAME (PCHAR) "WebrtcSDK" +#define CHANNEL_NAME_PREFIX (PCHAR) "DEFAULT" +#define AUDIO_CODEC RTC_CODEC_OPUS +#define VIDEO_CODEC RTC_CODEC_H264_PROFILE_42E01F_LEVEL_ASYMMETRY_ALLOWED_PACKETIZATION_MODE +#define DEFAULT_BITRATE (250 * 1024) +#define DEFAULT_FRAMERATE 30 +#ifdef __cplusplus +} +#endif + +#endif // KVS_SDK_SAMPLE_CONFIG_H diff --git a/cloudwatch-integ/configs/storage_single_reconnect.h b/cloudwatch-integ/configs/storage_single_reconnect.h new file mode 100644 index 0000000000..882bb6007c --- /dev/null +++ b/cloudwatch-integ/configs/storage_single_reconnect.h @@ -0,0 +1,30 @@ +#ifndef KVS_SDK_SAMPLE_CONFIG_H +#define KVS_SDK_SAMPLE_CONFIG_H + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif +#define USE_TRICKLE_ICE TRUE +#define FORCE_TURN_ONLY FALSE +#define RUNNER_LABEL (PCHAR) "StorageSingleReconnect" +#define SCENARIO_LABEL (PCHAR) "StorageSingleReconnect" +#define USE_TURN TRUE +#define ENABLE_TTFF_VIA_DC FALSE +#define USE_IOT FALSE +#define ENABLE_STORAGE TRUE +#define ENABLE_METRICS TRUE +#define SAMPLE_PRE_GENERATE_CERT TRUE +#define RUN_TIME (3900 * HUNDREDS_OF_NANOS_IN_A_SECOND) +#define LOG_GROUP_NAME (PCHAR) "WebrtcSDK" +#define CHANNEL_NAME_PREFIX (PCHAR) "DEFAULT" +#define AUDIO_CODEC RTC_CODEC_OPUS +#define VIDEO_CODEC RTC_CODEC_H264_PROFILE_42E01F_LEVEL_ASYMMETRY_ALLOWED_PACKETIZATION_MODE +#define DEFAULT_BITRATE (250 * 1024) +#define DEFAULT_FRAMERATE 30 +#ifdef __cplusplus +} +#endif + +#endif // KVS_SDK_SAMPLE_CONFIG_H diff --git a/cloudwatch-integ/configs/storage_sub_reconnect.h b/cloudwatch-integ/configs/storage_sub_reconnect.h new file mode 100644 index 0000000000..f057715fbe --- /dev/null +++ b/cloudwatch-integ/configs/storage_sub_reconnect.h @@ -0,0 +1,30 @@ +#ifndef KVS_SDK_SAMPLE_CONFIG_H +#define KVS_SDK_SAMPLE_CONFIG_H + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif +#define USE_TRICKLE_ICE TRUE +#define FORCE_TURN_ONLY FALSE +#define RUNNER_LABEL (PCHAR) "StorageSubReconnect" +#define SCENARIO_LABEL (PCHAR) "StorageSubReconnect" +#define USE_TURN TRUE +#define ENABLE_TTFF_VIA_DC FALSE +#define USE_IOT FALSE +#define ENABLE_STORAGE TRUE +#define ENABLE_METRICS TRUE +#define SAMPLE_PRE_GENERATE_CERT TRUE +#define RUN_TIME (2700 * HUNDREDS_OF_NANOS_IN_A_SECOND) +#define LOG_GROUP_NAME (PCHAR) "WebrtcSDK" +#define CHANNEL_NAME_PREFIX (PCHAR) "DEFAULT" +#define AUDIO_CODEC RTC_CODEC_OPUS +#define VIDEO_CODEC RTC_CODEC_H264_PROFILE_42E01F_LEVEL_ASYMMETRY_ALLOWED_PACKETIZATION_MODE +#define DEFAULT_BITRATE (250 * 1024) +#define DEFAULT_FRAMERATE 30 +#ifdef __cplusplus +} +#endif + +#endif // KVS_SDK_SAMPLE_CONFIG_H diff --git a/cloudwatch-integ/kvsWebRTCClientMasterCloudwatch.cpp b/cloudwatch-integ/kvsWebRTCClientMasterCloudwatch.cpp new file mode 100644 index 0000000000..803d24c8cf --- /dev/null +++ b/cloudwatch-integ/kvsWebRTCClientMasterCloudwatch.cpp @@ -0,0 +1,528 @@ +#include +#include "../samples/Samples.h" +#include "Cloudwatch.h" + +UINT32 outboundStatsTimerId = MAX_UINT32; + +// Save first-frame-sent time to file for consumer-end access. +STATUS writeFirstFrameSentTimeToFile(PCHAR fileName) { + STATUS retStatus = STATUS_SUCCESS; + DLOGI("Writing to %s file", fileName); + UINT64 currentTimeMillis = GETTIME() / HUNDREDS_OF_NANOS_IN_A_MILLISECOND; + CHAR currentTimeChars[MAX_UINT64_DIGIT_COUNT + 1]; // +1 accounts for null terminator + UINT64 writeSize = SNPRINTF(currentTimeChars, SIZEOF(currentTimeChars), "%llu", currentTimeMillis); + DLOGI("Timestamp written to file: %s", currentTimeChars); + CHK_STATUS(writeFile((PCHAR) fileName, false, false, static_cast(static_cast(currentTimeChars)), writeSize)); + CleanUp: + return retStatus; +} + +VOID calculateDisconnectToFrameSentTime(PSampleConfiguration pSampleConfiguration) +{ + UINT64 disconnectTime = pSampleConfiguration->storageDisconnectedTime; + if (disconnectTime != 0) { + DOUBLE storageDisconnectToFrameSentTime = (DOUBLE) (GETTIME() - disconnectTime) / HUNDREDS_OF_NANOS_IN_A_MILLISECOND; + CppInteg::Cloudwatch::getInstance().monitoring.pushStorageDisconnectToFrameSentTime(storageDisconnectToFrameSentTime, + Aws::CloudWatch::Model::StandardUnit::Milliseconds); + DLOGI("Setting storageDisconnectedTime to zero (not set)"); + pSampleConfiguration->storageDisconnectedTime = 0; + } else { + DLOGI("Not sending storageDisconnectToFrameSentTime metric, storageDisconnectedTime is zero (not set)"); + } +} + +STATUS publishStatsForCanary(UINT32 timerId, UINT64 currentTime, UINT64 customData) { + UNUSED_PARAM(timerId); + UNUSED_PARAM(currentTime); + STATUS retStatus = STATUS_SUCCESS; + PSampleConfiguration pSampleConfiguration = (PSampleConfiguration) customData; + PSampleStreamingSession pSampleStreamingSession = NULL; + BOOL sampleConfigurationObjLocked = FALSE; + BOOL statsLocked = FALSE; + + CHK_WARN(pSampleConfiguration != NULL, STATUS_NULL_ARG, "Sample config object not set up"); + + // Use MUTEX_TRYLOCK to avoid possible deadlock when canceling timerQueue + if (!MUTEX_TRYLOCK(pSampleConfiguration->sampleConfigurationObjLock)) { + return retStatus; + } else { + sampleConfigurationObjLocked = TRUE; + } + + pSampleStreamingSession = pSampleConfiguration->sampleStreamingSessionList[0]; + acquireMetricsCtx(pSampleStreamingSession); + CHK_WARN(pSampleStreamingSession != NULL || pSampleStreamingSession->pStatsCtx != NULL, STATUS_NULL_ARG, "Stats ctx object not set up"); + MUTEX_LOCK(pSampleStreamingSession->pStatsCtx->statsUpdateLock); + statsLocked = TRUE; + pSampleStreamingSession->pStatsCtx->kvsRtcStats.requestedTypeOfStats = RTC_STATS_TYPE_OUTBOUND_RTP; + if (!ATOMIC_LOAD_BOOL(&pSampleStreamingSession->pSampleConfiguration->appTerminateFlag)) { + CHK_LOG_ERR(rtcPeerConnectionGetMetrics(pSampleStreamingSession->pPeerConnection, pSampleStreamingSession->pVideoRtcRtpTransceiver, &pSampleStreamingSession->pStatsCtx->kvsRtcStats)); + CHK_STATUS(populateOutgoingRtpMetricsContext(pSampleStreamingSession)); + CppInteg::Cloudwatch::getInstance().monitoring.pushOutboundRtpStats(&pSampleStreamingSession->pStatsCtx->outgoingRTPStatsCtx); + } else { + retStatus = STATUS_TIMER_QUEUE_STOP_SCHEDULING; + } +CleanUp: + if(statsLocked) { + MUTEX_UNLOCK(pSampleStreamingSession->pStatsCtx->statsUpdateLock); + } + releaseMetricsCtx(pSampleStreamingSession); + if (sampleConfigurationObjLocked) { + MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); + } + + return STATUS_SUCCESS; +} + +PVOID sendProfilingMetrics(PVOID customData) +{ + STATUS retStatus = STATUS_SUCCESS; + PSampleConfiguration pSampleConfiguration = (PSampleConfiguration) customData; + if(pSampleConfiguration == NULL) { + return NULL; + } + + while((pSampleConfiguration->sampleStreamingSessionList[0]) == NULL && (!(ATOMIC_LOAD_BOOL(&pSampleConfiguration->interrupted)) || (!(ATOMIC_LOAD_BOOL(&pSampleConfiguration->appTerminateFlag))))) { + THREAD_SLEEP(HUNDREDS_OF_NANOS_IN_A_MILLISECOND * 100); + } + if(ATOMIC_LOAD_BOOL(&pSampleConfiguration->interrupted) || (ATOMIC_LOAD_BOOL(&pSampleConfiguration->appTerminateFlag))) { + DLOGW("Terminated before we could profile"); + return NULL; + } + while (!ATOMIC_LOAD_BOOL(&pSampleConfiguration->interrupted) || !(ATOMIC_LOAD_BOOL(&pSampleConfiguration->appTerminateFlag))) { + PSampleStreamingSession pSampleStreamingSession = pSampleConfiguration->sampleStreamingSessionList[0]; + retStatus = getSdkTimeProfile(&pSampleStreamingSession); + + if(STATUS_SUCCEEDED(retStatus)) { + DLOGI("Gathered time profile"); + CppInteg::Cloudwatch::getInstance().monitoring.pushSignalingClientMetrics(&pSampleConfiguration->signalingClientMetrics); + CppInteg::Cloudwatch::getInstance().monitoring.pushPeerConnectionMetrics(&pSampleStreamingSession->peerConnectionMetrics); + CppInteg::Cloudwatch::getInstance().monitoring.pushKvsIceAgentMetrics(&pSampleStreamingSession->iceMetrics); + return NULL; + } else if(retStatus == STATUS_WAITING_ON_FIRST_FRAME) { + DLOGI("Waiting on streaming to start 0x%08x", retStatus); + } else { + DLOGE("Failed to get profiling stats. (0x%08x)", retStatus); + } + THREAD_SLEEP(HUNDREDS_OF_NANOS_IN_A_MILLISECOND * 100); + } + return NULL; +} + +VOID addMetadataToFrameData(PBYTE buffer, PFrame pFrame) +{ + PBYTE pCurPtr = buffer + ANNEX_B_NALU_SIZE; + putUnalignedInt64BigEndian((PINT64) pCurPtr, pFrame->presentationTs); + pCurPtr += SIZEOF(UINT64); + putUnalignedInt32BigEndian((PINT32) pCurPtr, pFrame->size); + pCurPtr += SIZEOF(UINT32); + putUnalignedInt32BigEndian((PINT32) pCurPtr, COMPUTE_CRC32(buffer, pFrame->size)); +} + +VOID createMockFrames(PBYTE buffer, PFrame pFrame) +{ + UINT32 i; + // For decoding purposes, the first 4 bytes need to be a NALu + putUnalignedInt32BigEndian((PINT32) buffer, 0x00000001); + for (i = ANNEX_B_NALU_SIZE + FRAME_METADATA_SIZE; i < pFrame->size; i++) { + buffer[i] = RAND(); + } + addMetadataToFrameData(buffer, pFrame); +} + +PVOID sendMockVideoPackets(PVOID args) +{ + STATUS retStatus = STATUS_SUCCESS; + STATUS status = STATUS_SUCCESS; + Frame frame; + UINT32 i; + UINT32 hexStrLen = 0; + UINT32 actualFrameSize = 0; + UINT32 frameSizeWithoutNalu = 0; + UINT32 minFrameSize = FRAME_METADATA_SIZE + ((DEFAULT_BITRATE / 8) / DEFAULT_FRAMERATE); + UINT32 maxFrameSize = (FRAME_METADATA_SIZE + ((DEFAULT_BITRATE / 8) / DEFAULT_FRAMERATE)) * 2; + PBYTE frameData = NULL; + UINT64 firstFrameTime = 0; + frameData = (PBYTE) MEMALLOC(maxFrameSize); + + PSampleConfiguration pSampleConfiguration = (PSampleConfiguration) args; + CHK_ERR(pSampleConfiguration != NULL, STATUS_NULL_ARG, "[KVS Master] Streaming session not set up"); + + // We allocate a bigger buffer to accomodate the hex encoded string + frame.frameData = (PBYTE) MEMALLOC(maxFrameSize * 3 + 1 + ANNEX_B_NALU_SIZE); + frame.version = FRAME_CURRENT_VERSION; + frame.presentationTs = GETTIME(); + + if(pSampleConfiguration->enableMetrics) { + CHK_STATUS(timerQueueAddTimer(pSampleConfiguration->timerQueueHandle, OUTBOUND_RTP_STATS_TIMER_INTERVAL, + OUTBOUND_RTP_STATS_TIMER_INTERVAL, + publishStatsForCanary, + (UINT64) pSampleConfiguration, + &outboundStatsTimerId)); + } + while (!ATOMIC_LOAD_BOOL(&pSampleConfiguration->appTerminateFlag)) { + + // This is the actual frame size that includes the metadata and the actual frame data + // It generates a number between 1082 and 2164 but the actual frame.size may be larger + // because hexStrLen will vary + actualFrameSize = (RAND() % (maxFrameSize - minFrameSize + 1)) + minFrameSize; + frameSizeWithoutNalu = actualFrameSize - ANNEX_B_NALU_SIZE; + + frame.size = actualFrameSize; + createMockFrames(frameData, &frame); + + // Hex encode the data (without the ANNEX-B NALu) to ensure parts of random frame data is not skipped if they + // are the same as the ANNEX-B NALu + CHK_STATUS(hexEncode(frame.frameData + ANNEX_B_NALU_SIZE, frameSizeWithoutNalu, NULL, &hexStrLen)); + + // This re-alloc is done in case the estimated size does not match the actual requirement. + // We do not want to constantly malloc within a loop. Hence, we re-allocate only if required + // Either ways, the realloc should not happen + if (hexStrLen != (frameSizeWithoutNalu * 2 + 1)) { + DLOGW("Re allocating...this should not happen...something might be wrong"); + frame.frameData = (PBYTE) REALLOC(frame.frameData, hexStrLen + ANNEX_B_NALU_SIZE); + CHK_ERR(frame.frameData != NULL, STATUS_NOT_ENOUGH_MEMORY, "Failed to realloc media buffer"); + } + CHK_STATUS(hexEncode(frameData + ANNEX_B_NALU_SIZE, frameSizeWithoutNalu, (PCHAR)(frame.frameData + ANNEX_B_NALU_SIZE), &hexStrLen)); + MEMCPY(frame.frameData, frameData, ANNEX_B_NALU_SIZE); + + // We must update the size to reflect the original data with hex encoded data + frame.size = hexStrLen + ANNEX_B_NALU_SIZE; + MUTEX_LOCK(pSampleConfiguration->streamingSessionListReadLock); + for (i = 0; i < pSampleConfiguration->streamingSessionCount; ++i) { + status = writeFrame(pSampleConfiguration->sampleStreamingSessionList[i]->pVideoRtcRtpTransceiver, &frame); + if(pSampleConfiguration->enableMetrics) { + pSampleConfiguration->sampleStreamingSessionList[i]->pStatsCtx->outgoingRTPStatsCtx.videoFramesGenerated++; + pSampleConfiguration->sampleStreamingSessionList[i]->pStatsCtx->outgoingRTPStatsCtx.videoBytesGenerated += frame.size; + } + if (pSampleConfiguration->sampleStreamingSessionList[i]->firstFrame && status == STATUS_SUCCESS) { + PROFILE_WITH_START_TIME_OBJ(pSampleConfiguration->sampleStreamingSessionList[i]->offerReceiveTime, firstFrameTime, "Time to first frame"); + CppInteg::Cloudwatch::getInstance().monitoring.pushTimeToFirstFrame(firstFrameTime, Aws::CloudWatch::Model::StandardUnit::Milliseconds); + + pSampleConfiguration->sampleStreamingSessionList[i]->firstFrame = FALSE; + } + if (status != STATUS_SRTP_NOT_READY_YET) { + if (status != STATUS_SUCCESS) { + DLOGV("writeFrame() failed with 0x%08x", status); + } + } + } + MUTEX_UNLOCK(pSampleConfiguration->streamingSessionListReadLock); + THREAD_SLEEP(HUNDREDS_OF_NANOS_IN_A_SECOND / DEFAULT_FRAMERATE); + frame.presentationTs = GETTIME(); + } +CleanUp: + DLOGI("[KVS Master] Closing video thread"); + CHK_LOG_ERR(retStatus); + SAFE_MEMFREE(frame.frameData); + SAFE_MEMFREE(frameData); + return (PVOID) (ULONG_PTR) retStatus;; +} + +PVOID sendRealVideoPackets(PVOID args) +{ + STATUS retStatus = STATUS_SUCCESS; + PSampleConfiguration pSampleConfiguration = (PSampleConfiguration) args; + Frame frame; + UINT32 fileIndex = 0, frameSize; + CHAR filePath[MAX_PATH_LEN + 1]; + STATUS status; + UINT32 i; + UINT64 startTime, lastFrameTime, elapsed; + CHAR tsFileName[MAX_PATH_LEN + 1]; + UINT64 firstFrameTime = 0; + CHK_ERR(pSampleConfiguration != NULL, STATUS_NULL_ARG, "[KVS Master] Streaming session not set up"); + + frame.presentationTs = 0; + startTime = GETTIME(); + lastFrameTime = startTime; + SNPRINTF(tsFileName, SIZEOF(tsFileName), "%s%s", FIRST_FRAME_TS_FILE_PATH, STORAGE_DEFAULT_FIRST_FRAME_TS_FILE); + // Delete file if it exists + FREMOVE(tsFileName); + + if(pSampleConfiguration->enableMetrics) { + CHK_STATUS(timerQueueAddTimer(pSampleConfiguration->timerQueueHandle, OUTBOUND_RTP_STATS_TIMER_INTERVAL, OUTBOUND_RTP_STATS_TIMER_INTERVAL, + publishStatsForCanary, (UINT64) pSampleConfiguration, &outboundStatsTimerId)); + } + while (!ATOMIC_LOAD_BOOL(&pSampleConfiguration->appTerminateFlag)) { + fileIndex = fileIndex % NUMBER_OF_H264_FRAME_FILES + 1; + if (pSampleConfiguration->videoCodec == RTC_CODEC_H264_PROFILE_42E01F_LEVEL_ASYMMETRY_ALLOWED_PACKETIZATION_MODE) { + SNPRINTF(filePath, MAX_PATH_LEN, "./h264SampleFrames/frame-%04d.h264", fileIndex); + } else if (pSampleConfiguration->videoCodec == RTC_CODEC_H265) { + SNPRINTF(filePath, MAX_PATH_LEN, "./h265SampleFrames/frame-%04d.h265", fileIndex); + } + + CHK_STATUS(readFrameFromDisk(NULL, &frameSize, filePath)); + + // Re-alloc if needed + if (frameSize > pSampleConfiguration->videoBufferSize) { + pSampleConfiguration->pVideoFrameBuffer = (PBYTE) MEMREALLOC(pSampleConfiguration->pVideoFrameBuffer, frameSize); + CHK_ERR(pSampleConfiguration->pVideoFrameBuffer != NULL, STATUS_NOT_ENOUGH_MEMORY, "[KVS Master] Failed to allocate video frame buffer"); + pSampleConfiguration->videoBufferSize = frameSize; + } + + frame.frameData = pSampleConfiguration->pVideoFrameBuffer; + frame.size = frameSize; + + CHK_STATUS(readFrameFromDisk(frame.frameData, &frameSize, filePath)); + + // based on bitrate of samples/h264SampleFrames/frame-* + frame.presentationTs += SAMPLE_VIDEO_FRAME_DURATION; + MUTEX_LOCK(pSampleConfiguration->streamingSessionListReadLock); + for (i = 0; i < pSampleConfiguration->streamingSessionCount; ++i) { + status = writeFrame(pSampleConfiguration->sampleStreamingSessionList[i]->pVideoRtcRtpTransceiver, &frame); + if(pSampleConfiguration->enableMetrics) { + pSampleConfiguration->sampleStreamingSessionList[i]->pStatsCtx->outgoingRTPStatsCtx.videoFramesGenerated++; + pSampleConfiguration->sampleStreamingSessionList[i]->pStatsCtx->outgoingRTPStatsCtx.videoBytesGenerated += frame.size; + } + if (pSampleConfiguration->sampleStreamingSessionList[i]->firstFrame && status == STATUS_SUCCESS) { + CHK_STATUS(writeFirstFrameSentTimeToFile(tsFileName)); + PROFILE_WITH_START_TIME_OBJ(pSampleConfiguration->sampleStreamingSessionList[i]->offerReceiveTime, firstFrameTime, "Time to first frame"); + CppInteg::Cloudwatch::getInstance().monitoring.pushTimeToFirstFrame(firstFrameTime, + Aws::CloudWatch::Model::StandardUnit::Milliseconds); + calculateDisconnectToFrameSentTime(pSampleConfiguration); + pSampleConfiguration->sampleStreamingSessionList[i]->firstFrame = FALSE; + } + if (status != STATUS_SRTP_NOT_READY_YET) { + if (status != STATUS_SUCCESS) { + DLOGV("writeFrame() failed with 0x%08x", status); + } + } else { + // Reset file index to ensure first frame sent upon SRTP ready is a key frame. + fileIndex = 0; + } + } + MUTEX_UNLOCK(pSampleConfiguration->streamingSessionListReadLock); + + // Adjust sleep in the case the sleep itself and writeFrame take longer than expected. Since sleep makes sure that the thread + // will be paused at least until the given amount, we can assume that there's no too early frame scenario. + // Also, it's very unlikely to have a delay greater than SAMPLE_VIDEO_FRAME_DURATION, so the logic assumes that this is always + // true for simplicity. + elapsed = lastFrameTime - startTime; + THREAD_SLEEP(SAMPLE_VIDEO_FRAME_DURATION - elapsed % SAMPLE_VIDEO_FRAME_DURATION); + lastFrameTime = GETTIME(); + } + +CleanUp: + DLOGI("[KVS Master] Closing video thread"); + CHK_LOG_ERR(retStatus); + + return (PVOID) (ULONG_PTR) retStatus; +} + +PVOID sendRealAudioPackets(PVOID args) +{ + STATUS retStatus = STATUS_SUCCESS; + PSampleConfiguration pSampleConfiguration = (PSampleConfiguration) args; + Frame frame; + UINT32 fileIndex = 0, frameSize; + CHAR filePath[MAX_PATH_LEN + 1]; + CHAR tsFileName[MAX_PATH_LEN + 1]; + UINT64 firstFrameTime = 0; + UINT32 i; + STATUS status; + + CHK_ERR(pSampleConfiguration != NULL, STATUS_NULL_ARG, "[KVS Master] Streaming session is NULL"); + frame.presentationTs = 0; + + SNPRINTF(tsFileName, SIZEOF(tsFileName), "%s%s", FIRST_FRAME_TS_FILE_PATH, STORAGE_DEFAULT_FIRST_FRAME_TS_FILE); + // Delete file if it exists + FREMOVE(tsFileName); + + while (!ATOMIC_LOAD_BOOL(&pSampleConfiguration->appTerminateFlag)) { + fileIndex = fileIndex % NUMBER_OF_OPUS_FRAME_FILES + 1; + + if (pSampleConfiguration->audioCodec == RTC_CODEC_AAC) { + SNPRINTF(filePath, MAX_PATH_LEN, "./aacSampleFrames/sample-%03d.aac", fileIndex); + } else if (pSampleConfiguration->audioCodec == RTC_CODEC_OPUS) { + SNPRINTF(filePath, MAX_PATH_LEN, "./opusSampleFrames/sample-%03d.opus", fileIndex); + } + + CHK_STATUS(readFrameFromDisk(NULL, &frameSize, filePath)); + + // Re-alloc if needed + if (frameSize > pSampleConfiguration->audioBufferSize) { + pSampleConfiguration->pAudioFrameBuffer = (UINT8*) MEMREALLOC(pSampleConfiguration->pAudioFrameBuffer, frameSize); + CHK_ERR(pSampleConfiguration->pAudioFrameBuffer != NULL, STATUS_NOT_ENOUGH_MEMORY, "[KVS Master] Failed to allocate audio frame buffer"); + pSampleConfiguration->audioBufferSize = frameSize; + } + + frame.frameData = pSampleConfiguration->pAudioFrameBuffer; + frame.size = frameSize; + + CHK_STATUS(readFrameFromDisk(frame.frameData, &frameSize, filePath)); + + frame.presentationTs += SAMPLE_AUDIO_FRAME_DURATION; + + MUTEX_LOCK(pSampleConfiguration->streamingSessionListReadLock); + for (i = 0; i < pSampleConfiguration->streamingSessionCount; ++i) { + status = writeFrame(pSampleConfiguration->sampleStreamingSessionList[i]->pAudioRtcRtpTransceiver, &frame); + if (status != STATUS_SRTP_NOT_READY_YET) { + if (status != STATUS_SUCCESS) { + DLOGV("writeFrame() failed with 0x%08x", status); + } else if (pSampleConfiguration->sampleStreamingSessionList[i]->firstFrame && status == STATUS_SUCCESS) { + CHK_STATUS(writeFirstFrameSentTimeToFile(tsFileName)); + PROFILE_WITH_START_TIME_OBJ(pSampleConfiguration->sampleStreamingSessionList[i]->offerReceiveTime, firstFrameTime, "Time to first frame"); + CppInteg::Cloudwatch::getInstance().monitoring.pushTimeToFirstFrame(firstFrameTime, + Aws::CloudWatch::Model::StandardUnit::Milliseconds); + + calculateDisconnectToFrameSentTime(pSampleConfiguration); + pSampleConfiguration->sampleStreamingSessionList[i]->firstFrame = FALSE; + } + } else { + // Reset file index to stay in sync with video frames. + fileIndex = 0; + } + } + MUTEX_UNLOCK(pSampleConfiguration->streamingSessionListReadLock); + THREAD_SLEEP(SAMPLE_AUDIO_FRAME_DURATION); + } + + CleanUp: + DLOGI("[KVS Master] closing audio thread"); + return (PVOID) (ULONG_PTR) retStatus; +} + +INT32 main(INT32 argc, CHAR* argv[]) +{ + STATUS retStatus = STATUS_SUCCESS; + UINT32 frameSize; + PSampleConfiguration pSampleConfiguration = NULL; + PCHAR region; + UINT32 terminateTimerId = MAX_UINT32; + CHAR channelName[MAX_CHANNEL_NAME_LEN]; + PCHAR channelNamePrefix; + Aws::SDKOptions options; + options.httpOptions.installSigPipeHandler = TRUE; + CHAR tsFileName[MAX_PATH_LEN + 1]; + TID profilingThread; + Aws::InitAPI(options); + { + SET_INSTRUMENTED_ALLOCATORS(); + UINT32 logLevel = setLogLevel(); + // Initialize KVS WebRTC. This must be done before anything else, and must only be done once. + CHK_STATUS(initKvsWebRtc()); + + if (USE_IOT) { + PCHAR pChannelName; + CHK_ERR((pChannelName = GETENV(IOT_CORE_THING_NAME)) != NULL, STATUS_INVALID_OPERATION, "AWS_IOT_CORE_THING_NAME must be set since USE_IOT is enabled"); + STRNCPY(channelName, pChannelName, SIZEOF(channelName)); + } else { + channelNamePrefix = argc > 1 ? argv[1] : CHANNEL_NAME_PREFIX; + SNPRINTF(channelName, SIZEOF(channelName), CHANNEL_NAME_TEMPLATE, channelNamePrefix, RUNNER_LABEL); + } + CHK_STATUS(createSampleConfiguration(channelName, SIGNALING_CHANNEL_ROLE_TYPE_MASTER, TRUE, TRUE, logLevel, &pSampleConfiguration)); + CHK_STATUS(setUpCredentialProvider(pSampleConfiguration, USE_IOT)); + + pSampleConfiguration->forceTurn = FORCE_TURN_ONLY; + pSampleConfiguration->enableMetrics = ENABLE_METRICS; + pSampleConfiguration->channelInfo.useMediaStorage = ENABLE_STORAGE; + pSampleConfiguration->audioCodec = AUDIO_CODEC; + pSampleConfiguration->videoCodec = VIDEO_CODEC; + + if(pSampleConfiguration->channelInfo.useMediaStorage) { + pSampleConfiguration->audioSource = sendRealAudioPackets; + pSampleConfiguration->videoSource = sendRealVideoPackets; + pSampleConfiguration->receiveAudioVideoSource = NULL; + } else { + pSampleConfiguration->audioSource = sendRealAudioPackets; + pSampleConfiguration->videoSource = sendMockVideoPackets; + pSampleConfiguration->receiveAudioVideoSource = NULL; + } + + if ((region = GETENV(DEFAULT_REGION_ENV_VAR)) == NULL) { + region = (PCHAR) DEFAULT_AWS_REGION; + } + CppInteg::Cloudwatch::init(channelName, region, TRUE, pSampleConfiguration->channelInfo.useMediaStorage); + +#ifdef ENABLE_DATA_CHANNEL + pSampleConfiguration->onDataChannel = onDataChannel; +#endif + + pSampleConfiguration->mediaType = SAMPLE_STREAMING_AUDIO_VIDEO; + DLOGI("[KVS Master] Finished setting handlers"); + + DLOGI("[KVS Master] KVS WebRTC initialization completed successfully"); + + PROFILE_CALL_WITH_START_END_T_OBJ( + retStatus = initSignaling(pSampleConfiguration, (PCHAR) SAMPLE_MASTER_CLIENT_ID), pSampleConfiguration->signalingClientMetrics.signalingStartTime, + pSampleConfiguration->signalingClientMetrics.signalingEndTime, pSampleConfiguration->signalingClientMetrics.signalingCallTime, + "Initialize signaling client and connect to the signaling channel"); + + DLOGI("[KVS Master] Channel %s set up done ", channelName); + + THREAD_CREATE(&profilingThread, sendProfilingMetrics, (PVOID) pSampleConfiguration); + + if(RUN_TIME != 0) { + CHK_STATUS(timerQueueAddTimer(pSampleConfiguration->timerQueueHandle, RUN_TIME, TIMER_QUEUE_SINGLE_INVOCATION_PERIOD, terminate, + (UINT64) pSampleConfiguration, &terminateTimerId)); + } + // Checking for termination + CHK_STATUS(sessionCleanupWait(pSampleConfiguration)); + DLOGI("[KVS Master] Streaming session terminated"); + THREAD_JOIN(profilingThread, NULL); + } + +CleanUp: + + if (retStatus != STATUS_SUCCESS) { + DLOGE("[KVS Master] Terminated with status code 0x%08x", retStatus); + } + + DLOGI("[KVS Master] Cleaning up...."); + if (pSampleConfiguration != NULL) { + // Kick of the termination sequence + ATOMIC_STORE_BOOL(&pSampleConfiguration->appTerminateFlag, TRUE); + + if (pSampleConfiguration->mediaSenderTid != INVALID_TID_VALUE) { + THREAD_JOIN(pSampleConfiguration->mediaSenderTid, NULL); + } + + retStatus = freeSignalingClient(&pSampleConfiguration->signalingClientHandle); + if (retStatus != STATUS_SUCCESS) { + DLOGE("[KVS Master] freeSignalingClient(): operation returned status code: 0x%08x", retStatus); + } + + // Free all timer created here that belong to SampleConfiguration timer handle before invoking freeSampleConfiguration + if (IS_VALID_TIMER_QUEUE_HANDLE(pSampleConfiguration->timerQueueHandle)) { + if (outboundStatsTimerId != MAX_UINT32) { + retStatus = timerQueueCancelTimer(pSampleConfiguration->timerQueueHandle, outboundStatsTimerId, + (UINT64) pSampleConfiguration); + if (STATUS_FAILED(retStatus)) { + DLOGE("Failed to cancel outbound stats timer with: 0x%08x", retStatus); + } + outboundStatsTimerId = MAX_UINT32; + } + + if (terminateTimerId != MAX_UINT32) { + retStatus = timerQueueCancelTimer(pSampleConfiguration->timerQueueHandle, terminateTimerId, + (UINT64) pSampleConfiguration); + if (STATUS_FAILED(retStatus)) { + DLOGE("Failed to cancel terminate timer with: 0x%08x", retStatus); + } + terminateTimerId = MAX_UINT32; + } + } + retStatus = freeSampleConfiguration(&pSampleConfiguration); + if (retStatus != STATUS_SUCCESS) { + DLOGE("[KVS Master] freeSampleConfiguration(): operation returned status code: 0x%08x", retStatus); + } + } + DLOGI("[KVS Master] Cleanup done"); + CHK_LOG_ERR(retStatus); + + retStatus = RESET_INSTRUMENTED_ALLOCATORS(); + DLOGI("All SDK allocations freed? %s..0x%08x", retStatus == STATUS_SUCCESS ? "Yes" : "No", retStatus); + SNPRINTF(tsFileName, SIZEOF(tsFileName), "%s%s", FIRST_FRAME_TS_FILE_PATH, STORAGE_DEFAULT_FIRST_FRAME_TS_FILE); + FREMOVE(tsFileName); + CppInteg::Cloudwatch::getInstance().monitoring.pushExitStatus(retStatus); + CppInteg::Cloudwatch::deinit(); + Aws::ShutdownAPI(options); + + // https://www.gnu.org/software/libc/manual/html_node/Exit-Status.html + // We can only return with 0 - 127. Some platforms treat exit code >= 128 + // to be a success code, which might give an unintended behaviour. + // Some platforms also treat 1 or 0 differently, so it's better to use + // EXIT_FAILURE and EXIT_SUCCESS macros for portability. + return STATUS_FAILED(retStatus) ? EXIT_FAILURE : EXIT_SUCCESS; +} diff --git a/cloudwatch-integ/kvsWebRTCClientViewerCloudwatch.cpp b/cloudwatch-integ/kvsWebRTCClientViewerCloudwatch.cpp new file mode 100644 index 0000000000..e26a4a8526 --- /dev/null +++ b/cloudwatch-integ/kvsWebRTCClientViewerCloudwatch.cpp @@ -0,0 +1,363 @@ +#include +#include "../samples/Samples.h" +#include "Cloudwatch.h" + +extern PSampleConfiguration gSampleConfiguration; + +#ifdef ENABLE_DATA_CHANNEL +// onMessage callback for a message received by the viewer on a data channel +VOID dataChannelOnMessageCallback(UINT64 customData, PRtcDataChannel pDataChannel, BOOL isBinary, PBYTE pMessage, UINT32 pMessageLen) +{ + UNUSED_PARAM(customData); + UNUSED_PARAM(pDataChannel); + if (isBinary) { + DLOGI("DataChannel Binary Message"); + } else { + DLOGI("DataChannel String Message: %.*s", pMessageLen, pMessage); + } +} + +// onOpen callback for the onOpen event of a viewer created data channel +VOID dataChannelOnOpenCallback(UINT64 customData, PRtcDataChannel pDataChannel) +{ + STATUS retStatus = STATUS_SUCCESS; + DLOGI("New DataChannel has been opened %s ", pDataChannel->name); + dataChannelOnMessage(pDataChannel, customData, dataChannelOnMessageCallback); + ATOMIC_INCREMENT((PSIZE_T) customData); + // Sending first message to the master over the data channel + retStatus = dataChannelSend(pDataChannel, FALSE, (PBYTE) VIEWER_DATA_CHANNEL_MESSAGE, STRLEN(VIEWER_DATA_CHANNEL_MESSAGE)); + if (retStatus != STATUS_SUCCESS) { + DLOGI("[KVS Viewer] dataChannelSend(): operation returned status code: 0x%08x ", retStatus); + } +} +#endif + +STATUS publishStatsForCanary(UINT32 timerId, UINT64 currentTime, UINT64 customData) +{ + UNUSED_PARAM(timerId); + UNUSED_PARAM(currentTime); + STATUS retStatus = STATUS_SUCCESS; + PSampleConfiguration pSampleConfiguration = (PSampleConfiguration) customData; + PSampleStreamingSession pSampleStreamingSession = NULL; + BOOL sampleConfigurationObjLocked = FALSE; + BOOL statsLocked = FALSE; + + CHK_WARN(pSampleConfiguration != NULL, STATUS_NULL_ARG, "Sample config object not set up"); + + // Use MUTEX_TRYLOCK to avoid possible dead lock when canceling timerQueue + if (!MUTEX_TRYLOCK(pSampleConfiguration->sampleConfigurationObjLock)) { + return retStatus; + } else { + sampleConfigurationObjLocked = TRUE; + } + + pSampleStreamingSession = pSampleConfiguration->sampleStreamingSessionList[0]; + acquireMetricsCtx(pSampleStreamingSession); + CHK_WARN(pSampleStreamingSession != NULL || pSampleStreamingSession->pStatsCtx != NULL, STATUS_NULL_ARG, "Stats ctx object not set up"); + MUTEX_LOCK(pSampleStreamingSession->pStatsCtx->statsUpdateLock); + statsLocked = TRUE; + pSampleStreamingSession->pStatsCtx->kvsRtcStats.requestedTypeOfStats = RTC_STATS_TYPE_INBOUND_RTP; + if (!ATOMIC_LOAD_BOOL(&pSampleStreamingSession->pSampleConfiguration->appTerminateFlag)) { + CHK_LOG_ERR(rtcPeerConnectionGetMetrics(pSampleStreamingSession->pPeerConnection, + pSampleStreamingSession->pVideoRtcRtpTransceiver, + &pSampleStreamingSession->pStatsCtx->kvsRtcStats)); + populateIncomingRtpMetricsContext(pSampleStreamingSession); + CppInteg::Cloudwatch::getInstance().monitoring.pushInboundRtpStats( + &pSampleStreamingSession->pStatsCtx->incomingRTPStatsCtx); + } else { + retStatus = STATUS_TIMER_QUEUE_STOP_SCHEDULING; + } +CleanUp: + if(statsLocked) { + MUTEX_UNLOCK(pSampleStreamingSession->pStatsCtx->statsUpdateLock); + } + releaseMetricsCtx(pSampleStreamingSession); + if (sampleConfigurationObjLocked) { + MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); + } + return retStatus; +} + +STATUS publishEndToEndMetrics(UINT32 timerId, UINT64 currentTime, UINT64 customData) +{ + UNUSED_PARAM(timerId); + UNUSED_PARAM(currentTime); + STATUS retStatus = STATUS_SUCCESS; + PSampleStreamingSession pSampleStreamingSession = (PSampleStreamingSession) customData; + BOOL statsLocked = FALSE; + + acquireMetricsCtx(pSampleStreamingSession); + CHK_WARN(pSampleStreamingSession != NULL || pSampleStreamingSession->pStatsCtx != NULL, STATUS_NULL_ARG, "Stats ctx object not set up"); + MUTEX_LOCK(pSampleStreamingSession->pStatsCtx->statsUpdateLock); + statsLocked = TRUE; + + if (!ATOMIC_LOAD_BOOL(&pSampleStreamingSession->pSampleConfiguration->appTerminateFlag)) { + CppInteg::Cloudwatch::getInstance().monitoring.pushEndToEndMetrics( + &pSampleStreamingSession->pStatsCtx->endToEndMetricsCtx); + } else { + retStatus = STATUS_TIMER_QUEUE_STOP_SCHEDULING; + } +CleanUp: + if(statsLocked) { + MUTEX_UNLOCK(pSampleStreamingSession->pStatsCtx->statsUpdateLock); + } + releaseMetricsCtx(pSampleStreamingSession); + return STATUS_SUCCESS; +} + +VOID videoFrameHandler(UINT64 customData, PFrame pFrame) +{ + PSampleStreamingSession pSampleStreamingSession = (PSampleStreamingSession) customData; + PStatsCtx pStatsCtx = pSampleStreamingSession->pStatsCtx; + // Parse packet and ad e2e metrics + PBYTE frameDataPtr = pFrame->frameData + ANNEX_B_NALU_SIZE; + UINT32 rawPacketSize = 0; + // Get size of hex encoded data + hexDecode((PCHAR) frameDataPtr, pFrame->size - ANNEX_B_NALU_SIZE, NULL, &rawPacketSize); + PBYTE rawPacket = (PBYTE) MEMCALLOC(1, (rawPacketSize * SIZEOF(BYTE))); + hexDecode((PCHAR) frameDataPtr, pFrame->size - ANNEX_B_NALU_SIZE, rawPacket, &rawPacketSize); + + // Extract the timestamp field from raw packet + frameDataPtr = rawPacket; + UINT64 receivedTs = getUnalignedInt64BigEndian((PINT64)(frameDataPtr)); + frameDataPtr += SIZEOF(UINT64); + UINT32 receivedSize = getUnalignedInt32BigEndian((PINT32)(frameDataPtr)); + + if(pSampleStreamingSession == NULL || pStatsCtx == NULL) { + DLOGW("Streaming session freed. Doing nothing"); + return; + } + MUTEX_LOCK(pStatsCtx->statsUpdateLock); + if(pStatsCtx == NULL) { + DLOGW("pStatsCtx freed"); + return; + } + pStatsCtx->endToEndMetricsCtx.frameLatencyAvg = + EMA_ACCUMULATOR_GET_NEXT(pStatsCtx->endToEndMetricsCtx.frameLatencyAvg, GETTIME() - receivedTs); + + // Do a size match of the raw packet. Since raw packet does not contain the NALu, the + // comparison would be rawPacketSize + ANNEX_B_NALU_SIZE and the received size + if (rawPacketSize + ANNEX_B_NALU_SIZE == receivedSize) { + pStatsCtx->endToEndMetricsCtx.sizeMatchAvg = EMA_ACCUMULATOR_GET_NEXT(pSampleStreamingSession->pStatsCtx->endToEndMetricsCtx.sizeMatchAvg, 1); + } else { + pStatsCtx->endToEndMetricsCtx.sizeMatchAvg = EMA_ACCUMULATOR_GET_NEXT(pSampleStreamingSession->pStatsCtx->endToEndMetricsCtx.sizeMatchAvg, 0); + } + SAFE_MEMFREE(rawPacket); + MUTEX_UNLOCK(pStatsCtx->statsUpdateLock); +} + +INT32 main(INT32 argc, CHAR* argv[]) +{ + STATUS retStatus = STATUS_SUCCESS; + RtcSessionDescriptionInit offerSessionDescriptionInit; + UINT32 buffLen = 0; + SignalingMessage message; + PSampleConfiguration pSampleConfiguration = NULL; + PSampleStreamingSession pSampleStreamingSession = NULL; + BOOL locked = FALSE; + CHAR clientId[256]; + PCHAR region; + CHAR channelName[MAX_CHANNEL_NAME_LEN]; + PCHAR channelNamePrefix; + UINT32 e2eTimerId = MAX_UINT32; + UINT32 terminateId = MAX_UINT32; + UINT32 inboundTimerId = MAX_UINT32; + Aws::SDKOptions options; + Aws::InitAPI(options); + { + SET_INSTRUMENTED_ALLOCATORS(); + UINT32 logLevel = setLogLevel(); + +#ifndef _WIN32 + signal(SIGINT, sigintHandler); +#endif + if (USE_IOT) { + PCHAR pChannelName; + CHK_ERR((pChannelName = GETENV(IOT_CORE_THING_NAME)) != NULL, STATUS_INVALID_OPERATION, "AWS_IOT_CORE_THING_NAME must be set since USE_IOT is enabled"); + STRNCPY(channelName, pChannelName, SIZEOF(channelName)); + } else { + channelNamePrefix = argc > 1 ? argv[1] : CHANNEL_NAME_PREFIX; + SNPRINTF(channelName, SIZEOF(channelName), CHANNEL_NAME_TEMPLATE, channelNamePrefix, RUNNER_LABEL); + } + CHK_STATUS(createSampleConfiguration(channelName, SIGNALING_CHANNEL_ROLE_TYPE_VIEWER, USE_TRICKLE_ICE, USE_TURN, logLevel, &pSampleConfiguration)); + CHK_STATUS(setUpCredentialProvider(pSampleConfiguration, USE_IOT)); + pSampleConfiguration->mediaType = SAMPLE_STREAMING_AUDIO_VIDEO; + pSampleConfiguration->audioCodec = AUDIO_CODEC; + pSampleConfiguration->videoCodec = VIDEO_CODEC; + pSampleConfiguration->forceTurn = FORCE_TURN_ONLY; + pSampleConfiguration->enableMetrics = ENABLE_METRICS; + pSampleConfiguration->receiveAudioVideoSource = NULL; + + // Initialize KVS WebRTC. This must be done before anything else, and must only be done once. + CHK_STATUS(initKvsWebRtc()); + DLOGI("[KVS Viewer] KVS WebRTC initialization completed successfully"); + +#ifdef ENABLE_DATA_CHANNEL + pSampleConfiguration->onDataChannel = onDataChannel; +#endif + if ((region = GETENV(DEFAULT_REGION_ENV_VAR)) == NULL) { + region = (PCHAR) DEFAULT_AWS_REGION; + } + CppInteg::Cloudwatch::init(channelName, region, FALSE, FALSE); + + SNPRINTF(clientId, SIZEOF(clientId), "%s_%u", SAMPLE_VIEWER_CLIENT_ID, RAND() % MAX_UINT32); + CHK_STATUS(initSignaling(pSampleConfiguration, clientId)); + DLOGI("[KVS Viewer] Signaling client connection established"); + + // Initialize streaming session + MUTEX_LOCK(pSampleConfiguration->sampleConfigurationObjLock); + locked = TRUE; + CHK_STATUS(createSampleStreamingSession(pSampleConfiguration, NULL, FALSE, &pSampleStreamingSession)); + DLOGI("[KVS Viewer] Creating streaming session...completed"); + pSampleConfiguration->sampleStreamingSessionList[pSampleConfiguration->streamingSessionCount++] = pSampleStreamingSession; + + MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); + locked = FALSE; + + MEMSET(&offerSessionDescriptionInit, 0x00, SIZEOF(RtcSessionDescriptionInit)); + + offerSessionDescriptionInit.useTrickleIce = pSampleStreamingSession->remoteCanTrickleIce; + CHK_STATUS(setLocalDescription(pSampleStreamingSession->pPeerConnection, &offerSessionDescriptionInit)); + DLOGI("[KVS Viewer] Completed setting local description"); + + CHK_STATUS(transceiverOnFrame(pSampleStreamingSession->pVideoRtcRtpTransceiver, (UINT64) pSampleStreamingSession, videoFrameHandler)); + + if (!pSampleConfiguration->trickleIce) { + DLOGI("[KVS Viewer] Non trickle ice. Wait for Candidate collection to complete"); + MUTEX_LOCK(pSampleConfiguration->sampleConfigurationObjLock); + locked = TRUE; + + while (!ATOMIC_LOAD_BOOL(&pSampleStreamingSession->candidateGatheringDone)) { + CHK_WARN(!ATOMIC_LOAD_BOOL(&pSampleStreamingSession->terminateFlag), STATUS_OPERATION_TIMED_OUT, + "application terminated and candidate gathering still not done"); + CVAR_WAIT(pSampleConfiguration->cvar, pSampleConfiguration->sampleConfigurationObjLock, 5 * HUNDREDS_OF_NANOS_IN_A_SECOND); + } + + MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); + locked = FALSE; + + DLOGI("[KVS Viewer] Candidate collection completed"); + } + + CHK_STATUS(createOffer(pSampleStreamingSession->pPeerConnection, &offerSessionDescriptionInit)); + DLOGI("[KVS Viewer] Offer creation successful"); + + DLOGI("[KVS Viewer] Generating JSON of session description...."); + CHK_STATUS(serializeSessionDescriptionInit(&offerSessionDescriptionInit, NULL, &buffLen)); + + if (buffLen >= SIZEOF(message.payload)) { + DLOGE("[KVS Viewer] serializeSessionDescriptionInit(): operation returned status code: 0x%08x ", STATUS_INVALID_OPERATION); + retStatus = STATUS_INVALID_OPERATION; + goto CleanUp; + } + + CHK_STATUS(serializeSessionDescriptionInit(&offerSessionDescriptionInit, message.payload, &buffLen)); + + message.version = SIGNALING_MESSAGE_CURRENT_VERSION; + message.messageType = SIGNALING_MESSAGE_TYPE_OFFER; + STRCPY(message.peerClientId, SAMPLE_MASTER_CLIENT_ID); + message.payloadLen = (buffLen / SIZEOF(CHAR)) - 1; + message.correlationId[0] = '\0'; + + CHK_STATUS(signalingClientSendMessageSync(pSampleConfiguration->signalingClientHandle, &message)); +#ifdef ENABLE_DATA_CHANNEL + PRtcDataChannel pDataChannel = NULL; + PRtcPeerConnection pPeerConnection = pSampleStreamingSession->pPeerConnection; + SIZE_T datachannelLocalOpenCount = 0; + + // Creating a new datachannel on the peer connection of the existing sample streaming session + CHK_STATUS(createDataChannel(pPeerConnection, channelName, NULL, &pDataChannel)); + DLOGI("[KVS Viewer] Creating data channel...completed"); + + // Setting a callback for when the data channel is open + CHK_STATUS(dataChannelOnOpen(pDataChannel, (UINT64) &datachannelLocalOpenCount, dataChannelOnOpenCallback)); + DLOGI("[KVS Viewer] Data Channel open now..."); +#endif + CHK_STATUS(timerQueueAddTimer(pSampleConfiguration->timerQueueHandle, END_TO_END_METRICS_INVOCATION_PERIOD, END_TO_END_METRICS_INVOCATION_PERIOD, + publishEndToEndMetrics, (UINT64) pSampleStreamingSession, &e2eTimerId)); + + if(RUN_TIME != 0) { + CHK_STATUS(timerQueueAddTimer(pSampleConfiguration->timerQueueHandle, RUN_TIME, + TIMER_QUEUE_SINGLE_INVOCATION_PERIOD, terminate, + (UINT64) pSampleConfiguration, &terminateId)); + } + CHK_STATUS(timerQueueAddTimer(pSampleConfiguration->timerQueueHandle, END_TO_END_METRICS_INVOCATION_PERIOD, END_TO_END_METRICS_INVOCATION_PERIOD, + publishStatsForCanary, (UINT64) pSampleConfiguration, &inboundTimerId)); + // Block until interrupted + while (!ATOMIC_LOAD_BOOL(&pSampleConfiguration->interrupted) && !ATOMIC_LOAD_BOOL(&pSampleStreamingSession->terminateFlag)) { + THREAD_SLEEP(HUNDREDS_OF_NANOS_IN_A_SECOND); + } + } + +CleanUp: + + if (retStatus != STATUS_SUCCESS) { + DLOGE("[KVS Viewer] Terminated with status code 0x%08x", retStatus); + } + + DLOGI("[KVS Viewer] Cleaning up...."); + + if (locked) { + MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); + } + + if (pSampleConfiguration->enableFileLogging) { + freeFileLogger(); + } + if (pSampleConfiguration != NULL) { + retStatus = freeSignalingClient(&pSampleConfiguration->signalingClientHandle); + if (retStatus != STATUS_SUCCESS) { + DLOGE("[KVS Viewer] freeSignalingClient(): operation returned status code: 0x%08x ", retStatus); + } + + // Free all timer created here that belong to SampleConfiguration timer handle before invoking freeSampleConfiguration + if (IS_VALID_TIMER_QUEUE_HANDLE(pSampleConfiguration->timerQueueHandle)) { + if (inboundTimerId != MAX_UINT32) { + retStatus = timerQueueCancelTimer(pSampleConfiguration->timerQueueHandle, inboundTimerId, + (UINT64) pSampleConfiguration); + if (STATUS_FAILED(retStatus)) { + DLOGE("Failed to cancel inbound stats timer with: 0x%08x", retStatus); + } + inboundTimerId = MAX_UINT32; + } + + if (e2eTimerId != MAX_UINT32) { + retStatus = timerQueueCancelTimer(pSampleConfiguration->timerQueueHandle, e2eTimerId, + (UINT64) pSampleConfiguration); + if (STATUS_FAILED(retStatus)) { + DLOGE("Failed to cancel e2e timer with: 0x%08x", retStatus); + } + e2eTimerId = MAX_UINT32; + } + + if (terminateId != MAX_UINT32) { + retStatus = timerQueueCancelTimer(pSampleConfiguration->timerQueueHandle, terminateId, + (UINT64) pSampleConfiguration); + if (STATUS_FAILED(retStatus)) { + DLOGE("Failed to cancel terminate timer with: 0x%08x", retStatus); + } + terminateId = MAX_UINT32; + } + } + + retStatus = freeSampleConfiguration(&pSampleConfiguration); + if (retStatus != STATUS_SUCCESS) { + DLOGE("[KVS Viewer] freeSampleConfiguration(): operation returned status code: 0x%08x ", retStatus); + } + } + DLOGI("[KVS Viewer] Cleanup done"); + + retStatus = RESET_INSTRUMENTED_ALLOCATORS(); + DLOGI("All SDK allocations freed? %s..0x%08x", retStatus == STATUS_SUCCESS ? "Yes" : "No", retStatus); + + CppInteg::Cloudwatch::getInstance().monitoring.pushExitStatus(retStatus); + CppInteg::Cloudwatch::deinit(); + Aws::ShutdownAPI(options); + + // https://www.gnu.org/software/libc/manual/html_node/Exit-Status.html + // We can only return with 0 - 127. Some platforms treat exit code >= 128 + // to be a success code, which might give an unintended behaviour. + // Some platforms also treat 1 or 0 differently, so it's better to use + // EXIT_FAILURE and EXIT_SUCCESS macros for portability. + return STATUS_FAILED(retStatus) ? EXIT_FAILURE : EXIT_SUCCESS; +} diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt index ea9067bf18..d2005dc57b 100644 --- a/samples/CMakeLists.txt +++ b/samples/CMakeLists.txt @@ -2,8 +2,6 @@ cmake_minimum_required(VERSION 3.6.3) project(KinesisVideoWebRTCClientSamples LANGUAGES C) -#set(OPEN_SRC_INSTALL_PREFIX "${CMAKE_CURRENT_SOURCE_DIR}/open-source" CACHE PATH "Libraries will be downloaded and build in this directory.") - message("OPEN_SRC_INSTALL_PREFIX=${OPEN_SRC_INSTALL_PREFIX}") find_package(PkgConfig REQUIRED) @@ -49,6 +47,7 @@ endif() include_directories(${OPEN_SRC_INSTALL_PREFIX}/include) include_directories(${OPEN_SRC_INCLUDE_DIRS}) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}) link_directories(${OPEN_SRC_INSTALL_PREFIX}/lib) # copy sample frames to this subproject build folder, in case developer runs sample program with command `kvsWebrtcClientMaster` from `build/samples` dir. @@ -59,15 +58,26 @@ file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/h265SampleFrames" DESTINATION .) add_executable( kvsWebrtcClientMaster - Common.c - kvsWebRTCClientMaster.c) -target_link_libraries(kvsWebrtcClientMaster kvsWebrtcClient kvsWebrtcSignalingClient ${EXTRA_DEPS} kvsCommonLws kvspicUtils websockets) + lib/Common.c + lib/Utility.c + lib/MetricsHandling.c + lib/DataChannelHandling.c + lib/SignalingMsgHandler.c + lib/Media.c + kvsWebRTCClientMaster.c) + +target_link_libraries(kvsWebrtcClientMaster kvsWebrtcClient kvsWebrtcSignalingClient ${EXTRA_DEPS} kvsCommonLws kvspicUtils) add_executable( kvsWebrtcClientViewer - Common.c - kvsWebRTCClientViewer.c) -target_link_libraries(kvsWebrtcClientViewer kvsWebrtcClient kvsWebrtcSignalingClient ${EXTRA_DEPS} kvsCommonLws kvspicUtils websockets) + lib/Common.c + lib/Utility.c + lib/MetricsHandling.c + lib/DataChannelHandling.c + lib/SignalingMsgHandler.c + lib/Media.c + kvsWebRTCClientViewer.c) +target_link_libraries(kvsWebrtcClientViewer kvsWebrtcClient kvsWebrtcSignalingClient ${EXTRA_DEPS} kvsCommonLws kvspicUtils) add_executable( discoverNatBehavior @@ -77,9 +87,14 @@ target_link_libraries(discoverNatBehavior kvsWebrtcClient ${EXTRA_DEPS}) if(GST_FOUND) add_executable( kvsWebrtcClientMasterGstSample - Common.c - GstAudioVideoReceiver.c - kvsWebRTCClientMasterGstSample.c + lib/Common.c + lib/Utility.c + lib/MetricsHandling.c + GstAudioVideoReceiver.c + lib/DataChannelHandling.c + lib/SignalingMsgHandler.c + lib/Media.c + kvsWebRTCClientMasterGstSample.c ) target_link_libraries(kvsWebrtcClientMasterGstSample kvsWebrtcClient kvsWebrtcSignalingClient ${EXTRA_DEPS} ${GST_SAMPLE_LIBRARIES} kvsCommonLws kvspicUtils) @@ -89,9 +104,14 @@ if(GST_FOUND) add_executable( kvsWebrtcClientViewerGstSample - Common.c - GstAudioVideoReceiver.c - kvsWebRTCClientViewerGstSample.c + lib/Common.c + lib/Utility.c + lib/MetricsHandling.c + lib/DataChannelHandling.c + GstAudioVideoReceiver.c + lib/SignalingMsgHandler.c + lib/Media.c + kvsWebRTCClientViewerGstSample.c ) target_link_libraries(kvsWebrtcClientViewerGstSample kvsWebrtcClient kvsWebrtcSignalingClient ${EXTRA_DEPS} ${GST_SAMPLE_LIBRARIES} kvsCommonLws kvspicUtils) diff --git a/samples/Common.c b/samples/Common.c deleted file mode 100644 index d512c4c559..0000000000 --- a/samples/Common.c +++ /dev/null @@ -1,1930 +0,0 @@ -#define LOG_CLASS "WebRtcSamples" -#include "Samples.h" - -#define KVS_DEFAULT_MEDIA_SENDER_THREAD_STACK_SIZE 64 * 1024 -#define KVS_MINIMUM_THREAD_STACK_SIZE 16 * 1024 - -PSampleConfiguration gSampleConfiguration = NULL; - -VOID sigintHandler(INT32 sigNum) -{ - UNUSED_PARAM(sigNum); - if (gSampleConfiguration != NULL) { - ATOMIC_STORE_BOOL(&gSampleConfiguration->interrupted, TRUE); - CVAR_BROADCAST(gSampleConfiguration->cvar); - } -} - -UINT32 setLogLevel() -{ - PCHAR pLogLevel; - UINT32 logLevel = LOG_LEVEL_DEBUG; - if (NULL == (pLogLevel = GETENV(DEBUG_LOG_LEVEL_ENV_VAR)) || STATUS_SUCCESS != STRTOUI32(pLogLevel, NULL, 10, &logLevel) || - logLevel < LOG_LEVEL_VERBOSE || logLevel > LOG_LEVEL_SILENT) { - logLevel = LOG_LEVEL_WARN; - } - SET_LOGGER_LOG_LEVEL(logLevel); - return logLevel; -} - -STATUS signalingCallFailed(STATUS status) -{ - return (STATUS_SIGNALING_GET_TOKEN_CALL_FAILED == status || STATUS_SIGNALING_DESCRIBE_CALL_FAILED == status || - STATUS_SIGNALING_CREATE_CALL_FAILED == status || STATUS_SIGNALING_GET_ENDPOINT_CALL_FAILED == status || - STATUS_SIGNALING_GET_ICE_CONFIG_CALL_FAILED == status || STATUS_SIGNALING_CONNECT_CALL_FAILED == status || - STATUS_SIGNALING_DESCRIBE_MEDIA_CALL_FAILED == status); -} - -VOID onConnectionStateChange(UINT64 customData, RTC_PEER_CONNECTION_STATE newState) -{ - STATUS retStatus = STATUS_SUCCESS; - PSampleStreamingSession pSampleStreamingSession = (PSampleStreamingSession) customData; - CHK(pSampleStreamingSession != NULL && pSampleStreamingSession->pSampleConfiguration != NULL, STATUS_INTERNAL_ERROR); - - PSampleConfiguration pSampleConfiguration = pSampleStreamingSession->pSampleConfiguration; - DLOGI("New connection state %u", newState); - - switch (newState) { - case RTC_PEER_CONNECTION_STATE_CONNECTED: - ATOMIC_STORE_BOOL(&pSampleConfiguration->connected, TRUE); - CVAR_BROADCAST(pSampleConfiguration->cvar); - - pSampleStreamingSession->peerConnectionMetrics.peerConnectionStats.peerConnectionConnectedTime = - GETTIME() / HUNDREDS_OF_NANOS_IN_A_MILLISECOND; - CHK_STATUS(peerConnectionGetMetrics(pSampleStreamingSession->pPeerConnection, &pSampleStreamingSession->peerConnectionMetrics)); - CHK_STATUS(iceAgentGetMetrics(pSampleStreamingSession->pPeerConnection, &pSampleStreamingSession->iceMetrics)); - - if (pSampleConfiguration->enableIceStats) { - CHK_LOG_ERR(logSelectedIceCandidatesInformation(pSampleStreamingSession)); - } - break; - case RTC_PEER_CONNECTION_STATE_FAILED: - // explicit fallthrough - case RTC_PEER_CONNECTION_STATE_CLOSED: - // explicit fallthrough - case RTC_PEER_CONNECTION_STATE_DISCONNECTED: - DLOGD("p2p connection disconnected"); - ATOMIC_STORE_BOOL(&pSampleStreamingSession->terminateFlag, TRUE); - CVAR_BROADCAST(pSampleConfiguration->cvar); - // explicit fallthrough - default: - ATOMIC_STORE_BOOL(&pSampleConfiguration->connected, FALSE); - CVAR_BROADCAST(pSampleConfiguration->cvar); - - break; - } - -CleanUp: - - CHK_LOG_ERR(retStatus); -} - -STATUS signalingClientStateChanged(UINT64 customData, SIGNALING_CLIENT_STATE state) -{ - UNUSED_PARAM(customData); - STATUS retStatus = STATUS_SUCCESS; - PCHAR pStateStr; - - signalingClientGetStateString(state, &pStateStr); - - DLOGV("Signaling client state changed to %d - '%s'", state, pStateStr); - - // Return success to continue - return retStatus; -} - -STATUS signalingClientError(UINT64 customData, STATUS status, PCHAR msg, UINT32 msgLen) -{ - PSampleConfiguration pSampleConfiguration = (PSampleConfiguration) customData; - - DLOGW("Signaling client generated an error 0x%08x - '%.*s'", status, msgLen, msg); - - // We will force re-create the signaling client on the following errors - if (status == STATUS_SIGNALING_ICE_CONFIG_REFRESH_FAILED || status == STATUS_SIGNALING_RECONNECT_FAILED) { - ATOMIC_STORE_BOOL(&pSampleConfiguration->recreateSignalingClient, TRUE); - CVAR_BROADCAST(pSampleConfiguration->cvar); - } - - return STATUS_SUCCESS; -} - -STATUS logSelectedIceCandidatesInformation(PSampleStreamingSession pSampleStreamingSession) -{ - ENTERS(); - STATUS retStatus = STATUS_SUCCESS; - RtcStats rtcMetrics; - - CHK(pSampleStreamingSession != NULL, STATUS_NULL_ARG); - rtcMetrics.requestedTypeOfStats = RTC_STATS_TYPE_LOCAL_CANDIDATE; - CHK_STATUS(rtcPeerConnectionGetMetrics(pSampleStreamingSession->pPeerConnection, NULL, &rtcMetrics)); - DLOGI("Local Candidate IP Address: %s", rtcMetrics.rtcStatsObject.localIceCandidateStats.address); - DLOGI("Local Candidate type: %s", rtcMetrics.rtcStatsObject.localIceCandidateStats.candidateType); - DLOGI("Local Candidate port: %d", rtcMetrics.rtcStatsObject.localIceCandidateStats.port); - DLOGI("Local Candidate priority: %d", rtcMetrics.rtcStatsObject.localIceCandidateStats.priority); - DLOGI("Local Candidate transport protocol: %s", rtcMetrics.rtcStatsObject.localIceCandidateStats.protocol); - DLOGI("Local Candidate relay protocol: %s", rtcMetrics.rtcStatsObject.localIceCandidateStats.relayProtocol); - DLOGI("Local Candidate Ice server source: %s", rtcMetrics.rtcStatsObject.localIceCandidateStats.url); - - rtcMetrics.requestedTypeOfStats = RTC_STATS_TYPE_REMOTE_CANDIDATE; - CHK_STATUS(rtcPeerConnectionGetMetrics(pSampleStreamingSession->pPeerConnection, NULL, &rtcMetrics)); - DLOGI("Remote Candidate IP Address: %s", rtcMetrics.rtcStatsObject.remoteIceCandidateStats.address); - DLOGI("Remote Candidate type: %s", rtcMetrics.rtcStatsObject.remoteIceCandidateStats.candidateType); - DLOGI("Remote Candidate port: %d", rtcMetrics.rtcStatsObject.remoteIceCandidateStats.port); - DLOGI("Remote Candidate priority: %d", rtcMetrics.rtcStatsObject.remoteIceCandidateStats.priority); - DLOGI("Remote Candidate transport protocol: %s", rtcMetrics.rtcStatsObject.remoteIceCandidateStats.protocol); -CleanUp: - LEAVES(); - return retStatus; -} - -STATUS handleAnswer(PSampleConfiguration pSampleConfiguration, PSampleStreamingSession pSampleStreamingSession, PSignalingMessage pSignalingMessage) -{ - UNUSED_PARAM(pSampleConfiguration); - STATUS retStatus = STATUS_SUCCESS; - PRtcSessionDescriptionInit pAnswerSessionDescriptionInit = NULL; - - pAnswerSessionDescriptionInit = (PRtcSessionDescriptionInit) MEMCALLOC(1, SIZEOF(RtcSessionDescriptionInit)); - - CHK_STATUS(deserializeSessionDescriptionInit(pSignalingMessage->payload, pSignalingMessage->payloadLen, pAnswerSessionDescriptionInit)); - CHK_STATUS(setRemoteDescription(pSampleStreamingSession->pPeerConnection, pAnswerSessionDescriptionInit)); - - // The audio video receive routine should be per streaming session - if (pSampleConfiguration->receiveAudioVideoSource != NULL) { - THREAD_CREATE(&pSampleStreamingSession->receiveAudioVideoSenderTid, pSampleConfiguration->receiveAudioVideoSource, - (PVOID) pSampleStreamingSession); - } -CleanUp: - - if (pAnswerSessionDescriptionInit != NULL) { - SAFE_MEMFREE(pAnswerSessionDescriptionInit); - } - - CHK_LOG_ERR(retStatus); - - return retStatus; -} - -PVOID mediaSenderRoutine(PVOID customData) -{ - STATUS retStatus = STATUS_SUCCESS; - PSampleConfiguration pSampleConfiguration = (PSampleConfiguration) customData; - CHK(pSampleConfiguration != NULL, STATUS_NULL_ARG); - pSampleConfiguration->videoSenderTid = INVALID_TID_VALUE; - pSampleConfiguration->audioSenderTid = INVALID_TID_VALUE; - - MUTEX_LOCK(pSampleConfiguration->sampleConfigurationObjLock); - while (!ATOMIC_LOAD_BOOL(&pSampleConfiguration->connected) && !ATOMIC_LOAD_BOOL(&pSampleConfiguration->appTerminateFlag)) { - CVAR_WAIT(pSampleConfiguration->cvar, pSampleConfiguration->sampleConfigurationObjLock, 5 * HUNDREDS_OF_NANOS_IN_A_SECOND); - } - MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); - - CHK(!ATOMIC_LOAD_BOOL(&pSampleConfiguration->appTerminateFlag), retStatus); - - if (pSampleConfiguration->videoSource != NULL) { - THREAD_CREATE_WITH_PARAMS(&pSampleConfiguration->videoSenderTid, pSampleConfiguration->videoSource, - KVS_DEFAULT_MEDIA_SENDER_THREAD_STACK_SIZE, (PVOID) pSampleConfiguration); - } - - if (pSampleConfiguration->audioSource != NULL) { - THREAD_CREATE_WITH_PARAMS(&pSampleConfiguration->audioSenderTid, pSampleConfiguration->audioSource, - KVS_DEFAULT_MEDIA_SENDER_THREAD_STACK_SIZE, (PVOID) pSampleConfiguration); - } - - if (pSampleConfiguration->videoSenderTid != INVALID_TID_VALUE) { - THREAD_JOIN(pSampleConfiguration->videoSenderTid, NULL); - } - - if (pSampleConfiguration->audioSenderTid != INVALID_TID_VALUE) { - THREAD_JOIN(pSampleConfiguration->audioSenderTid, NULL); - } - -CleanUp: - // clean the flag of the media thread. - ATOMIC_STORE_BOOL(&pSampleConfiguration->mediaThreadStarted, FALSE); - CHK_LOG_ERR(retStatus); - return NULL; -} - -STATUS handleOffer(PSampleConfiguration pSampleConfiguration, PSampleStreamingSession pSampleStreamingSession, PSignalingMessage pSignalingMessage) -{ - STATUS retStatus = STATUS_SUCCESS; - PRtcSessionDescriptionInit pOfferSessionDescriptionInit = NULL; - NullableBool canTrickle; - BOOL mediaThreadStarted; - - CHK(pSampleConfiguration != NULL && pSignalingMessage != NULL, STATUS_NULL_ARG); - - pOfferSessionDescriptionInit = (PRtcSessionDescriptionInit) MEMCALLOC(1, SIZEOF(RtcSessionDescriptionInit)); - MEMSET(&pSampleStreamingSession->answerSessionDescriptionInit, 0x00, SIZEOF(RtcSessionDescriptionInit)); - DLOGD("**offer:%s", pSignalingMessage->payload); - CHK_STATUS(deserializeSessionDescriptionInit(pSignalingMessage->payload, pSignalingMessage->payloadLen, pOfferSessionDescriptionInit)); - CHK_STATUS(setRemoteDescription(pSampleStreamingSession->pPeerConnection, pOfferSessionDescriptionInit)); - canTrickle = canTrickleIceCandidates(pSampleStreamingSession->pPeerConnection); - /* cannot be null after setRemoteDescription */ - CHECK(!NULLABLE_CHECK_EMPTY(canTrickle)); - pSampleStreamingSession->remoteCanTrickleIce = canTrickle.value; - CHK_STATUS(setLocalDescription(pSampleStreamingSession->pPeerConnection, &pSampleStreamingSession->answerSessionDescriptionInit)); - - /* - * If remote support trickle ice, send answer now. Otherwise answer will be sent once ice candidate gathering is complete. - */ - if (pSampleStreamingSession->remoteCanTrickleIce) { - CHK_STATUS(createAnswer(pSampleStreamingSession->pPeerConnection, &pSampleStreamingSession->answerSessionDescriptionInit)); - CHK_STATUS(respondWithAnswer(pSampleStreamingSession)); - } - - mediaThreadStarted = ATOMIC_EXCHANGE_BOOL(&pSampleConfiguration->mediaThreadStarted, TRUE); - if (!mediaThreadStarted) { - THREAD_CREATE_WITH_PARAMS(&pSampleConfiguration->mediaSenderTid, mediaSenderRoutine, KVS_MINIMUM_THREAD_STACK_SIZE, - (PVOID) pSampleConfiguration); - } - - // The audio video receive routine should be per streaming session - if (pSampleConfiguration->receiveAudioVideoSource != NULL) { - THREAD_CREATE(&pSampleStreamingSession->receiveAudioVideoSenderTid, pSampleConfiguration->receiveAudioVideoSource, - (PVOID) pSampleStreamingSession); - } -CleanUp: - if (pOfferSessionDescriptionInit != NULL) { - SAFE_MEMFREE(pOfferSessionDescriptionInit); - } - - CHK_LOG_ERR(retStatus); - - return retStatus; -} - -STATUS sendSignalingMessage(PSampleStreamingSession pSampleStreamingSession, PSignalingMessage pMessage) -{ - STATUS retStatus = STATUS_SUCCESS; - BOOL locked = FALSE; - PSampleConfiguration pSampleConfiguration; - // Validate the input params - CHK(pSampleStreamingSession != NULL && pSampleStreamingSession->pSampleConfiguration != NULL && pMessage != NULL, STATUS_NULL_ARG); - - pSampleConfiguration = pSampleStreamingSession->pSampleConfiguration; - - CHK(IS_VALID_MUTEX_VALUE(pSampleConfiguration->signalingSendMessageLock) && - IS_VALID_SIGNALING_CLIENT_HANDLE(pSampleConfiguration->signalingClientHandle), - STATUS_INVALID_OPERATION); - - MUTEX_LOCK(pSampleConfiguration->signalingSendMessageLock); - locked = TRUE; - CHK_STATUS(signalingClientSendMessageSync(pSampleConfiguration->signalingClientHandle, pMessage)); - if (pMessage->messageType == SIGNALING_MESSAGE_TYPE_ANSWER) { - CHK_STATUS(signalingClientGetMetrics(pSampleConfiguration->signalingClientHandle, &pSampleConfiguration->signalingClientMetrics)); - DLOGP("[Signaling offer received to answer sent time] %" PRIu64 " ms", - pSampleConfiguration->signalingClientMetrics.signalingClientStats.offerToAnswerTime); - } - -CleanUp: - - if (locked) { - MUTEX_UNLOCK(pSampleStreamingSession->pSampleConfiguration->signalingSendMessageLock); - } - - CHK_LOG_ERR(retStatus); - return retStatus; -} - -STATUS respondWithAnswer(PSampleStreamingSession pSampleStreamingSession) -{ - STATUS retStatus = STATUS_SUCCESS; - SignalingMessage message; - UINT32 buffLen = MAX_SIGNALING_MESSAGE_LEN; - - CHK_STATUS(serializeSessionDescriptionInit(&pSampleStreamingSession->answerSessionDescriptionInit, message.payload, &buffLen)); - - message.version = SIGNALING_MESSAGE_CURRENT_VERSION; - message.messageType = SIGNALING_MESSAGE_TYPE_ANSWER; - STRNCPY(message.peerClientId, pSampleStreamingSession->peerId, MAX_SIGNALING_CLIENT_ID_LEN); - message.payloadLen = (UINT32) STRLEN(message.payload); - // SNPRINTF appends null terminator, so we do not manually add it - SNPRINTF(message.correlationId, MAX_CORRELATION_ID_LEN, "%llu_%llu", GETTIME(), ATOMIC_INCREMENT(&pSampleStreamingSession->correlationIdPostFix)); - DLOGD("Responding With Answer With correlationId: %s", message.correlationId); - CHK_STATUS(sendSignalingMessage(pSampleStreamingSession, &message)); - -CleanUp: - - CHK_LOG_ERR(retStatus); - return retStatus; -} - -BOOL sampleFilterNetworkInterfaces(UINT64 customData, PCHAR networkInt) -{ - UNUSED_PARAM(customData); - BOOL useInterface = FALSE; - if (STRNCMP(networkInt, (PCHAR) "eth0", ARRAY_SIZE("eth0")) == 0) { - useInterface = TRUE; - } - DLOGD("%s %s", networkInt, (useInterface) ? ("allowed. Candidates to be gathered") : ("blocked. Candidates will not be gathered")); - return useInterface; -} - -VOID onIceCandidateHandler(UINT64 customData, PCHAR candidateJson) -{ - STATUS retStatus = STATUS_SUCCESS; - PSampleStreamingSession pSampleStreamingSession = (PSampleStreamingSession) customData; - SignalingMessage message; - - CHK(pSampleStreamingSession != NULL, STATUS_NULL_ARG); - - if (candidateJson == NULL) { - DLOGD("ice candidate gathering finished"); - ATOMIC_STORE_BOOL(&pSampleStreamingSession->candidateGatheringDone, TRUE); - - // if application is master and non-trickle ice, send answer now. - if (pSampleStreamingSession->pSampleConfiguration->channelInfo.channelRoleType == SIGNALING_CHANNEL_ROLE_TYPE_MASTER && - !pSampleStreamingSession->remoteCanTrickleIce) { - CHK_STATUS(createAnswer(pSampleStreamingSession->pPeerConnection, &pSampleStreamingSession->answerSessionDescriptionInit)); - CHK_STATUS(respondWithAnswer(pSampleStreamingSession)); - } else if (pSampleStreamingSession->pSampleConfiguration->channelInfo.channelRoleType == SIGNALING_CHANNEL_ROLE_TYPE_VIEWER && - !pSampleStreamingSession->pSampleConfiguration->trickleIce) { - CVAR_BROADCAST(pSampleStreamingSession->pSampleConfiguration->cvar); - } - - } else if (pSampleStreamingSession->remoteCanTrickleIce && ATOMIC_LOAD_BOOL(&pSampleStreamingSession->peerIdReceived)) { - message.version = SIGNALING_MESSAGE_CURRENT_VERSION; - message.messageType = SIGNALING_MESSAGE_TYPE_ICE_CANDIDATE; - STRNCPY(message.peerClientId, pSampleStreamingSession->peerId, MAX_SIGNALING_CLIENT_ID_LEN); - message.payloadLen = (UINT32) STRNLEN(candidateJson, MAX_SIGNALING_MESSAGE_LEN); - STRNCPY(message.payload, candidateJson, message.payloadLen); - message.correlationId[0] = '\0'; - CHK_STATUS(sendSignalingMessage(pSampleStreamingSession, &message)); - } - -CleanUp: - - CHK_LOG_ERR(retStatus); -} - -PVOID asyncGetIceConfigInfo(PVOID args) -{ - STATUS retStatus = STATUS_SUCCESS; - AsyncGetIceStruct* data = (AsyncGetIceStruct*) args; - PIceConfigInfo pIceConfigInfo = NULL; - UINT32 uriCount = 0; - UINT32 i = 0, maxTurnServer = 1; - - if (data != NULL) { - /* signalingClientGetIceConfigInfoCount can return more than one turn server. Use only one to optimize - * candidate gathering latency. But user can also choose to use more than 1 turn server. */ - for (uriCount = 0, i = 0; i < maxTurnServer; i++) { - /* - * if configuration.iceServers[uriCount + 1].urls is "turn:ip:port?transport=udp" then ICE will try TURN over UDP - * if configuration.iceServers[uriCount + 1].urls is "turn:ip:port?transport=tcp" then ICE will try TURN over TCP/TLS - * if configuration.iceServers[uriCount + 1].urls is "turns:ip:port?transport=udp", it's currently ignored because sdk dont do TURN - * over DTLS yet. if configuration.iceServers[uriCount + 1].urls is "turns:ip:port?transport=tcp" then ICE will try TURN over TCP/TLS - * if configuration.iceServers[uriCount + 1].urls is "turn:ip:port" then ICE will try both TURN over UDP and TCP/TLS - * - * It's recommended to not pass too many TURN iceServers to configuration because it will slow down ice gathering in non-trickle mode. - */ - CHK_STATUS(signalingClientGetIceConfigInfo(data->signalingClientHandle, i, &pIceConfigInfo)); - CHECK(uriCount < MAX_ICE_SERVERS_COUNT); - uriCount += pIceConfigInfo->uriCount; - CHK_STATUS(addConfigToServerList(&(data->pRtcPeerConnection), pIceConfigInfo)); - } - } - *(data->pUriCount) += uriCount; - -CleanUp: - SAFE_MEMFREE(data); - CHK_LOG_ERR(retStatus); - return NULL; -} - -STATUS initializePeerConnection(PSampleConfiguration pSampleConfiguration, PRtcPeerConnection* ppRtcPeerConnection) -{ - ENTERS(); - STATUS retStatus = STATUS_SUCCESS; - RtcConfiguration configuration; -#ifndef ENABLE_KVS_THREADPOOL - UINT32 i, j, maxTurnServer = 1; - PIceConfigInfo pIceConfigInfo; - UINT32 uriCount = 0; -#endif - UINT64 data; - PRtcCertificate pRtcCertificate = NULL; - - CHK(pSampleConfiguration != NULL && ppRtcPeerConnection != NULL, STATUS_NULL_ARG); - - MEMSET(&configuration, 0x00, SIZEOF(RtcConfiguration)); - - // Set this to custom callback to enable filtering of interfaces - configuration.kvsRtcConfiguration.iceSetInterfaceFilterFunc = NULL; - - // disable TWCC - configuration.kvsRtcConfiguration.disableSenderSideBandwidthEstimation = !(pSampleConfiguration->enableTwcc); - DLOGI("TWCC is : %s", configuration.kvsRtcConfiguration.disableSenderSideBandwidthEstimation ? "Disabled" : "Enabled"); - - // Set the ICE mode explicitly - configuration.iceTransportPolicy = ICE_TRANSPORT_POLICY_ALL; - - configuration.kvsRtcConfiguration.enableIceStats = pSampleConfiguration->enableIceStats; - // Set the STUN server - PCHAR pKinesisVideoStunUrlPostFix = KINESIS_VIDEO_STUN_URL_POSTFIX; - // If region is in CN, add CN region uri postfix - if (STRSTR(pSampleConfiguration->channelInfo.pRegion, "cn-")) { - pKinesisVideoStunUrlPostFix = KINESIS_VIDEO_STUN_URL_POSTFIX_CN; - } - SNPRINTF(configuration.iceServers[0].urls, MAX_ICE_CONFIG_URI_LEN, KINESIS_VIDEO_STUN_URL, pSampleConfiguration->channelInfo.pRegion, - pKinesisVideoStunUrlPostFix); - - // Check if we have any pregenerated certs and use them - // NOTE: We are running under the config lock - retStatus = stackQueueDequeue(pSampleConfiguration->pregeneratedCertificates, &data); - CHK(retStatus == STATUS_SUCCESS || retStatus == STATUS_NOT_FOUND, retStatus); - - if (retStatus == STATUS_NOT_FOUND) { - retStatus = STATUS_SUCCESS; - } else { - // Use the pre-generated cert and get rid of it to not reuse again - pRtcCertificate = (PRtcCertificate) data; - configuration.certificates[0] = *pRtcCertificate; - } - - CHK_STATUS(createPeerConnection(&configuration, ppRtcPeerConnection)); - - if (pSampleConfiguration->useTurn) { -#ifdef ENABLE_KVS_THREADPOOL - pSampleConfiguration->iceUriCount = 1; - AsyncGetIceStruct* pAsyncData = NULL; - - pAsyncData = (AsyncGetIceStruct*) MEMCALLOC(1, SIZEOF(AsyncGetIceStruct)); - pAsyncData->signalingClientHandle = pSampleConfiguration->signalingClientHandle; - pAsyncData->pRtcPeerConnection = *ppRtcPeerConnection; - pAsyncData->pUriCount = &(pSampleConfiguration->iceUriCount); - CHK_STATUS(peerConnectionAsync(asyncGetIceConfigInfo, (PVOID) pAsyncData)); -#else - - /* signalingClientGetIceConfigInfoCount can return more than one turn server. Use only one to optimize - * candidate gathering latency. But user can also choose to use more than 1 turn server. */ - for (uriCount = 0, i = 0; i < maxTurnServer; i++) { - /* - * if configuration.iceServers[uriCount + 1].urls is "turn:ip:port?transport=udp" then ICE will try TURN over UDP - * if configuration.iceServers[uriCount + 1].urls is "turn:ip:port?transport=tcp" then ICE will try TURN over TCP/TLS - * if configuration.iceServers[uriCount + 1].urls is "turns:ip:port?transport=udp", it's currently ignored because sdk dont do TURN - * over DTLS yet. if configuration.iceServers[uriCount + 1].urls is "turns:ip:port?transport=tcp" then ICE will try TURN over TCP/TLS - * if configuration.iceServers[uriCount + 1].urls is "turn:ip:port" then ICE will try both TURN over UDP and TCP/TLS - * - * It's recommended to not pass too many TURN iceServers to configuration because it will slow down ice gathering in non-trickle mode. - */ - CHK_STATUS(signalingClientGetIceConfigInfo(pSampleConfiguration->signalingClientHandle, i, &pIceConfigInfo)); - CHECK(uriCount < MAX_ICE_SERVERS_COUNT); - uriCount += pIceConfigInfo->uriCount; - CHK_STATUS(addConfigToServerList(ppRtcPeerConnection, pIceConfigInfo)); - } - pSampleConfiguration->iceUriCount = uriCount + 1; -#endif - } - -CleanUp: - - CHK_LOG_ERR(retStatus); - - // Free the certificate which can be NULL as we no longer need it and won't reuse - freeRtcCertificate(pRtcCertificate); - - LEAVES(); - return retStatus; -} - -// Return ICE server stats for a specific streaming session -STATUS gatherIceServerStats(PSampleStreamingSession pSampleStreamingSession) -{ - ENTERS(); - STATUS retStatus = STATUS_SUCCESS; - RtcStats rtcmetrics; - UINT32 j = 0; - rtcmetrics.requestedTypeOfStats = RTC_STATS_TYPE_ICE_SERVER; - for (; j < pSampleStreamingSession->pSampleConfiguration->iceUriCount; j++) { - rtcmetrics.rtcStatsObject.iceServerStats.iceServerIndex = j; - CHK_STATUS(rtcPeerConnectionGetMetrics(pSampleStreamingSession->pPeerConnection, NULL, &rtcmetrics)); - DLOGD("ICE Server URL: %s", rtcmetrics.rtcStatsObject.iceServerStats.url); - DLOGD("ICE Server port: %d", rtcmetrics.rtcStatsObject.iceServerStats.port); - DLOGD("ICE Server protocol: %s", rtcmetrics.rtcStatsObject.iceServerStats.protocol); - DLOGD("Total requests sent:%" PRIu64, rtcmetrics.rtcStatsObject.iceServerStats.totalRequestsSent); - DLOGD("Total responses received: %" PRIu64, rtcmetrics.rtcStatsObject.iceServerStats.totalResponsesReceived); - DLOGD("Total round trip time: %" PRIu64 "ms", - rtcmetrics.rtcStatsObject.iceServerStats.totalRoundTripTime / HUNDREDS_OF_NANOS_IN_A_MILLISECOND); - } -CleanUp: - LEAVES(); - return retStatus; -} - -STATUS createSampleStreamingSession(PSampleConfiguration pSampleConfiguration, PCHAR peerId, BOOL isMaster, - PSampleStreamingSession* ppSampleStreamingSession) -{ - STATUS retStatus = STATUS_SUCCESS; - RtcMediaStreamTrack videoTrack, audioTrack; - PSampleStreamingSession pSampleStreamingSession = NULL; - RtcRtpTransceiverInit audioRtpTransceiverInit; - RtcRtpTransceiverInit videoRtpTransceiverInit; - - MEMSET(&videoTrack, 0x00, SIZEOF(RtcMediaStreamTrack)); - MEMSET(&audioTrack, 0x00, SIZEOF(RtcMediaStreamTrack)); - - CHK(pSampleConfiguration != NULL && ppSampleStreamingSession != NULL, STATUS_NULL_ARG); - CHK((isMaster && peerId != NULL) || !isMaster, STATUS_INVALID_ARG); - - pSampleStreamingSession = (PSampleStreamingSession) MEMCALLOC(1, SIZEOF(SampleStreamingSession)); - pSampleStreamingSession->firstFrame = TRUE; - pSampleStreamingSession->offerReceiveTime = GETTIME(); - CHK(pSampleStreamingSession != NULL, STATUS_NOT_ENOUGH_MEMORY); - - if (isMaster) { - STRCPY(pSampleStreamingSession->peerId, peerId); - } else { - STRCPY(pSampleStreamingSession->peerId, SAMPLE_VIEWER_CLIENT_ID); - } - ATOMIC_STORE_BOOL(&pSampleStreamingSession->peerIdReceived, TRUE); - - pSampleStreamingSession->pAudioRtcRtpTransceiver = NULL; - pSampleStreamingSession->pVideoRtcRtpTransceiver = NULL; - - pSampleStreamingSession->pSampleConfiguration = pSampleConfiguration; - pSampleStreamingSession->rtcMetricsHistory.prevTs = GETTIME(); - - pSampleStreamingSession->peerConnectionMetrics.version = PEER_CONNECTION_METRICS_CURRENT_VERSION; - pSampleStreamingSession->iceMetrics.version = ICE_AGENT_METRICS_CURRENT_VERSION; - - // if we're the viewer, we control the trickle ice mode - pSampleStreamingSession->remoteCanTrickleIce = !isMaster && pSampleConfiguration->trickleIce; - - ATOMIC_STORE_BOOL(&pSampleStreamingSession->terminateFlag, FALSE); - ATOMIC_STORE_BOOL(&pSampleStreamingSession->candidateGatheringDone, FALSE); - pSampleStreamingSession->peerConnectionMetrics.peerConnectionStats.peerConnectionStartTime = GETTIME() / HUNDREDS_OF_NANOS_IN_A_MILLISECOND; - - if (pSampleConfiguration->enableTwcc) { - pSampleStreamingSession->twccMetadata.updateLock = MUTEX_CREATE(TRUE); - } - - CHK_STATUS(initializePeerConnection(pSampleConfiguration, &pSampleStreamingSession->pPeerConnection)); - CHK_STATUS(peerConnectionOnIceCandidate(pSampleStreamingSession->pPeerConnection, (UINT64) pSampleStreamingSession, onIceCandidateHandler)); - CHK_STATUS( - peerConnectionOnConnectionStateChange(pSampleStreamingSession->pPeerConnection, (UINT64) pSampleStreamingSession, onConnectionStateChange)); -#ifdef ENABLE_DATA_CHANNEL - if (pSampleConfiguration->onDataChannel != NULL) { - CHK_STATUS(peerConnectionOnDataChannel(pSampleStreamingSession->pPeerConnection, (UINT64) pSampleStreamingSession, - pSampleConfiguration->onDataChannel)); - } -#endif - - CHK_STATUS(addSupportedCodec(pSampleStreamingSession->pPeerConnection, pSampleConfiguration->videoCodec)); - CHK_STATUS(addSupportedCodec(pSampleStreamingSession->pPeerConnection, pSampleConfiguration->audioCodec)); - - // Add a SendRecv Transceiver of type video - videoTrack.kind = MEDIA_STREAM_TRACK_KIND_VIDEO; - videoTrack.codec = pSampleConfiguration->videoCodec; - videoRtpTransceiverInit.direction = RTC_RTP_TRANSCEIVER_DIRECTION_SENDRECV; - videoRtpTransceiverInit.rollingBufferDurationSec = 3; - // Considering 4 Mbps for 720p (which is what our samples use). This is for H.264. - // The value could be different for other codecs. - videoRtpTransceiverInit.rollingBufferBitratebps = 4 * 1024 * 1024; - STRCPY(videoTrack.streamId, "myKvsVideoStream"); - STRCPY(videoTrack.trackId, "myVideoTrack"); - CHK_STATUS(addTransceiver(pSampleStreamingSession->pPeerConnection, &videoTrack, &videoRtpTransceiverInit, - &pSampleStreamingSession->pVideoRtcRtpTransceiver)); - - CHK_STATUS(transceiverOnBandwidthEstimation(pSampleStreamingSession->pVideoRtcRtpTransceiver, (UINT64) pSampleStreamingSession, - sampleBandwidthEstimationHandler)); - - // Add a SendRecv Transceiver of type audio - audioTrack.kind = MEDIA_STREAM_TRACK_KIND_AUDIO; - audioTrack.codec = pSampleConfiguration->audioCodec; - audioRtpTransceiverInit.direction = RTC_RTP_TRANSCEIVER_DIRECTION_SENDRECV; - audioRtpTransceiverInit.rollingBufferDurationSec = 3; - // For opus, the bitrate could be between 6 Kbps to 510 Kbps - audioRtpTransceiverInit.rollingBufferBitratebps = 510 * 1024; - STRCPY(audioTrack.streamId, "myKvsVideoStream"); - STRCPY(audioTrack.trackId, "myAudioTrack"); - CHK_STATUS(addTransceiver(pSampleStreamingSession->pPeerConnection, &audioTrack, &audioRtpTransceiverInit, - &pSampleStreamingSession->pAudioRtcRtpTransceiver)); - - CHK_STATUS(transceiverOnBandwidthEstimation(pSampleStreamingSession->pAudioRtcRtpTransceiver, (UINT64) pSampleStreamingSession, - sampleBandwidthEstimationHandler)); - // twcc bandwidth estimation - if (pSampleConfiguration->enableTwcc) { - CHK_STATUS(peerConnectionOnSenderBandwidthEstimation(pSampleStreamingSession->pPeerConnection, (UINT64) pSampleStreamingSession, - sampleSenderBandwidthEstimationHandler)); - } - pSampleStreamingSession->startUpLatency = 0; -CleanUp: - - if (STATUS_FAILED(retStatus) && pSampleStreamingSession != NULL) { - freeSampleStreamingSession(&pSampleStreamingSession); - pSampleStreamingSession = NULL; - } - - if (ppSampleStreamingSession != NULL) { - *ppSampleStreamingSession = pSampleStreamingSession; - } - - return retStatus; -} - -STATUS freeSampleStreamingSession(PSampleStreamingSession* ppSampleStreamingSession) -{ - STATUS retStatus = STATUS_SUCCESS; - PSampleStreamingSession pSampleStreamingSession = NULL; - PSampleConfiguration pSampleConfiguration; - - CHK(ppSampleStreamingSession != NULL, STATUS_NULL_ARG); - pSampleStreamingSession = *ppSampleStreamingSession; - CHK(pSampleStreamingSession != NULL && pSampleStreamingSession->pSampleConfiguration != NULL, retStatus); - pSampleConfiguration = pSampleStreamingSession->pSampleConfiguration; - - DLOGD("Freeing streaming session with peer id: %s ", pSampleStreamingSession->peerId); - - ATOMIC_STORE_BOOL(&pSampleStreamingSession->terminateFlag, TRUE); - - if (pSampleStreamingSession->shutdownCallback != NULL) { - pSampleStreamingSession->shutdownCallback(pSampleStreamingSession->shutdownCallbackCustomData, pSampleStreamingSession); - } - - if (IS_VALID_TID_VALUE(pSampleStreamingSession->receiveAudioVideoSenderTid)) { - THREAD_JOIN(pSampleStreamingSession->receiveAudioVideoSenderTid, NULL); - } - - // De-initialize the session stats timer if there are no active sessions - // NOTE: we need to perform this under the lock which might be acquired by - // the running thread but it's OK as it's re-entrant - MUTEX_LOCK(pSampleConfiguration->sampleConfigurationObjLock); - if (pSampleConfiguration->iceCandidatePairStatsTimerId != MAX_UINT32 && pSampleConfiguration->streamingSessionCount == 0 && - IS_VALID_TIMER_QUEUE_HANDLE(pSampleConfiguration->timerQueueHandle)) { - CHK_LOG_ERR(timerQueueCancelTimer(pSampleConfiguration->timerQueueHandle, pSampleConfiguration->iceCandidatePairStatsTimerId, - (UINT64) pSampleConfiguration)); - pSampleConfiguration->iceCandidatePairStatsTimerId = MAX_UINT32; - } - MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); - - if (pSampleConfiguration->enableTwcc) { - if (IS_VALID_MUTEX_VALUE(pSampleStreamingSession->twccMetadata.updateLock)) { - MUTEX_FREE(pSampleStreamingSession->twccMetadata.updateLock); - } - } - - CHK_LOG_ERR(closePeerConnection(pSampleStreamingSession->pPeerConnection)); - CHK_LOG_ERR(freePeerConnection(&pSampleStreamingSession->pPeerConnection)); - SAFE_MEMFREE(pSampleStreamingSession); - -CleanUp: - - CHK_LOG_ERR(retStatus); - - return retStatus; -} - -STATUS streamingSessionOnShutdown(PSampleStreamingSession pSampleStreamingSession, UINT64 customData, - StreamSessionShutdownCallback streamSessionShutdownCallback) -{ - STATUS retStatus = STATUS_SUCCESS; - - CHK(pSampleStreamingSession != NULL && streamSessionShutdownCallback != NULL, STATUS_NULL_ARG); - - pSampleStreamingSession->shutdownCallbackCustomData = customData; - pSampleStreamingSession->shutdownCallback = streamSessionShutdownCallback; - -CleanUp: - - return retStatus; -} - -VOID sampleVideoFrameHandler(UINT64 customData, PFrame pFrame) -{ - UNUSED_PARAM(customData); - DLOGV("Video Frame received. TrackId: %" PRIu64 ", Size: %u, Flags %u", pFrame->trackId, pFrame->size, pFrame->flags); -} - -VOID sampleAudioFrameHandler(UINT64 customData, PFrame pFrame) -{ - UNUSED_PARAM(customData); - DLOGV("Audio Frame received. TrackId: %" PRIu64 ", Size: %u, Flags %u", pFrame->trackId, pFrame->size, pFrame->flags); -} - -VOID sampleFrameHandler(UINT64 customData, PFrame pFrame) -{ - UNUSED_PARAM(customData); - DLOGV("Video Frame received. TrackId: %" PRIu64 ", Size: %u, Flags %u", pFrame->trackId, pFrame->size, pFrame->flags); -} - -VOID sampleBandwidthEstimationHandler(UINT64 customData, DOUBLE maximumBitrate) -{ - UNUSED_PARAM(customData); - DLOGV("received bitrate suggestion: %f", maximumBitrate); -} - -// Sample callback for TWCC. Average packet is calculated with exponential moving average (EMA). If average packet lost is <= 5%, -// the current bitrate is increased by 5%. If more than 5%, the current bitrate -// is reduced by percent lost. Bitrate update is allowed every second and is increased/decreased upto the limits -VOID sampleSenderBandwidthEstimationHandler(UINT64 customData, UINT32 txBytes, UINT32 rxBytes, UINT32 txPacketsCnt, UINT32 rxPacketsCnt, - UINT64 duration) -{ - UNUSED_PARAM(duration); - UINT64 videoBitrate, audioBitrate; - UINT64 currentTimeMs, timeDiff; - UINT32 lostPacketsCnt = txPacketsCnt - rxPacketsCnt; - DOUBLE percentLost = (DOUBLE) ((txPacketsCnt > 0) ? (lostPacketsCnt * 100 / txPacketsCnt) : 0.0); - SampleStreamingSession* pSampleStreamingSession = (SampleStreamingSession*) customData; - - if (pSampleStreamingSession == NULL) { - DLOGW("Invalid streaming session (NULL object)"); - return; - } - - // Calculate packet loss - pSampleStreamingSession->twccMetadata.averagePacketLoss = - EMA_ACCUMULATOR_GET_NEXT(pSampleStreamingSession->twccMetadata.averagePacketLoss, ((DOUBLE) percentLost)); - - currentTimeMs = GETTIME(); - timeDiff = currentTimeMs - pSampleStreamingSession->twccMetadata.lastAdjustmentTimeMs; - if (timeDiff < TWCC_BITRATE_ADJUSTMENT_INTERVAL_MS) { - // Too soon for another adjustment - return; - } - - MUTEX_LOCK(pSampleStreamingSession->twccMetadata.updateLock); - videoBitrate = pSampleStreamingSession->twccMetadata.currentVideoBitrate; - audioBitrate = pSampleStreamingSession->twccMetadata.currentAudioBitrate; - - if (pSampleStreamingSession->twccMetadata.averagePacketLoss <= 5) { - // increase encoder bitrate by 5 percent with a cap at MAX_BITRATE - videoBitrate = (UINT64) MIN(videoBitrate * 1.05, MAX_VIDEO_BITRATE_KBPS); - // increase encoder bitrate by 5 percent with a cap at MAX_BITRATE - audioBitrate = (UINT64) MIN(audioBitrate * 1.05, MAX_AUDIO_BITRATE_BPS); - } else { - // decrease encoder bitrate by average packet loss percent, with a cap at MIN_BITRATE - videoBitrate = (UINT64) MAX(videoBitrate * (1.0 - pSampleStreamingSession->twccMetadata.averagePacketLoss / 100.0), MIN_VIDEO_BITRATE_KBPS); - // decrease encoder bitrate by average packet loss percent, with a cap at MIN_BITRATE - audioBitrate = (UINT64) MAX(audioBitrate * (1.0 - pSampleStreamingSession->twccMetadata.averagePacketLoss / 100.0), MIN_AUDIO_BITRATE_BPS); - } - - // Update the session with the new bitrate and adjustment time - pSampleStreamingSession->twccMetadata.newVideoBitrate = videoBitrate; - pSampleStreamingSession->twccMetadata.newAudioBitrate = audioBitrate; - MUTEX_UNLOCK(pSampleStreamingSession->twccMetadata.updateLock); - - pSampleStreamingSession->twccMetadata.lastAdjustmentTimeMs = currentTimeMs; - - DLOGI("Adjustment made: average packet loss = %.2f%%, timediff: %llu ms", pSampleStreamingSession->twccMetadata.averagePacketLoss, timeDiff); - DLOGI("Suggested video bitrate %u kbps, suggested audio bitrate: %u bps, sent: %u bytes %u packets received: %u bytes %u packets in %lu msec", - videoBitrate, audioBitrate, txBytes, txPacketsCnt, rxBytes, rxPacketsCnt, duration / 10000ULL); -} - -STATUS handleRemoteCandidate(PSampleStreamingSession pSampleStreamingSession, PSignalingMessage pSignalingMessage) -{ - STATUS retStatus = STATUS_SUCCESS; - RtcIceCandidateInit iceCandidate; - CHK(pSampleStreamingSession != NULL && pSignalingMessage != NULL, STATUS_NULL_ARG); - - CHK_STATUS(deserializeRtcIceCandidateInit(pSignalingMessage->payload, pSignalingMessage->payloadLen, &iceCandidate)); - CHK_STATUS(addIceCandidate(pSampleStreamingSession->pPeerConnection, iceCandidate.candidate)); - -CleanUp: - - CHK_LOG_ERR(retStatus); - return retStatus; -} - -STATUS traverseDirectoryPEMFileScan(UINT64 customData, DIR_ENTRY_TYPES entryType, PCHAR fullPath, PCHAR fileName) -{ - UNUSED_PARAM(entryType); - UNUSED_PARAM(fullPath); - - PCHAR certName = (PCHAR) customData; - UINT32 fileNameLen = STRLEN(fileName); - - if (fileNameLen > ARRAY_SIZE(CA_CERT_PEM_FILE_EXTENSION) + 1 && - (STRCMPI(CA_CERT_PEM_FILE_EXTENSION, &fileName[fileNameLen - ARRAY_SIZE(CA_CERT_PEM_FILE_EXTENSION) + 1]) == 0)) { - certName[0] = FPATHSEPARATOR; - certName++; - STRCPY(certName, fileName); - } - - return STATUS_SUCCESS; -} - -STATUS lookForSslCert(PSampleConfiguration* ppSampleConfiguration) -{ - STATUS retStatus = STATUS_SUCCESS; - struct stat pathStat; - CHAR certName[MAX_PATH_LEN]; - PSampleConfiguration pSampleConfiguration = *ppSampleConfiguration; - - MEMSET(certName, 0x0, ARRAY_SIZE(certName)); - pSampleConfiguration->pCaCertPath = GETENV(CACERT_PATH_ENV_VAR); - - // if ca cert path is not set from the environment, try to use the one that cmake detected - if (pSampleConfiguration->pCaCertPath == NULL) { - CHK_ERR(STRNLEN(DEFAULT_KVS_CACERT_PATH, MAX_PATH_LEN) > 0, STATUS_INVALID_OPERATION, "No ca cert path given (error:%s)", strerror(errno)); - pSampleConfiguration->pCaCertPath = DEFAULT_KVS_CACERT_PATH; - } else { - // Check if the environment variable is a path - CHK(0 == FSTAT(pSampleConfiguration->pCaCertPath, &pathStat), STATUS_DIRECTORY_ENTRY_STAT_ERROR); - - if (S_ISDIR(pathStat.st_mode)) { - CHK_STATUS(traverseDirectory(pSampleConfiguration->pCaCertPath, (UINT64) &certName, /* iterate */ FALSE, traverseDirectoryPEMFileScan)); - - if (certName[0] != 0x0) { - STRCAT(pSampleConfiguration->pCaCertPath, certName); - } else { - DLOGW("Cert not found in path set...checking if CMake detected a path\n"); - CHK_ERR(STRNLEN(DEFAULT_KVS_CACERT_PATH, MAX_PATH_LEN) > 0, STATUS_INVALID_OPERATION, "No ca cert path given (error:%s)", - strerror(errno)); - DLOGI("CMake detected cert path\n"); - pSampleConfiguration->pCaCertPath = DEFAULT_KVS_CACERT_PATH; - } - } - } - -CleanUp: - - CHK_LOG_ERR(retStatus); - return retStatus; -} - -STATUS createSampleConfiguration(PCHAR channelName, SIGNALING_CHANNEL_ROLE_TYPE roleType, BOOL trickleIce, BOOL useTurn, UINT32 logLevel, - PSampleConfiguration* ppSampleConfiguration) -{ - STATUS retStatus = STATUS_SUCCESS; - PCHAR pAccessKey, pSecretKey, pSessionToken; - PSampleConfiguration pSampleConfiguration = NULL; - - CHK(ppSampleConfiguration != NULL, STATUS_NULL_ARG); - - CHK(NULL != (pSampleConfiguration = (PSampleConfiguration) MEMCALLOC(1, SIZEOF(SampleConfiguration))), STATUS_NOT_ENOUGH_MEMORY); - -#ifdef IOT_CORE_ENABLE_CREDENTIALS - PCHAR pIotCoreCredentialEndPoint, pIotCoreCert, pIotCorePrivateKey, pIotCoreRoleAlias, pIotCoreCertificateId, pIotCoreThingName; - CHK_ERR((pIotCoreCredentialEndPoint = GETENV(IOT_CORE_CREDENTIAL_ENDPOINT)) != NULL, STATUS_INVALID_OPERATION, - "AWS_IOT_CORE_CREDENTIAL_ENDPOINT must be set"); - CHK_ERR((pIotCoreCert = GETENV(IOT_CORE_CERT)) != NULL, STATUS_INVALID_OPERATION, "AWS_IOT_CORE_CERT must be set"); - CHK_ERR((pIotCorePrivateKey = GETENV(IOT_CORE_PRIVATE_KEY)) != NULL, STATUS_INVALID_OPERATION, "AWS_IOT_CORE_PRIVATE_KEY must be set"); - CHK_ERR((pIotCoreRoleAlias = GETENV(IOT_CORE_ROLE_ALIAS)) != NULL, STATUS_INVALID_OPERATION, "AWS_IOT_CORE_ROLE_ALIAS must be set"); - CHK_ERR((pIotCoreThingName = GETENV(IOT_CORE_THING_NAME)) != NULL, STATUS_INVALID_OPERATION, "AWS_IOT_CORE_THING_NAME must be set"); -#else - CHK_ERR((pAccessKey = GETENV(ACCESS_KEY_ENV_VAR)) != NULL, STATUS_INVALID_OPERATION, "AWS_ACCESS_KEY_ID must be set"); - CHK_ERR((pSecretKey = GETENV(SECRET_KEY_ENV_VAR)) != NULL, STATUS_INVALID_OPERATION, "AWS_SECRET_ACCESS_KEY must be set"); -#endif - - pSessionToken = GETENV(SESSION_TOKEN_ENV_VAR); - if (pSessionToken != NULL && IS_EMPTY_STRING(pSessionToken)) { - DLOGW("Session token is set but its value is empty. Ignoring."); - pSessionToken = NULL; - } - - // If the env is set, we generate normal log files apart from filtered profile log files - // If not set, we generate only the filtered profile log files - if (NULL != GETENV(ENABLE_FILE_LOGGING)) { - retStatus = createFileLoggerWithLevelFiltering(FILE_LOGGING_BUFFER_SIZE, MAX_NUMBER_OF_LOG_FILES, (PCHAR) FILE_LOGGER_LOG_FILE_DIRECTORY_PATH, - TRUE, TRUE, TRUE, LOG_LEVEL_PROFILE, NULL); - - if (retStatus != STATUS_SUCCESS) { - DLOGW("[KVS Master] createFileLogger(): operation returned status code: 0x%08x", retStatus); - } else { - pSampleConfiguration->enableFileLogging = TRUE; - } - } else { - retStatus = createFileLoggerWithLevelFiltering(FILE_LOGGING_BUFFER_SIZE, MAX_NUMBER_OF_LOG_FILES, (PCHAR) FILE_LOGGER_LOG_FILE_DIRECTORY_PATH, - TRUE, TRUE, FALSE, LOG_LEVEL_PROFILE, NULL); - - if (retStatus != STATUS_SUCCESS) { - DLOGW("[KVS Master] createFileLogger(): operation returned status code: 0x%08x", retStatus); - } else { - pSampleConfiguration->enableFileLogging = TRUE; - } - } - - if ((pSampleConfiguration->channelInfo.pRegion = GETENV(DEFAULT_REGION_ENV_VAR)) == NULL) { - pSampleConfiguration->channelInfo.pRegion = DEFAULT_AWS_REGION; - } - - CHK_STATUS(lookForSslCert(&pSampleConfiguration)); - -#ifdef IOT_CORE_ENABLE_CREDENTIALS - CHK_STATUS(createLwsIotCredentialProvider(pIotCoreCredentialEndPoint, pIotCoreCert, pIotCorePrivateKey, pSampleConfiguration->pCaCertPath, - pIotCoreRoleAlias, pIotCoreThingName, &pSampleConfiguration->pCredentialProvider)); -#else - CHK_STATUS( - createStaticCredentialProvider(pAccessKey, 0, pSecretKey, 0, pSessionToken, 0, MAX_UINT64, &pSampleConfiguration->pCredentialProvider)); -#endif - - pSampleConfiguration->mediaSenderTid = INVALID_TID_VALUE; - pSampleConfiguration->audioSenderTid = INVALID_TID_VALUE; - pSampleConfiguration->videoSenderTid = INVALID_TID_VALUE; - pSampleConfiguration->signalingClientHandle = INVALID_SIGNALING_CLIENT_HANDLE_VALUE; - pSampleConfiguration->sampleConfigurationObjLock = MUTEX_CREATE(TRUE); - pSampleConfiguration->cvar = CVAR_CREATE(); - pSampleConfiguration->streamingSessionListReadLock = MUTEX_CREATE(FALSE); - pSampleConfiguration->signalingSendMessageLock = MUTEX_CREATE(FALSE); - /* This is ignored for master. Master can extract the info from offer. Viewer has to know if peer can trickle or - * not ahead of time. */ - pSampleConfiguration->trickleIce = trickleIce; - pSampleConfiguration->useTurn = useTurn; - pSampleConfiguration->enableSendingMetricsToViewerViaDc = FALSE; - pSampleConfiguration->receiveAudioVideoSource = NULL; - - pSampleConfiguration->channelInfo.version = CHANNEL_INFO_CURRENT_VERSION; - pSampleConfiguration->channelInfo.pChannelName = channelName; -#ifdef IOT_CORE_ENABLE_CREDENTIALS - if ((pIotCoreCertificateId = GETENV(IOT_CORE_CERTIFICATE_ID)) != NULL) { - pSampleConfiguration->channelInfo.pChannelName = pIotCoreCertificateId; - } -#endif - pSampleConfiguration->channelInfo.pKmsKeyId = NULL; - pSampleConfiguration->channelInfo.tagCount = 0; - pSampleConfiguration->channelInfo.pTags = NULL; - pSampleConfiguration->channelInfo.channelType = SIGNALING_CHANNEL_TYPE_SINGLE_MASTER; - pSampleConfiguration->channelInfo.channelRoleType = roleType; - pSampleConfiguration->channelInfo.cachingPolicy = SIGNALING_API_CALL_CACHE_TYPE_FILE; - pSampleConfiguration->channelInfo.cachingPeriod = SIGNALING_API_CALL_CACHE_TTL_SENTINEL_VALUE; - pSampleConfiguration->channelInfo.asyncIceServerConfig = TRUE; // has no effect - pSampleConfiguration->channelInfo.retry = TRUE; - pSampleConfiguration->channelInfo.reconnect = TRUE; - pSampleConfiguration->channelInfo.pCertPath = pSampleConfiguration->pCaCertPath; - pSampleConfiguration->channelInfo.messageTtl = 0; // Default is 60 seconds - - pSampleConfiguration->signalingClientCallbacks.version = SIGNALING_CLIENT_CALLBACKS_CURRENT_VERSION; - pSampleConfiguration->signalingClientCallbacks.errorReportFn = signalingClientError; - pSampleConfiguration->signalingClientCallbacks.stateChangeFn = signalingClientStateChanged; - pSampleConfiguration->signalingClientCallbacks.customData = (UINT64) pSampleConfiguration; - - pSampleConfiguration->clientInfo.version = SIGNALING_CLIENT_INFO_CURRENT_VERSION; - pSampleConfiguration->clientInfo.loggingLevel = logLevel; - pSampleConfiguration->clientInfo.cacheFilePath = NULL; // Use the default path - pSampleConfiguration->clientInfo.signalingClientCreationMaxRetryAttempts = CREATE_SIGNALING_CLIENT_RETRY_ATTEMPTS_SENTINEL_VALUE; - pSampleConfiguration->iceCandidatePairStatsTimerId = MAX_UINT32; - pSampleConfiguration->pregenerateCertTimerId = MAX_UINT32; - pSampleConfiguration->signalingClientMetrics.version = SIGNALING_CLIENT_METRICS_CURRENT_VERSION; - - // Flag to enable SDK to calculate selected ice server, local, remote and candidate pair stats. - pSampleConfiguration->enableIceStats = FALSE; - - // Flag to enable/disable TWCC - pSampleConfiguration->enableTwcc = TRUE; - - ATOMIC_STORE_BOOL(&pSampleConfiguration->interrupted, FALSE); - ATOMIC_STORE_BOOL(&pSampleConfiguration->mediaThreadStarted, FALSE); - ATOMIC_STORE_BOOL(&pSampleConfiguration->appTerminateFlag, FALSE); - ATOMIC_STORE_BOOL(&pSampleConfiguration->recreateSignalingClient, FALSE); - ATOMIC_STORE_BOOL(&pSampleConfiguration->connected, FALSE); - - CHK_STATUS(timerQueueCreate(&pSampleConfiguration->timerQueueHandle)); - - CHK_STATUS(stackQueueCreate(&pSampleConfiguration->pregeneratedCertificates)); - - // Start the cert pre-gen timer callback - if (SAMPLE_PRE_GENERATE_CERT) { - CHK_LOG_ERR(retStatus = - timerQueueAddTimer(pSampleConfiguration->timerQueueHandle, 0, SAMPLE_PRE_GENERATE_CERT_PERIOD, pregenerateCertTimerCallback, - (UINT64) pSampleConfiguration, &pSampleConfiguration->pregenerateCertTimerId)); - } - - pSampleConfiguration->iceUriCount = 0; - - CHK_STATUS(stackQueueCreate(&pSampleConfiguration->pPendingSignalingMessageForRemoteClient)); - CHK_STATUS(hashTableCreateWithParams(SAMPLE_HASH_TABLE_BUCKET_COUNT, SAMPLE_HASH_TABLE_BUCKET_LENGTH, - &pSampleConfiguration->pRtcPeerConnectionForRemoteClient)); - -CleanUp: - - if (STATUS_FAILED(retStatus)) { - freeSampleConfiguration(&pSampleConfiguration); - } - - if (ppSampleConfiguration != NULL) { - *ppSampleConfiguration = pSampleConfiguration; - } - - return retStatus; -} - -STATUS initSignaling(PSampleConfiguration pSampleConfiguration, PCHAR clientId) -{ - STATUS retStatus = STATUS_SUCCESS; - SignalingClientMetrics signalingClientMetrics = pSampleConfiguration->signalingClientMetrics; - pSampleConfiguration->signalingClientCallbacks.messageReceivedFn = signalingMessageReceived; - STRCPY(pSampleConfiguration->clientInfo.clientId, clientId); - CHK_STATUS(createSignalingClientSync(&pSampleConfiguration->clientInfo, &pSampleConfiguration->channelInfo, - &pSampleConfiguration->signalingClientCallbacks, pSampleConfiguration->pCredentialProvider, - &pSampleConfiguration->signalingClientHandle)); - - // Enable the processing of the messages - CHK_STATUS(signalingClientFetchSync(pSampleConfiguration->signalingClientHandle)); - -#ifdef ENABLE_DATA_CHANNEL - pSampleConfiguration->onDataChannel = onDataChannel; -#endif - - CHK_STATUS(signalingClientConnectSync(pSampleConfiguration->signalingClientHandle)); - - signalingClientGetMetrics(pSampleConfiguration->signalingClientHandle, &signalingClientMetrics); - - // Logging this here since the logs in signaling library do not get routed to file - DLOGP("[Signaling Get token] %" PRIu64 " ms", signalingClientMetrics.signalingClientStats.getTokenCallTime); - DLOGP("[Signaling Describe] %" PRIu64 " ms", signalingClientMetrics.signalingClientStats.describeCallTime); - DLOGP("[Signaling Describe Media] %" PRIu64 " ms", signalingClientMetrics.signalingClientStats.describeMediaCallTime); - DLOGP("[Signaling Create Channel] %" PRIu64 " ms", signalingClientMetrics.signalingClientStats.createCallTime); - DLOGP("[Signaling Get endpoint] %" PRIu64 " ms", signalingClientMetrics.signalingClientStats.getEndpointCallTime); - DLOGP("[Signaling Get ICE config] %" PRIu64 " ms", signalingClientMetrics.signalingClientStats.getIceConfigCallTime); - DLOGP("[Signaling Connect] %" PRIu64 " ms", signalingClientMetrics.signalingClientStats.connectCallTime); - if (signalingClientMetrics.signalingClientStats.joinSessionCallTime != 0) { - DLOGP("[Signaling Join Session] %" PRIu64 " ms", signalingClientMetrics.signalingClientStats.joinSessionCallTime); - } - DLOGP("[Signaling create client] %" PRIu64 " ms", signalingClientMetrics.signalingClientStats.createClientTime); - DLOGP("[Signaling fetch client] %" PRIu64 " ms", signalingClientMetrics.signalingClientStats.fetchClientTime); - DLOGP("[Signaling connect client] %" PRIu64 " ms", signalingClientMetrics.signalingClientStats.connectClientTime); - pSampleConfiguration->signalingClientMetrics = signalingClientMetrics; - gSampleConfiguration = pSampleConfiguration; -CleanUp: - return retStatus; -} - -STATUS logSignalingClientStats(PSignalingClientMetrics pSignalingClientMetrics) -{ - ENTERS(); - STATUS retStatus = STATUS_SUCCESS; - CHK(pSignalingClientMetrics != NULL, STATUS_NULL_ARG); - DLOGD("Signaling client connection duration: %" PRIu64 " ms", - (pSignalingClientMetrics->signalingClientStats.connectionDuration / HUNDREDS_OF_NANOS_IN_A_MILLISECOND)); - DLOGD("Number of signaling client API errors: %d", pSignalingClientMetrics->signalingClientStats.numberOfErrors); - DLOGD("Number of runtime errors in the session: %d", pSignalingClientMetrics->signalingClientStats.numberOfRuntimeErrors); - DLOGD("Signaling client uptime: %" PRIu64 " ms", - (pSignalingClientMetrics->signalingClientStats.connectionDuration / HUNDREDS_OF_NANOS_IN_A_MILLISECOND)); - // This gives the EMA of the createChannel, describeChannel, getChannelEndpoint and deleteChannel calls - DLOGD("Control Plane API call latency: %" PRIu64 " ms", - (pSignalingClientMetrics->signalingClientStats.cpApiCallLatency / HUNDREDS_OF_NANOS_IN_A_MILLISECOND)); - // This gives the EMA of the getIceConfig() call. - DLOGD("Data Plane API call latency: %" PRIu64 " ms", - (pSignalingClientMetrics->signalingClientStats.dpApiCallLatency / HUNDREDS_OF_NANOS_IN_A_MILLISECOND)); - DLOGD("API call retry count: %d", pSignalingClientMetrics->signalingClientStats.apiCallRetryCount); -CleanUp: - LEAVES(); - return retStatus; -} - -STATUS getIceCandidatePairStatsCallback(UINT32 timerId, UINT64 currentTime, UINT64 customData) -{ - UNUSED_PARAM(timerId); - UNUSED_PARAM(currentTime); - STATUS retStatus = STATUS_SUCCESS; - PSampleConfiguration pSampleConfiguration = (PSampleConfiguration) customData; - UINT32 i; - UINT64 currentMeasureDuration = 0; - DOUBLE averagePacketsDiscardedOnSend = 0.0; - DOUBLE averageNumberOfPacketsSentPerSecond = 0.0; - DOUBLE averageNumberOfPacketsReceivedPerSecond = 0.0; - DOUBLE outgoingBitrate = 0.0; - DOUBLE incomingBitrate = 0.0; - BOOL locked = FALSE; - - CHK_WARN(pSampleConfiguration != NULL, STATUS_NULL_ARG, "[KVS Master] getPeriodicStats(): Passed argument is NULL"); - - pSampleConfiguration->rtcIceCandidatePairMetrics.requestedTypeOfStats = RTC_STATS_TYPE_CANDIDATE_PAIR; - - // Use MUTEX_TRYLOCK to avoid possible dead lock when canceling timerQueue - if (!MUTEX_TRYLOCK(pSampleConfiguration->sampleConfigurationObjLock)) { - return retStatus; - } else { - locked = TRUE; - } - - for (i = 0; i < pSampleConfiguration->streamingSessionCount; ++i) { - if (STATUS_SUCCEEDED(rtcPeerConnectionGetMetrics(pSampleConfiguration->sampleStreamingSessionList[i]->pPeerConnection, NULL, - &pSampleConfiguration->rtcIceCandidatePairMetrics))) { - currentMeasureDuration = (pSampleConfiguration->rtcIceCandidatePairMetrics.timestamp - - pSampleConfiguration->sampleStreamingSessionList[i]->rtcMetricsHistory.prevTs) / - HUNDREDS_OF_NANOS_IN_A_SECOND; - DLOGD("Current duration: %" PRIu64 " seconds", currentMeasureDuration); - if (currentMeasureDuration > 0) { - DLOGD("Selected local candidate ID: %s", - pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.localCandidateId); - DLOGD("Selected remote candidate ID: %s", - pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.remoteCandidateId); - // TODO: Display state as a string for readability - DLOGD("Ice Candidate Pair state: %d", pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.state); - DLOGD("Nomination state: %s", - pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.nominated ? "nominated" - : "not nominated"); - averageNumberOfPacketsSentPerSecond = - (DOUBLE) (pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.packetsSent - - pSampleConfiguration->sampleStreamingSessionList[i]->rtcMetricsHistory.prevNumberOfPacketsSent) / - (DOUBLE) currentMeasureDuration; - DLOGD("Packet send rate: %lf pkts/sec", averageNumberOfPacketsSentPerSecond); - - averageNumberOfPacketsReceivedPerSecond = - (DOUBLE) (pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.packetsReceived - - pSampleConfiguration->sampleStreamingSessionList[i]->rtcMetricsHistory.prevNumberOfPacketsReceived) / - (DOUBLE) currentMeasureDuration; - DLOGD("Packet receive rate: %lf pkts/sec", averageNumberOfPacketsReceivedPerSecond); - - outgoingBitrate = (DOUBLE) ((pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.bytesSent - - pSampleConfiguration->sampleStreamingSessionList[i]->rtcMetricsHistory.prevNumberOfBytesSent) * - 8.0) / - currentMeasureDuration; - DLOGD("Outgoing bit rate: %lf bps", outgoingBitrate); - - incomingBitrate = (DOUBLE) ((pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.bytesReceived - - pSampleConfiguration->sampleStreamingSessionList[i]->rtcMetricsHistory.prevNumberOfBytesReceived) * - 8.0) / - currentMeasureDuration; - DLOGD("Incoming bit rate: %lf bps", incomingBitrate); - - averagePacketsDiscardedOnSend = - (DOUBLE) (pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.packetsDiscardedOnSend - - pSampleConfiguration->sampleStreamingSessionList[i]->rtcMetricsHistory.prevPacketsDiscardedOnSend) / - (DOUBLE) currentMeasureDuration; - DLOGD("Packet discard rate: %lf pkts/sec", averagePacketsDiscardedOnSend); - - DLOGD("Current STUN request round trip time: %lf sec", - pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.currentRoundTripTime); - DLOGD("Number of STUN responses received: %llu", - pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.responsesReceived); - - pSampleConfiguration->sampleStreamingSessionList[i]->rtcMetricsHistory.prevTs = - pSampleConfiguration->rtcIceCandidatePairMetrics.timestamp; - pSampleConfiguration->sampleStreamingSessionList[i]->rtcMetricsHistory.prevNumberOfPacketsSent = - pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.packetsSent; - pSampleConfiguration->sampleStreamingSessionList[i]->rtcMetricsHistory.prevNumberOfPacketsReceived = - pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.packetsReceived; - pSampleConfiguration->sampleStreamingSessionList[i]->rtcMetricsHistory.prevNumberOfBytesSent = - pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.bytesSent; - pSampleConfiguration->sampleStreamingSessionList[i]->rtcMetricsHistory.prevNumberOfBytesReceived = - pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.bytesReceived; - pSampleConfiguration->sampleStreamingSessionList[i]->rtcMetricsHistory.prevPacketsDiscardedOnSend = - pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.packetsDiscardedOnSend; - } - } - } - -CleanUp: - - if (locked) { - MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); - } - - return retStatus; -} - -STATUS pregenerateCertTimerCallback(UINT32 timerId, UINT64 currentTime, UINT64 customData) -{ - UNUSED_PARAM(timerId); - UNUSED_PARAM(currentTime); - STATUS retStatus = STATUS_SUCCESS; - PSampleConfiguration pSampleConfiguration = (PSampleConfiguration) customData; - BOOL locked = FALSE; - UINT32 certCount; - PRtcCertificate pRtcCertificate = NULL; - - CHK_WARN(pSampleConfiguration != NULL, STATUS_NULL_ARG, "[KVS Master] pregenerateCertTimerCallback(): Passed argument is NULL"); - - // Use MUTEX_TRYLOCK to avoid possible dead lock when canceling timerQueue - if (!MUTEX_TRYLOCK(pSampleConfiguration->sampleConfigurationObjLock)) { - return retStatus; - } else { - locked = TRUE; - } - - // Quick check if there is anything that needs to be done. - CHK_STATUS(stackQueueGetCount(pSampleConfiguration->pregeneratedCertificates, &certCount)); - CHK(certCount != MAX_RTCCONFIGURATION_CERTIFICATES, retStatus); - - // Generate the certificate with the keypair - CHK_STATUS(createRtcCertificate(&pRtcCertificate)); - - // Add to the stack queue - CHK_STATUS(stackQueueEnqueue(pSampleConfiguration->pregeneratedCertificates, (UINT64) pRtcCertificate)); - - DLOGV("New certificate has been pre-generated and added to the queue"); - - // Reset it so it won't be freed on exit - pRtcCertificate = NULL; - - MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); - locked = FALSE; - -CleanUp: - - if (pRtcCertificate != NULL) { - freeRtcCertificate(pRtcCertificate); - } - - if (locked) { - MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); - } - - return retStatus; -} - -STATUS freeSampleConfiguration(PSampleConfiguration* ppSampleConfiguration) -{ - ENTERS(); - STATUS retStatus = STATUS_SUCCESS; - PSampleConfiguration pSampleConfiguration; - UINT32 i; - UINT64 data; - StackQueueIterator iterator; - BOOL locked = FALSE; - - CHK(ppSampleConfiguration != NULL, STATUS_NULL_ARG); - pSampleConfiguration = *ppSampleConfiguration; - - CHK(pSampleConfiguration != NULL, retStatus); - - if (IS_VALID_TIMER_QUEUE_HANDLE(pSampleConfiguration->timerQueueHandle)) { - if (pSampleConfiguration->iceCandidatePairStatsTimerId != MAX_UINT32) { - retStatus = timerQueueCancelTimer(pSampleConfiguration->timerQueueHandle, pSampleConfiguration->iceCandidatePairStatsTimerId, - (UINT64) pSampleConfiguration); - if (STATUS_FAILED(retStatus)) { - DLOGE("Failed to cancel stats timer with: 0x%08x", retStatus); - } - pSampleConfiguration->iceCandidatePairStatsTimerId = MAX_UINT32; - } - - if (pSampleConfiguration->pregenerateCertTimerId != MAX_UINT32) { - retStatus = timerQueueCancelTimer(pSampleConfiguration->timerQueueHandle, pSampleConfiguration->pregenerateCertTimerId, - (UINT64) pSampleConfiguration); - if (STATUS_FAILED(retStatus)) { - DLOGE("Failed to cancel certificate pre-generation timer with: 0x%08x", retStatus); - } - pSampleConfiguration->pregenerateCertTimerId = MAX_UINT32; - } - - timerQueueFree(&pSampleConfiguration->timerQueueHandle); - } - - if (pSampleConfiguration->pPendingSignalingMessageForRemoteClient != NULL) { - // Iterate and free all the pending queues - stackQueueGetIterator(pSampleConfiguration->pPendingSignalingMessageForRemoteClient, &iterator); - while (IS_VALID_ITERATOR(iterator)) { - stackQueueIteratorGetItem(iterator, &data); - stackQueueIteratorNext(&iterator); - freeMessageQueue((PPendingMessageQueue) data); - } - - stackQueueClear(pSampleConfiguration->pPendingSignalingMessageForRemoteClient, FALSE); - stackQueueFree(pSampleConfiguration->pPendingSignalingMessageForRemoteClient); - pSampleConfiguration->pPendingSignalingMessageForRemoteClient = NULL; - } - - hashTableClear(pSampleConfiguration->pRtcPeerConnectionForRemoteClient); - hashTableFree(pSampleConfiguration->pRtcPeerConnectionForRemoteClient); - - if (IS_VALID_MUTEX_VALUE(pSampleConfiguration->sampleConfigurationObjLock)) { - MUTEX_LOCK(pSampleConfiguration->sampleConfigurationObjLock); - locked = TRUE; - } - - for (i = 0; i < pSampleConfiguration->streamingSessionCount; ++i) { - if (pSampleConfiguration->enableIceStats) { - CHK_LOG_ERR(gatherIceServerStats(pSampleConfiguration->sampleStreamingSessionList[i])); - } - freeSampleStreamingSession(&pSampleConfiguration->sampleStreamingSessionList[i]); - } - if (locked) { - MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); - } - deinitKvsWebRtc(); - - SAFE_MEMFREE(pSampleConfiguration->pVideoFrameBuffer); - SAFE_MEMFREE(pSampleConfiguration->pAudioFrameBuffer); - - if (IS_VALID_CVAR_VALUE(pSampleConfiguration->cvar) && IS_VALID_MUTEX_VALUE(pSampleConfiguration->sampleConfigurationObjLock)) { - CVAR_BROADCAST(pSampleConfiguration->cvar); - MUTEX_LOCK(pSampleConfiguration->sampleConfigurationObjLock); - MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); - } - - if (IS_VALID_MUTEX_VALUE(pSampleConfiguration->sampleConfigurationObjLock)) { - MUTEX_FREE(pSampleConfiguration->sampleConfigurationObjLock); - } - - if (IS_VALID_MUTEX_VALUE(pSampleConfiguration->streamingSessionListReadLock)) { - MUTEX_FREE(pSampleConfiguration->streamingSessionListReadLock); - } - - if (IS_VALID_MUTEX_VALUE(pSampleConfiguration->signalingSendMessageLock)) { - MUTEX_FREE(pSampleConfiguration->signalingSendMessageLock); - } - - if (IS_VALID_CVAR_VALUE(pSampleConfiguration->cvar)) { - CVAR_FREE(pSampleConfiguration->cvar); - } - -#ifdef IOT_CORE_ENABLE_CREDENTIALS - freeIotCredentialProvider(&pSampleConfiguration->pCredentialProvider); -#else - freeStaticCredentialProvider(&pSampleConfiguration->pCredentialProvider); -#endif - - if (pSampleConfiguration->pregeneratedCertificates != NULL) { - stackQueueGetIterator(pSampleConfiguration->pregeneratedCertificates, &iterator); - while (IS_VALID_ITERATOR(iterator)) { - stackQueueIteratorGetItem(iterator, &data); - stackQueueIteratorNext(&iterator); - freeRtcCertificate((PRtcCertificate) data); - } - - CHK_LOG_ERR(stackQueueClear(pSampleConfiguration->pregeneratedCertificates, FALSE)); - CHK_LOG_ERR(stackQueueFree(pSampleConfiguration->pregeneratedCertificates)); - pSampleConfiguration->pregeneratedCertificates = NULL; - } - if (pSampleConfiguration->enableFileLogging) { - freeFileLogger(); - } - SAFE_MEMFREE(*ppSampleConfiguration); - -CleanUp: - - LEAVES(); - return retStatus; -} - -STATUS sessionCleanupWait(PSampleConfiguration pSampleConfiguration) -{ - ENTERS(); - STATUS retStatus = STATUS_SUCCESS; - PSampleStreamingSession pSampleStreamingSession = NULL; - UINT32 i, clientIdHash; - BOOL sampleConfigurationObjLockLocked = FALSE, streamingSessionListReadLockLocked = FALSE, peerConnectionFound = FALSE, sessionFreed = FALSE; - SIGNALING_CLIENT_STATE signalingClientState; - - CHK(pSampleConfiguration != NULL, STATUS_NULL_ARG); - - while (!ATOMIC_LOAD_BOOL(&pSampleConfiguration->interrupted)) { - // Keep the main set of operations interlocked until cvar wait which would atomically unlock - MUTEX_LOCK(pSampleConfiguration->sampleConfigurationObjLock); - sampleConfigurationObjLockLocked = TRUE; - - // scan and cleanup terminated streaming session - for (i = 0; i < pSampleConfiguration->streamingSessionCount; ++i) { - if (ATOMIC_LOAD_BOOL(&pSampleConfiguration->sampleStreamingSessionList[i]->terminateFlag)) { - pSampleStreamingSession = pSampleConfiguration->sampleStreamingSessionList[i]; - - MUTEX_LOCK(pSampleConfiguration->streamingSessionListReadLock); - streamingSessionListReadLockLocked = TRUE; - - // swap with last element and decrement count - pSampleConfiguration->streamingSessionCount--; - pSampleConfiguration->sampleStreamingSessionList[i] = - pSampleConfiguration->sampleStreamingSessionList[pSampleConfiguration->streamingSessionCount]; - - // Remove from the hash table - clientIdHash = COMPUTE_CRC32((PBYTE) pSampleStreamingSession->peerId, (UINT32) STRLEN(pSampleStreamingSession->peerId)); - CHK_STATUS(hashTableContains(pSampleConfiguration->pRtcPeerConnectionForRemoteClient, clientIdHash, &peerConnectionFound)); - if (peerConnectionFound) { - CHK_STATUS(hashTableRemove(pSampleConfiguration->pRtcPeerConnectionForRemoteClient, clientIdHash)); - } - - MUTEX_UNLOCK(pSampleConfiguration->streamingSessionListReadLock); - streamingSessionListReadLockLocked = FALSE; - - CHK_STATUS(freeSampleStreamingSession(&pSampleStreamingSession)); - sessionFreed = TRUE; - } - } - - if (sessionFreed && pSampleConfiguration->channelInfo.useMediaStorage && !ATOMIC_LOAD_BOOL(&pSampleConfiguration->recreateSignalingClient)) { - // In the WebRTC Media Storage Ingestion Case the backend will terminate the session after - // 1 hour. The SDK needs to make a new JoinSession Call in order to receive a new - // offer from the backend. We will create a new sample streaming session upon receipt of the - // offer. The signalingClientConnectSync call will result in a JoinSession API call being made. - CHK_STATUS(signalingClientDisconnectSync(pSampleConfiguration->signalingClientHandle)); - CHK_STATUS(signalingClientFetchSync(pSampleConfiguration->signalingClientHandle)); - CHK_STATUS(signalingClientConnectSync(pSampleConfiguration->signalingClientHandle)); - sessionFreed = FALSE; - } - - // Check if we need to re-create the signaling client on-the-fly - if (ATOMIC_LOAD_BOOL(&pSampleConfiguration->recreateSignalingClient)) { - retStatus = signalingClientFetchSync(pSampleConfiguration->signalingClientHandle); - if (STATUS_SUCCEEDED(retStatus)) { - // Re-set the variable again - ATOMIC_STORE_BOOL(&pSampleConfiguration->recreateSignalingClient, FALSE); - } else if (signalingCallFailed(retStatus)) { - printf("[KVS Common] recreating Signaling Client\n"); - freeSignalingClient(&pSampleConfiguration->signalingClientHandle); - createSignalingClientSync(&pSampleConfiguration->clientInfo, &pSampleConfiguration->channelInfo, - &pSampleConfiguration->signalingClientCallbacks, pSampleConfiguration->pCredentialProvider, - &pSampleConfiguration->signalingClientHandle); - } - } - - // Check the signaling client state and connect if needed - if (IS_VALID_SIGNALING_CLIENT_HANDLE(pSampleConfiguration->signalingClientHandle)) { - CHK_STATUS(signalingClientGetCurrentState(pSampleConfiguration->signalingClientHandle, &signalingClientState)); - if (signalingClientState == SIGNALING_CLIENT_STATE_READY) { - UNUSED_PARAM(signalingClientConnectSync(pSampleConfiguration->signalingClientHandle)); - } - } - - // Check if any lingering pending message queues - CHK_STATUS(removeExpiredMessageQueues(pSampleConfiguration->pPendingSignalingMessageForRemoteClient)); - - // periodically wake up and clean up terminated streaming session - CVAR_WAIT(pSampleConfiguration->cvar, pSampleConfiguration->sampleConfigurationObjLock, SAMPLE_SESSION_CLEANUP_WAIT_PERIOD); - MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); - sampleConfigurationObjLockLocked = FALSE; - } - -CleanUp: - - CHK_LOG_ERR(retStatus); - - if (sampleConfigurationObjLockLocked) { - MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); - } - - if (streamingSessionListReadLockLocked) { - MUTEX_UNLOCK(pSampleConfiguration->streamingSessionListReadLock); - } - - LEAVES(); - return retStatus; -} - -STATUS submitPendingIceCandidate(PPendingMessageQueue pPendingMessageQueue, PSampleStreamingSession pSampleStreamingSession) -{ - STATUS retStatus = STATUS_SUCCESS; - BOOL noPendingSignalingMessageForClient = FALSE; - PReceivedSignalingMessage pReceivedSignalingMessage = NULL; - UINT64 hashValue; - - CHK(pPendingMessageQueue != NULL && pPendingMessageQueue->messageQueue != NULL && pSampleStreamingSession != NULL, STATUS_NULL_ARG); - - do { - CHK_STATUS(stackQueueIsEmpty(pPendingMessageQueue->messageQueue, &noPendingSignalingMessageForClient)); - if (!noPendingSignalingMessageForClient) { - hashValue = 0; - CHK_STATUS(stackQueueDequeue(pPendingMessageQueue->messageQueue, &hashValue)); - pReceivedSignalingMessage = (PReceivedSignalingMessage) hashValue; - CHK(pReceivedSignalingMessage != NULL, STATUS_INTERNAL_ERROR); - if (pReceivedSignalingMessage->signalingMessage.messageType == SIGNALING_MESSAGE_TYPE_ICE_CANDIDATE) { - CHK_STATUS(handleRemoteCandidate(pSampleStreamingSession, &pReceivedSignalingMessage->signalingMessage)); - } - SAFE_MEMFREE(pReceivedSignalingMessage); - } - } while (!noPendingSignalingMessageForClient); - - CHK_STATUS(freeMessageQueue(pPendingMessageQueue)); - -CleanUp: - - SAFE_MEMFREE(pReceivedSignalingMessage); - CHK_LOG_ERR(retStatus); - return retStatus; -} - -STATUS signalingMessageReceived(UINT64 customData, PReceivedSignalingMessage pReceivedSignalingMessage) -{ - STATUS retStatus = STATUS_SUCCESS; - PSampleConfiguration pSampleConfiguration = (PSampleConfiguration) customData; - BOOL peerConnectionFound = FALSE, locked = FALSE, startStats = FALSE, freeStreamingSession = FALSE; - UINT32 clientIdHash; - UINT64 hashValue = 0; - PPendingMessageQueue pPendingMessageQueue = NULL; - PSampleStreamingSession pSampleStreamingSession = NULL; - PReceivedSignalingMessage pReceivedSignalingMessageCopy = NULL; - - CHK(pSampleConfiguration != NULL, STATUS_NULL_ARG); - - MUTEX_LOCK(pSampleConfiguration->sampleConfigurationObjLock); - locked = TRUE; - - clientIdHash = COMPUTE_CRC32((PBYTE) pReceivedSignalingMessage->signalingMessage.peerClientId, - (UINT32) STRLEN(pReceivedSignalingMessage->signalingMessage.peerClientId)); - CHK_STATUS(hashTableContains(pSampleConfiguration->pRtcPeerConnectionForRemoteClient, clientIdHash, &peerConnectionFound)); - if (peerConnectionFound) { - CHK_STATUS(hashTableGet(pSampleConfiguration->pRtcPeerConnectionForRemoteClient, clientIdHash, &hashValue)); - pSampleStreamingSession = (PSampleStreamingSession) hashValue; - } - - switch (pReceivedSignalingMessage->signalingMessage.messageType) { - case SIGNALING_MESSAGE_TYPE_OFFER: - // Check if we already have an ongoing master session with the same peer - CHK_ERR(!peerConnectionFound, STATUS_INVALID_OPERATION, "Peer connection %s is in progress", - pReceivedSignalingMessage->signalingMessage.peerClientId); - - /* - * Create new streaming session for each offer, then insert the client id and streaming session into - * pRtcPeerConnectionForRemoteClient for subsequent ice candidate messages. Lastly check if there is - * any ice candidate messages queued in pPendingSignalingMessageForRemoteClient. If so then submit - * all of them. - */ - - if (pSampleConfiguration->streamingSessionCount == ARRAY_SIZE(pSampleConfiguration->sampleStreamingSessionList)) { - DLOGW("Max simultaneous streaming session count reached."); - - // Need to remove the pending queue if any. - // This is a simple optimization as the session cleanup will - // handle the cleanup of pending message queue after a while - CHK_STATUS(getPendingMessageQueueForHash(pSampleConfiguration->pPendingSignalingMessageForRemoteClient, clientIdHash, TRUE, - &pPendingMessageQueue)); - - CHK(FALSE, retStatus); - } - CHK_STATUS(createSampleStreamingSession(pSampleConfiguration, pReceivedSignalingMessage->signalingMessage.peerClientId, TRUE, - &pSampleStreamingSession)); - freeStreamingSession = TRUE; - CHK_STATUS(handleOffer(pSampleConfiguration, pSampleStreamingSession, &pReceivedSignalingMessage->signalingMessage)); - CHK_STATUS(hashTablePut(pSampleConfiguration->pRtcPeerConnectionForRemoteClient, clientIdHash, (UINT64) pSampleStreamingSession)); - - // If there are any ice candidate messages in the queue for this client id, submit them now. - CHK_STATUS(getPendingMessageQueueForHash(pSampleConfiguration->pPendingSignalingMessageForRemoteClient, clientIdHash, TRUE, - &pPendingMessageQueue)); - if (pPendingMessageQueue != NULL) { - CHK_STATUS(submitPendingIceCandidate(pPendingMessageQueue, pSampleStreamingSession)); - - // NULL the pointer to avoid it being freed in the cleanup - pPendingMessageQueue = NULL; - } - - MUTEX_LOCK(pSampleConfiguration->streamingSessionListReadLock); - pSampleConfiguration->sampleStreamingSessionList[pSampleConfiguration->streamingSessionCount++] = pSampleStreamingSession; - MUTEX_UNLOCK(pSampleConfiguration->streamingSessionListReadLock); - freeStreamingSession = FALSE; - - startStats = pSampleConfiguration->iceCandidatePairStatsTimerId == MAX_UINT32; - break; - - case SIGNALING_MESSAGE_TYPE_ANSWER: - /* - * for viewer, pSampleStreamingSession should've already been created. insert the client id and - * streaming session into pRtcPeerConnectionForRemoteClient for subsequent ice candidate messages. - * Lastly check if there is any ice candidate messages queued in pPendingSignalingMessageForRemoteClient. - * If so then submit all of them. - */ - pSampleStreamingSession = pSampleConfiguration->sampleStreamingSessionList[0]; - CHK_STATUS(handleAnswer(pSampleConfiguration, pSampleStreamingSession, &pReceivedSignalingMessage->signalingMessage)); - CHK_STATUS(hashTablePut(pSampleConfiguration->pRtcPeerConnectionForRemoteClient, clientIdHash, (UINT64) pSampleStreamingSession)); - - // If there are any ice candidate messages in the queue for this client id, submit them now. - CHK_STATUS(getPendingMessageQueueForHash(pSampleConfiguration->pPendingSignalingMessageForRemoteClient, clientIdHash, TRUE, - &pPendingMessageQueue)); - if (pPendingMessageQueue != NULL) { - CHK_STATUS(submitPendingIceCandidate(pPendingMessageQueue, pSampleStreamingSession)); - - // NULL the pointer to avoid it being freed in the cleanup - pPendingMessageQueue = NULL; - } - - startStats = pSampleConfiguration->iceCandidatePairStatsTimerId == MAX_UINT32; - CHK_STATUS(signalingClientGetMetrics(pSampleConfiguration->signalingClientHandle, &pSampleConfiguration->signalingClientMetrics)); - DLOGP("[Signaling offer sent to answer received time] %" PRIu64 " ms", - pSampleConfiguration->signalingClientMetrics.signalingClientStats.offerToAnswerTime); - break; - - case SIGNALING_MESSAGE_TYPE_ICE_CANDIDATE: - /* - * if peer connection hasn't been created, create an queue to store the ice candidate message. Otherwise - * submit the signaling message into the corresponding streaming session. - */ - if (!peerConnectionFound) { - CHK_STATUS(getPendingMessageQueueForHash(pSampleConfiguration->pPendingSignalingMessageForRemoteClient, clientIdHash, FALSE, - &pPendingMessageQueue)); - if (pPendingMessageQueue == NULL) { - CHK_STATUS(createMessageQueue(clientIdHash, &pPendingMessageQueue)); - CHK_STATUS(stackQueueEnqueue(pSampleConfiguration->pPendingSignalingMessageForRemoteClient, (UINT64) pPendingMessageQueue)); - } - - pReceivedSignalingMessageCopy = (PReceivedSignalingMessage) MEMCALLOC(1, SIZEOF(ReceivedSignalingMessage)); - - *pReceivedSignalingMessageCopy = *pReceivedSignalingMessage; - - CHK_STATUS(stackQueueEnqueue(pPendingMessageQueue->messageQueue, (UINT64) pReceivedSignalingMessageCopy)); - - // NULL the pointers to not free any longer - pPendingMessageQueue = NULL; - pReceivedSignalingMessageCopy = NULL; - } else { - CHK_STATUS(handleRemoteCandidate(pSampleStreamingSession, &pReceivedSignalingMessage->signalingMessage)); - } - break; - - default: - DLOGD("Unhandled signaling message type %u", pReceivedSignalingMessage->signalingMessage.messageType); - break; - } - - MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); - locked = FALSE; - - if (pSampleConfiguration->enableIceStats && startStats && - STATUS_FAILED(retStatus = timerQueueAddTimer(pSampleConfiguration->timerQueueHandle, SAMPLE_STATS_DURATION, SAMPLE_STATS_DURATION, - getIceCandidatePairStatsCallback, (UINT64) pSampleConfiguration, - &pSampleConfiguration->iceCandidatePairStatsTimerId))) { - DLOGW("Failed to add getIceCandidatePairStatsCallback to add to timer queue (code 0x%08x). " - "Cannot pull ice candidate pair metrics periodically", - retStatus); - - // Reset the returned status - retStatus = STATUS_SUCCESS; - } - -CleanUp: - - SAFE_MEMFREE(pReceivedSignalingMessageCopy); - if (pPendingMessageQueue != NULL) { - freeMessageQueue(pPendingMessageQueue); - } - - if (freeStreamingSession && pSampleStreamingSession != NULL) { - freeSampleStreamingSession(&pSampleStreamingSession); - } - - if (locked) { - MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); - } - - CHK_LOG_ERR(retStatus); - return retStatus; -} - -STATUS createMessageQueue(UINT64 hashValue, PPendingMessageQueue* ppPendingMessageQueue) -{ - STATUS retStatus = STATUS_SUCCESS; - PPendingMessageQueue pPendingMessageQueue = NULL; - - CHK(ppPendingMessageQueue != NULL, STATUS_NULL_ARG); - - CHK(NULL != (pPendingMessageQueue = (PPendingMessageQueue) MEMCALLOC(1, SIZEOF(PendingMessageQueue))), STATUS_NOT_ENOUGH_MEMORY); - pPendingMessageQueue->hashValue = hashValue; - pPendingMessageQueue->createTime = GETTIME(); - CHK_STATUS(stackQueueCreate(&pPendingMessageQueue->messageQueue)); - -CleanUp: - - if (STATUS_FAILED(retStatus) && pPendingMessageQueue != NULL) { - freeMessageQueue(pPendingMessageQueue); - pPendingMessageQueue = NULL; - } - - if (ppPendingMessageQueue != NULL) { - *ppPendingMessageQueue = pPendingMessageQueue; - } - - return retStatus; -} - -STATUS freeMessageQueue(PPendingMessageQueue pPendingMessageQueue) -{ - STATUS retStatus = STATUS_SUCCESS; - - // free is idempotent - CHK(pPendingMessageQueue != NULL, retStatus); - - if (pPendingMessageQueue->messageQueue != NULL) { - stackQueueClear(pPendingMessageQueue->messageQueue, TRUE); - stackQueueFree(pPendingMessageQueue->messageQueue); - } - - MEMFREE(pPendingMessageQueue); - -CleanUp: - return retStatus; -} - -STATUS getPendingMessageQueueForHash(PStackQueue pPendingQueue, UINT64 clientHash, BOOL remove, PPendingMessageQueue* ppPendingMessageQueue) -{ - STATUS retStatus = STATUS_SUCCESS; - PPendingMessageQueue pPendingMessageQueue = NULL; - StackQueueIterator iterator; - BOOL iterate = TRUE; - UINT64 data; - - CHK(pPendingQueue != NULL && ppPendingMessageQueue != NULL, STATUS_NULL_ARG); - - CHK_STATUS(stackQueueGetIterator(pPendingQueue, &iterator)); - while (iterate && IS_VALID_ITERATOR(iterator)) { - CHK_STATUS(stackQueueIteratorGetItem(iterator, &data)); - CHK_STATUS(stackQueueIteratorNext(&iterator)); - - pPendingMessageQueue = (PPendingMessageQueue) data; - - if (clientHash == pPendingMessageQueue->hashValue) { - *ppPendingMessageQueue = pPendingMessageQueue; - iterate = FALSE; - - // Check if the item needs to be removed - if (remove) { - // This is OK to do as we are terminating the iterator anyway - CHK_STATUS(stackQueueRemoveItem(pPendingQueue, data)); - } - } - } - -CleanUp: - - return retStatus; -} - -STATUS removeExpiredMessageQueues(PStackQueue pPendingQueue) -{ - STATUS retStatus = STATUS_SUCCESS; - PPendingMessageQueue pPendingMessageQueue = NULL; - UINT32 i, count; - UINT64 data, curTime; - - CHK(pPendingQueue != NULL, STATUS_NULL_ARG); - - curTime = GETTIME(); - CHK_STATUS(stackQueueGetCount(pPendingQueue, &count)); - - // Dequeue and enqueue in order to not break the iterator while removing an item - for (i = 0; i < count; i++) { - CHK_STATUS(stackQueueDequeue(pPendingQueue, &data)); - - // Check for expiry - pPendingMessageQueue = (PPendingMessageQueue) data; - if (pPendingMessageQueue->createTime + SAMPLE_PENDING_MESSAGE_CLEANUP_DURATION < curTime) { - // Message queue has expired and needs to be freed - CHK_STATUS(freeMessageQueue(pPendingMessageQueue)); - } else { - // Enqueue back again as it's still valued - CHK_STATUS(stackQueueEnqueue(pPendingQueue, data)); - } - } - -CleanUp: - - return retStatus; -} - -#ifdef ENABLE_DATA_CHANNEL -VOID onDataChannelMessage(UINT64 customData, PRtcDataChannel pDataChannel, BOOL isBinary, PBYTE pMessage, UINT32 pMessageLen) -{ - STATUS retStatus = STATUS_SUCCESS; - UINT32 i, strLen, tokenCount; - CHAR pMessageSend[MAX_DATA_CHANNEL_METRICS_MESSAGE_SIZE], errorMessage[200]; - PCHAR json; - PSampleStreamingSession pSampleStreamingSession = (PSampleStreamingSession) customData; - PSampleConfiguration pSampleConfiguration; - DataChannelMessage dataChannelMessage; - jsmn_parser parser; - jsmntok_t tokens[MAX_JSON_TOKEN_COUNT]; - - CHK(pMessage != NULL && pDataChannel != NULL, STATUS_NULL_ARG); - - if (pSampleStreamingSession == NULL) { - STRCPY(errorMessage, "Could not generate stats since the streaming session is NULL"); - retStatus = dataChannelSend(pDataChannel, FALSE, (PBYTE) errorMessage, STRLEN(errorMessage)); - DLOGE("%s", errorMessage); - goto CleanUp; - } - - pSampleConfiguration = pSampleStreamingSession->pSampleConfiguration; - if (pSampleConfiguration == NULL) { - STRCPY(errorMessage, "Could not generate stats since the sample configuration is NULL"); - retStatus = dataChannelSend(pDataChannel, FALSE, (PBYTE) errorMessage, STRLEN(errorMessage)); - DLOGE("%s", errorMessage); - goto CleanUp; - } - - if (pSampleConfiguration->enableSendingMetricsToViewerViaDc) { - jsmn_init(&parser); - json = (PCHAR) pMessage; - tokenCount = jsmn_parse(&parser, json, STRLEN(json), tokens, SIZEOF(tokens) / SIZEOF(jsmntok_t)); - - MEMSET(dataChannelMessage.content, '\0', SIZEOF(dataChannelMessage.content)); - MEMSET(dataChannelMessage.firstMessageFromViewerTs, '\0', SIZEOF(dataChannelMessage.firstMessageFromViewerTs)); - MEMSET(dataChannelMessage.firstMessageFromMasterTs, '\0', SIZEOF(dataChannelMessage.firstMessageFromMasterTs)); - MEMSET(dataChannelMessage.secondMessageFromViewerTs, '\0', SIZEOF(dataChannelMessage.secondMessageFromViewerTs)); - MEMSET(dataChannelMessage.secondMessageFromMasterTs, '\0', SIZEOF(dataChannelMessage.secondMessageFromMasterTs)); - MEMSET(dataChannelMessage.lastMessageFromViewerTs, '\0', SIZEOF(dataChannelMessage.lastMessageFromViewerTs)); - - if (tokenCount > 1) { - if (tokens[0].type != JSMN_OBJECT) { - STRCPY(errorMessage, "Invalid JSON received, please send a valid json as the SDK is operating in datachannel-benchmarking mode"); - retStatus = dataChannelSend(pDataChannel, FALSE, (PBYTE) errorMessage, STRLEN(errorMessage)); - DLOGE("%s", errorMessage); - retStatus = STATUS_INVALID_API_CALL_RETURN_JSON; - goto CleanUp; - } - DLOGI("DataChannel json message: %.*s\n", pMessageLen, pMessage); - - for (i = 1; i < tokenCount; i++) { - if (compareJsonString(json, &tokens[i], JSMN_STRING, (PCHAR) "content")) { - strLen = (UINT32) (tokens[i + 1].end - tokens[i + 1].start); - if (strLen != 0) { - STRNCPY(dataChannelMessage.content, json + tokens[i + 1].start, tokens[i + 1].end - tokens[i + 1].start); - } - } else if (compareJsonString(json, &tokens[i], JSMN_STRING, (PCHAR) "firstMessageFromViewerTs")) { - strLen = (UINT32) (tokens[i + 1].end - tokens[i + 1].start); - // parse and retain this message from the viewer to send it back again - if (strLen != 0) { - // since the length is not zero, we have already attached this timestamp to structure in the last iteration - STRNCPY(dataChannelMessage.firstMessageFromViewerTs, json + tokens[i + 1].start, tokens[i + 1].end - tokens[i + 1].start); - } - } else if (compareJsonString(json, &tokens[i], JSMN_STRING, (PCHAR) "firstMessageFromMasterTs")) { - strLen = (UINT32) (tokens[i + 1].end - tokens[i + 1].start); - if (strLen != 0) { - // since the length is not zero, we have already attached this timestamp to structure in the last iteration - STRNCPY(dataChannelMessage.firstMessageFromMasterTs, json + tokens[i + 1].start, tokens[i + 1].end - tokens[i + 1].start); - } else { - // if this timestamp was not assigned during the previous message session, add it now - SNPRINTF(dataChannelMessage.firstMessageFromMasterTs, 20, "%llu", GETTIME() / 10000); - break; - } - } else if (compareJsonString(json, &tokens[i], JSMN_STRING, (PCHAR) "secondMessageFromViewerTs")) { - strLen = (UINT32) (tokens[i + 1].end - tokens[i + 1].start); - // parse and retain this message from the viewer to send it back again - if (strLen != 0) { - STRNCPY(dataChannelMessage.secondMessageFromViewerTs, json + tokens[i + 1].start, tokens[i + 1].end - tokens[i + 1].start); - } - } else if (compareJsonString(json, &tokens[i], JSMN_STRING, (PCHAR) "secondMessageFromMasterTs")) { - strLen = (UINT32) (tokens[i + 1].end - tokens[i + 1].start); - if (strLen != 0) { - // since the length is not zero, we have already attached this timestamp to structure in the last iteration - STRNCPY(dataChannelMessage.secondMessageFromMasterTs, json + tokens[i + 1].start, tokens[i + 1].end - tokens[i + 1].start); - } else { - // if this timestamp was not assigned during the previous message session, add it now - SNPRINTF(dataChannelMessage.secondMessageFromMasterTs, 20, "%llu", GETTIME() / 10000); - break; - } - } else if (compareJsonString(json, &tokens[i], JSMN_STRING, (PCHAR) "lastMessageFromViewerTs")) { - strLen = (UINT32) (tokens[i + 1].end - tokens[i + 1].start); - if (strLen != 0) { - STRNCPY(dataChannelMessage.lastMessageFromViewerTs, json + tokens[i + 1].start, tokens[i + 1].end - tokens[i + 1].start); - } - } - } - - if (STRLEN(dataChannelMessage.lastMessageFromViewerTs) == 0) { - // continue sending the data_channel_metrics_message with new timestamps until we receive the lastMessageFromViewerTs from the viewer - SNPRINTF(pMessageSend, MAX_DATA_CHANNEL_METRICS_MESSAGE_SIZE, DATA_CHANNEL_MESSAGE_TEMPLATE, MASTER_DATA_CHANNEL_MESSAGE, - dataChannelMessage.firstMessageFromViewerTs, dataChannelMessage.firstMessageFromMasterTs, - dataChannelMessage.secondMessageFromViewerTs, dataChannelMessage.secondMessageFromMasterTs, - dataChannelMessage.lastMessageFromViewerTs); - DLOGI("Master's response: %s", pMessageSend); - - retStatus = dataChannelSend(pDataChannel, FALSE, (PBYTE) pMessageSend, STRLEN(pMessageSend)); - } else { - // now that we've received the last message, send across the signaling, peerConnection, ice metrics - SNPRINTF(pSampleStreamingSession->pSignalingClientMetricsMessage, MAX_SIGNALING_CLIENT_METRICS_MESSAGE_SIZE, - SIGNALING_CLIENT_METRICS_JSON_TEMPLATE, pSampleConfiguration->signalingClientMetrics.signalingStartTime, - pSampleConfiguration->signalingClientMetrics.signalingEndTime, - pSampleConfiguration->signalingClientMetrics.signalingClientStats.offerReceivedTime, - pSampleConfiguration->signalingClientMetrics.signalingClientStats.answerTime, - pSampleConfiguration->signalingClientMetrics.signalingClientStats.describeChannelStartTime, - pSampleConfiguration->signalingClientMetrics.signalingClientStats.describeChannelEndTime, - pSampleConfiguration->signalingClientMetrics.signalingClientStats.getSignalingChannelEndpointStartTime, - pSampleConfiguration->signalingClientMetrics.signalingClientStats.getSignalingChannelEndpointEndTime, - pSampleConfiguration->signalingClientMetrics.signalingClientStats.getIceServerConfigStartTime, - pSampleConfiguration->signalingClientMetrics.signalingClientStats.getIceServerConfigEndTime, - pSampleConfiguration->signalingClientMetrics.signalingClientStats.getTokenStartTime, - pSampleConfiguration->signalingClientMetrics.signalingClientStats.getTokenEndTime, - pSampleConfiguration->signalingClientMetrics.signalingClientStats.createChannelStartTime, - pSampleConfiguration->signalingClientMetrics.signalingClientStats.createChannelEndTime, - pSampleConfiguration->signalingClientMetrics.signalingClientStats.connectStartTime, - pSampleConfiguration->signalingClientMetrics.signalingClientStats.connectEndTime); - DLOGI("Sending signaling metrics to the viewer: %s", pSampleStreamingSession->pSignalingClientMetricsMessage); - - CHK_STATUS(peerConnectionGetMetrics(pSampleStreamingSession->pPeerConnection, &pSampleStreamingSession->peerConnectionMetrics)); - SNPRINTF(pSampleStreamingSession->pPeerConnectionMetricsMessage, MAX_PEER_CONNECTION_METRICS_MESSAGE_SIZE, - PEER_CONNECTION_METRICS_JSON_TEMPLATE, - pSampleStreamingSession->peerConnectionMetrics.peerConnectionStats.peerConnectionStartTime, - pSampleStreamingSession->peerConnectionMetrics.peerConnectionStats.peerConnectionConnectedTime); - DLOGI("Sending peer-connection metrics to the viewer: %s", pSampleStreamingSession->pPeerConnectionMetricsMessage); - - CHK_STATUS(iceAgentGetMetrics(pSampleStreamingSession->pPeerConnection, &pSampleStreamingSession->iceMetrics)); - SNPRINTF(pSampleStreamingSession->pIceAgentMetricsMessage, MAX_ICE_AGENT_METRICS_MESSAGE_SIZE, ICE_AGENT_METRICS_JSON_TEMPLATE, - pSampleStreamingSession->iceMetrics.kvsIceAgentStats.candidateGatheringStartTime, - pSampleStreamingSession->iceMetrics.kvsIceAgentStats.candidateGatheringEndTime); - DLOGI("Sending ice-agent metrics to the viewer: %s", pSampleStreamingSession->pIceAgentMetricsMessage); - - retStatus = dataChannelSend(pDataChannel, FALSE, (PBYTE) pSampleStreamingSession->pSignalingClientMetricsMessage, - STRLEN(pSampleStreamingSession->pSignalingClientMetricsMessage)); - retStatus = dataChannelSend(pDataChannel, FALSE, (PBYTE) pSampleStreamingSession->pPeerConnectionMetricsMessage, - STRLEN(pSampleStreamingSession->pPeerConnectionMetricsMessage)); - retStatus = dataChannelSend(pDataChannel, FALSE, (PBYTE) pSampleStreamingSession->pIceAgentMetricsMessage, - STRLEN(pSampleStreamingSession->pIceAgentMetricsMessage)); - } - } else { - DLOGI("DataChannel string message: %.*s\n", pMessageLen, pMessage); - STRCPY(errorMessage, "Send a json message for benchmarking as the C SDK is operating in benchmarking mode"); - retStatus = dataChannelSend(pDataChannel, FALSE, (PBYTE) errorMessage, STRLEN(errorMessage)); - } - } else { - if (isBinary) { - DLOGI("DataChannel Binary Message"); - } else { - DLOGI("DataChannel String Message: %.*s\n", pMessageLen, pMessage); - } - // Send a response to the message sent by the viewer - retStatus = dataChannelSend(pDataChannel, FALSE, (PBYTE) MASTER_DATA_CHANNEL_MESSAGE, STRLEN(MASTER_DATA_CHANNEL_MESSAGE)); - } - if (retStatus != STATUS_SUCCESS) { - DLOGI("[KVS Master] dataChannelSend(): operation returned status code: 0x%08x \n", retStatus); - } - -CleanUp: - CHK_LOG_ERR(retStatus); -} - -VOID onDataChannel(UINT64 customData, PRtcDataChannel pRtcDataChannel) -{ - DLOGI("New DataChannel has been opened %s \n", pRtcDataChannel->name); - dataChannelOnMessage(pRtcDataChannel, customData, onDataChannelMessage); -} -#endif diff --git a/samples/Samples.h b/samples/Samples.h index e409a9b5dd..0ed8f0794d 100644 --- a/samples/Samples.h +++ b/samples/Samples.h @@ -12,19 +12,21 @@ extern "C" { #include -#define NUMBER_OF_H264_FRAME_FILES 1500 -#define NUMBER_OF_H265_FRAME_FILES 1500 -#define NUMBER_OF_OPUS_FRAME_FILES 618 -#define DEFAULT_FPS_VALUE 25 -#define DEFAULT_VIDEO_HEIGHT_PIXELS 720 -#define DEFAULT_VIDEO_WIDTH_PIXELS 1280 -#define DEFAULT_AUDIO_OPUS_CHANNELS 2 -#define DEFAULT_AUDIO_AAC_CHANNELS 2 -#define DEFAULT_AUDIO_OPUS_SAMPLE_RATE_HZ 48000 -#define DEFAULT_AUDIO_AAC_SAMPLE_RATE_HZ 16000 -#define DEFAULT_AUDIO_OPUS_BITS_PER_SAMPLE 16 -#define DEFAULT_AUDIO_AAC_BITS_PER_SAMPLE 16 -#define DEFAULT_MAX_CONCURRENT_STREAMING_SESSION 10 +#define KVS_DEFAULT_MEDIA_SENDER_THREAD_STACK_SIZE 64 * 1024 +#define KVS_MINIMUM_THREAD_STACK_SIZE 16 * 1024 +#define NUMBER_OF_H264_FRAME_FILES 1500 +#define NUMBER_OF_H265_FRAME_FILES 1500 +#define NUMBER_OF_OPUS_FRAME_FILES 618 +#define DEFAULT_FPS_VALUE 25 +#define DEFAULT_VIDEO_HEIGHT_PIXELS 720 +#define DEFAULT_VIDEO_WIDTH_PIXELS 1280 +#define DEFAULT_AUDIO_OPUS_CHANNELS 2 +#define DEFAULT_AUDIO_AAC_CHANNELS 2 +#define DEFAULT_AUDIO_OPUS_SAMPLE_RATE_HZ 48000 +#define DEFAULT_AUDIO_AAC_SAMPLE_RATE_HZ 16000 +#define DEFAULT_AUDIO_OPUS_BITS_PER_SAMPLE 16 +#define DEFAULT_AUDIO_AAC_BITS_PER_SAMPLE 16 +#define DEFAULT_MAX_CONCURRENT_STREAMING_SESSION 10 #define AUDIO_CODEC_NAME_ALAW "alaw" #define AUDIO_CODEC_NAME_MULAW "mulaw" @@ -69,8 +71,7 @@ extern "C" { #define IOT_CORE_THING_NAME ((PCHAR) "AWS_IOT_CORE_THING_NAME") #define IOT_CORE_CERTIFICATE_ID ((PCHAR) "AWS_IOT_CORE_CERTIFICATE_ID") -/* Uncomment the following line in order to enable IoT credentials checks in the provided samples */ -// #define IOT_CORE_ENABLE_CREDENTIALS 1 +#define IOT_CORE_ENABLE_CREDENTIALS FALSE #define MASTER_DATA_CHANNEL_MESSAGE "This message is from the KVS Master" #define VIEWER_DATA_CHANNEL_MESSAGE "This message is from the KVS Viewer" @@ -98,6 +99,9 @@ extern "C" { #define MIN_AUDIO_BITRATE_BPS 4000 // Unit bits/sec. Value could change based on codec. #define MAX_AUDIO_BITRATE_BPS 650000 // Unit bits/sec. Value could change based on codec. +#define STATUS_WEBRTC_SAMPLE_BASE 0x74000000 +#define STATUS_WAITING_ON_FIRST_FRAME STATUS_WEBRTC_SAMPLE_BASE + 0x00000001 + typedef enum { SAMPLE_STREAMING_VIDEO_ONLY, SAMPLE_STREAMING_AUDIO_VIDEO, @@ -119,7 +123,7 @@ typedef struct { UINT64 prevNumberOfBytesReceived; UINT64 prevPacketsDiscardedOnSend; UINT64 prevTs; -} RtcMetricsHistory, *PRtcMetricsHistory; +} RtpMetricsHistory, *PRtpMetricsHistory; typedef struct { volatile ATOMIC_BOOL appTerminateFlag; @@ -148,7 +152,6 @@ typedef struct { startRoutine videoSource; startRoutine receiveAudioVideoSource; RtcOnDataChannel onDataChannel; - SignalingClientMetrics signalingClientMetrics; PStackQueue pPendingSignalingMessageForRemoteClient; PHashTable pRtcPeerConnectionForRemoteClient; @@ -178,6 +181,11 @@ typedef struct { UINT32 logLevel; BOOL enableIceStats; BOOL enableTwcc; + BOOL forceTurn; + BOOL enableMetrics; + BOOL useIot; + UINT64 storageDisconnectedTime; + SignalingClientMetrics signalingClientMetrics; } SampleConfiguration, *PSampleConfiguration; typedef struct { @@ -207,6 +215,51 @@ typedef struct { DOUBLE averagePacketLoss; } TwccMetadata, *PTwccMetadata; +typedef struct { + UINT64 prevNumberOfPacketsSent; + UINT64 prevNumberOfPacketsReceived; + UINT64 prevNumberOfBytesSent; + UINT64 prevNumberOfBytesReceived; + UINT64 prevFramesDiscardedOnSend; + UINT64 prevTs; + UINT64 prevVideoFramesGenerated; + UINT64 prevFramesSent; + UINT64 prevNackCount; + UINT64 prevRetxBytesSent; + UINT64 videoFramesGenerated; + UINT64 videoBytesGenerated; + DOUBLE framesPercentageDiscarded; + DOUBLE nacksPerSecond; + DOUBLE averageFramesSentPerSecond; + DOUBLE retxBytesPercentage; + BOOL recorded; +} OutgoingRTPStatsCtx, *POutgoingRTPStatsCtx; + +typedef struct { + DOUBLE frameLatencyAvg; + DOUBLE dataMatchAvg; + DOUBLE sizeMatchAvg; +} EndToEndMetricsCtx, *PEndToEndMetricsCtx; + +typedef struct { + UINT64 prevPacketsReceived; + UINT64 prevTs; + UINT64 prevBytesReceived; + UINT64 prevFramesDropped; + DOUBLE packetReceiveRate; + DOUBLE incomingBitRate; + DOUBLE framesDroppedPerSecond; +} IncomingRTPStatsCtx, *PIncomingRTPStatsCtx; + +typedef struct { + OutgoingRTPStatsCtx outgoingRTPStatsCtx; + IncomingRTPStatsCtx incomingRTPStatsCtx; + EndToEndMetricsCtx endToEndMetricsCtx; + RtcStats kvsRtcStats; + MUTEX statsUpdateLock; + volatile SIZE_T statsContextRefCnt; +} StatsCtx, *PStatsCtx; + struct __SampleStreamingSession { volatile ATOMIC_BOOL terminateFlag; volatile ATOMIC_BOOL candidateGatheringDone; @@ -224,7 +277,7 @@ struct __SampleStreamingSession { CHAR peerId[MAX_SIGNALING_CLIENT_ID_LEN + 1]; TID receiveAudioVideoSenderTid; UINT64 startUpLatency; - RtcMetricsHistory rtcMetricsHistory; + RtpMetricsHistory rtpMetricsHistory; BOOL remoteCanTrickleIce; TwccMetadata twccMetadata; @@ -232,11 +285,12 @@ struct __SampleStreamingSession { StreamSessionShutdownCallback shutdownCallback; UINT64 shutdownCallbackCustomData; UINT64 offerReceiveTime; - PeerConnectionMetrics peerConnectionMetrics; - KvsIceAgentMetrics iceMetrics; CHAR pPeerConnectionMetricsMessage[MAX_PEER_CONNECTION_METRICS_MESSAGE_SIZE]; CHAR pSignalingClientMetricsMessage[MAX_SIGNALING_CLIENT_METRICS_MESSAGE_SIZE]; CHAR pIceAgentMetricsMessage[MAX_ICE_AGENT_METRICS_MESSAGE_SIZE]; + PeerConnectionMetrics peerConnectionMetrics; + KvsIceAgentMetrics iceMetrics; + PStatsCtx pStatsCtx; }; // TODO this should all be in a higher webrtccontext layer above PeerConnection @@ -255,7 +309,6 @@ PVOID sendVideoPackets(PVOID); PVOID sendAudioPackets(PVOID); PVOID sendGstreamerAudioVideo(PVOID); PVOID sampleReceiveAudioVideoFrame(PVOID); -PVOID getPeriodicIceCandidatePairStats(PVOID); STATUS getIceCandidatePairStatsCallback(UINT32, UINT64, UINT64); STATUS pregenerateCertTimerCallback(UINT32, UINT64, UINT64); STATUS createSampleConfiguration(PCHAR, SIGNALING_CHANNEL_ROLE_TYPE, BOOL, BOOL, UINT32, PSampleConfiguration*); @@ -279,6 +332,8 @@ VOID sampleFrameHandler(UINT64, PFrame); VOID sampleBandwidthEstimationHandler(UINT64, DOUBLE); VOID sampleSenderBandwidthEstimationHandler(UINT64, UINT32, UINT32, UINT32, UINT32, UINT64); VOID onDataChannel(UINT64, PRtcDataChannel); +VOID onDataChannelMessage(UINT64, PRtcDataChannel, BOOL, PBYTE, UINT32); + VOID onConnectionStateChange(UINT64, RTC_PEER_CONNECTION_STATE); STATUS sessionCleanupWait(PSampleConfiguration); STATUS logSignalingClientStats(PSignalingClientMetrics); @@ -293,6 +348,18 @@ STATUS initSignaling(PSampleConfiguration, PCHAR); BOOL sampleFilterNetworkInterfaces(UINT64, PCHAR); UINT32 setLogLevel(); +STATUS populateOutgoingRtpMetricsContext(PSampleStreamingSession pSampleStreamingSession); +STATUS populateIncomingRtpMetricsContext(PSampleStreamingSession pSampleStreamingSession); +STATUS gatherIceServerStats(PSampleStreamingSession pSampleStreamingSession); +VOID onIceCandidateHandler(UINT64, PCHAR); +PVOID mediaSenderRoutine(PVOID); +STATUS setupMetricsCtx(PSampleStreamingSession); +STATUS getSdkTimeProfile(PSampleStreamingSession*); +STATUS terminate(UINT32, UINT64, UINT64); +STATUS setUpCredentialProvider(PSampleConfiguration, BOOL); +STATUS freeMetricsCtx(PStatsCtx*); +VOID acquireMetricsCtx(PSampleStreamingSession); +VOID releaseMetricsCtx(PSampleStreamingSession); #ifdef __cplusplus } #endif diff --git a/samples/kvsWebRTCClientMaster.c b/samples/kvsWebRTCClientMaster.c index 17411238a0..9418c55825 100644 --- a/samples/kvsWebRTCClientMaster.c +++ b/samples/kvsWebRTCClientMaster.c @@ -1,4 +1,4 @@ -#include "Samples.h" +#include "../samples/Samples.h" extern PSampleConfiguration gSampleConfiguration; @@ -20,15 +20,15 @@ INT32 main(INT32 argc, CHAR* argv[]) signal(SIGINT, sigintHandler); #endif -#ifdef IOT_CORE_ENABLE_CREDENTIALS - CHK_ERR((pChannelName = argc > 1 ? argv[1] : GETENV(IOT_CORE_THING_NAME)) != NULL, STATUS_INVALID_OPERATION, - "AWS_IOT_CORE_THING_NAME must be set"); -#else - pChannelName = argc > 1 ? argv[1] : SAMPLE_CHANNEL_NAME; -#endif + if (IOT_CORE_ENABLE_CREDENTIALS) { + CHK_ERR((pChannelName = argc > 1 ? argv[1] : GETENV(IOT_CORE_THING_NAME)) != NULL, STATUS_INVALID_OPERATION, + "AWS_IOT_CORE_THING_NAME must be set"); + } else { + pChannelName = argc > 1 ? argv[1] : SAMPLE_CHANNEL_NAME; + } CHK_STATUS(createSampleConfiguration(pChannelName, SIGNALING_CHANNEL_ROLE_TYPE_MASTER, TRUE, TRUE, logLevel, &pSampleConfiguration)); - + CHK_STATUS(setUpCredentialProvider(pSampleConfiguration, IOT_CORE_ENABLE_CREDENTIALS)); if (argc > 3) { if (!STRCMP(argv[3], AUDIO_CODEC_NAME_AAC)) { audioCodec = RTC_CODEC_AAC; @@ -139,23 +139,6 @@ INT32 main(INT32 argc, CHAR* argv[]) return STATUS_FAILED(retStatus) ? EXIT_FAILURE : EXIT_SUCCESS; } -STATUS readFrameFromDisk(PBYTE pFrame, PUINT32 pSize, PCHAR frameFilePath) -{ - STATUS retStatus = STATUS_SUCCESS; - UINT64 size = 0; - CHK_ERR(pSize != NULL, STATUS_NULL_ARG, "[KVS Master] Invalid file size"); - size = *pSize; - // Get the size and read into frame - CHK_STATUS(readFile(frameFilePath, TRUE, pFrame, &size)); -CleanUp: - - if (pSize != NULL) { - *pSize = (UINT32) size; - } - - return retStatus; -} - PVOID sendVideoPackets(PVOID args) { STATUS retStatus = STATUS_SUCCESS; diff --git a/samples/kvsWebRTCClientMasterGstSample.c b/samples/kvsWebRTCClientMasterGstSample.c index 6b60070d9f..3e3c837793 100644 --- a/samples/kvsWebRTCClientMasterGstSample.c +++ b/samples/kvsWebRTCClientMasterGstSample.c @@ -396,7 +396,7 @@ INT32 main(INT32 argc, CHAR* argv[]) #endif CHK_STATUS(createSampleConfiguration(pChannelName, SIGNALING_CHANNEL_ROLE_TYPE_MASTER, TRUE, TRUE, logLevel, &pSampleConfiguration)); - + CHK_STATUS(setUpCredentialProvider(pSampleConfiguration, IOT_CORE_ENABLE_CREDENTIALS)); if (argc > 3 && STRCMP(argv[3], "testsrc") == 0) { if (argc > 4) { if (!STRCMP(argv[4], AUDIO_CODEC_NAME_AAC)) { diff --git a/samples/kvsWebRTCClientViewer.c b/samples/kvsWebRTCClientViewer.c index bc60e12cad..f9d74f028e 100644 --- a/samples/kvsWebRTCClientViewer.c +++ b/samples/kvsWebRTCClientViewer.c @@ -82,6 +82,7 @@ INT32 main(INT32 argc, CHAR* argv[]) } CHK_STATUS(createSampleConfiguration(pChannelName, SIGNALING_CHANNEL_ROLE_TYPE_VIEWER, TRUE, TRUE, logLevel, &pSampleConfiguration)); + CHK_STATUS(setUpCredentialProvider(pSampleConfiguration, IOT_CORE_ENABLE_CREDENTIALS)); pSampleConfiguration->mediaType = SAMPLE_STREAMING_AUDIO_VIDEO; pSampleConfiguration->audioCodec = audioCodec; pSampleConfiguration->videoCodec = videoCodec; @@ -202,7 +203,8 @@ INT32 main(INT32 argc, CHAR* argv[]) } DLOGI("[KVS Viewer] Cleanup done"); - RESET_INSTRUMENTED_ALLOCATORS(); + retStatus = RESET_INSTRUMENTED_ALLOCATORS(); + DLOGI("All SDK allocations freed? %s..0x%08x", retStatus == STATUS_SUCCESS ? "Yes" : "No", retStatus); // https://www.gnu.org/software/libc/manual/html_node/Exit-Status.html // We can only return with 0 - 127. Some platforms treat exit code >= 128 diff --git a/samples/kvsWebRTCClientViewerGstSample.c b/samples/kvsWebRTCClientViewerGstSample.c index 18ac6625a4..f2a1130a3e 100644 --- a/samples/kvsWebRTCClientViewerGstSample.c +++ b/samples/kvsWebRTCClientViewerGstSample.c @@ -76,6 +76,7 @@ INT32 main(INT32 argc, CHAR* argv[]) } CHK_STATUS(createSampleConfiguration(pChannelName, SIGNALING_CHANNEL_ROLE_TYPE_VIEWER, TRUE, TRUE, logLevel, &pSampleConfiguration)); + CHK_STATUS(setUpCredentialProvider(pSampleConfiguration, IOT_CORE_ENABLE_CREDENTIALS)); pSampleConfiguration->mediaType = SAMPLE_STREAMING_AUDIO_VIDEO; pSampleConfiguration->receiveAudioVideoSource = receiveGstreamerAudioVideo; pSampleConfiguration->audioCodec = audioCodec; @@ -208,7 +209,8 @@ INT32 main(INT32 argc, CHAR* argv[]) } DLOGI("[KVS Gstreamer Viewer] Cleanup done"); - RESET_INSTRUMENTED_ALLOCATORS(); + retStatus = RESET_INSTRUMENTED_ALLOCATORS(); + DLOGI("All SDK allocations freed? %s..0x%08x", retStatus == STATUS_SUCCESS ? "Yes" : "No", retStatus); // https://www.gnu.org/software/libc/manual/html_node/Exit-Status.html // We can only return with 0 - 127. Some platforms treat exit code >= 128 diff --git a/samples/lib/Common.c b/samples/lib/Common.c new file mode 100644 index 0000000000..9075823f4f --- /dev/null +++ b/samples/lib/Common.c @@ -0,0 +1,981 @@ +#define LOG_CLASS "WebRtcSamples" +#include "../Samples.h" + +PSampleConfiguration gSampleConfiguration = NULL; + +STATUS terminate(UINT32 timerId, UINT64 currentTime, UINT64 customData) +{ + UNUSED_PARAM(timerId); + UNUSED_PARAM(currentTime); + UNUSED_PARAM(customData); + DLOGI("Terminating the app"); + if (gSampleConfiguration != NULL) { + ATOMIC_STORE_BOOL(&gSampleConfiguration->interrupted, TRUE); + ATOMIC_STORE_BOOL(&gSampleConfiguration->appTerminateFlag, TRUE); + CVAR_BROADCAST(gSampleConfiguration->cvar); + } + return STATUS_SUCCESS; +} + +VOID sigintHandler(INT32 sigNum) +{ + UNUSED_PARAM(sigNum); + if (gSampleConfiguration != NULL) { + ATOMIC_STORE_BOOL(&gSampleConfiguration->interrupted, TRUE); + CVAR_BROADCAST(gSampleConfiguration->cvar); + } +} + +STATUS signalingCallFailed(STATUS status) +{ + return (STATUS_SIGNALING_GET_TOKEN_CALL_FAILED == status || STATUS_SIGNALING_DESCRIBE_CALL_FAILED == status || + STATUS_SIGNALING_CREATE_CALL_FAILED == status || STATUS_SIGNALING_GET_ENDPOINT_CALL_FAILED == status || + STATUS_SIGNALING_GET_ICE_CONFIG_CALL_FAILED == status || STATUS_SIGNALING_CONNECT_CALL_FAILED == status || + STATUS_SIGNALING_DESCRIBE_MEDIA_CALL_FAILED == status); +} + +VOID onConnectionStateChange(UINT64 customData, RTC_PEER_CONNECTION_STATE newState) +{ + STATUS retStatus = STATUS_SUCCESS; + PSampleStreamingSession pSampleStreamingSession = (PSampleStreamingSession) customData; + CHK(pSampleStreamingSession != NULL && pSampleStreamingSession->pSampleConfiguration != NULL, STATUS_INTERNAL_ERROR); + + PSampleConfiguration pSampleConfiguration = pSampleStreamingSession->pSampleConfiguration; + DLOGI("New connection state %u", newState); + + switch (newState) { + case RTC_PEER_CONNECTION_STATE_CONNECTED: + ATOMIC_STORE_BOOL(&pSampleConfiguration->connected, TRUE); + CVAR_BROADCAST(pSampleConfiguration->cvar); + if (pSampleConfiguration->enableIceStats) { + CHK_LOG_ERR(logSelectedIceCandidatesInformation(pSampleStreamingSession)); + } + break; + case RTC_PEER_CONNECTION_STATE_FAILED: + // explicit fallthrough + case RTC_PEER_CONNECTION_STATE_CLOSED: + // explicit fallthrough + case RTC_PEER_CONNECTION_STATE_DISCONNECTED: + DLOGD("p2p connection disconnected"); + ATOMIC_STORE_BOOL(&pSampleStreamingSession->terminateFlag, TRUE); + CVAR_BROADCAST(pSampleConfiguration->cvar); + pSampleConfiguration->storageDisconnectedTime = GETTIME(); + // explicit fallthrough + default: + ATOMIC_STORE_BOOL(&pSampleConfiguration->connected, FALSE); + CVAR_BROADCAST(pSampleConfiguration->cvar); + + break; + } + +CleanUp: + + CHK_LOG_ERR(retStatus); +} + +STATUS signalingClientStateChanged(UINT64 customData, SIGNALING_CLIENT_STATE state) +{ + UNUSED_PARAM(customData); + STATUS retStatus = STATUS_SUCCESS; + PCHAR pStateStr; + + signalingClientGetStateString(state, &pStateStr); + + DLOGV("Signaling client state changed to %d - '%s'", state, pStateStr); + + // Return success to continue + return retStatus; +} + +STATUS signalingClientError(UINT64 customData, STATUS status, PCHAR msg, UINT32 msgLen) +{ + PSampleConfiguration pSampleConfiguration = (PSampleConfiguration) customData; + + DLOGW("Signaling client generated an error 0x%08x - '%.*s'", status, msgLen, msg); + + // We will force re-create the signaling client on the following errors + if (status == STATUS_SIGNALING_ICE_CONFIG_REFRESH_FAILED || status == STATUS_SIGNALING_RECONNECT_FAILED) { + ATOMIC_STORE_BOOL(&pSampleConfiguration->recreateSignalingClient, TRUE); + CVAR_BROADCAST(pSampleConfiguration->cvar); + } + + return STATUS_SUCCESS; +} + +BOOL sampleFilterNetworkInterfaces(UINT64 customData, PCHAR networkInt) +{ + UNUSED_PARAM(customData); + BOOL useInterface = FALSE; + if (STRNCMP(networkInt, (PCHAR) "eth0", ARRAY_SIZE("eth0")) == 0) { + useInterface = TRUE; + } + DLOGD("%s %s", networkInt, (useInterface) ? ("allowed. Candidates to be gathered") : ("blocked. Candidates will not be gathered")); + return useInterface; +} + +PVOID asyncGetIceConfigInfo(PVOID args) +{ + STATUS retStatus = STATUS_SUCCESS; + AsyncGetIceStruct* data = (AsyncGetIceStruct*) args; + PIceConfigInfo pIceConfigInfo = NULL; + UINT32 uriCount = 0; + UINT32 i = 0, maxTurnServer = 1; + + if (data != NULL) { + /* signalingClientGetIceConfigInfoCount can return more than one turn server. Use only one to optimize + * candidate gathering latency. But user can also choose to use more than 1 turn server. */ + for (uriCount = 0, i = 0; i < maxTurnServer; i++) { + /* + * if configuration.iceServers[uriCount + 1].urls is "turn:ip:port?transport=udp" then ICE will try TURN over UDP + * if configuration.iceServers[uriCount + 1].urls is "turn:ip:port?transport=tcp" then ICE will try TURN over TCP/TLS + * if configuration.iceServers[uriCount + 1].urls is "turns:ip:port?transport=udp", it's currently ignored because sdk dont do TURN + * over DTLS yet. if configuration.iceServers[uriCount + 1].urls is "turns:ip:port?transport=tcp" then ICE will try TURN over TCP/TLS + * if configuration.iceServers[uriCount + 1].urls is "turn:ip:port" then ICE will try both TURN over UDP and TCP/TLS + * + * It's recommended to not pass too many TURN iceServers to configuration because it will slow down ice gathering in non-trickle mode. + */ + CHK_STATUS(signalingClientGetIceConfigInfo(data->signalingClientHandle, i, &pIceConfigInfo)); + CHECK(uriCount < MAX_ICE_SERVERS_COUNT); + uriCount += pIceConfigInfo->uriCount; + CHK_STATUS(addConfigToServerList(&(data->pRtcPeerConnection), pIceConfigInfo)); + } + } + *(data->pUriCount) += uriCount; + +CleanUp: + SAFE_MEMFREE(data); + CHK_LOG_ERR(retStatus); + return NULL; +} + +STATUS initializePeerConnection(PSampleConfiguration pSampleConfiguration, PRtcPeerConnection* ppRtcPeerConnection) +{ + ENTERS(); + STATUS retStatus = STATUS_SUCCESS; + RtcConfiguration configuration; +#ifndef ENABLE_KVS_THREADPOOL + UINT32 i, j, maxTurnServer = 1; + PIceConfigInfo pIceConfigInfo; + UINT32 uriCount = 0; +#endif + UINT64 data; + PRtcCertificate pRtcCertificate = NULL; + + CHK(pSampleConfiguration != NULL && ppRtcPeerConnection != NULL, STATUS_NULL_ARG); + + MEMSET(&configuration, 0x00, SIZEOF(RtcConfiguration)); + + // Set this to custom callback to enable filtering of interfaces + configuration.kvsRtcConfiguration.iceSetInterfaceFilterFunc = NULL; + + // disable TWCC + configuration.kvsRtcConfiguration.disableSenderSideBandwidthEstimation = !(pSampleConfiguration->enableTwcc); + DLOGI("TWCC is : %s", configuration.kvsRtcConfiguration.disableSenderSideBandwidthEstimation ? "Disabled" : "Enabled"); + + // Set the ICE mode explicitly + + if (pSampleConfiguration->forceTurn) { + configuration.iceTransportPolicy = ICE_TRANSPORT_POLICY_RELAY; + } else { + configuration.iceTransportPolicy = ICE_TRANSPORT_POLICY_ALL; + } + + configuration.kvsRtcConfiguration.enableIceStats = pSampleConfiguration->enableIceStats; + // Set the STUN server + PCHAR pKinesisVideoStunUrlPostFix = KINESIS_VIDEO_STUN_URL_POSTFIX; + // If region is in CN, add CN region uri postfix + if (STRSTR(pSampleConfiguration->channelInfo.pRegion, "cn-")) { + pKinesisVideoStunUrlPostFix = KINESIS_VIDEO_STUN_URL_POSTFIX_CN; + } + SNPRINTF(configuration.iceServers[0].urls, MAX_ICE_CONFIG_URI_LEN, KINESIS_VIDEO_STUN_URL, pSampleConfiguration->channelInfo.pRegion, + pKinesisVideoStunUrlPostFix); + + // Check if we have any pregenerated certs and use them + // NOTE: We are running under the config lock + retStatus = stackQueueDequeue(pSampleConfiguration->pregeneratedCertificates, &data); + CHK(retStatus == STATUS_SUCCESS || retStatus == STATUS_NOT_FOUND, retStatus); + + if (retStatus == STATUS_NOT_FOUND) { + retStatus = STATUS_SUCCESS; + } else { + // Use the pre-generated cert and get rid of it to not reuse again + pRtcCertificate = (PRtcCertificate) data; + configuration.certificates[0] = *pRtcCertificate; + } + + CHK_STATUS(createPeerConnection(&configuration, ppRtcPeerConnection)); + + if (pSampleConfiguration->useTurn) { +#ifdef ENABLE_KVS_THREADPOOL + pSampleConfiguration->iceUriCount = 1; + AsyncGetIceStruct* pAsyncData = NULL; + + pAsyncData = (AsyncGetIceStruct*) MEMCALLOC(1, SIZEOF(AsyncGetIceStruct)); + pAsyncData->signalingClientHandle = pSampleConfiguration->signalingClientHandle; + pAsyncData->pRtcPeerConnection = *ppRtcPeerConnection; + pAsyncData->pUriCount = &(pSampleConfiguration->iceUriCount); + CHK_STATUS(peerConnectionAsync(asyncGetIceConfigInfo, (PVOID) pAsyncData)); +#else + + /* signalingClientGetIceConfigInfoCount can return more than one turn server. Use only one to optimize + * candidate gathering latency. But user can also choose to use more than 1 turn server. */ + for (uriCount = 0, i = 0; i < maxTurnServer; i++) { + /* + * if configuration.iceServers[uriCount + 1].urls is "turn:ip:port?transport=udp" then ICE will try TURN over UDP + * if configuration.iceServers[uriCount + 1].urls is "turn:ip:port?transport=tcp" then ICE will try TURN over TCP/TLS + * if configuration.iceServers[uriCount + 1].urls is "turns:ip:port?transport=udp", it's currently ignored because sdk dont do TURN + * over DTLS yet. if configuration.iceServers[uriCount + 1].urls is "turns:ip:port?transport=tcp" then ICE will try TURN over TCP/TLS + * if configuration.iceServers[uriCount + 1].urls is "turn:ip:port" then ICE will try both TURN over UDP and TCP/TLS + * + * It's recommended to not pass too many TURN iceServers to configuration because it will slow down ice gathering in non-trickle mode. + */ + CHK_STATUS(signalingClientGetIceConfigInfo(pSampleConfiguration->signalingClientHandle, i, &pIceConfigInfo)); + CHECK(uriCount < MAX_ICE_SERVERS_COUNT); + uriCount += pIceConfigInfo->uriCount; + CHK_STATUS(addConfigToServerList(ppRtcPeerConnection, pIceConfigInfo)); + } + pSampleConfiguration->iceUriCount = uriCount + 1; +#endif + } + +CleanUp: + + CHK_LOG_ERR(retStatus); + + // Free the certificate which can be NULL as we no longer need it and won't reuse + freeRtcCertificate(pRtcCertificate); + + LEAVES(); + return retStatus; +} + +STATUS createSampleStreamingSession(PSampleConfiguration pSampleConfiguration, PCHAR peerId, BOOL isMaster, + PSampleStreamingSession* ppSampleStreamingSession) +{ + STATUS retStatus = STATUS_SUCCESS; + RtcMediaStreamTrack videoTrack, audioTrack; + PSampleStreamingSession pSampleStreamingSession = NULL; + RtcRtpTransceiverInit audioRtpTransceiverInit; + RtcRtpTransceiverInit videoRtpTransceiverInit; + + MEMSET(&videoTrack, 0x00, SIZEOF(RtcMediaStreamTrack)); + MEMSET(&audioTrack, 0x00, SIZEOF(RtcMediaStreamTrack)); + + CHK(pSampleConfiguration != NULL && ppSampleStreamingSession != NULL, STATUS_NULL_ARG); + CHK((isMaster && peerId != NULL) || !isMaster, STATUS_INVALID_ARG); + + pSampleStreamingSession = (PSampleStreamingSession) MEMCALLOC(1, SIZEOF(SampleStreamingSession)); + pSampleStreamingSession->firstFrame = TRUE; + pSampleStreamingSession->offerReceiveTime = GETTIME(); + CHK(pSampleStreamingSession != NULL, STATUS_NOT_ENOUGH_MEMORY); + + if (isMaster) { + STRCPY(pSampleStreamingSession->peerId, peerId); + } else { + STRCPY(pSampleStreamingSession->peerId, SAMPLE_VIEWER_CLIENT_ID); + } + ATOMIC_STORE_BOOL(&pSampleStreamingSession->peerIdReceived, TRUE); + + pSampleStreamingSession->pAudioRtcRtpTransceiver = NULL; + pSampleStreamingSession->pVideoRtcRtpTransceiver = NULL; + + pSampleStreamingSession->pSampleConfiguration = pSampleConfiguration; + pSampleStreamingSession->rtpMetricsHistory.prevTs = GETTIME(); + + // if we're the viewer, we control the trickle ice mode + pSampleStreamingSession->remoteCanTrickleIce = !isMaster && pSampleConfiguration->trickleIce; + + ATOMIC_STORE_BOOL(&pSampleStreamingSession->terminateFlag, FALSE); + ATOMIC_STORE_BOOL(&pSampleStreamingSession->candidateGatheringDone, FALSE); + if (pSampleConfiguration->enableTwcc) { + pSampleStreamingSession->twccMetadata.updateLock = MUTEX_CREATE(TRUE); + } + + if (pSampleConfiguration->enableMetrics) { + DLOGI("Setting up metrics context"); + CHK_STATUS(setupMetricsCtx(pSampleStreamingSession)); + } + + pSampleStreamingSession->peerConnectionMetrics.peerConnectionStats.peerConnectionStartTime = GETTIME() / HUNDREDS_OF_NANOS_IN_A_MILLISECOND; + + CHK_STATUS(initializePeerConnection(pSampleConfiguration, &pSampleStreamingSession->pPeerConnection)); + CHK_STATUS(peerConnectionOnIceCandidate(pSampleStreamingSession->pPeerConnection, (UINT64) pSampleStreamingSession, onIceCandidateHandler)); + CHK_STATUS( + peerConnectionOnConnectionStateChange(pSampleStreamingSession->pPeerConnection, (UINT64) pSampleStreamingSession, onConnectionStateChange)); + +#ifdef ENABLE_DATA_CHANNEL + if (pSampleConfiguration->onDataChannel != NULL) { + CHK_STATUS(peerConnectionOnDataChannel(pSampleStreamingSession->pPeerConnection, (UINT64) pSampleStreamingSession, + pSampleConfiguration->onDataChannel)); + } +#endif + + CHK_STATUS(addSupportedCodec(pSampleStreamingSession->pPeerConnection, pSampleConfiguration->videoCodec)); + CHK_STATUS(addSupportedCodec(pSampleStreamingSession->pPeerConnection, pSampleConfiguration->audioCodec)); + + // Add a SendRecv Transceiver of type video + videoTrack.kind = MEDIA_STREAM_TRACK_KIND_VIDEO; + videoTrack.codec = pSampleConfiguration->videoCodec; + videoRtpTransceiverInit.direction = RTC_RTP_TRANSCEIVER_DIRECTION_SENDRECV; + videoRtpTransceiverInit.rollingBufferDurationSec = 3; + // Considering 4 Mbps for 720p (which is what our samples use). This is for H.264. + // The value could be different for other codecs. + videoRtpTransceiverInit.rollingBufferBitratebps = 4 * 1024 * 1024; + STRCPY(videoTrack.streamId, "myKvsVideoStream"); + STRCPY(videoTrack.trackId, "myVideoTrack"); + CHK_STATUS(addTransceiver(pSampleStreamingSession->pPeerConnection, &videoTrack, &videoRtpTransceiverInit, + &pSampleStreamingSession->pVideoRtcRtpTransceiver)); + + CHK_STATUS(transceiverOnBandwidthEstimation(pSampleStreamingSession->pVideoRtcRtpTransceiver, (UINT64) pSampleStreamingSession, + sampleBandwidthEstimationHandler)); + + // Add a SendRecv Transceiver of type audio + audioTrack.kind = MEDIA_STREAM_TRACK_KIND_AUDIO; + audioTrack.codec = pSampleConfiguration->audioCodec; + audioRtpTransceiverInit.direction = RTC_RTP_TRANSCEIVER_DIRECTION_SENDRECV; + audioRtpTransceiverInit.rollingBufferDurationSec = 3; + // For opus, the bitrate could be between 6 Kbps to 510 Kbps + audioRtpTransceiverInit.rollingBufferBitratebps = 510 * 1024; + STRCPY(audioTrack.streamId, "myKvsVideoStream"); + STRCPY(audioTrack.trackId, "myAudioTrack"); + CHK_STATUS(addTransceiver(pSampleStreamingSession->pPeerConnection, &audioTrack, &audioRtpTransceiverInit, + &pSampleStreamingSession->pAudioRtcRtpTransceiver)); + + CHK_STATUS(transceiverOnBandwidthEstimation(pSampleStreamingSession->pAudioRtcRtpTransceiver, (UINT64) pSampleStreamingSession, + sampleBandwidthEstimationHandler)); + // twcc bandwidth estimation + if (pSampleConfiguration->enableTwcc) { + CHK_STATUS(peerConnectionOnSenderBandwidthEstimation(pSampleStreamingSession->pPeerConnection, (UINT64) pSampleStreamingSession, + sampleSenderBandwidthEstimationHandler)); + } + pSampleStreamingSession->startUpLatency = 0; + +CleanUp: + + if (STATUS_FAILED(retStatus) && pSampleStreamingSession != NULL) { + freeSampleStreamingSession(&pSampleStreamingSession); + pSampleStreamingSession = NULL; + } + + if (ppSampleStreamingSession != NULL) { + *ppSampleStreamingSession = pSampleStreamingSession; + } + + return retStatus; +} + +STATUS freeSampleStreamingSession(PSampleStreamingSession* ppSampleStreamingSession) +{ + STATUS retStatus = STATUS_SUCCESS; + PSampleStreamingSession pSampleStreamingSession = NULL; + PSampleConfiguration pSampleConfiguration; + + CHK(ppSampleStreamingSession != NULL, STATUS_NULL_ARG); + pSampleStreamingSession = *ppSampleStreamingSession; + CHK(pSampleStreamingSession != NULL && pSampleStreamingSession->pSampleConfiguration != NULL, retStatus); + pSampleConfiguration = pSampleStreamingSession->pSampleConfiguration; + + DLOGD("Freeing streaming session with peer id: %s ", pSampleStreamingSession->peerId); + + ATOMIC_STORE_BOOL(&pSampleStreamingSession->terminateFlag, TRUE); + + if (pSampleStreamingSession->shutdownCallback != NULL) { + pSampleStreamingSession->shutdownCallback(pSampleStreamingSession->shutdownCallbackCustomData, pSampleStreamingSession); + } + + if (IS_VALID_TID_VALUE(pSampleStreamingSession->receiveAudioVideoSenderTid)) { + THREAD_JOIN(pSampleStreamingSession->receiveAudioVideoSenderTid, NULL); + } + + // De-initialize the session stats timer if there are no active sessions + // NOTE: we need to perform this under the lock which might be acquired by + // the running thread but it's OK as it's re-entrant + MUTEX_LOCK(pSampleConfiguration->sampleConfigurationObjLock); + if (pSampleConfiguration->iceCandidatePairStatsTimerId != MAX_UINT32 && pSampleConfiguration->streamingSessionCount == 0 && + IS_VALID_TIMER_QUEUE_HANDLE(pSampleConfiguration->timerQueueHandle)) { + CHK_LOG_ERR(timerQueueCancelTimer(pSampleConfiguration->timerQueueHandle, pSampleConfiguration->iceCandidatePairStatsTimerId, + (UINT64) pSampleConfiguration)); + pSampleConfiguration->iceCandidatePairStatsTimerId = MAX_UINT32; + } + MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); + + if (pSampleConfiguration->enableTwcc) { + if (IS_VALID_MUTEX_VALUE(pSampleStreamingSession->twccMetadata.updateLock)) { + MUTEX_FREE(pSampleStreamingSession->twccMetadata.updateLock); + } + } + + if (pSampleConfiguration->enableIceStats) { + DLOGI("ICE Stats enabled"); + CHK_LOG_ERR(gatherIceServerStats(pSampleStreamingSession)); + } + if (pSampleConfiguration->enableMetrics) { + CHK_LOG_ERR(freeMetricsCtx(&pSampleStreamingSession->pStatsCtx)); + } + CHK_LOG_ERR(closePeerConnection(pSampleStreamingSession->pPeerConnection)); + CHK_LOG_ERR(freePeerConnection(&pSampleStreamingSession->pPeerConnection)); + SAFE_MEMFREE(pSampleStreamingSession); + +CleanUp: + + CHK_LOG_ERR(retStatus); + + return retStatus; +} + +STATUS streamingSessionOnShutdown(PSampleStreamingSession pSampleStreamingSession, UINT64 customData, + StreamSessionShutdownCallback streamSessionShutdownCallback) +{ + STATUS retStatus = STATUS_SUCCESS; + + CHK(pSampleStreamingSession != NULL && streamSessionShutdownCallback != NULL, STATUS_NULL_ARG); + + pSampleStreamingSession->shutdownCallbackCustomData = customData; + pSampleStreamingSession->shutdownCallback = streamSessionShutdownCallback; + +CleanUp: + + return retStatus; +} + +VOID sampleVideoFrameHandler(UINT64 customData, PFrame pFrame) +{ + UNUSED_PARAM(customData); + DLOGV("Video Frame received. TrackId: %" PRIu64 ", Size: %u, Flags %u", pFrame->trackId, pFrame->size, pFrame->flags); +} + +VOID sampleAudioFrameHandler(UINT64 customData, PFrame pFrame) +{ + UNUSED_PARAM(customData); + DLOGV("Audio Frame received. TrackId: %" PRIu64 ", Size: %u, Flags %u", pFrame->trackId, pFrame->size, pFrame->flags); +} + +VOID sampleFrameHandler(UINT64 customData, PFrame pFrame) +{ + UNUSED_PARAM(customData); + DLOGV("Video Frame received. TrackId: %" PRIu64 ", Size: %u, Flags %u", pFrame->trackId, pFrame->size, pFrame->flags); +} + +VOID sampleBandwidthEstimationHandler(UINT64 customData, DOUBLE maximumBitrate) +{ + UNUSED_PARAM(customData); + DLOGV("received bitrate suggestion: %f", maximumBitrate); +} + +// Sample callback for TWCC. Average packet is calculated with exponential moving average (EMA). If average packet lost is <= 5%, +// the current bitrate is increased by 5%. If more than 5%, the current bitrate +// is reduced by percent lost. Bitrate update is allowed every second and is increased/decreased upto the limits +VOID sampleSenderBandwidthEstimationHandler(UINT64 customData, UINT32 txBytes, UINT32 rxBytes, UINT32 txPacketsCnt, UINT32 rxPacketsCnt, + UINT64 duration) +{ + UNUSED_PARAM(duration); + UINT64 videoBitrate, audioBitrate; + UINT64 currentTimeMs, timeDiff; + UINT32 lostPacketsCnt = txPacketsCnt - rxPacketsCnt; + DOUBLE percentLost = (DOUBLE) ((txPacketsCnt > 0) ? (lostPacketsCnt * 100 / txPacketsCnt) : 0.0); + SampleStreamingSession* pSampleStreamingSession = (SampleStreamingSession*) customData; + + if (pSampleStreamingSession == NULL) { + DLOGW("Invalid streaming session (NULL object)"); + return; + } + + // Calculate packet loss + pSampleStreamingSession->twccMetadata.averagePacketLoss = + EMA_ACCUMULATOR_GET_NEXT(pSampleStreamingSession->twccMetadata.averagePacketLoss, ((DOUBLE) percentLost)); + + currentTimeMs = GETTIME(); + timeDiff = currentTimeMs - pSampleStreamingSession->twccMetadata.lastAdjustmentTimeMs; + if (timeDiff < TWCC_BITRATE_ADJUSTMENT_INTERVAL_MS) { + // Too soon for another adjustment + return; + } + + MUTEX_LOCK(pSampleStreamingSession->twccMetadata.updateLock); + videoBitrate = pSampleStreamingSession->twccMetadata.currentVideoBitrate; + audioBitrate = pSampleStreamingSession->twccMetadata.currentAudioBitrate; + + if (pSampleStreamingSession->twccMetadata.averagePacketLoss <= 5) { + // increase encoder bitrate by 5 percent with a cap at MAX_BITRATE + videoBitrate = (UINT64) MIN(videoBitrate * 1.05, MAX_VIDEO_BITRATE_KBPS); + // increase encoder bitrate by 5 percent with a cap at MAX_BITRATE + audioBitrate = (UINT64) MIN(audioBitrate * 1.05, MAX_AUDIO_BITRATE_BPS); + } else { + // decrease encoder bitrate by average packet loss percent, with a cap at MIN_BITRATE + videoBitrate = (UINT64) MAX(videoBitrate * (1.0 - pSampleStreamingSession->twccMetadata.averagePacketLoss / 100.0), MIN_VIDEO_BITRATE_KBPS); + // decrease encoder bitrate by average packet loss percent, with a cap at MIN_BITRATE + audioBitrate = (UINT64) MAX(audioBitrate * (1.0 - pSampleStreamingSession->twccMetadata.averagePacketLoss / 100.0), MIN_AUDIO_BITRATE_BPS); + } + + // Update the session with the new bitrate and adjustment time + pSampleStreamingSession->twccMetadata.newVideoBitrate = videoBitrate; + pSampleStreamingSession->twccMetadata.newAudioBitrate = audioBitrate; + MUTEX_UNLOCK(pSampleStreamingSession->twccMetadata.updateLock); + + pSampleStreamingSession->twccMetadata.lastAdjustmentTimeMs = currentTimeMs; + + DLOGI("Adjustment made: average packet loss = %.2f%%, timediff: %llu ms", pSampleStreamingSession->twccMetadata.averagePacketLoss, timeDiff); + DLOGI("Suggested video bitrate %u kbps, suggested audio bitrate: %u bps, sent: %u bytes %u packets received: %u bytes %u packets in %lu msec", + videoBitrate, audioBitrate, txBytes, txPacketsCnt, rxBytes, rxPacketsCnt, duration / 10000ULL); +} + +STATUS createSampleConfiguration(PCHAR channelName, SIGNALING_CHANNEL_ROLE_TYPE roleType, BOOL trickleIce, BOOL useTurn, UINT32 logLevel, + PSampleConfiguration* ppSampleConfiguration) +{ + STATUS retStatus = STATUS_SUCCESS; + PSampleConfiguration pSampleConfiguration = NULL; + + CHK(ppSampleConfiguration != NULL, STATUS_NULL_ARG); + + CHK(NULL != (pSampleConfiguration = (PSampleConfiguration) MEMCALLOC(1, SIZEOF(SampleConfiguration))), STATUS_NOT_ENOUGH_MEMORY); + + // If the env is set, we generate normal log files apart from filtered profile log files + // If not set, we generate only the filtered profile log files + if (NULL != GETENV(ENABLE_FILE_LOGGING)) { + retStatus = createFileLoggerWithLevelFiltering(FILE_LOGGING_BUFFER_SIZE, MAX_NUMBER_OF_LOG_FILES, (PCHAR) FILE_LOGGER_LOG_FILE_DIRECTORY_PATH, + TRUE, TRUE, TRUE, LOG_LEVEL_PROFILE, NULL); + + if (retStatus != STATUS_SUCCESS) { + DLOGW("[KVS Master] createFileLogger(): operation returned status code: 0x%08x", retStatus); + } else { + pSampleConfiguration->enableFileLogging = TRUE; + } + } else { + retStatus = createFileLoggerWithLevelFiltering(FILE_LOGGING_BUFFER_SIZE, MAX_NUMBER_OF_LOG_FILES, (PCHAR) FILE_LOGGER_LOG_FILE_DIRECTORY_PATH, + TRUE, TRUE, FALSE, LOG_LEVEL_PROFILE, NULL); + + if (retStatus != STATUS_SUCCESS) { + DLOGW("[KVS Master] createFileLogger(): operation returned status code: 0x%08x", retStatus); + } else { + pSampleConfiguration->enableFileLogging = TRUE; + } + } + + if ((pSampleConfiguration->channelInfo.pRegion = GETENV(DEFAULT_REGION_ENV_VAR)) == NULL) { + pSampleConfiguration->channelInfo.pRegion = DEFAULT_AWS_REGION; + } + + pSampleConfiguration->mediaSenderTid = INVALID_TID_VALUE; + pSampleConfiguration->audioSenderTid = INVALID_TID_VALUE; + pSampleConfiguration->videoSenderTid = INVALID_TID_VALUE; + pSampleConfiguration->signalingClientHandle = INVALID_SIGNALING_CLIENT_HANDLE_VALUE; + pSampleConfiguration->sampleConfigurationObjLock = MUTEX_CREATE(TRUE); + pSampleConfiguration->cvar = CVAR_CREATE(); + pSampleConfiguration->streamingSessionListReadLock = MUTEX_CREATE(FALSE); + pSampleConfiguration->signalingSendMessageLock = MUTEX_CREATE(FALSE); + pSampleConfiguration->forceTurn = FALSE; + + /* This is ignored for master. Master can extract the info from offer. Viewer has to know if peer can trickle or + * not ahead of time. */ + + pSampleConfiguration->trickleIce = trickleIce; + pSampleConfiguration->useTurn = useTurn; + pSampleConfiguration->enableSendingMetricsToViewerViaDc = FALSE; + pSampleConfiguration->receiveAudioVideoSource = NULL; + + pSampleConfiguration->channelInfo.version = CHANNEL_INFO_CURRENT_VERSION; + DLOGI("Channel name: %s", channelName); + pSampleConfiguration->channelInfo.pChannelName = channelName; + + pSampleConfiguration->channelInfo.pKmsKeyId = NULL; + pSampleConfiguration->channelInfo.tagCount = 0; + pSampleConfiguration->channelInfo.pTags = NULL; + pSampleConfiguration->channelInfo.channelType = SIGNALING_CHANNEL_TYPE_SINGLE_MASTER; + pSampleConfiguration->channelInfo.channelRoleType = roleType; + pSampleConfiguration->channelInfo.cachingPolicy = SIGNALING_API_CALL_CACHE_TYPE_FILE; + pSampleConfiguration->channelInfo.cachingPeriod = SIGNALING_API_CALL_CACHE_TTL_SENTINEL_VALUE; + pSampleConfiguration->channelInfo.asyncIceServerConfig = TRUE; // has no effect + pSampleConfiguration->channelInfo.retry = TRUE; + pSampleConfiguration->channelInfo.reconnect = TRUE; + pSampleConfiguration->channelInfo.messageTtl = 0; // Default is 60 seconds + + pSampleConfiguration->signalingClientCallbacks.version = SIGNALING_CLIENT_CALLBACKS_CURRENT_VERSION; + pSampleConfiguration->signalingClientCallbacks.errorReportFn = signalingClientError; + pSampleConfiguration->signalingClientCallbacks.stateChangeFn = signalingClientStateChanged; + pSampleConfiguration->signalingClientCallbacks.customData = (UINT64) pSampleConfiguration; + + pSampleConfiguration->clientInfo.version = SIGNALING_CLIENT_INFO_CURRENT_VERSION; + pSampleConfiguration->clientInfo.loggingLevel = logLevel; + pSampleConfiguration->clientInfo.cacheFilePath = NULL; // Use the default path + pSampleConfiguration->clientInfo.signalingClientCreationMaxRetryAttempts = CREATE_SIGNALING_CLIENT_RETRY_ATTEMPTS_SENTINEL_VALUE; + pSampleConfiguration->iceCandidatePairStatsTimerId = MAX_UINT32; + pSampleConfiguration->pregenerateCertTimerId = MAX_UINT32; + pSampleConfiguration->signalingClientMetrics.version = SIGNALING_CLIENT_METRICS_CURRENT_VERSION; + + // Flag to enable SDK to calculate selected ice server, local, remote and candidate pair stats. + pSampleConfiguration->enableIceStats = FALSE; + + // Flag to enable/disable TWCC + pSampleConfiguration->enableTwcc = TRUE; + + ATOMIC_STORE_BOOL(&pSampleConfiguration->interrupted, FALSE); + ATOMIC_STORE_BOOL(&pSampleConfiguration->mediaThreadStarted, FALSE); + ATOMIC_STORE_BOOL(&pSampleConfiguration->appTerminateFlag, FALSE); + ATOMIC_STORE_BOOL(&pSampleConfiguration->recreateSignalingClient, FALSE); + ATOMIC_STORE_BOOL(&pSampleConfiguration->connected, FALSE); + + CHK_STATUS(timerQueueCreate(&pSampleConfiguration->timerQueueHandle)); + + CHK_STATUS(stackQueueCreate(&pSampleConfiguration->pregeneratedCertificates)); + + // Start the cert pre-gen timer callback +#ifdef SAMPLE_PRE_GENERATE_CERT + CHK_LOG_ERR(retStatus = + timerQueueAddTimer(pSampleConfiguration->timerQueueHandle, 0, SAMPLE_PRE_GENERATE_CERT_PERIOD, pregenerateCertTimerCallback, + (UINT64) pSampleConfiguration, &pSampleConfiguration->pregenerateCertTimerId)); +#endif + + pSampleConfiguration->iceUriCount = 0; + + CHK_STATUS(stackQueueCreate(&pSampleConfiguration->pPendingSignalingMessageForRemoteClient)); + CHK_STATUS(hashTableCreateWithParams(SAMPLE_HASH_TABLE_BUCKET_COUNT, SAMPLE_HASH_TABLE_BUCKET_LENGTH, + &pSampleConfiguration->pRtcPeerConnectionForRemoteClient)); +CleanUp: + + if (STATUS_FAILED(retStatus)) { + freeSampleConfiguration(&pSampleConfiguration); + } + + if (ppSampleConfiguration != NULL) { + *ppSampleConfiguration = pSampleConfiguration; + } + + return retStatus; +} + +STATUS initSignaling(PSampleConfiguration pSampleConfiguration, PCHAR clientId) +{ + STATUS retStatus = STATUS_SUCCESS; + SignalingClientMetrics signalingClientMetrics = pSampleConfiguration->signalingClientMetrics; + pSampleConfiguration->signalingClientCallbacks.messageReceivedFn = signalingMessageReceived; + STRCPY(pSampleConfiguration->clientInfo.clientId, clientId); + CHK_STATUS(createSignalingClientSync(&pSampleConfiguration->clientInfo, &pSampleConfiguration->channelInfo, + &pSampleConfiguration->signalingClientCallbacks, pSampleConfiguration->pCredentialProvider, + &pSampleConfiguration->signalingClientHandle)); + + // Enable the processing of the messages + CHK_STATUS(signalingClientFetchSync(pSampleConfiguration->signalingClientHandle)); + +#ifdef ENABLE_DATA_CHANNEL + pSampleConfiguration->onDataChannel = onDataChannel; +#endif + + CHK_STATUS(signalingClientConnectSync(pSampleConfiguration->signalingClientHandle)); + + signalingClientGetMetrics(pSampleConfiguration->signalingClientHandle, &signalingClientMetrics); + + // Logging this here since the logs in signaling library do not get routed to file + DLOGP("[Signaling Get token] %" PRIu64 " ms", signalingClientMetrics.signalingClientStats.getTokenCallTime); + DLOGP("[Signaling Describe] %" PRIu64 " ms", signalingClientMetrics.signalingClientStats.describeCallTime); + DLOGP("[Signaling Describe Media] %" PRIu64 " ms", signalingClientMetrics.signalingClientStats.describeMediaCallTime); + DLOGP("[Signaling Create Channel] %" PRIu64 " ms", signalingClientMetrics.signalingClientStats.createCallTime); + DLOGP("[Signaling Get endpoint] %" PRIu64 " ms", signalingClientMetrics.signalingClientStats.getEndpointCallTime); + DLOGP("[Signaling Get ICE config] %" PRIu64 " ms", signalingClientMetrics.signalingClientStats.getIceConfigCallTime); + DLOGP("[Signaling Connect] %" PRIu64 " ms", signalingClientMetrics.signalingClientStats.connectCallTime); + if (signalingClientMetrics.signalingClientStats.joinSessionCallTime != 0) { + DLOGP("[Signaling Join Session] %" PRIu64 " ms", signalingClientMetrics.signalingClientStats.joinSessionCallTime); + } + DLOGP("[Signaling create client] %" PRIu64 " ms", signalingClientMetrics.signalingClientStats.createClientTime); + DLOGP("[Signaling fetch client] %" PRIu64 " ms", signalingClientMetrics.signalingClientStats.fetchClientTime); + DLOGP("[Signaling connect client] %" PRIu64 " ms", signalingClientMetrics.signalingClientStats.connectClientTime); + pSampleConfiguration->signalingClientMetrics = signalingClientMetrics; + gSampleConfiguration = pSampleConfiguration; +CleanUp: + return retStatus; +} + +STATUS logSignalingClientStats(PSignalingClientMetrics pSignalingClientMetrics) +{ + ENTERS(); + STATUS retStatus = STATUS_SUCCESS; + CHK(pSignalingClientMetrics != NULL, STATUS_NULL_ARG); + DLOGD("Signaling client connection duration: %" PRIu64 " ms", + (pSignalingClientMetrics->signalingClientStats.connectionDuration / HUNDREDS_OF_NANOS_IN_A_MILLISECOND)); + DLOGD("Number of signaling client API errors: %d", pSignalingClientMetrics->signalingClientStats.numberOfErrors); + DLOGD("Number of runtime errors in the session: %d", pSignalingClientMetrics->signalingClientStats.numberOfRuntimeErrors); + DLOGD("Signaling client uptime: %" PRIu64 " ms", + (pSignalingClientMetrics->signalingClientStats.connectionDuration / HUNDREDS_OF_NANOS_IN_A_MILLISECOND)); + // This gives the EMA of the createChannel, describeChannel, getChannelEndpoint and deleteChannel calls + DLOGD("Control Plane API call latency: %" PRIu64 " ms", + (pSignalingClientMetrics->signalingClientStats.cpApiCallLatency / HUNDREDS_OF_NANOS_IN_A_MILLISECOND)); + // This gives the EMA of the getIceConfig() call. + DLOGD("Data Plane API call latency: %" PRIu64 " ms", + (pSignalingClientMetrics->signalingClientStats.dpApiCallLatency / HUNDREDS_OF_NANOS_IN_A_MILLISECOND)); + DLOGD("API call retry count: %d", pSignalingClientMetrics->signalingClientStats.apiCallRetryCount); +CleanUp: + LEAVES(); + return retStatus; +} + +STATUS pregenerateCertTimerCallback(UINT32 timerId, UINT64 currentTime, UINT64 customData) +{ + UNUSED_PARAM(timerId); + UNUSED_PARAM(currentTime); + STATUS retStatus = STATUS_SUCCESS; + PSampleConfiguration pSampleConfiguration = (PSampleConfiguration) customData; + BOOL locked = FALSE; + UINT32 certCount; + PRtcCertificate pRtcCertificate = NULL; + + CHK_WARN(pSampleConfiguration != NULL, STATUS_NULL_ARG, "[KVS Master] pregenerateCertTimerCallback(): Passed argument is NULL"); + + // Use MUTEX_TRYLOCK to avoid possible dead lock when canceling timerQueue + if (!MUTEX_TRYLOCK(pSampleConfiguration->sampleConfigurationObjLock)) { + return retStatus; + } else { + locked = TRUE; + } + + // Quick check if there is anything that needs to be done. + CHK_STATUS(stackQueueGetCount(pSampleConfiguration->pregeneratedCertificates, &certCount)); + CHK(certCount != MAX_RTCCONFIGURATION_CERTIFICATES, retStatus); + + // Generate the certificate with the keypair + CHK_STATUS(createRtcCertificate(&pRtcCertificate)); + + // Add to the stack queue + CHK_STATUS(stackQueueEnqueue(pSampleConfiguration->pregeneratedCertificates, (UINT64) pRtcCertificate)); + + DLOGV("New certificate has been pre-generated and added to the queue"); + + // Reset it so it won't be freed on exit + pRtcCertificate = NULL; + + MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); + locked = FALSE; + +CleanUp: + + if (pRtcCertificate != NULL) { + freeRtcCertificate(pRtcCertificate); + } + + if (locked) { + MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); + } + + return retStatus; +} + +STATUS freeSampleConfiguration(PSampleConfiguration* ppSampleConfiguration) +{ + ENTERS(); + STATUS retStatus = STATUS_SUCCESS; + PSampleConfiguration pSampleConfiguration; + UINT32 i; + UINT64 data; + StackQueueIterator iterator; + BOOL locked = FALSE; + + CHK(ppSampleConfiguration != NULL, STATUS_NULL_ARG); + pSampleConfiguration = *ppSampleConfiguration; + + CHK(pSampleConfiguration != NULL, retStatus); + if (IS_VALID_TIMER_QUEUE_HANDLE(pSampleConfiguration->timerQueueHandle)) { + if (pSampleConfiguration->iceCandidatePairStatsTimerId != MAX_UINT32) { + retStatus = timerQueueCancelTimer(pSampleConfiguration->timerQueueHandle, pSampleConfiguration->iceCandidatePairStatsTimerId, + (UINT64) pSampleConfiguration); + if (STATUS_FAILED(retStatus)) { + DLOGE("Failed to cancel stats timer with: 0x%08x", retStatus); + } + pSampleConfiguration->iceCandidatePairStatsTimerId = MAX_UINT32; + } + + if (pSampleConfiguration->pregenerateCertTimerId != MAX_UINT32) { + retStatus = timerQueueCancelTimer(pSampleConfiguration->timerQueueHandle, pSampleConfiguration->pregenerateCertTimerId, + (UINT64) pSampleConfiguration); + if (STATUS_FAILED(retStatus)) { + DLOGE("Failed to cancel certificate pre-generation timer with: 0x%08x", retStatus); + } + pSampleConfiguration->pregenerateCertTimerId = MAX_UINT32; + } + + timerQueueFree(&pSampleConfiguration->timerQueueHandle); + } + + if (pSampleConfiguration->pPendingSignalingMessageForRemoteClient != NULL) { + // Iterate and free all the pending queues + stackQueueGetIterator(pSampleConfiguration->pPendingSignalingMessageForRemoteClient, &iterator); + while (IS_VALID_ITERATOR(iterator)) { + stackQueueIteratorGetItem(iterator, &data); + stackQueueIteratorNext(&iterator); + freeMessageQueue((PPendingMessageQueue) data); + } + + stackQueueClear(pSampleConfiguration->pPendingSignalingMessageForRemoteClient, FALSE); + stackQueueFree(pSampleConfiguration->pPendingSignalingMessageForRemoteClient); + pSampleConfiguration->pPendingSignalingMessageForRemoteClient = NULL; + } + + hashTableClear(pSampleConfiguration->pRtcPeerConnectionForRemoteClient); + hashTableFree(pSampleConfiguration->pRtcPeerConnectionForRemoteClient); + + if (IS_VALID_MUTEX_VALUE(pSampleConfiguration->sampleConfigurationObjLock)) { + MUTEX_LOCK(pSampleConfiguration->sampleConfigurationObjLock); + locked = TRUE; + } + + for (i = 0; i < pSampleConfiguration->streamingSessionCount; ++i) { + freeSampleStreamingSession(&pSampleConfiguration->sampleStreamingSessionList[i]); + } + if (locked) { + MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); + } + deinitKvsWebRtc(); + + SAFE_MEMFREE(pSampleConfiguration->pVideoFrameBuffer); + SAFE_MEMFREE(pSampleConfiguration->pAudioFrameBuffer); + + if (IS_VALID_CVAR_VALUE(pSampleConfiguration->cvar) && IS_VALID_MUTEX_VALUE(pSampleConfiguration->sampleConfigurationObjLock)) { + CVAR_BROADCAST(pSampleConfiguration->cvar); + MUTEX_LOCK(pSampleConfiguration->sampleConfigurationObjLock); + MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); + } + + if (IS_VALID_MUTEX_VALUE(pSampleConfiguration->sampleConfigurationObjLock)) { + MUTEX_FREE(pSampleConfiguration->sampleConfigurationObjLock); + } + + if (IS_VALID_MUTEX_VALUE(pSampleConfiguration->streamingSessionListReadLock)) { + MUTEX_FREE(pSampleConfiguration->streamingSessionListReadLock); + } + + if (IS_VALID_MUTEX_VALUE(pSampleConfiguration->signalingSendMessageLock)) { + MUTEX_FREE(pSampleConfiguration->signalingSendMessageLock); + } + + if (IS_VALID_CVAR_VALUE(pSampleConfiguration->cvar)) { + CVAR_FREE(pSampleConfiguration->cvar); + } + + if (pSampleConfiguration->useIot) { + freeIotCredentialProvider(&pSampleConfiguration->pCredentialProvider); + } else { + freeStaticCredentialProvider(&pSampleConfiguration->pCredentialProvider); + } + + if (pSampleConfiguration->pregeneratedCertificates != NULL) { + stackQueueGetIterator(pSampleConfiguration->pregeneratedCertificates, &iterator); + while (IS_VALID_ITERATOR(iterator)) { + stackQueueIteratorGetItem(iterator, &data); + stackQueueIteratorNext(&iterator); + freeRtcCertificate((PRtcCertificate) data); + } + + CHK_LOG_ERR(stackQueueClear(pSampleConfiguration->pregeneratedCertificates, FALSE)); + CHK_LOG_ERR(stackQueueFree(pSampleConfiguration->pregeneratedCertificates)); + pSampleConfiguration->pregeneratedCertificates = NULL; + } + if (pSampleConfiguration->enableFileLogging) { + freeFileLogger(); + } + SAFE_MEMFREE(*ppSampleConfiguration); + +CleanUp: + + LEAVES(); + return retStatus; +} + +STATUS sessionCleanupWait(PSampleConfiguration pSampleConfiguration) +{ + ENTERS(); + STATUS retStatus = STATUS_SUCCESS; + PSampleStreamingSession pSampleStreamingSession = NULL; + UINT32 i, clientIdHash; + BOOL sampleConfigurationObjLockLocked = FALSE, streamingSessionListReadLockLocked = FALSE, peerConnectionFound = FALSE, sessionFreed = FALSE; + SIGNALING_CLIENT_STATE signalingClientState; + + CHK(pSampleConfiguration != NULL, STATUS_NULL_ARG); + + while (!ATOMIC_LOAD_BOOL(&pSampleConfiguration->interrupted)) { + // Keep the main set of operations interlocked until cvar wait which would atomically unlock + MUTEX_LOCK(pSampleConfiguration->sampleConfigurationObjLock); + sampleConfigurationObjLockLocked = TRUE; + + // scan and cleanup terminated streaming session + for (i = 0; i < pSampleConfiguration->streamingSessionCount; ++i) { + if (ATOMIC_LOAD_BOOL(&pSampleConfiguration->sampleStreamingSessionList[i]->terminateFlag)) { + pSampleStreamingSession = pSampleConfiguration->sampleStreamingSessionList[i]; + + MUTEX_LOCK(pSampleConfiguration->streamingSessionListReadLock); + streamingSessionListReadLockLocked = TRUE; + + // swap with last element and decrement count + pSampleConfiguration->streamingSessionCount--; + pSampleConfiguration->sampleStreamingSessionList[i] = + pSampleConfiguration->sampleStreamingSessionList[pSampleConfiguration->streamingSessionCount]; + + // Remove from the hash table + clientIdHash = COMPUTE_CRC32((PBYTE) pSampleStreamingSession->peerId, (UINT32) STRLEN(pSampleStreamingSession->peerId)); + CHK_STATUS(hashTableContains(pSampleConfiguration->pRtcPeerConnectionForRemoteClient, clientIdHash, &peerConnectionFound)); + if (peerConnectionFound) { + CHK_STATUS(hashTableRemove(pSampleConfiguration->pRtcPeerConnectionForRemoteClient, clientIdHash)); + } + + MUTEX_UNLOCK(pSampleConfiguration->streamingSessionListReadLock); + streamingSessionListReadLockLocked = FALSE; + + CHK_STATUS(freeSampleStreamingSession(&pSampleStreamingSession)); + sessionFreed = TRUE; + } + } + + if (sessionFreed && pSampleConfiguration->channelInfo.useMediaStorage && !ATOMIC_LOAD_BOOL(&pSampleConfiguration->recreateSignalingClient)) { + // In the WebRTC Media Storage Ingestion Case the backend will terminate the session after + // 1 hour. The SDK needs to make a new JoinSession Call in order to receive a new + // offer from the backend. We will create a new sample streaming session upon receipt of the + // offer. The signalingClientConnectSync call will result in a JoinSession API call being made. + CHK_STATUS(signalingClientDisconnectSync(pSampleConfiguration->signalingClientHandle)); + CHK_STATUS(signalingClientFetchSync(pSampleConfiguration->signalingClientHandle)); + CHK_STATUS(signalingClientConnectSync(pSampleConfiguration->signalingClientHandle)); + sessionFreed = FALSE; + } + + // Check if we need to re-create the signaling client on-the-fly + if (ATOMIC_LOAD_BOOL(&pSampleConfiguration->recreateSignalingClient)) { + retStatus = signalingClientFetchSync(pSampleConfiguration->signalingClientHandle); + if (STATUS_SUCCEEDED(retStatus)) { + // Re-set the variable again + ATOMIC_STORE_BOOL(&pSampleConfiguration->recreateSignalingClient, FALSE); + } else if (signalingCallFailed(retStatus)) { + printf("[KVS Common] recreating Signaling Client\n"); + freeSignalingClient(&pSampleConfiguration->signalingClientHandle); + createSignalingClientSync(&pSampleConfiguration->clientInfo, &pSampleConfiguration->channelInfo, + &pSampleConfiguration->signalingClientCallbacks, pSampleConfiguration->pCredentialProvider, + &pSampleConfiguration->signalingClientHandle); + } + } + + // Check the signaling client state and connect if needed + if (IS_VALID_SIGNALING_CLIENT_HANDLE(pSampleConfiguration->signalingClientHandle)) { + CHK_STATUS(signalingClientGetCurrentState(pSampleConfiguration->signalingClientHandle, &signalingClientState)); + if (signalingClientState == SIGNALING_CLIENT_STATE_READY) { + UNUSED_PARAM(signalingClientConnectSync(pSampleConfiguration->signalingClientHandle)); + } + } + + // Check if any lingering pending message queues + CHK_STATUS(removeExpiredMessageQueues(pSampleConfiguration->pPendingSignalingMessageForRemoteClient)); + + // periodically wake up and clean up terminated streaming session + CVAR_WAIT(pSampleConfiguration->cvar, pSampleConfiguration->sampleConfigurationObjLock, SAMPLE_SESSION_CLEANUP_WAIT_PERIOD); + MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); + sampleConfigurationObjLockLocked = FALSE; + } + +CleanUp: + + CHK_LOG_ERR(retStatus); + + if (sampleConfigurationObjLockLocked) { + MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); + } + + if (streamingSessionListReadLockLocked) { + MUTEX_UNLOCK(pSampleConfiguration->streamingSessionListReadLock); + } + + LEAVES(); + return retStatus; +} diff --git a/samples/lib/DataChannelHandling.c b/samples/lib/DataChannelHandling.c new file mode 100644 index 0000000000..71124ba334 --- /dev/null +++ b/samples/lib/DataChannelHandling.c @@ -0,0 +1,180 @@ +#include "../Samples.h" + +#ifdef ENABLE_DATA_CHANNEL +VOID onDataChannelMessage(UINT64 customData, PRtcDataChannel pDataChannel, BOOL isBinary, PBYTE pMessage, UINT32 pMessageLen) +{ + STATUS retStatus = STATUS_SUCCESS; + UINT32 i, strLen, tokenCount; + CHAR pMessageSend[MAX_DATA_CHANNEL_METRICS_MESSAGE_SIZE], errorMessage[200]; + PCHAR json; + PSampleStreamingSession pSampleStreamingSession = (PSampleStreamingSession) customData; + PSampleConfiguration pSampleConfiguration; + DataChannelMessage dataChannelMessage; + jsmn_parser parser; + jsmntok_t tokens[MAX_JSON_TOKEN_COUNT]; + + CHK(pMessage != NULL && pDataChannel != NULL, STATUS_NULL_ARG); + + if (pSampleStreamingSession == NULL) { + STRCPY(errorMessage, "Could not generate stats since the streaming session is NULL"); + retStatus = dataChannelSend(pDataChannel, FALSE, (PBYTE) errorMessage, STRLEN(errorMessage)); + DLOGE("%s", errorMessage); + goto CleanUp; + } + + pSampleConfiguration = pSampleStreamingSession->pSampleConfiguration; + if (pSampleConfiguration == NULL) { + STRCPY(errorMessage, "Could not generate stats since the sample configuration is NULL"); + retStatus = dataChannelSend(pDataChannel, FALSE, (PBYTE) errorMessage, STRLEN(errorMessage)); + DLOGE("%s", errorMessage); + goto CleanUp; + } + + if (pSampleConfiguration->enableSendingMetricsToViewerViaDc) { + jsmn_init(&parser); + json = (PCHAR) pMessage; + tokenCount = jsmn_parse(&parser, json, STRLEN(json), tokens, SIZEOF(tokens) / SIZEOF(jsmntok_t)); + + MEMSET(dataChannelMessage.content, '\0', SIZEOF(dataChannelMessage.content)); + MEMSET(dataChannelMessage.firstMessageFromViewerTs, '\0', SIZEOF(dataChannelMessage.firstMessageFromViewerTs)); + MEMSET(dataChannelMessage.firstMessageFromMasterTs, '\0', SIZEOF(dataChannelMessage.firstMessageFromMasterTs)); + MEMSET(dataChannelMessage.secondMessageFromViewerTs, '\0', SIZEOF(dataChannelMessage.secondMessageFromViewerTs)); + MEMSET(dataChannelMessage.secondMessageFromMasterTs, '\0', SIZEOF(dataChannelMessage.secondMessageFromMasterTs)); + MEMSET(dataChannelMessage.lastMessageFromViewerTs, '\0', SIZEOF(dataChannelMessage.lastMessageFromViewerTs)); + + if (tokenCount > 1) { + if (tokens[0].type != JSMN_OBJECT) { + STRCPY(errorMessage, "Invalid JSON received, please send a valid json as the SDK is operating in datachannel-benchmarking mode"); + retStatus = dataChannelSend(pDataChannel, FALSE, (PBYTE) errorMessage, STRLEN(errorMessage)); + DLOGE("%s", errorMessage); + retStatus = STATUS_INVALID_API_CALL_RETURN_JSON; + goto CleanUp; + } + DLOGI("DataChannel json message: %.*s\n", pMessageLen, pMessage); + + for (i = 1; i < tokenCount; i++) { + if (compareJsonString(json, &tokens[i], JSMN_STRING, (PCHAR) "content")) { + strLen = (UINT32) (tokens[i + 1].end - tokens[i + 1].start); + if (strLen != 0) { + STRNCPY(dataChannelMessage.content, json + tokens[i + 1].start, tokens[i + 1].end - tokens[i + 1].start); + } + } else if (compareJsonString(json, &tokens[i], JSMN_STRING, (PCHAR) "firstMessageFromViewerTs")) { + strLen = (UINT32) (tokens[i + 1].end - tokens[i + 1].start); + // parse and retain this message from the viewer to send it back again + if (strLen != 0) { + // since the length is not zero, we have already attached this timestamp to structure in the last iteration + STRNCPY(dataChannelMessage.firstMessageFromViewerTs, json + tokens[i + 1].start, tokens[i + 1].end - tokens[i + 1].start); + } + } else if (compareJsonString(json, &tokens[i], JSMN_STRING, (PCHAR) "firstMessageFromMasterTs")) { + strLen = (UINT32) (tokens[i + 1].end - tokens[i + 1].start); + if (strLen != 0) { + // since the length is not zero, we have already attached this timestamp to structure in the last iteration + STRNCPY(dataChannelMessage.firstMessageFromMasterTs, json + tokens[i + 1].start, tokens[i + 1].end - tokens[i + 1].start); + } else { + // if this timestamp was not assigned during the previous message session, add it now + SNPRINTF(dataChannelMessage.firstMessageFromMasterTs, 20, "%llu", GETTIME() / 10000); + break; + } + } else if (compareJsonString(json, &tokens[i], JSMN_STRING, (PCHAR) "secondMessageFromViewerTs")) { + strLen = (UINT32) (tokens[i + 1].end - tokens[i + 1].start); + // parse and retain this message from the viewer to send it back again + if (strLen != 0) { + STRNCPY(dataChannelMessage.secondMessageFromViewerTs, json + tokens[i + 1].start, tokens[i + 1].end - tokens[i + 1].start); + } + } else if (compareJsonString(json, &tokens[i], JSMN_STRING, (PCHAR) "secondMessageFromMasterTs")) { + strLen = (UINT32) (tokens[i + 1].end - tokens[i + 1].start); + if (strLen != 0) { + // since the length is not zero, we have already attached this timestamp to structure in the last iteration + STRNCPY(dataChannelMessage.secondMessageFromMasterTs, json + tokens[i + 1].start, tokens[i + 1].end - tokens[i + 1].start); + } else { + // if this timestamp was not assigned during the previous message session, add it now + SNPRINTF(dataChannelMessage.secondMessageFromMasterTs, 20, "%llu", GETTIME() / 10000); + break; + } + } else if (compareJsonString(json, &tokens[i], JSMN_STRING, (PCHAR) "lastMessageFromViewerTs")) { + strLen = (UINT32) (tokens[i + 1].end - tokens[i + 1].start); + if (strLen != 0) { + STRNCPY(dataChannelMessage.lastMessageFromViewerTs, json + tokens[i + 1].start, tokens[i + 1].end - tokens[i + 1].start); + } + } + } + + if (STRLEN(dataChannelMessage.lastMessageFromViewerTs) == 0) { + // continue sending the data_channel_metrics_message with new timestamps until we receive the lastMessageFromViewerTs from the viewer + SNPRINTF(pMessageSend, MAX_DATA_CHANNEL_METRICS_MESSAGE_SIZE, DATA_CHANNEL_MESSAGE_TEMPLATE, MASTER_DATA_CHANNEL_MESSAGE, + dataChannelMessage.firstMessageFromViewerTs, dataChannelMessage.firstMessageFromMasterTs, + dataChannelMessage.secondMessageFromViewerTs, dataChannelMessage.secondMessageFromMasterTs, + dataChannelMessage.lastMessageFromViewerTs); + DLOGI("Master's response: %s", pMessageSend); + + retStatus = dataChannelSend(pDataChannel, FALSE, (PBYTE) pMessageSend, STRLEN(pMessageSend)); + } else { + // now that we've received the last message, send across the signaling, peerConnection, ice metrics + SNPRINTF(pSampleStreamingSession->pSignalingClientMetricsMessage, MAX_SIGNALING_CLIENT_METRICS_MESSAGE_SIZE, + SIGNALING_CLIENT_METRICS_JSON_TEMPLATE, pSampleConfiguration->signalingClientMetrics.signalingStartTime, + pSampleConfiguration->signalingClientMetrics.signalingEndTime, + pSampleConfiguration->signalingClientMetrics.signalingClientStats.offerReceivedTime, + pSampleConfiguration->signalingClientMetrics.signalingClientStats.answerTime, + pSampleConfiguration->signalingClientMetrics.signalingClientStats.describeChannelStartTime, + pSampleConfiguration->signalingClientMetrics.signalingClientStats.describeChannelEndTime, + pSampleConfiguration->signalingClientMetrics.signalingClientStats.getSignalingChannelEndpointStartTime, + pSampleConfiguration->signalingClientMetrics.signalingClientStats.getSignalingChannelEndpointEndTime, + pSampleConfiguration->signalingClientMetrics.signalingClientStats.getIceServerConfigStartTime, + pSampleConfiguration->signalingClientMetrics.signalingClientStats.getIceServerConfigEndTime, + pSampleConfiguration->signalingClientMetrics.signalingClientStats.getTokenStartTime, + pSampleConfiguration->signalingClientMetrics.signalingClientStats.getTokenEndTime, + pSampleConfiguration->signalingClientMetrics.signalingClientStats.createChannelStartTime, + pSampleConfiguration->signalingClientMetrics.signalingClientStats.createChannelEndTime, + pSampleConfiguration->signalingClientMetrics.signalingClientStats.connectStartTime, + pSampleConfiguration->signalingClientMetrics.signalingClientStats.connectEndTime); + DLOGI("Sending signaling metrics to the viewer: %s", pSampleStreamingSession->pSignalingClientMetricsMessage); + + CHK_STATUS(peerConnectionGetMetrics(pSampleStreamingSession->pPeerConnection, &pSampleStreamingSession->peerConnectionMetrics)); + SNPRINTF(pSampleStreamingSession->pPeerConnectionMetricsMessage, MAX_PEER_CONNECTION_METRICS_MESSAGE_SIZE, + PEER_CONNECTION_METRICS_JSON_TEMPLATE, + pSampleStreamingSession->peerConnectionMetrics.peerConnectionStats.peerConnectionStartTime, + pSampleStreamingSession->peerConnectionMetrics.peerConnectionStats.peerConnectionConnectedTime); + DLOGI("Sending peer-connection metrics to the viewer: %s", pSampleStreamingSession->pPeerConnectionMetricsMessage); + + CHK_STATUS(iceAgentGetMetrics(pSampleStreamingSession->pPeerConnection, &pSampleStreamingSession->iceMetrics)); + SNPRINTF(pSampleStreamingSession->pIceAgentMetricsMessage, MAX_ICE_AGENT_METRICS_MESSAGE_SIZE, ICE_AGENT_METRICS_JSON_TEMPLATE, + pSampleStreamingSession->iceMetrics.kvsIceAgentStats.candidateGatheringStartTime, + pSampleStreamingSession->iceMetrics.kvsIceAgentStats.candidateGatheringEndTime); + DLOGI("Sending ice-agent metrics to the viewer: %s", pSampleStreamingSession->pIceAgentMetricsMessage); + + retStatus = dataChannelSend(pDataChannel, FALSE, (PBYTE) pSampleStreamingSession->pSignalingClientMetricsMessage, + STRLEN(pSampleStreamingSession->pSignalingClientMetricsMessage)); + retStatus = dataChannelSend(pDataChannel, FALSE, (PBYTE) pSampleStreamingSession->pPeerConnectionMetricsMessage, + STRLEN(pSampleStreamingSession->pPeerConnectionMetricsMessage)); + retStatus = dataChannelSend(pDataChannel, FALSE, (PBYTE) pSampleStreamingSession->pIceAgentMetricsMessage, + STRLEN(pSampleStreamingSession->pIceAgentMetricsMessage)); + } + } else { + DLOGI("DataChannel string message: %.*s\n", pMessageLen, pMessage); + STRCPY(errorMessage, "Send a json message for benchmarking as the C SDK is operating in benchmarking mode"); + retStatus = dataChannelSend(pDataChannel, FALSE, (PBYTE) errorMessage, STRLEN(errorMessage)); + } + } else { + if (isBinary) { + DLOGI("DataChannel Binary Message"); + } else { + DLOGI("DataChannel String Message: %.*s\n", pMessageLen, pMessage); + } + // Send a response to the message sent by the viewer + retStatus = dataChannelSend(pDataChannel, FALSE, (PBYTE) MASTER_DATA_CHANNEL_MESSAGE, STRLEN(MASTER_DATA_CHANNEL_MESSAGE)); + } + if (retStatus != STATUS_SUCCESS) { + DLOGI("[KVS Master] dataChannelSend(): operation returned status code: 0x%08x \n", retStatus); + } + +CleanUp: + CHK_LOG_ERR(retStatus); +} + +VOID onDataChannel(UINT64 customData, PRtcDataChannel pRtcDataChannel) +{ + DLOGI("New DataChannel has been opened %s \n", pRtcDataChannel->name); + dataChannelOnMessage(pRtcDataChannel, customData, onDataChannelMessage); +} + +#endif diff --git a/samples/lib/Media.c b/samples/lib/Media.c new file mode 100644 index 0000000000..dd56fcbe70 --- /dev/null +++ b/samples/lib/Media.c @@ -0,0 +1,42 @@ +#include "../Samples.h" + +PVOID mediaSenderRoutine(PVOID customData) +{ + STATUS retStatus = STATUS_SUCCESS; + PSampleConfiguration pSampleConfiguration = (PSampleConfiguration) customData; + CHK(pSampleConfiguration != NULL, STATUS_NULL_ARG); + pSampleConfiguration->videoSenderTid = INVALID_TID_VALUE; + pSampleConfiguration->audioSenderTid = INVALID_TID_VALUE; + + MUTEX_LOCK(pSampleConfiguration->sampleConfigurationObjLock); + while (!ATOMIC_LOAD_BOOL(&pSampleConfiguration->connected) && !ATOMIC_LOAD_BOOL(&pSampleConfiguration->appTerminateFlag)) { + CVAR_WAIT(pSampleConfiguration->cvar, pSampleConfiguration->sampleConfigurationObjLock, 5 * HUNDREDS_OF_NANOS_IN_A_SECOND); + } + MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); + + CHK(!ATOMIC_LOAD_BOOL(&pSampleConfiguration->appTerminateFlag), retStatus); + + if (pSampleConfiguration->videoSource != NULL) { + THREAD_CREATE_WITH_PARAMS(&pSampleConfiguration->videoSenderTid, pSampleConfiguration->videoSource, + KVS_DEFAULT_MEDIA_SENDER_THREAD_STACK_SIZE, (PVOID) pSampleConfiguration); + } + + if (pSampleConfiguration->audioSource != NULL) { + THREAD_CREATE_WITH_PARAMS(&pSampleConfiguration->audioSenderTid, pSampleConfiguration->audioSource, + KVS_DEFAULT_MEDIA_SENDER_THREAD_STACK_SIZE, (PVOID) pSampleConfiguration); + } + + if (pSampleConfiguration->videoSenderTid != INVALID_TID_VALUE) { + THREAD_JOIN(pSampleConfiguration->videoSenderTid, NULL); + } + + if (pSampleConfiguration->audioSenderTid != INVALID_TID_VALUE) { + THREAD_JOIN(pSampleConfiguration->audioSenderTid, NULL); + } + +CleanUp: + // clean the flag of the media thread. + ATOMIC_STORE_BOOL(&pSampleConfiguration->mediaThreadStarted, FALSE); + CHK_LOG_ERR(retStatus); + return NULL; +} diff --git a/samples/lib/MetricsHandling.c b/samples/lib/MetricsHandling.c new file mode 100644 index 0000000000..4d5ce06c82 --- /dev/null +++ b/samples/lib/MetricsHandling.c @@ -0,0 +1,322 @@ +#include "../Samples.h" + +STATUS setupMetricsCtx(PSampleStreamingSession pSampleStreamingSession) +{ + STATUS retStatus = STATUS_SUCCESS; + CHK(pSampleStreamingSession != NULL && pSampleStreamingSession->pSampleConfiguration != NULL, STATUS_NULL_ARG); + if (pSampleStreamingSession->pSampleConfiguration->enableMetrics) { + CHK(NULL != (pSampleStreamingSession->pStatsCtx = (PStatsCtx) MEMCALLOC(1, SIZEOF(StatsCtx))), STATUS_NOT_ENOUGH_MEMORY); + } + pSampleStreamingSession->pStatsCtx->statsUpdateLock = MUTEX_CREATE(TRUE); +CleanUp: + return retStatus; +} + +VOID acquireMetricsCtx(PSampleStreamingSession pSampleStreamingSession) +{ + if (pSampleStreamingSession != NULL && pSampleStreamingSession->pStatsCtx != NULL) { + ATOMIC_INCREMENT(&pSampleStreamingSession->pStatsCtx->statsContextRefCnt); + } +} + +VOID releaseMetricsCtx(PSampleStreamingSession pSampleStreamingSession) +{ + if (pSampleStreamingSession != NULL && pSampleStreamingSession->pStatsCtx != NULL) { + ATOMIC_DECREMENT(&pSampleStreamingSession->pStatsCtx->statsContextRefCnt); + } +} + +STATUS logSelectedIceCandidatesInformation(PSampleStreamingSession pSampleStreamingSession) +{ + ENTERS(); + STATUS retStatus = STATUS_SUCCESS; + RtcStats rtcMetrics; + + CHK(pSampleStreamingSession != NULL, STATUS_NULL_ARG); + rtcMetrics.requestedTypeOfStats = RTC_STATS_TYPE_LOCAL_CANDIDATE; + CHK_STATUS(rtcPeerConnectionGetMetrics(pSampleStreamingSession->pPeerConnection, NULL, &rtcMetrics)); + DLOGI("Local Candidate IP Address: %s", rtcMetrics.rtcStatsObject.localIceCandidateStats.address); + DLOGI("Local Candidate type: %s", rtcMetrics.rtcStatsObject.localIceCandidateStats.candidateType); + DLOGI("Local Candidate port: %d", rtcMetrics.rtcStatsObject.localIceCandidateStats.port); + DLOGI("Local Candidate priority: %d", rtcMetrics.rtcStatsObject.localIceCandidateStats.priority); + DLOGI("Local Candidate transport protocol: %s", rtcMetrics.rtcStatsObject.localIceCandidateStats.protocol); + DLOGI("Local Candidate relay protocol: %s", rtcMetrics.rtcStatsObject.localIceCandidateStats.relayProtocol); + DLOGI("Local Candidate Ice server source: %s", rtcMetrics.rtcStatsObject.localIceCandidateStats.url); + + rtcMetrics.requestedTypeOfStats = RTC_STATS_TYPE_REMOTE_CANDIDATE; + CHK_STATUS(rtcPeerConnectionGetMetrics(pSampleStreamingSession->pPeerConnection, NULL, &rtcMetrics)); + DLOGI("Remote Candidate IP Address: %s", rtcMetrics.rtcStatsObject.remoteIceCandidateStats.address); + DLOGI("Remote Candidate type: %s", rtcMetrics.rtcStatsObject.remoteIceCandidateStats.candidateType); + DLOGI("Remote Candidate port: %d", rtcMetrics.rtcStatsObject.remoteIceCandidateStats.port); + DLOGI("Remote Candidate priority: %d", rtcMetrics.rtcStatsObject.remoteIceCandidateStats.priority); + DLOGI("Remote Candidate transport protocol: %s", rtcMetrics.rtcStatsObject.remoteIceCandidateStats.protocol); +CleanUp: + LEAVES(); + return retStatus; +} + +STATUS gatherIceServerStats(PSampleStreamingSession pSampleStreamingSession) +{ + ENTERS(); + STATUS retStatus = STATUS_SUCCESS; + RtcStats rtcmetrics; + UINT32 j = 0; + rtcmetrics.requestedTypeOfStats = RTC_STATS_TYPE_ICE_SERVER; + + CHK_WARN(pSampleStreamingSession != NULL, STATUS_NULL_ARG, "gatherIceServerStats(): Passed argument is NULL"); + for (; j < pSampleStreamingSession->pSampleConfiguration->iceUriCount; j++) { + rtcmetrics.rtcStatsObject.iceServerStats.iceServerIndex = j; + CHK_STATUS(rtcPeerConnectionGetMetrics(pSampleStreamingSession->pPeerConnection, NULL, &rtcmetrics)); + DLOGD("ICE Server URL: %s", rtcmetrics.rtcStatsObject.iceServerStats.url); + DLOGD("ICE Server port: %d", rtcmetrics.rtcStatsObject.iceServerStats.port); + DLOGD("ICE Server protocol: %s", rtcmetrics.rtcStatsObject.iceServerStats.protocol); + DLOGD("Total requests sent:%" PRIu64, rtcmetrics.rtcStatsObject.iceServerStats.totalRequestsSent); + DLOGD("Total responses received: %" PRIu64, rtcmetrics.rtcStatsObject.iceServerStats.totalResponsesReceived); + DLOGD("Total round trip time: %" PRIu64 "ms", + rtcmetrics.rtcStatsObject.iceServerStats.totalRoundTripTime / HUNDREDS_OF_NANOS_IN_A_MILLISECOND); + } +CleanUp: + LEAVES(); + return retStatus; +} + +STATUS getIceCandidatePairStatsCallback(UINT32 timerId, UINT64 currentTime, UINT64 customData) +{ + UNUSED_PARAM(timerId); + UNUSED_PARAM(currentTime); + STATUS retStatus = STATUS_SUCCESS; + PSampleConfiguration pSampleConfiguration = (PSampleConfiguration) customData; + UINT32 i; + UINT64 currentMeasureDuration = 0; + DOUBLE averagePacketsDiscardedOnSend = 0.0; + DOUBLE averageNumberOfPacketsSentPerSecond = 0.0; + DOUBLE averageNumberOfPacketsReceivedPerSecond = 0.0; + DOUBLE outgoingBitrate = 0.0; + DOUBLE incomingBitrate = 0.0; + BOOL locked = FALSE; + + CHK_WARN(pSampleConfiguration != NULL, STATUS_NULL_ARG, "getPeriodicStats(): Passed argument is NULL"); + + // Use MUTEX_TRYLOCK to avoid possible dead lock when canceling timerQueue + if (!MUTEX_TRYLOCK(pSampleConfiguration->sampleConfigurationObjLock)) { + return retStatus; + } else { + locked = TRUE; + } + + pSampleConfiguration->rtcIceCandidatePairMetrics.requestedTypeOfStats = RTC_STATS_TYPE_CANDIDATE_PAIR; + + for (i = 0; i < pSampleConfiguration->streamingSessionCount; ++i) { + if (STATUS_SUCCEEDED(rtcPeerConnectionGetMetrics(pSampleConfiguration->sampleStreamingSessionList[i]->pPeerConnection, NULL, + &pSampleConfiguration->rtcIceCandidatePairMetrics))) { + currentMeasureDuration = (pSampleConfiguration->rtcIceCandidatePairMetrics.timestamp - + pSampleConfiguration->sampleStreamingSessionList[i]->rtpMetricsHistory.prevTs) / + HUNDREDS_OF_NANOS_IN_A_SECOND; + DLOGD("Current duration: %" PRIu64 " seconds", currentMeasureDuration); + if (currentMeasureDuration > 0) { + DLOGD("Selected local candidate ID: %s", + pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.localCandidateId); + DLOGD("Selected remote candidate ID: %s", + pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.remoteCandidateId); + // TODO: Display state as a string for readability + DLOGD("Ice Candidate Pair state: %d", pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.state); + DLOGD("Nomination state: %s", + pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.nominated ? "nominated" + : "not nominated"); + averageNumberOfPacketsSentPerSecond = + (DOUBLE) (pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.packetsSent - + pSampleConfiguration->sampleStreamingSessionList[i]->rtpMetricsHistory.prevNumberOfPacketsSent) / + (DOUBLE) currentMeasureDuration; + DLOGD("Packet send rate: %lf pkts/sec", averageNumberOfPacketsSentPerSecond); + + averageNumberOfPacketsReceivedPerSecond = + (DOUBLE) (pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.packetsReceived - + pSampleConfiguration->sampleStreamingSessionList[i]->rtpMetricsHistory.prevNumberOfPacketsReceived) / + (DOUBLE) currentMeasureDuration; + DLOGD("Packet receive rate: %lf pkts/sec", averageNumberOfPacketsReceivedPerSecond); + + outgoingBitrate = (DOUBLE) ((pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.bytesSent - + pSampleConfiguration->sampleStreamingSessionList[i]->rtpMetricsHistory.prevNumberOfBytesSent) * + 8.0) / + currentMeasureDuration; + DLOGD("Outgoing bit rate: %lf bps", outgoingBitrate); + + incomingBitrate = (DOUBLE) ((pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.bytesReceived - + pSampleConfiguration->sampleStreamingSessionList[i]->rtpMetricsHistory.prevNumberOfBytesReceived) * + 8.0) / + currentMeasureDuration; + DLOGD("Incoming bit rate: %lf bps", incomingBitrate); + + averagePacketsDiscardedOnSend = + (DOUBLE) (pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.packetsDiscardedOnSend - + pSampleConfiguration->sampleStreamingSessionList[i]->rtpMetricsHistory.prevPacketsDiscardedOnSend) / + (DOUBLE) currentMeasureDuration; + DLOGD("Packet discard rate: %lf pkts/sec", averagePacketsDiscardedOnSend); + + DLOGD("Current STUN request round trip time: %lf sec", + pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.currentRoundTripTime); + DLOGD("Number of STUN responses received: %llu", + pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.responsesReceived); + + pSampleConfiguration->sampleStreamingSessionList[i]->rtpMetricsHistory.prevTs = + pSampleConfiguration->rtcIceCandidatePairMetrics.timestamp; + pSampleConfiguration->sampleStreamingSessionList[i]->rtpMetricsHistory.prevNumberOfPacketsSent = + pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.packetsSent; + pSampleConfiguration->sampleStreamingSessionList[i]->rtpMetricsHistory.prevNumberOfPacketsReceived = + pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.packetsReceived; + pSampleConfiguration->sampleStreamingSessionList[i]->rtpMetricsHistory.prevNumberOfBytesSent = + pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.bytesSent; + pSampleConfiguration->sampleStreamingSessionList[i]->rtpMetricsHistory.prevNumberOfBytesReceived = + pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.bytesReceived; + pSampleConfiguration->sampleStreamingSessionList[i]->rtpMetricsHistory.prevPacketsDiscardedOnSend = + pSampleConfiguration->rtcIceCandidatePairMetrics.rtcStatsObject.iceCandidatePairStats.packetsDiscardedOnSend; + } + } + } + +CleanUp: + + if (locked) { + MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); + } + + return retStatus; +} + +STATUS populateOutgoingRtpMetricsContext(PSampleStreamingSession pSampleStreamingSession) +{ + DOUBLE currentDuration = 0; + BOOL locked = FALSE; + STATUS retStatus = STATUS_SUCCESS; + + acquireMetricsCtx(pSampleStreamingSession); + + CHK_WARN(pSampleStreamingSession->pStatsCtx != NULL, STATUS_NULL_ARG, "Stats object not set up. Nothing to report"); + + MUTEX_LOCK(pSampleStreamingSession->pStatsCtx->statsUpdateLock); + locked = TRUE; + + currentDuration = + (DOUBLE) (pSampleStreamingSession->pStatsCtx->kvsRtcStats.timestamp - pSampleStreamingSession->pStatsCtx->outgoingRTPStatsCtx.prevTs) / + HUNDREDS_OF_NANOS_IN_A_SECOND; + pSampleStreamingSession->pStatsCtx->outgoingRTPStatsCtx.framesPercentageDiscarded = + ((DOUBLE) (pSampleStreamingSession->pStatsCtx->kvsRtcStats.rtcStatsObject.outboundRtpStreamStats.framesDiscardedOnSend - + pSampleStreamingSession->pStatsCtx->outgoingRTPStatsCtx.prevFramesDiscardedOnSend) / + (DOUBLE) pSampleStreamingSession->pStatsCtx->outgoingRTPStatsCtx.videoFramesGenerated) * + 100.0; + pSampleStreamingSession->pStatsCtx->outgoingRTPStatsCtx.retxBytesPercentage = + (((DOUBLE) pSampleStreamingSession->pStatsCtx->kvsRtcStats.rtcStatsObject.outboundRtpStreamStats.retransmittedBytesSent - + (DOUBLE) (pSampleStreamingSession->pStatsCtx->outgoingRTPStatsCtx.prevRetxBytesSent)) / + (DOUBLE) pSampleStreamingSession->pStatsCtx->outgoingRTPStatsCtx.videoBytesGenerated) * + 100.0; + + // This flag ensures the reset of video bytes count is done only when this flag is set + pSampleStreamingSession->pStatsCtx->outgoingRTPStatsCtx.recorded = TRUE; + pSampleStreamingSession->pStatsCtx->outgoingRTPStatsCtx.averageFramesSentPerSecond = + ((DOUBLE) (pSampleStreamingSession->pStatsCtx->kvsRtcStats.rtcStatsObject.outboundRtpStreamStats.framesSent - + (DOUBLE) pSampleStreamingSession->pStatsCtx->outgoingRTPStatsCtx.prevFramesSent)) / + currentDuration; + pSampleStreamingSession->pStatsCtx->outgoingRTPStatsCtx.nacksPerSecond = + ((DOUBLE) pSampleStreamingSession->pStatsCtx->kvsRtcStats.rtcStatsObject.outboundRtpStreamStats.nackCount - + pSampleStreamingSession->pStatsCtx->outgoingRTPStatsCtx.prevNackCount) / + currentDuration; + pSampleStreamingSession->pStatsCtx->outgoingRTPStatsCtx.prevFramesSent = + pSampleStreamingSession->pStatsCtx->kvsRtcStats.rtcStatsObject.outboundRtpStreamStats.framesSent; + pSampleStreamingSession->pStatsCtx->outgoingRTPStatsCtx.prevTs = pSampleStreamingSession->pStatsCtx->kvsRtcStats.timestamp; + pSampleStreamingSession->pStatsCtx->outgoingRTPStatsCtx.prevFramesDiscardedOnSend = + pSampleStreamingSession->pStatsCtx->kvsRtcStats.rtcStatsObject.outboundRtpStreamStats.framesDiscardedOnSend; + pSampleStreamingSession->pStatsCtx->outgoingRTPStatsCtx.prevNackCount = + pSampleStreamingSession->pStatsCtx->kvsRtcStats.rtcStatsObject.outboundRtpStreamStats.nackCount; + pSampleStreamingSession->pStatsCtx->outgoingRTPStatsCtx.prevRetxBytesSent = + pSampleStreamingSession->pStatsCtx->kvsRtcStats.rtcStatsObject.outboundRtpStreamStats.retransmittedBytesSent; +CleanUp: + if (locked) { + MUTEX_UNLOCK(pSampleStreamingSession->pStatsCtx->statsUpdateLock); + } + releaseMetricsCtx(pSampleStreamingSession); + return retStatus; +} + +STATUS populateIncomingRtpMetricsContext(PSampleStreamingSession pSampleStreamingSession) +{ + DOUBLE currentDuration = 0; + STATUS retStatus = STATUS_SUCCESS; + BOOL locked = FALSE; + + acquireMetricsCtx(pSampleStreamingSession); + + CHK_WARN(pSampleStreamingSession != NULL && pSampleStreamingSession->pStatsCtx != NULL, STATUS_NULL_ARG, + "Stats object not set up. Nothing to report"); + + MUTEX_LOCK(pSampleStreamingSession->pStatsCtx->statsUpdateLock); + locked = TRUE; + currentDuration = + (DOUBLE) (pSampleStreamingSession->pStatsCtx->kvsRtcStats.timestamp - pSampleStreamingSession->pStatsCtx->incomingRTPStatsCtx.prevTs) / + HUNDREDS_OF_NANOS_IN_A_SECOND; + pSampleStreamingSession->pStatsCtx->incomingRTPStatsCtx.packetReceiveRate = + (DOUBLE) (pSampleStreamingSession->pStatsCtx->kvsRtcStats.rtcStatsObject.inboundRtpStreamStats.received.packetsReceived - + pSampleStreamingSession->pStatsCtx->incomingRTPStatsCtx.prevPacketsReceived) / + currentDuration; + pSampleStreamingSession->pStatsCtx->incomingRTPStatsCtx.incomingBitRate = + ((DOUBLE) (pSampleStreamingSession->pStatsCtx->kvsRtcStats.rtcStatsObject.inboundRtpStreamStats.bytesReceived - + pSampleStreamingSession->pStatsCtx->incomingRTPStatsCtx.prevBytesReceived) / + currentDuration) / + 0.008; + pSampleStreamingSession->pStatsCtx->incomingRTPStatsCtx.framesDroppedPerSecond = + ((DOUBLE) pSampleStreamingSession->pStatsCtx->kvsRtcStats.rtcStatsObject.inboundRtpStreamStats.received.framesDropped - + pSampleStreamingSession->pStatsCtx->incomingRTPStatsCtx.prevFramesDropped) / + currentDuration; + + pSampleStreamingSession->pStatsCtx->incomingRTPStatsCtx.prevPacketsReceived = + pSampleStreamingSession->pStatsCtx->kvsRtcStats.rtcStatsObject.inboundRtpStreamStats.received.packetsReceived; + pSampleStreamingSession->pStatsCtx->incomingRTPStatsCtx.prevBytesReceived = + pSampleStreamingSession->pStatsCtx->kvsRtcStats.rtcStatsObject.inboundRtpStreamStats.bytesReceived; + pSampleStreamingSession->pStatsCtx->incomingRTPStatsCtx.prevFramesDropped = + pSampleStreamingSession->pStatsCtx->kvsRtcStats.rtcStatsObject.inboundRtpStreamStats.received.framesDropped; + pSampleStreamingSession->pStatsCtx->incomingRTPStatsCtx.prevTs = pSampleStreamingSession->pStatsCtx->kvsRtcStats.timestamp; + +CleanUp: + if (locked) { + MUTEX_UNLOCK(pSampleStreamingSession->pStatsCtx->statsUpdateLock); + } + releaseMetricsCtx(pSampleStreamingSession); + return retStatus; +} + +STATUS getSdkTimeProfile(PSampleStreamingSession* ppSampleStreamingSession) +{ + STATUS retStatus = STATUS_SUCCESS; + PSampleStreamingSession pSampleStreamingSession = *ppSampleStreamingSession; + + CHK_WARN(pSampleStreamingSession != NULL, STATUS_NULL_ARG, "Streaming session object not set up. Nothing to report"); + CHK(!pSampleStreamingSession->firstFrame, STATUS_WAITING_ON_FIRST_FRAME); + + pSampleStreamingSession->pSampleConfiguration->signalingClientMetrics.version = SIGNALING_CLIENT_METRICS_CURRENT_VERSION; + CHK_STATUS(signalingClientGetMetrics(pSampleStreamingSession->pSampleConfiguration->signalingClientHandle, + &pSampleStreamingSession->pSampleConfiguration->signalingClientMetrics)); + CHK_STATUS(peerConnectionGetMetrics(pSampleStreamingSession->pPeerConnection, &pSampleStreamingSession->peerConnectionMetrics)); + CHK_STATUS(iceAgentGetMetrics(pSampleStreamingSession->pPeerConnection, &pSampleStreamingSession->iceMetrics)); +CleanUp: + return retStatus; +} + +STATUS freeMetricsCtx(PStatsCtx* ppStatsCtx) +{ + STATUS retStatus = STATUS_SUCCESS; + PStatsCtx pStatsCtx = NULL; + + pStatsCtx = *ppStatsCtx; + CHK(pStatsCtx != NULL, STATUS_NULL_ARG); + + while (ATOMIC_LOAD(&pStatsCtx->statsContextRefCnt) > 0) { + THREAD_SLEEP(100 * HUNDREDS_OF_NANOS_IN_A_MILLISECOND); + } + DLOGI("All references freed for stats ctx object"); + MUTEX_LOCK(pStatsCtx->statsUpdateLock); + MUTEX_UNLOCK(pStatsCtx->statsUpdateLock); + MUTEX_FREE(pStatsCtx->statsUpdateLock); + SAFE_MEMFREE(pStatsCtx); + *ppStatsCtx = NULL; +CleanUp: + return retStatus; +} diff --git a/samples/lib/SignalingMsgHandler.c b/samples/lib/SignalingMsgHandler.c new file mode 100644 index 0000000000..ba1d39bb5f --- /dev/null +++ b/samples/lib/SignalingMsgHandler.c @@ -0,0 +1,382 @@ +#include "../Samples.h" + +STATUS handleAnswer(PSampleConfiguration pSampleConfiguration, PSampleStreamingSession pSampleStreamingSession, PSignalingMessage pSignalingMessage) +{ + UNUSED_PARAM(pSampleConfiguration); + STATUS retStatus = STATUS_SUCCESS; + PRtcSessionDescriptionInit pAnswerSessionDescriptionInit = NULL; + + pAnswerSessionDescriptionInit = (PRtcSessionDescriptionInit) MEMCALLOC(1, SIZEOF(RtcSessionDescriptionInit)); + + CHK_STATUS(deserializeSessionDescriptionInit(pSignalingMessage->payload, pSignalingMessage->payloadLen, pAnswerSessionDescriptionInit)); + CHK_STATUS(setRemoteDescription(pSampleStreamingSession->pPeerConnection, pAnswerSessionDescriptionInit)); + + // The audio video receive routine should be per streaming session + if (pSampleConfiguration->receiveAudioVideoSource != NULL) { + THREAD_CREATE(&pSampleStreamingSession->receiveAudioVideoSenderTid, pSampleConfiguration->receiveAudioVideoSource, + (PVOID) pSampleStreamingSession); + } +CleanUp: + + if (pAnswerSessionDescriptionInit != NULL) { + SAFE_MEMFREE(pAnswerSessionDescriptionInit); + } + + CHK_LOG_ERR(retStatus); + + return retStatus; +} + +STATUS handleOffer(PSampleConfiguration pSampleConfiguration, PSampleStreamingSession pSampleStreamingSession, PSignalingMessage pSignalingMessage) +{ + STATUS retStatus = STATUS_SUCCESS; + PRtcSessionDescriptionInit pOfferSessionDescriptionInit = NULL; + NullableBool canTrickle; + BOOL mediaThreadStarted; + + CHK(pSampleConfiguration != NULL && pSignalingMessage != NULL, STATUS_NULL_ARG); + + pOfferSessionDescriptionInit = (PRtcSessionDescriptionInit) MEMCALLOC(1, SIZEOF(RtcSessionDescriptionInit)); + MEMSET(&pSampleStreamingSession->answerSessionDescriptionInit, 0x00, SIZEOF(RtcSessionDescriptionInit)); + DLOGD("**offer:%s", pSignalingMessage->payload); + CHK_STATUS(deserializeSessionDescriptionInit(pSignalingMessage->payload, pSignalingMessage->payloadLen, pOfferSessionDescriptionInit)); + CHK_STATUS(setRemoteDescription(pSampleStreamingSession->pPeerConnection, pOfferSessionDescriptionInit)); + canTrickle = canTrickleIceCandidates(pSampleStreamingSession->pPeerConnection); + /* cannot be null after setRemoteDescription */ + CHECK(!NULLABLE_CHECK_EMPTY(canTrickle)); + pSampleStreamingSession->remoteCanTrickleIce = canTrickle.value; + CHK_STATUS(setLocalDescription(pSampleStreamingSession->pPeerConnection, &pSampleStreamingSession->answerSessionDescriptionInit)); + + /* + * If remote support trickle ice, send answer now. Otherwise answer will be sent once ice candidate gathering is complete. + */ + if (pSampleStreamingSession->remoteCanTrickleIce) { + CHK_STATUS(createAnswer(pSampleStreamingSession->pPeerConnection, &pSampleStreamingSession->answerSessionDescriptionInit)); + CHK_STATUS(respondWithAnswer(pSampleStreamingSession)); + } + + mediaThreadStarted = ATOMIC_EXCHANGE_BOOL(&pSampleConfiguration->mediaThreadStarted, TRUE); + if (!mediaThreadStarted) { + THREAD_CREATE_WITH_PARAMS(&pSampleConfiguration->mediaSenderTid, mediaSenderRoutine, KVS_MINIMUM_THREAD_STACK_SIZE, + (PVOID) pSampleConfiguration); + } + + // The audio video receive routine should be per streaming session + if (pSampleConfiguration->receiveAudioVideoSource != NULL) { + THREAD_CREATE(&pSampleStreamingSession->receiveAudioVideoSenderTid, pSampleConfiguration->receiveAudioVideoSource, + (PVOID) pSampleStreamingSession); + } +CleanUp: + if (pOfferSessionDescriptionInit != NULL) { + SAFE_MEMFREE(pOfferSessionDescriptionInit); + } + + CHK_LOG_ERR(retStatus); + + return retStatus; +} + +STATUS sendSignalingMessage(PSampleStreamingSession pSampleStreamingSession, PSignalingMessage pMessage) +{ + STATUS retStatus = STATUS_SUCCESS; + BOOL locked = FALSE; + PSampleConfiguration pSampleConfiguration; + // Validate the input params + CHK(pSampleStreamingSession != NULL && pSampleStreamingSession->pSampleConfiguration != NULL && pMessage != NULL, STATUS_NULL_ARG); + + pSampleConfiguration = pSampleStreamingSession->pSampleConfiguration; + + CHK(IS_VALID_MUTEX_VALUE(pSampleConfiguration->signalingSendMessageLock) && + IS_VALID_SIGNALING_CLIENT_HANDLE(pSampleConfiguration->signalingClientHandle), + STATUS_INVALID_OPERATION); + + MUTEX_LOCK(pSampleConfiguration->signalingSendMessageLock); + locked = TRUE; + CHK_STATUS(signalingClientSendMessageSync(pSampleConfiguration->signalingClientHandle, pMessage)); + if (pMessage->messageType == SIGNALING_MESSAGE_TYPE_ANSWER) { + CHK_STATUS(signalingClientGetMetrics(pSampleConfiguration->signalingClientHandle, &pSampleConfiguration->signalingClientMetrics)); + DLOGP("[Signaling offer received to answer sent time] %" PRIu64 " ms", + pSampleConfiguration->signalingClientMetrics.signalingClientStats.offerToAnswerTime); + } + +CleanUp: + + if (locked) { + MUTEX_UNLOCK(pSampleStreamingSession->pSampleConfiguration->signalingSendMessageLock); + } + + CHK_LOG_ERR(retStatus); + return retStatus; +} + +STATUS respondWithAnswer(PSampleStreamingSession pSampleStreamingSession) +{ + STATUS retStatus = STATUS_SUCCESS; + SignalingMessage message; + UINT32 buffLen = MAX_SIGNALING_MESSAGE_LEN; + + CHK_STATUS(serializeSessionDescriptionInit(&pSampleStreamingSession->answerSessionDescriptionInit, message.payload, &buffLen)); + + message.version = SIGNALING_MESSAGE_CURRENT_VERSION; + message.messageType = SIGNALING_MESSAGE_TYPE_ANSWER; + STRNCPY(message.peerClientId, pSampleStreamingSession->peerId, MAX_SIGNALING_CLIENT_ID_LEN); + message.payloadLen = (UINT32) STRLEN(message.payload); + // SNPRINTF appends null terminator, so we do not manually add it + SNPRINTF(message.correlationId, MAX_CORRELATION_ID_LEN, "%llu_%llu", GETTIME(), ATOMIC_INCREMENT(&pSampleStreamingSession->correlationIdPostFix)); + DLOGD("Responding With Answer With correlationId: %s", message.correlationId); + CHK_STATUS(sendSignalingMessage(pSampleStreamingSession, &message)); + +CleanUp: + + CHK_LOG_ERR(retStatus); + return retStatus; +} + +VOID onIceCandidateHandler(UINT64 customData, PCHAR candidateJson) +{ + STATUS retStatus = STATUS_SUCCESS; + PSampleStreamingSession pSampleStreamingSession = (PSampleStreamingSession) customData; + SignalingMessage message; + + CHK(pSampleStreamingSession != NULL, STATUS_NULL_ARG); + + if (candidateJson == NULL) { + DLOGD("ice candidate gathering finished"); + ATOMIC_STORE_BOOL(&pSampleStreamingSession->candidateGatheringDone, TRUE); + + // if application is master and non-trickle ice, send answer now. + if (pSampleStreamingSession->pSampleConfiguration->channelInfo.channelRoleType == SIGNALING_CHANNEL_ROLE_TYPE_MASTER && + !pSampleStreamingSession->remoteCanTrickleIce) { + CHK_STATUS(createAnswer(pSampleStreamingSession->pPeerConnection, &pSampleStreamingSession->answerSessionDescriptionInit)); + CHK_STATUS(respondWithAnswer(pSampleStreamingSession)); + } else if (pSampleStreamingSession->pSampleConfiguration->channelInfo.channelRoleType == SIGNALING_CHANNEL_ROLE_TYPE_VIEWER && + !pSampleStreamingSession->pSampleConfiguration->trickleIce) { + CVAR_BROADCAST(pSampleStreamingSession->pSampleConfiguration->cvar); + } + + } else if (pSampleStreamingSession->remoteCanTrickleIce && ATOMIC_LOAD_BOOL(&pSampleStreamingSession->peerIdReceived)) { + message.version = SIGNALING_MESSAGE_CURRENT_VERSION; + message.messageType = SIGNALING_MESSAGE_TYPE_ICE_CANDIDATE; + STRNCPY(message.peerClientId, pSampleStreamingSession->peerId, MAX_SIGNALING_CLIENT_ID_LEN); + message.payloadLen = (UINT32) STRNLEN(candidateJson, MAX_SIGNALING_MESSAGE_LEN); + STRNCPY(message.payload, candidateJson, message.payloadLen); + message.correlationId[0] = '\0'; + CHK_STATUS(sendSignalingMessage(pSampleStreamingSession, &message)); + } + +CleanUp: + + CHK_LOG_ERR(retStatus); +} + +STATUS submitPendingIceCandidate(PPendingMessageQueue pPendingMessageQueue, PSampleStreamingSession pSampleStreamingSession) +{ + STATUS retStatus = STATUS_SUCCESS; + BOOL noPendingSignalingMessageForClient = FALSE; + PReceivedSignalingMessage pReceivedSignalingMessage = NULL; + UINT64 hashValue; + + CHK(pPendingMessageQueue != NULL && pPendingMessageQueue->messageQueue != NULL && pSampleStreamingSession != NULL, STATUS_NULL_ARG); + + do { + CHK_STATUS(stackQueueIsEmpty(pPendingMessageQueue->messageQueue, &noPendingSignalingMessageForClient)); + if (!noPendingSignalingMessageForClient) { + hashValue = 0; + CHK_STATUS(stackQueueDequeue(pPendingMessageQueue->messageQueue, &hashValue)); + pReceivedSignalingMessage = (PReceivedSignalingMessage) hashValue; + CHK(pReceivedSignalingMessage != NULL, STATUS_INTERNAL_ERROR); + if (pReceivedSignalingMessage->signalingMessage.messageType == SIGNALING_MESSAGE_TYPE_ICE_CANDIDATE) { + CHK_STATUS(handleRemoteCandidate(pSampleStreamingSession, &pReceivedSignalingMessage->signalingMessage)); + } + SAFE_MEMFREE(pReceivedSignalingMessage); + } + } while (!noPendingSignalingMessageForClient); + + CHK_STATUS(freeMessageQueue(pPendingMessageQueue)); + +CleanUp: + + SAFE_MEMFREE(pReceivedSignalingMessage); + CHK_LOG_ERR(retStatus); + return retStatus; +} + +STATUS signalingMessageReceived(UINT64 customData, PReceivedSignalingMessage pReceivedSignalingMessage) +{ + STATUS retStatus = STATUS_SUCCESS; + PSampleConfiguration pSampleConfiguration = (PSampleConfiguration) customData; + BOOL peerConnectionFound = FALSE, locked = FALSE, startStats = FALSE, freeStreamingSession = FALSE; + UINT32 clientIdHash; + UINT64 hashValue = 0; + PPendingMessageQueue pPendingMessageQueue = NULL; + PSampleStreamingSession pSampleStreamingSession = NULL; + PReceivedSignalingMessage pReceivedSignalingMessageCopy = NULL; + + CHK(pSampleConfiguration != NULL, STATUS_NULL_ARG); + + MUTEX_LOCK(pSampleConfiguration->sampleConfigurationObjLock); + locked = TRUE; + + clientIdHash = COMPUTE_CRC32((PBYTE) pReceivedSignalingMessage->signalingMessage.peerClientId, + (UINT32) STRLEN(pReceivedSignalingMessage->signalingMessage.peerClientId)); + CHK_STATUS(hashTableContains(pSampleConfiguration->pRtcPeerConnectionForRemoteClient, clientIdHash, &peerConnectionFound)); + if (peerConnectionFound) { + CHK_STATUS(hashTableGet(pSampleConfiguration->pRtcPeerConnectionForRemoteClient, clientIdHash, &hashValue)); + pSampleStreamingSession = (PSampleStreamingSession) hashValue; + } + + switch (pReceivedSignalingMessage->signalingMessage.messageType) { + case SIGNALING_MESSAGE_TYPE_OFFER: + // Check if we already have an ongoing master session with the same peer + CHK_ERR(!peerConnectionFound, STATUS_INVALID_OPERATION, "Peer connection %s is in progress", + pReceivedSignalingMessage->signalingMessage.peerClientId); + + /* + * Create new streaming session for each offer, then insert the client id and streaming session into + * pRtcPeerConnectionForRemoteClient for subsequent ice candidate messages. Lastly check if there is + * any ice candidate messages queued in pPendingSignalingMessageForRemoteClient. If so then submit + * all of them. + */ + + if (pSampleConfiguration->streamingSessionCount == ARRAY_SIZE(pSampleConfiguration->sampleStreamingSessionList)) { + DLOGW("Max simultaneous streaming session count reached."); + + // Need to remove the pending queue if any. + // This is a simple optimization as the session cleanup will + // handle the cleanup of pending message queue after a while + CHK_STATUS(getPendingMessageQueueForHash(pSampleConfiguration->pPendingSignalingMessageForRemoteClient, clientIdHash, TRUE, + &pPendingMessageQueue)); + + CHK(FALSE, retStatus); + } + CHK_STATUS(createSampleStreamingSession(pSampleConfiguration, pReceivedSignalingMessage->signalingMessage.peerClientId, TRUE, + &pSampleStreamingSession)); + freeStreamingSession = TRUE; + CHK_STATUS(handleOffer(pSampleConfiguration, pSampleStreamingSession, &pReceivedSignalingMessage->signalingMessage)); + CHK_STATUS(hashTablePut(pSampleConfiguration->pRtcPeerConnectionForRemoteClient, clientIdHash, (UINT64) pSampleStreamingSession)); + + // If there are any ice candidate messages in the queue for this client id, submit them now. + CHK_STATUS(getPendingMessageQueueForHash(pSampleConfiguration->pPendingSignalingMessageForRemoteClient, clientIdHash, TRUE, + &pPendingMessageQueue)); + if (pPendingMessageQueue != NULL) { + CHK_STATUS(submitPendingIceCandidate(pPendingMessageQueue, pSampleStreamingSession)); + + // NULL the pointer to avoid it being freed in the cleanup + pPendingMessageQueue = NULL; + } + + MUTEX_LOCK(pSampleConfiguration->streamingSessionListReadLock); + pSampleConfiguration->sampleStreamingSessionList[pSampleConfiguration->streamingSessionCount++] = pSampleStreamingSession; + MUTEX_UNLOCK(pSampleConfiguration->streamingSessionListReadLock); + freeStreamingSession = FALSE; + + startStats = pSampleConfiguration->iceCandidatePairStatsTimerId == MAX_UINT32; + break; + + case SIGNALING_MESSAGE_TYPE_ANSWER: + /* + * for viewer, pSampleStreamingSession should've already been created. insert the client id and + * streaming session into pRtcPeerConnectionForRemoteClient for subsequent ice candidate messages. + * Lastly check if there is any ice candidate messages queued in pPendingSignalingMessageForRemoteClient. + * If so then submit all of them. + */ + pSampleStreamingSession = pSampleConfiguration->sampleStreamingSessionList[0]; + CHK_STATUS(handleAnswer(pSampleConfiguration, pSampleStreamingSession, &pReceivedSignalingMessage->signalingMessage)); + CHK_STATUS(hashTablePut(pSampleConfiguration->pRtcPeerConnectionForRemoteClient, clientIdHash, (UINT64) pSampleStreamingSession)); + + // If there are any ice candidate messages in the queue for this client id, submit them now. + CHK_STATUS(getPendingMessageQueueForHash(pSampleConfiguration->pPendingSignalingMessageForRemoteClient, clientIdHash, TRUE, + &pPendingMessageQueue)); + if (pPendingMessageQueue != NULL) { + CHK_STATUS(submitPendingIceCandidate(pPendingMessageQueue, pSampleStreamingSession)); + + // NULL the pointer to avoid it being freed in the cleanup + pPendingMessageQueue = NULL; + } + + startStats = pSampleConfiguration->iceCandidatePairStatsTimerId == MAX_UINT32; + CHK_STATUS(signalingClientGetMetrics(pSampleConfiguration->signalingClientHandle, &pSampleConfiguration->signalingClientMetrics)); + DLOGP("[Signaling offer sent to answer received time] %" PRIu64 " ms", + pSampleConfiguration->signalingClientMetrics.signalingClientStats.offerToAnswerTime); + break; + + case SIGNALING_MESSAGE_TYPE_ICE_CANDIDATE: + /* + * if peer connection hasn't been created, create an queue to store the ice candidate message. Otherwise + * submit the signaling message into the corresponding streaming session. + */ + if (!peerConnectionFound) { + CHK_STATUS(getPendingMessageQueueForHash(pSampleConfiguration->pPendingSignalingMessageForRemoteClient, clientIdHash, FALSE, + &pPendingMessageQueue)); + if (pPendingMessageQueue == NULL) { + CHK_STATUS(createMessageQueue(clientIdHash, &pPendingMessageQueue)); + CHK_STATUS(stackQueueEnqueue(pSampleConfiguration->pPendingSignalingMessageForRemoteClient, (UINT64) pPendingMessageQueue)); + } + + pReceivedSignalingMessageCopy = (PReceivedSignalingMessage) MEMCALLOC(1, SIZEOF(ReceivedSignalingMessage)); + + *pReceivedSignalingMessageCopy = *pReceivedSignalingMessage; + + CHK_STATUS(stackQueueEnqueue(pPendingMessageQueue->messageQueue, (UINT64) pReceivedSignalingMessageCopy)); + + // NULL the pointers to not free any longer + pPendingMessageQueue = NULL; + pReceivedSignalingMessageCopy = NULL; + } else { + CHK_STATUS(handleRemoteCandidate(pSampleStreamingSession, &pReceivedSignalingMessage->signalingMessage)); + } + break; + + default: + DLOGD("Unhandled signaling message type %u", pReceivedSignalingMessage->signalingMessage.messageType); + break; + } + + MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); + locked = FALSE; + + if (pSampleConfiguration->enableIceStats && startStats && + STATUS_FAILED(retStatus = timerQueueAddTimer(pSampleConfiguration->timerQueueHandle, SAMPLE_STATS_DURATION, SAMPLE_STATS_DURATION, + getIceCandidatePairStatsCallback, (UINT64) pSampleConfiguration, + &pSampleConfiguration->iceCandidatePairStatsTimerId))) { + DLOGW("Failed to add getIceCandidatePairStatsCallback to add to timer queue (code 0x%08x). " + "Cannot pull ice candidate pair metrics periodically", + retStatus); + + // Reset the returned status + retStatus = STATUS_SUCCESS; + } + +CleanUp: + + SAFE_MEMFREE(pReceivedSignalingMessageCopy); + if (pPendingMessageQueue != NULL) { + freeMessageQueue(pPendingMessageQueue); + } + + if (freeStreamingSession && pSampleStreamingSession != NULL) { + freeSampleStreamingSession(&pSampleStreamingSession); + } + + if (locked) { + MUTEX_UNLOCK(pSampleConfiguration->sampleConfigurationObjLock); + } + + CHK_LOG_ERR(retStatus); + return retStatus; +} + +STATUS handleRemoteCandidate(PSampleStreamingSession pSampleStreamingSession, PSignalingMessage pSignalingMessage) +{ + STATUS retStatus = STATUS_SUCCESS; + RtcIceCandidateInit iceCandidate; + CHK(pSampleStreamingSession != NULL && pSignalingMessage != NULL, STATUS_NULL_ARG); + + CHK_STATUS(deserializeRtcIceCandidateInit(pSignalingMessage->payload, pSignalingMessage->payloadLen, &iceCandidate)); + CHK_STATUS(addIceCandidate(pSampleStreamingSession->pPeerConnection, iceCandidate.candidate)); + +CleanUp: + + CHK_LOG_ERR(retStatus); + return retStatus; +} diff --git a/samples/lib/Utility.c b/samples/lib/Utility.c new file mode 100644 index 0000000000..448c89c5ea --- /dev/null +++ b/samples/lib/Utility.c @@ -0,0 +1,230 @@ +#include "../Samples.h" + +STATUS readFrameFromDisk(PBYTE pFrame, PUINT32 pSize, PCHAR frameFilePath) +{ + STATUS retStatus = STATUS_SUCCESS; + UINT64 size = 0; + CHK_ERR(pSize != NULL, STATUS_NULL_ARG, "[KVS Master] Invalid file size"); + size = *pSize; + // Get the size and read into frame + CHK_STATUS(readFile(frameFilePath, TRUE, pFrame, &size)); +CleanUp: + + if (pSize != NULL) { + *pSize = (UINT32) size; + } + + return retStatus; +} + +UINT32 setLogLevel() +{ + PCHAR pLogLevel; + UINT32 logLevel = LOG_LEVEL_DEBUG; + if (NULL == (pLogLevel = GETENV(DEBUG_LOG_LEVEL_ENV_VAR)) || STATUS_SUCCESS != STRTOUI32(pLogLevel, NULL, 10, &logLevel) || + logLevel < LOG_LEVEL_VERBOSE || logLevel > LOG_LEVEL_SILENT) { + logLevel = LOG_LEVEL_WARN; + } + SET_LOGGER_LOG_LEVEL(logLevel); + return logLevel; +} + +STATUS createMessageQueue(UINT64 hashValue, PPendingMessageQueue* ppPendingMessageQueue) +{ + STATUS retStatus = STATUS_SUCCESS; + PPendingMessageQueue pPendingMessageQueue = NULL; + + CHK(ppPendingMessageQueue != NULL, STATUS_NULL_ARG); + + CHK(NULL != (pPendingMessageQueue = (PPendingMessageQueue) MEMCALLOC(1, SIZEOF(PendingMessageQueue))), STATUS_NOT_ENOUGH_MEMORY); + pPendingMessageQueue->hashValue = hashValue; + pPendingMessageQueue->createTime = GETTIME(); + CHK_STATUS(stackQueueCreate(&pPendingMessageQueue->messageQueue)); + +CleanUp: + + if (STATUS_FAILED(retStatus) && pPendingMessageQueue != NULL) { + freeMessageQueue(pPendingMessageQueue); + pPendingMessageQueue = NULL; + } + + if (ppPendingMessageQueue != NULL) { + *ppPendingMessageQueue = pPendingMessageQueue; + } + + return retStatus; +} + +STATUS freeMessageQueue(PPendingMessageQueue pPendingMessageQueue) +{ + STATUS retStatus = STATUS_SUCCESS; + + // free is idempotent + CHK(pPendingMessageQueue != NULL, retStatus); + + if (pPendingMessageQueue->messageQueue != NULL) { + stackQueueClear(pPendingMessageQueue->messageQueue, TRUE); + stackQueueFree(pPendingMessageQueue->messageQueue); + } + + MEMFREE(pPendingMessageQueue); + +CleanUp: + return retStatus; +} + +STATUS getPendingMessageQueueForHash(PStackQueue pPendingQueue, UINT64 clientHash, BOOL remove, PPendingMessageQueue* ppPendingMessageQueue) +{ + STATUS retStatus = STATUS_SUCCESS; + PPendingMessageQueue pPendingMessageQueue = NULL; + StackQueueIterator iterator; + BOOL iterate = TRUE; + UINT64 data; + + CHK(pPendingQueue != NULL && ppPendingMessageQueue != NULL, STATUS_NULL_ARG); + + CHK_STATUS(stackQueueGetIterator(pPendingQueue, &iterator)); + while (iterate && IS_VALID_ITERATOR(iterator)) { + CHK_STATUS(stackQueueIteratorGetItem(iterator, &data)); + CHK_STATUS(stackQueueIteratorNext(&iterator)); + + pPendingMessageQueue = (PPendingMessageQueue) data; + + if (clientHash == pPendingMessageQueue->hashValue) { + *ppPendingMessageQueue = pPendingMessageQueue; + iterate = FALSE; + + // Check if the item needs to be removed + if (remove) { + // This is OK to do as we are terminating the iterator anyway + CHK_STATUS(stackQueueRemoveItem(pPendingQueue, data)); + } + } + } +CleanUp: + return retStatus; +} + +STATUS removeExpiredMessageQueues(PStackQueue pPendingQueue) +{ + STATUS retStatus = STATUS_SUCCESS; + PPendingMessageQueue pPendingMessageQueue = NULL; + UINT32 i, count; + UINT64 data, curTime; + + CHK(pPendingQueue != NULL, STATUS_NULL_ARG); + + curTime = GETTIME(); + CHK_STATUS(stackQueueGetCount(pPendingQueue, &count)); + + // Dequeue and enqueue in order to not break the iterator while removing an item + for (i = 0; i < count; i++) { + CHK_STATUS(stackQueueDequeue(pPendingQueue, &data)); + + // Check for expiry + pPendingMessageQueue = (PPendingMessageQueue) data; + if (pPendingMessageQueue->createTime + SAMPLE_PENDING_MESSAGE_CLEANUP_DURATION < curTime) { + // Message queue has expired and needs to be freed + CHK_STATUS(freeMessageQueue(pPendingMessageQueue)); + } else { + // Enqueue back again as it's still valued + CHK_STATUS(stackQueueEnqueue(pPendingQueue, data)); + } + } +CleanUp: + return retStatus; +} + +STATUS traverseDirectoryPEMFileScan(UINT64 customData, DIR_ENTRY_TYPES entryType, PCHAR fullPath, PCHAR fileName) +{ + UNUSED_PARAM(entryType); + UNUSED_PARAM(fullPath); + + PCHAR certName = (PCHAR) customData; + UINT32 fileNameLen = STRLEN(fileName); + + if (fileNameLen > ARRAY_SIZE(CA_CERT_PEM_FILE_EXTENSION) + 1 && + (STRCMPI(CA_CERT_PEM_FILE_EXTENSION, &fileName[fileNameLen - ARRAY_SIZE(CA_CERT_PEM_FILE_EXTENSION) + 1]) == 0)) { + certName[0] = FPATHSEPARATOR; + certName++; + STRCPY(certName, fileName); + } + + return STATUS_SUCCESS; +} + +STATUS lookForSslCert(PSampleConfiguration* ppSampleConfiguration) +{ + STATUS retStatus = STATUS_SUCCESS; + struct stat pathStat; + CHAR certName[MAX_PATH_LEN]; + PSampleConfiguration pSampleConfiguration = *ppSampleConfiguration; + + MEMSET(certName, 0x0, ARRAY_SIZE(certName)); + pSampleConfiguration->pCaCertPath = GETENV(CACERT_PATH_ENV_VAR); + + // if ca cert path is not set from the environment, try to use the one that cmake detected + if (pSampleConfiguration->pCaCertPath == NULL) { + CHK_ERR(STRNLEN(DEFAULT_KVS_CACERT_PATH, MAX_PATH_LEN) > 0, STATUS_INVALID_OPERATION, "No ca cert path given (error:%s)", strerror(errno)); + pSampleConfiguration->pCaCertPath = DEFAULT_KVS_CACERT_PATH; + } else { + // Check if the environment variable is a path + CHK(0 == FSTAT(pSampleConfiguration->pCaCertPath, &pathStat), STATUS_DIRECTORY_ENTRY_STAT_ERROR); + + if (S_ISDIR(pathStat.st_mode)) { + CHK_STATUS(traverseDirectory(pSampleConfiguration->pCaCertPath, (UINT64) &certName, /* iterate */ FALSE, traverseDirectoryPEMFileScan)); + + if (certName[0] != 0x0) { + STRCAT(pSampleConfiguration->pCaCertPath, certName); + } else { + DLOGW("Cert not found in path set...checking if CMake detected a path\n"); + CHK_ERR(STRNLEN(DEFAULT_KVS_CACERT_PATH, MAX_PATH_LEN) > 0, STATUS_INVALID_OPERATION, "No ca cert path given (error:%s)", + strerror(errno)); + DLOGI("CMake detected cert path\n"); + pSampleConfiguration->pCaCertPath = DEFAULT_KVS_CACERT_PATH; + } + } + } + +CleanUp: + + CHK_LOG_ERR(retStatus); + return retStatus; +} + +STATUS setUpCredentialProvider(PSampleConfiguration pSampleConfiguration, BOOL useIot) +{ + STATUS retStatus = STATUS_SUCCESS; + PCHAR pAccessKey, pSecretKey, pSessionToken; + PCHAR pIotCoreCredentialEndPoint, pIotCoreCert, pIotCorePrivateKey, pIotCoreRoleAlias, pIotCoreThingName; + CHK_ERR(pSampleConfiguration != NULL, STATUS_NULL_ARG, "Set up pSampleConfiguration first by invoking createSampleConfiguration"); + + pSampleConfiguration->useIot = useIot; + CHK_WARN(pSampleConfiguration->pCredentialProvider == NULL, STATUS_SUCCESS, "Credential provider already set, nothing to do"); + + CHK_STATUS(lookForSslCert(&pSampleConfiguration)); + pSampleConfiguration->channelInfo.pCertPath = pSampleConfiguration->pCaCertPath; + if (useIot) { + DLOGI("USE IOT Credential provider"); + CHK_ERR((pIotCoreCredentialEndPoint = GETENV(IOT_CORE_CREDENTIAL_ENDPOINT)) != NULL, STATUS_INVALID_OPERATION, + "AWS_IOT_CORE_CREDENTIAL_ENDPOINT must be set"); + CHK_ERR((pIotCoreCert = GETENV(IOT_CORE_CERT)) != NULL, STATUS_INVALID_OPERATION, "AWS_IOT_CORE_CERT must be set"); + CHK_ERR((pIotCorePrivateKey = GETENV(IOT_CORE_PRIVATE_KEY)) != NULL, STATUS_INVALID_OPERATION, "AWS_IOT_CORE_PRIVATE_KEY must be set"); + CHK_ERR((pIotCoreRoleAlias = GETENV(IOT_CORE_ROLE_ALIAS)) != NULL, STATUS_INVALID_OPERATION, "AWS_IOT_CORE_ROLE_ALIAS must be set"); + CHK_ERR((pIotCoreThingName = GETENV(IOT_CORE_THING_NAME)) != NULL, STATUS_INVALID_OPERATION, "AWS_IOT_CORE_THING_NAME must be set"); + CHK_STATUS(createLwsIotCredentialProvider(pIotCoreCredentialEndPoint, pIotCoreCert, pIotCorePrivateKey, pSampleConfiguration->pCaCertPath, + pIotCoreRoleAlias, pIotCoreThingName, &pSampleConfiguration->pCredentialProvider)); + } else { + CHK_ERR((pAccessKey = GETENV(ACCESS_KEY_ENV_VAR)) != NULL, STATUS_INVALID_OPERATION, "AWS_ACCESS_KEY_ID must be set"); + CHK_ERR((pSecretKey = GETENV(SECRET_KEY_ENV_VAR)) != NULL, STATUS_INVALID_OPERATION, "AWS_SECRET_ACCESS_KEY must be set"); + pSessionToken = GETENV(SESSION_TOKEN_ENV_VAR); + if (pSessionToken != NULL && IS_EMPTY_STRING(pSessionToken)) { + DLOGW("Session token is set but its value is empty. Ignoring."); + pSessionToken = NULL; + } + CHK_STATUS( + createStaticCredentialProvider(pAccessKey, 0, pSecretKey, 0, pSessionToken, 0, MAX_UINT64, &pSampleConfiguration->pCredentialProvider)); + } +CleanUp: + return retStatus; +} diff --git a/scripts/cert_setup.sh b/scripts/cert_setup.sh new file mode 100755 index 0000000000..36efdc5a41 --- /dev/null +++ b/scripts/cert_setup.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +prefix=$1 +thingName="${prefix}_thing" +thingTypeName="${prefix}_thing_type" +iotPolicyName="${prefix}_policy" +kvsPolicyName="${prefix}_policy" +iotRoleName="${prefix}_role" +iotRoleAlias="${prefix}_role_alias" +iotCert="${prefix}_certificate.pem" +iotPublicKey="${prefix}_public.key" +iotPrivateKey="${prefix}_private.key" + +# Create the certificate to which you must attach the policy for IoT that you created above. +aws --profile default iot create-keys-and-certificate --set-as-active --certificate-pem-outfile $iotCert --public-key-outfile $iotPublicKey --private-key-outfile $iotPrivateKey > certificate +# Attach the policy for IoT (KvsCameraIoTPolicy created above) to this certificate. +aws --profile default iot attach-policy --policy-name $iotPolicyName --target $(jq --raw-output '.certificateArn' certificate) +# Attach your IoT thing (kvs_example_camera_stream) to the certificate you just created: +aws --profile default iot attach-thing-principal --thing-name $thingName --principal $(jq --raw-output '.certificateArn' certificate) +# In order to authorize requests through the IoT credentials provider, you need the IoT credentials endpoint which is unique to your AWS account ID. You can use the following command to get the IoT credentials endpoint. +aws --profile default iot describe-endpoint --endpoint-type iot:CredentialProvider --output text > iot-credential-provider.txt +# In addition to the X.509 cerficiate created above, you must also have a CA certificate to establish trust with the back-end service through TLS. You can get the CA certificate using the following command: +curl --silent 'https://www.amazontrust.com/repository/SFSRootCAG2.pem' --output cacert.pem \ No newline at end of file diff --git a/scripts/generate-iot-credential.sh b/scripts/generate-iot-credential.sh index ae3b3c720b..fb12dc4ca7 100755 --- a/scripts/generate-iot-credential.sh +++ b/scripts/generate-iot-credential.sh @@ -2,15 +2,17 @@ # You need to setup your aws cli first, because this script is based on aws cli. # You can use this script to setup environment variables in the shell which samples run on. # https://docs.aws.amazon.com/kinesisvideostreams/latest/dg/how-iot.html -thingName="webrtc_iot_thing" -thingTypeName="webrtc_iot_thing_type" -iotPolicyName="webrtc_iot_policy" -kvsPolicyName="webrtc_policy" -iotRoleName="webrtc_iot_role" -iotRoleAlias="webrtc_iot_role_alias" -iotCert="webrtc_iot_certifcate.pem" -iotPublicKey="webrtc_iot_public.key" -iotPrivateKey="webrtc_iot_private.key" + +prefix=$1 +thingName="${prefix}_thing" +thingTypeName="${prefix}_thing_type" +iotPolicyName="${prefix}_policy" +kvsPolicyName="${prefix}_policy" +iotRoleName="${prefix}_role" +iotRoleAlias="${prefix}_role_alias" +iotCert="${prefix}_certificate.pem" +iotPublicKey="${prefix}_public.key" +iotPrivateKey="${prefix}_private.key" # Step 1: Create an IoT Thing Type and an IoT Thing # The following example command creates a thing type $thingTypeName @@ -65,43 +67,23 @@ cat > iot-policy-document.json < certificate -# Attach the policy for IoT (KvsCameraIoTPolicy created above) to this certificate. -aws --profile default iot attach-policy --policy-name $iotPolicyName --target $(jq --raw-output '.certificateArn' certificate) -# Attach your IoT thing (kvs_example_camera_stream) to the certificate you just created: -aws --profile default iot attach-thing-principal --thing-name $thingName --principal $(jq --raw-output '.certificateArn' certificate) -# In order to authorize requests through the IoT credentials provider, you need the IoT credentials endpoint which is unique to your AWS account ID. You can use the following command to get the IoT credentials endpoint. -aws --profile default iot describe-endpoint --endpoint-type iot:CredentialProvider --output text > iot-credential-provider.txt -# In addition to the X.509 cerficiate created above, you must also have a CA certificate to establish trust with the back-end service through TLS. You can get the CA certificate using the following command: -curl --silent 'https://www.amazontrust.com/repository/SFSRootCAG2.pem' --output cacert.pem - - -export AWS_IOT_CORE_CREDENTIAL_ENDPOINT=$(cat iot-credential-provider.txt) -export AWS_IOT_CORE_CERT=$(pwd)"/"$iotCert -export AWS_IOT_CORE_PRIVATE_KEY=$(pwd)"/"$iotPrivateKey -export AWS_IOT_CORE_ROLE_ALIAS=$iotRoleAlias -export AWS_IOT_CORE_THING_NAME=$thingName - +aws --profile default iot create-policy --policy-name $iotPolicyName --policy-document 'file://iot-policy-document.json' diff --git a/src/source/Ice/TurnConnection.c b/src/source/Ice/TurnConnection.c index a939ee6530..2d9fda4fcf 100644 --- a/src/source/Ice/TurnConnection.c +++ b/src/source/Ice/TurnConnection.c @@ -1032,7 +1032,6 @@ STATUS checkTurnPeerConnections(PTurnConnection pTurnConnection) PStunAttributeAddress pStunAttributeAddress = NULL; PStunAttributeChannelNumber pStunAttributeChannelNumber = NULL; UINT32 i = 0; - UNUSED_PARAM(sendStatus); // turn mutex is assumed to be locked. CHK(pTurnConnection != NULL, STATUS_NULL_ARG); diff --git a/src/source/Sctp/Sctp.c b/src/source/Sctp/Sctp.c index 1043987276..c9fd117d7d 100644 --- a/src/source/Sctp/Sctp.c +++ b/src/source/Sctp/Sctp.c @@ -365,7 +365,6 @@ INT32 onSctpInboundPacket(struct socket* sock, union sctp_sockstore addr, PVOID if (data != NULL) { free(data); } - if (STATUS_FAILED(retStatus)) { return -1; }