// Copyright Epic Games, Inc. All Rights Reserved. #include "IbmLiveStreaming.h" #include "HttpModule.h" #include "Interfaces/IHttpResponse.h" #include "Serialization/JsonReader.h" #include "Dom/JsonObject.h" #include "Serialization/JsonSerializer.h" #include "Misc/ScopeLock.h" #include "VideoRecordingSystem.h" #include "Algo/FindSequence.h" #if defined(WITH_IBMRTMPINGEST) && LIVESTREAMING DEFINE_LOG_CATEGORY(IbmLiveStreaming); extern "C" { // rtmp_c_static.lib needs us to define this symbol char* build_number = "1"; } // From IBM's sample typedef enum _AACPacketType { AACSequenceHeader, AACRaw } AACPacketType; FAutoConsoleCommand LiveStreamingStart( TEXT("LiveStreaming.Start"), TEXT("Starts live streaming gameplay to IBM Cloud Video"), FConsoleCommandDelegate::CreateStatic(&FIbmLiveStreaming::StartCmd) ); FAutoConsoleCommand LiveStreamingStop( TEXT("LiveStreaming.Stop"), TEXT("Stops live streaming"), FConsoleCommandDelegate::CreateStatic(&FIbmLiveStreaming::StopCmd) ); static TAutoConsoleVariable CVarLiveStreamingClientId( TEXT("LiveStreaming.ClientId"), TEXT("76898b5ebb31c697023fccb47a3e7a7eb6aacb83"), TEXT("IBMCloudVideo ClientId to use for live streaming")); static TAutoConsoleVariable CVarLiveStreamingClientSecret( TEXT("LiveStreaming.ClientSecret"), TEXT("11b3d99c37f85943224d710f037f2c5e8c8fe673"), TEXT("IBMCloudVideo ClientSecret to use for live streaming")); static TAutoConsoleVariable CVarLiveStreamingChannel( TEXT("LiveStreaming.Channel"), TEXT("23619107"), TEXT("IBMCloudVideo Channel to use for live streaming")); static TAutoConsoleVariable CVarLiveStreamingMaxBitrate( TEXT("LiveStreaming.MaxBitrate"), 30, TEXT("LiveStreaming: max allowed bitrate, in Mbps")); static TAutoConsoleVariable CVarLiveStreamingSpaceToEmptyQueue( TEXT("LiveStreaming.SpaceToEmptyQueue"), 0.8, TEXT("LiveStreaming: factor to set bitrate a bit lower than available b/w to let IBMRTMP lib to empty the queue")); static TAutoConsoleVariable CVarLiveStreamingNonCongestedBitrateIncrease( TEXT("LiveStreaming.NonCongestedBitrateIncrease"), 1.2, TEXT("LiveStreaming: rate of growth of non-congested bitrate")); static TAutoConsoleVariable CVarLiveStreamingBitrateThresholdToSwitchFPS( TEXT("LiveStreaming.BitrateThresholdToSwitchFPS"), 15, TEXT("LiveStreaming: bitrate threshold to switch to lower FPS, in Mbps")); static TAutoConsoleVariable CVarLiveStreamingDownscaledFPS( TEXT("LiveStreaming.DownscaledFPS"), 30, TEXT("LiveStreaming: framerate to switch if poor uplink is detected")); static TAutoConsoleVariable CVarLiveStreamingBitrateProximityTolerance( TEXT("LiveStreaming.BitrateProximityTolerance"), 1.2, TEXT("LiveStreaming: How close should the bitrate be to the reported bandwidth, to be recognized as fulfilling the available bandwidth")); // // FIbmLiveStreaming::FFormData // FIbmLiveStreaming::FFormData::FFormData() { // To understand the need of the Boundary string, see https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html BoundaryLabel = FString(TEXT("e543322540af456f9a3773049ca02529-")) + FString::FromInt(FMath::Rand()); BoundaryBegin = FString(TEXT("--")) + BoundaryLabel + FString(TEXT("\r\n")); BoundaryEnd = FString(TEXT("\r\n--")) + BoundaryLabel + FString(TEXT("--\r\n")); } void FIbmLiveStreaming::FFormData::AddField(const FString& Name, const FString& Value) { Data += FString(TEXT("\r\n")) + BoundaryBegin + FString(TEXT("Content-Disposition: form-data; name=\"")) + Name + FString(TEXT("\"\r\n\r\n")) + Value; } // Convert FString to UTF8 and put it in a TArray TArray FIbmLiveStreaming::FFormData::FStringToUint8(const FString& InString) { TArray OutBytes; // Handle empty strings if (InString.Len() > 0) { FTCHARToUTF8 Converted(*InString); // Convert to UTF8 OutBytes.Append(reinterpret_cast(Converted.Get()), Converted.Length()); } return OutBytes; } TSharedRef FIbmLiveStreaming::FFormData::CreateHttpPostRequestImpl(const FString& URL) { TSharedRef HttpRequest = FHttpModule::Get().CreateRequest(); TArray CombinedContent; CombinedContent.Append(FStringToUint8(Data)); //CombinedContent.Append(MultiPartContent); CombinedContent.Append(FStringToUint8(BoundaryEnd)); HttpRequest->SetHeader(TEXT("Content-Type"), FString(TEXT("multipart/form-data; boundary=")) + BoundaryLabel); HttpRequest->SetURL(URL); HttpRequest->SetVerb(TEXT("POST")); HttpRequest->SetContent(CombinedContent); return HttpRequest; } // // FIbmLiveStreaming // FIbmLiveStreaming* FIbmLiveStreaming::Singleton = nullptr; FIbmLiveStreaming::FIbmLiveStreaming() { // // Create the RTMCData instance. // Ideally, we should be using rtmp_c_getinstance to query if the internal RTMPCData instance was set, // so thus initialize it or destroy it accordingly, but at the time of writing, rtmp_c_getinstance // logs an error if the internal instance is not set, so we avoid using rtmp_c_getinstance to keep // the logs clean. UE_LOG(IbmLiveStreaming, Log, TEXT("Creating rtmpc object")); RTMPCData* rtmp_c = rtmp_c_alloc(502, 1, 1, "Windows", "Windows device", TCHAR_TO_ANSI(*FGuid::NewGuid().ToString()), "en"); check(rtmp_c); rtmp_c->appFlavor = AppFlavorUstream; rtmp_c_start(rtmp_c); } FIbmLiveStreaming::~FIbmLiveStreaming() { Stop(); RTMPCData* rtmp_c = rtmp_c_getinstance(); check(rtmp_c); UE_LOG(IbmLiveStreaming, Verbose, TEXT("Stopping rtmp_c instance")); rtmp_c_stop(rtmp_c); UE_LOG(IbmLiveStreaming, Verbose, TEXT("Freeing rtmp_c instance")); rtmp_c_free(rtmp_c); } FIbmLiveStreaming* FIbmLiveStreaming::Get() { if (!Singleton) { Singleton = new FIbmLiveStreaming(); } return Singleton; } void FIbmLiveStreaming::CustomLogMsg(const char* Msg) { FString FinalMsg(Msg); // IBM RTMP Ingest appends a \n to the end of the string. We need to remove it so we don't have empty lines in our logs FinalMsg.TrimEndInline(); UE_LOG(IbmLiveStreaming, Verbose, TEXT("IBMRTMPIngest: %s"), *FinalMsg); } bool FIbmLiveStreaming::CheckState(const wchar_t* FuncName, EState ExpectedState) { if (State == ExpectedState) { return true; } else { UE_LOG(IbmLiveStreaming, Error, TEXT("%s: Wrong state (%d instead of %d)"), FuncName, static_cast(State.Load()), static_cast(ExpectedState)); return false; } } bool FIbmLiveStreaming::CheckState(const wchar_t* FuncName, EState ExpectedStateMin, EState ExpectedStateMax) { if (State >= ExpectedStateMin && State<=ExpectedStateMax) { return true; } else { UE_LOG(IbmLiveStreaming, Error, TEXT("%s: Wrong state (%d instead of [%d..%d])"), FuncName, static_cast(State.Load()), static_cast(ExpectedStateMin), static_cast(ExpectedStateMax)); return false; } } bool FIbmLiveStreaming::Start() { if (!CheckState(__FUNCTIONW__, EState::None)) { return false; } FGameplayMediaEncoder* MediaEncoder = FGameplayMediaEncoder::Get(); // First thing we need to do is call RegisterListener, which initializes FGameplayMediaEncoder if this is the first listener MediaEncoder->RegisterListener(this); // Get SampleRate and NumChannels uint32 AudioSampleRate, AudioNumChannels, AudioBitrate; TRefCountPtr AudioOutputType; verify(MediaEncoder->GetAudioOutputType(AudioOutputType)); CHECK_HR(AudioOutputType->GetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, &AudioSampleRate)); CHECK_HR(AudioOutputType->GetUINT32(MF_MT_AUDIO_NUM_CHANNELS, &AudioNumChannels)); CHECK_HR(AudioOutputType->GetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, &AudioBitrate)); if (CVarLiveStreamingClientId.GetValueOnAnyThread().IsEmpty() || CVarLiveStreamingClientSecret.GetValueOnAnyThread().IsEmpty() || CVarLiveStreamingChannel.GetValueOnAnyThread().IsEmpty()) { UE_LOG(IbmLiveStreaming, Error, TEXT("ClientId/ClientSecret/Channel not specified.")); return false; } return Start( CVarLiveStreamingClientId.GetValueOnAnyThread(), CVarLiveStreamingClientSecret.GetValueOnAnyThread(), CVarLiveStreamingChannel.GetValueOnAnyThread(), AudioSampleRate, AudioNumChannels, AudioBitrate); } bool FIbmLiveStreaming::Start(const FString& ClientId, const FString& ClientSecret, const FString& Channel, uint32 AudioSampleRate, uint32 AudioNumChannels, uint32 AudioBitrate) { MemoryCheckpoint("IbmLiveStreaming: Before start"); check(IsInGameThread()); if (!CheckState(__FUNCTIONW__, EState::None)) { return false; } // NOTE: Repeated calls to RegisterListener(this) are ignored FGameplayMediaEncoder::Get()->RegisterListener(this); if (!((AudioSampleRate == 44100 || AudioSampleRate == 48000) && (AudioNumChannels == 2))) { UE_LOG(IbmLiveStreaming, Error, TEXT("Unsupported audio settings")); return false; } custom_c_log_msg = &FIbmLiveStreaming::CustomLogMsg; AudioPacketsSent = 0; VideoPacketsSent = 0; Config.ClientId = ClientId; Config.ClientSecret = ClientSecret; Config.Channel = Channel; Config.AudioSampleRate = AudioSampleRate; Config.AudioNumChannels = AudioNumChannels; Config.AudioBitrate = AudioBitrate; UE_LOG(IbmLiveStreaming, Log, TEXT("Initializing with ClientId=%s, ClientSecret=%s, Channel=%s, AudioSampleRate=%d, AudioNumChannels=%d"), *ClientId, *ClientSecret, *Channel, AudioSampleRate, AudioNumChannels); FFormData FormData; FormData.AddField(TEXT("grant_type"), TEXT("client_credentials")); FormData.AddField(TEXT("client_id"), Config.ClientId); FormData.AddField(TEXT("client_secret"), Config.ClientSecret); FormData.AddField(TEXT("scope"), TEXT("broadcaster")); TSharedRef HttpRequest = FormData.CreateHttpPostRequest("https://www.ustream.tv/oauth2/token", this, &FIbmLiveStreaming::OnProcessRequestComplete); if (!HttpRequest->ProcessRequest()) { UE_LOG(IbmLiveStreaming, Error, TEXT("Failed to initialize request to get access token")); return false; } State = EState::GettingAccessToken; MemoryCheckpoint("IbmLiveStreaming: After start"); return true; } bool FIbmLiveStreaming::GetJsonField(const TSharedPtr& JsonObject, const FString& FieldName, FString& DestStr) { if (JsonObject->TryGetStringField(FieldName, DestStr)) { return true; } else { UE_LOG(IbmLiveStreaming, Error, TEXT("Json field '%s' not found or not a string"), *FieldName); return false; } } bool FIbmLiveStreaming::GetJsonField(const TSharedPtr& JsonObject, const FString& FieldName, const TSharedPtr*& DestObj) { if (JsonObject->TryGetObjectField(FieldName, DestObj)) { return true; } else { UE_LOG(IbmLiveStreaming, Error, TEXT("Json field '%s' not found or not an object"), *FieldName); return false; } } void FIbmLiveStreaming::OnProcessRequestComplete(FHttpRequestPtr SourceHttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { FString Content = HttpResponse->GetContentAsString(); EHttpResponseCodes::Type ResponseCode = (EHttpResponseCodes::Type) HttpResponse->GetResponseCode(); UE_LOG(IbmLiveStreaming, Verbose, TEXT("Ingest reply: Code %d, Content= %s"), (int)ResponseCode, *Content); if (!EHttpResponseCodes::IsOk(ResponseCode)) { UE_LOG(IbmLiveStreaming, Error, TEXT("Http Request failed.")); State = EState::None; return; } TSharedRef> JsonReader = TJsonReaderFactory::Create(Content); TSharedPtr JsonObject = MakeShareable(new FJsonObject()); if (!(FJsonSerializer::Deserialize(JsonReader, JsonObject) && JsonObject.IsValid())) { UE_LOG(IbmLiveStreaming, Error, TEXT("Failed to deserialize http request reply as json.")); State = EState::None; return; } if (State == EState::GettingAccessToken) { FString AccessToken, TokenType; if (!GetJsonField(JsonObject, TEXT("access_token"), AccessToken) || !GetJsonField(JsonObject, TEXT("token_type"), TokenType)) { State = EState::None; return; } UE_LOG(IbmLiveStreaming, Verbose, TEXT("Token: %s, Type:%s"), *AccessToken, *TokenType); // // Fire up the next stage's http request // TSharedRef HttpRequest = FHttpModule::Get().CreateRequest(); HttpRequest->OnProcessRequestComplete().BindRaw(this, &FIbmLiveStreaming::OnProcessRequestComplete); HttpRequest->SetURL(FString::Printf(TEXT("https://api.ustream.tv/channels/%s/ingest.json"), *Config.Channel)); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->SetHeader(TEXT("Authorization"), TokenType + " " + AccessToken); if (!HttpRequest->ProcessRequest()) { UE_LOG(IbmLiveStreaming, Error, TEXT("Failed to initialize IBMRTMP Ingest")); State = EState::None; return; } State = EState::GettingIngestSettings; } else if (State == EState::GettingIngestSettings) { const TSharedPtr* IngestObject; FString RtmpUrl; if (!GetJsonField(JsonObject, TEXT("ingest"), IngestObject) || !GetJsonField(*IngestObject, TEXT("rtmp_url"), RtmpUrl) || !GetJsonField(*IngestObject, TEXT("streaming_key"), Ingest.StreamingKey)) { State = EState::None; return; } UE_LOG(IbmLiveStreaming, Verbose, TEXT("rtmp_url: %s, streaming_key: %s"), *RtmpUrl, *Ingest.StreamingKey); TArray Tokens; RtmpUrl.ParseIntoArray(Tokens, TEXT("/")); if (Tokens.Num() != 4) { UE_LOG(IbmLiveStreaming, Error, TEXT("Failed to initialize IBMRTMP Ingest. 'rtmp_url' field doesn't have the expected format.")); State = EState::None; return; } Ingest.RtmpUrl.Host = Tokens[1]; Ingest.RtmpUrl.Application = Tokens[2]; Ingest.RtmpUrl.Channel = Tokens[3]; Ingest.RtmpUrl.ApplicationName = Ingest.RtmpUrl.Application + TEXT("/") + Ingest.RtmpUrl.Channel; Ingest.RtmpUrl.StreamNamePrefix = FString::FromInt(FMath::Rand() % 9000 + 1000); Ingest.RtmpUrl.StreamName = TEXT("broadcaster/live") + Ingest.RtmpUrl.StreamNamePrefix; Connect(); } } TAutoConsoleVariable CVarLiveStreamingBwMeasureWithEmptyQueue( TEXT("LiveStreaming.bwMeasureWithEmptyQueue"), 1000, TEXT("LiveStreaming: interval of reporting measured bandwdith when send queue is empty, in msecs")); TAutoConsoleVariable CVarLiveStreamingBwMeasureWithNotEmptyQueue( TEXT("LiveStreaming.bwMeasureWithNotEmptyQueue"), 300, TEXT("LiveStreaming: interval of reporting measured bandwdith when send queue is not empty, in msecs")); void FIbmLiveStreaming::Connect() { if (!CheckState(__FUNCTIONW__, EState::GettingIngestSettings)) { return ; } State = EState::Connecting; Ctx.Client = rtmpclient_alloc(); check(Ctx.Client); Ctx.Client->server = strcpy_alloc(TCHAR_TO_ANSI(*Ingest.RtmpUrl.Host)); // // Connect module // Ctx.ConnectModule = rtmpmodule_connect_create(Ctx.Client, TCHAR_TO_ANSI(*Ingest.RtmpUrl.ApplicationName)); check(Ctx.ConnectModule); Ctx.ConnectModule->rpin = strcpy_alloc(rtmp_c_getinstance()->rpin); Ctx.ConnectModule->streamingKey = strcpy_alloc(TCHAR_TO_ANSI(*Ingest.StreamingKey)); Ctx.ConnectModule->clientType = strcpy_alloc("broadcaster"); Ctx.ConnectModule->hlsCompatible = 1; Ctx.ConnectModule->streamNamePrefix = strcpy_alloc(TCHAR_TO_ANSI(*Ingest.RtmpUrl.StreamNamePrefix)); Ctx.ConnectModule->onConnectionSuccess = FIbmLiveStreaming::OnConnectionSuccess; Ctx.ConnectModule->onConnectionError = FIbmLiveStreaming::OnConnectionError; Ctx.ConnectModule->deviceType = RTMPDeviceTypeMobile; Ctx.ConnectModule->customData = this; // Ctx.ConnectModule->hasPermanentContentUrl = 0; // // Broadcaster module // Ctx.BroadcasterModule = rtmpmodule_broadcaster_create(Ctx.Client, TCHAR_TO_ANSI(*Ingest.RtmpUrl.StreamName)); check(Ctx.BroadcasterModule); Ctx.BroadcasterModule->onStreamPublished = FIbmLiveStreaming::OnStreamPublished; Ctx.BroadcasterModule->onStreamError = FIbmLiveStreaming::OnStreamError; Ctx.BroadcasterModule->onStreamDeleted = FIbmLiveStreaming::OnStreamDeleted; Ctx.BroadcasterModule->onStopPublish = FIbmLiveStreaming::OnStopPublish; Ctx.BroadcasterModule->autoPublish = 0; Ctx.BroadcasterModule->customData = this; Ctx.BroadcasterModule->bwMeasureMinInterval = CVarLiveStreamingBwMeasureWithEmptyQueue.GetValueOnAnyThread(); Ctx.BroadcasterModule->bwMeasureMaxInterval = CVarLiveStreamingBwMeasureWithNotEmptyQueue.GetValueOnAnyThread(); Ctx.BroadcasterModule->onStreamBandwidthChanged = FIbmLiveStreaming::OnStreamBandwidthChanged; rtmpclient_start(Ctx.Client); } void FIbmLiveStreaming::Stop() { if (State == EState::Stopping) { return; } check(IsInGameThread()); UE_LOG(IbmLiveStreaming, Log, TEXT("Stopping")); if (!CheckState(__FUNCTIONW__, EState::GettingAccessToken, EState::Connected)) { return; } State = EState::Stopping; FGameplayMediaEncoder::Get()->UnregisterListener(this); { FScopeLock Lock(&CtxMutex); if (Ctx.BroadcasterModule) { rtmpmodule_broadcaster_stop_publish(Ctx.BroadcasterModule); } } } void FIbmLiveStreaming::FinishStopOnGameThread() { check(IsInGameThread()); UE_LOG(IbmLiveStreaming, Log, TEXT("Finalizing stop on game thread")); FGameplayMediaEncoder::Get()->UnregisterListener(this); FScopeLock Lock(&CtxMutex); if (Ctx.Client) { rtmpclient_free(Ctx.Client); Ctx.Client = nullptr; } Ctx.BroadcasterModule = nullptr; Ctx.ConnectModule = nullptr; State = EState::None; } // // Utility helper functions // namespace { static constexpr uint8 NAL[4] = { 0, 0, 0, 1 }; const TArrayView MakeArrayViewFromRange(const uint8* Begin, const uint8* End) { return TArrayView(const_cast(Begin), static_cast(End - Begin)); }; // // Helper functions to make it easier to write to RawData objects // void RawDataPush(RawData* Pkt, uint8 Val) { check(Pkt->offset < Pkt->length); Pkt->data[Pkt->offset++] = Val; } void RawDataPush(RawData* Pkt, const uint8* Begin, const uint8* End) { int Len = static_cast(End - Begin); check((Pkt->offset + Len) <= Pkt->length); FMemory::Memcpy(Pkt->data + Pkt->offset, Begin, Len); Pkt->offset += Len; } } RawData* FIbmLiveStreaming::GetAvccHeader(const TArrayView& DataView, const uint8** OutPpsEnd) { // Begin/End To make it easier to use const uint8* DataBegin = DataView.GetData(); const uint8* DataEnd = DataView.GetData() + DataView.Num(); // encoded frame should begin with NALU start code check(FMemory::Memcmp(DataBegin, NAL, sizeof(NAL)) == 0); const uint8* SpsEnd = Algo::FindSequence(MakeArrayViewFromRange(DataBegin + sizeof(NAL), DataEnd), NAL); check(SpsEnd); TArrayView SPS = MakeArrayViewFromRange(DataBegin + sizeof(NAL), SpsEnd); if (SPS[0] == 0x09) { // now it's not an SPS but AUD and so we need to skip it. happens with AMD AMF encoder const uint8* SpsBegin = SpsEnd + sizeof(NAL); SpsEnd = Algo::FindSequence(MakeArrayViewFromRange(SpsBegin, DataEnd), NAL); check(SpsEnd); SPS = MakeArrayViewFromRange(SpsBegin, SpsEnd); check(SPS[0] == 0x67); // SPS first byte: [forbidden_zero_bit:1 = 0, nal_ref_idc:2 = 3, nal_unit_type:5 = 7] } const uint8* PpsBegin = SpsEnd + sizeof(NAL); const uint8* PpsEnd = Algo::FindSequence(MakeArrayViewFromRange(PpsBegin, DataEnd), NAL); // encoded frame can contain just SPS/PPS if (PpsEnd == nullptr) { PpsEnd = DataEnd; } const TArrayView PPS = MakeArrayViewFromRange(PpsBegin, PpsEnd); // To avoid reallocating, calculate the required final size (This is checked for correctness at the end) int32 FinalSize = 16 + SPS.Num() + PPS.Num(); RawData* Pkt = rawdata_alloc(FinalSize); static constexpr uint8 Hdr[5] = { 0x17, 0, 0, 0, 0 }; RawDataPush(Pkt, Hdr, Hdr + sizeof(Hdr)); // http://neurocline.github.io/dev/2016/07/28/video-and-containers.html // http://aviadr1.blogspot.com/2010/05/h264-extradata-partially-explained-for.html RawDataPush(Pkt, 0x01); // AVCC version RawDataPush(Pkt, SPS[1]); // profile RawDataPush(Pkt, SPS[2]); // compatibility RawDataPush(Pkt, SPS[3]); // level RawDataPush(Pkt, 0xFC | 3); // reserved (6 bits), NALU length size - 1 (2 bits) RawDataPush(Pkt, 0xE0 | 1); // reserved (3 bits), num of SPSs (5 bits) const uint8* SPS_size = Bytes(static_cast(SPS.Num())); // 2 bytes for length of SPS RawDataPush(Pkt, SPS_size[1]); RawDataPush(Pkt, SPS_size[0]); RawDataPush(Pkt, SPS.GetData(), SPS.GetData() + SPS.Num()); RawDataPush(Pkt, 1); // num of PPSs const uint8* PPS_size = Bytes(static_cast(PPS.Num())); // 2 bytes for length of PPS RawDataPush(Pkt, PPS_size[1]); RawDataPush(Pkt, PPS_size[0]); RawDataPush(Pkt, PPS.GetData(), PPS.GetData() + PPS.Num()); // PPS data // Check if we calculated the required size exactly check(Pkt->offset == Pkt->length); *OutPpsEnd = PpsEnd; Pkt->offset = 0; return Pkt; } RawData* FIbmLiveStreaming::GetVideoPacket(const TArrayView& DataView, bool bVideoKeyframe, const uint8* DataBegin) { check(FMemory::Memcmp(DataBegin, NAL, sizeof(NAL)) == 0); // To make it easier to use const uint8* DataEnd = DataView.GetData() + DataView.Num(); // To avoid reallocating, calculate the required final size (This is checked for correctness at the end) int FinalSize = 5 + 4 + (DataEnd - (DataBegin + sizeof(NAL))); RawData * Pkt = rawdata_alloc(FinalSize); RawDataPush(Pkt, bVideoKeyframe ? 0x17 : 0x27); RawDataPush(Pkt, 0x1); RawDataPush(Pkt, 0x0); RawDataPush(Pkt, 0x0); RawDataPush(Pkt, 0x0); int32 DataSizeValue = static_cast(DataEnd - DataBegin - sizeof(NAL));; verify(DataSizeValue > 0); const uint8* DataSize = Bytes(DataSizeValue); RawDataPush(Pkt, DataSize[3]); RawDataPush(Pkt, DataSize[2]); RawDataPush(Pkt, DataSize[1]); RawDataPush(Pkt, DataSize[0]); RawDataPush(Pkt, DataBegin + sizeof(NAL), DataEnd); // Check if we calculated the required size exactly check(Pkt->offset == Pkt->length); Pkt->offset = 0; return Pkt; } void FIbmLiveStreaming::QueueFrame(RTMPContentType FrameType, RawData* Pkt, uint32 TimestampMs) { FScopeLock Lock(&CtxMutex); rtmpmodule_broadcaster_queue_frame(Ctx.BroadcasterModule, FrameType, Pkt, TimestampMs); } DECLARE_CYCLE_STAT(TEXT("IBMRTMP_Inject"), STAT_FIbmLiveStreaming_Inject, STATGROUP_VideoRecordingSystem); DECLARE_CYCLE_STAT(TEXT("IBMRTMP_InjectVideo"), STAT_FIbmLiveStreaming_InjectVideo, STATGROUP_VideoRecordingSystem); DECLARE_CYCLE_STAT(TEXT("IBMRTMP_InjectAudio"), STAT_FIbmLiveStreaming_InjectAudio, STATGROUP_VideoRecordingSystem); void FIbmLiveStreaming::InjectVideo(uint32 TimestampMs, const TArrayView& DataView, bool bIsKeyFrame) { SCOPE_CYCLE_COUNTER(STAT_FIbmLiveStreaming_InjectVideo); UE_LOG(IbmLiveStreaming, Verbose, TEXT("Injecting Video. TimestampMs=%u, Size=%u, bIsKeyFrame=%s"), TimestampMs, DataView.Num(), bIsKeyFrame ? TEXT("true"):TEXT("false")); // // From IBM's sample... // //video packet: //#1 config: 0x17,0,0,0,0,avcc | ts: 0 //..n data: key 0x17 | inter: 0x27, 0x01, 0, 0, 0, data size without nal, video data after nal // sample_add_frame(ctx, data, data_lenght, timestamp, RTMPVideoDataPacketType); if (VideoPacketsSent == 0) { check(bIsKeyFrame); // the first packet always should be key-frame const uint8* PpsEnd; RawData* Pkt = GetAvccHeader(DataView, &PpsEnd); QueueFrame(RTMPVideoDataPacketType, Pkt, 0); if (PpsEnd != DataView.GetData() + DataView.Num()) // do we have any other NALUs in this frame? { Pkt = GetVideoPacket(DataView, bIsKeyFrame, PpsEnd); QueueFrame(RTMPVideoDataPacketType, Pkt, TimestampMs); } } else { RawData* Pkt = GetVideoPacket(DataView, bIsKeyFrame, DataView.GetData()); QueueFrame(RTMPVideoDataPacketType, Pkt, TimestampMs); } ++VideoPacketsSent; // Every N seconds, we log the average framerate { double Now = FPlatformTime::Seconds(); double Elapsed = Now - FpsCalculationStartTime; if (Elapsed >= 4.0f) { UE_LOG(IbmLiveStreaming, Verbose, TEXT("LiveStreaming Framerate = %3.2f"), (VideoPacketsSent - FpsCalculationStartVideoPackets) / Elapsed); FpsCalculationStartVideoPackets = VideoPacketsSent; FpsCalculationStartTime = Now; } } } void FIbmLiveStreaming::InjectAudio(uint32 TimestampMs, const TArrayView& DataView) { SCOPE_CYCLE_COUNTER(STAT_FIbmLiveStreaming_InjectAudio); UE_LOG(IbmLiveStreaming, Verbose, TEXT("Injecting Audio. TimestampMs=%u, Size=%u"), TimestampMs, DataView.Num()); // // From IBM's sample // //audio packet: //#1 config: aac config (parser: aac_parse_extradata) 1byte (FLVCodecAAC|FLVAudioSampleRate(select rate)|FLVAudioSize(mostly 16b)|FLVAudioChannels(mostly stereo)), AACSequenceHeader, aac_lc_write_extradata() output | ts: 0 //..n data: 1byte aac config, AACRaw, audio data // sample_add_frame(ctx, data, data_lenght, timestamp, RTMPAudioDataPacketType); // NOTE: FLVAudioSampleRate only goes up to 44kHz (FLVAudioSampleRate44kHz), but audio works fine at 48hkz too // because aac_lc_write_extradata allows specifying the correct sample rate constexpr unsigned char ConfigByte = FLVCodecAAC | FLVAudioSampleRate44kHz | FLVAudio16bit | FLVAudioStereo; if (!AudioPacketsSent == 0) { RawData* Pkt= rawdata_alloc(2); Pkt->data[0] = ConfigByte; Pkt->data[1] = AACSequenceHeader; Pkt->offset = 2; aac_lc_write_extradata(Pkt, Config.AudioSampleRate, Config.AudioNumChannels); // This adds another 2 bytes of configuration data Pkt->offset = 0; QueueFrame(RTMPAudioDataPacketType, Pkt, 0); } RawData* Pkt= rawdata_alloc(2 + DataView.Num()); Pkt->data[0] = ConfigByte; Pkt->data[1] = AACRaw; FMemory::Memcpy(Pkt->data + 2, DataView.GetData(), DataView.Num()); QueueFrame(RTMPAudioDataPacketType, Pkt, TimestampMs); ++AudioPacketsSent; } void FIbmLiveStreaming::OnMediaSample(const FGameplayMediaEncoderSample& Sample) { SCOPE_CYCLE_COUNTER(STAT_FIbmLiveStreaming_Inject); FScopeLock Lock(&CtxMutex); if (State != EState::Connected) { return; } if (VideoPacketsSent == 0 && AudioPacketsSent == 0) { // We only start injecting when we receive a keyframe if (!Sample.IsVideoKeyFrame()) { return; } LiveStreamStartTimespan = Sample.GetTime(); FpsCalculationStartTime = LiveStreamStartTimespan.GetTotalSeconds(); } uint32 TimestampMs = static_cast((Sample.GetTime() - LiveStreamStartTimespan).GetTotalMilliseconds()); TRefCountPtr MediaBuffer = nullptr; verify(SUCCEEDED(const_cast(Sample.GetSample())->GetBufferByIndex(0, MediaBuffer.GetInitReference()))); BYTE* SrcData = nullptr; DWORD MaxLength = 0; DWORD CurrentLength = 0; verify(SUCCEEDED(MediaBuffer->Lock(&SrcData, &MaxLength, &CurrentLength))); // Append the payload to Data (in case it has any custom data there already) TArrayView DataView(SrcData, CurrentLength); if (Sample.GetType() == EMediaType::Video) { InjectVideo(TimestampMs, DataView, Sample.IsVideoKeyFrame()); } else if (Sample.GetType() == EMediaType::Audio) { InjectAudio(TimestampMs, DataView); } verify(SUCCEEDED(MediaBuffer->Unlock())); // #RVF : remove this once the code is production ready static bool FirstPacket = true; if (FirstPacket) { FirstPacket = false; MemoryCheckpoint("IbmLiveStreaming: After sending first packet"); LogMemoryCheckpoints("After live streaming the first packet"); } } void FIbmLiveStreaming::StopFromSocketThread() { { FScopeLock Lock(&CtxMutex); rtmpclient_stop(Ctx.Client); } // Execute the Finalize event on the GameThread FGraphEventRef FinalizeEvent = FSimpleDelegateGraphTask::CreateAndDispatchWhenReady( FSimpleDelegateGraphTask::FDelegate::CreateRaw(this, &FIbmLiveStreaming::FinishStopOnGameThread), TStatId(), nullptr, ENamedThreads::GameThread); } void FIbmLiveStreaming::OnConnectionErrorImpl(RTMPModuleConnect* Module, RTMPEvent Evt, void* RejectInfoObj) { UE_LOG(IbmLiveStreaming, Error, TEXT("%s: Connect failed. Reason: %d"), __FUNCTIONW__, Module->rejectReason); StopFromSocketThread(); } void FIbmLiveStreaming::OnConnectionSuccessImpl(RTMPModuleConnect* Module) { UE_LOG(IbmLiveStreaming, Log, TEXT("%s"), __FUNCTIONW__); check(State == EState::Connecting); { FScopeLock Lock(&CtxMutex); rtmpmodule_broadcaster_start(Ctx.BroadcasterModule); } // Changing the state AFTER making the required IBM calls State = EState::Connected; } void FIbmLiveStreaming::OnStreamPublishedImpl(RTMPModuleBroadcaster* Module) { UE_LOG(IbmLiveStreaming, Log, TEXT("%s"), __FUNCTIONW__); check(State == EState::Connected); { FScopeLock Lock(&CtxMutex); rtmpmodule_broadcaster_start_publish(Ctx.BroadcasterModule); } } void FIbmLiveStreaming::OnStreamDeletedImpl(RTMPModuleBroadcaster* Module) { UE_LOG(IbmLiveStreaming, Log, TEXT("%s"), __FUNCTIONW__); StopFromSocketThread(); } void FIbmLiveStreaming::OnStreamErrorImpl(RTMPModuleBroadcaster* Module) { UE_LOG(IbmLiveStreaming, Log, TEXT("%s"), __FUNCTIONW__); StopFromSocketThread(); } void FIbmLiveStreaming::OnStopPublishImpl(RTMPModuleBroadcaster* Module) { UE_LOG(IbmLiveStreaming, Log, TEXT("%s"), __FUNCTIONW__); { FScopeLock Lock(&CtxMutex); rtmpmodule_broadcaster_stop(Ctx.BroadcasterModule); } } void FIbmLiveStreaming::OnStreamBandwidthChangedImpl(RTMPModuleBroadcaster* Module, uint32 Bandwidth, bool bQueueWasEmpty) { UE_LOG(IbmLiveStreaming, Verbose, TEXT("%s"), __FUNCTIONW__); check(State == EState::Connected); // if debugger is stopped on a breakpoint and then resumed, measured b/w will be very small. just ignore it if (Bandwidth <= Config.AudioBitrate) { UE_LOG(IbmLiveStreaming, Warning, TEXT("low traffic detected (%.3f Mbps), have debugger been stopped on breakpoint?"), Bandwidth / 10000000.0); return; } uint32 VideoBandwidth = Bandwidth - Config.AudioBitrate; // // Get current video framerate and framerate // TRefCountPtr VideoOutputType; uint32 CurrentBitrate, CurrentFramerate, CurrentFramerateDenominator; verify(FGameplayMediaEncoder::Get()->GetVideoOutputType(VideoOutputType)); verify(SUCCEEDED(MFGetAttributeRatio(VideoOutputType, MF_MT_FRAME_RATE, &CurrentFramerate, &CurrentFramerateDenominator))); verify(SUCCEEDED(VideoOutputType->GetUINT32(MF_MT_AVG_BITRATE, &CurrentBitrate))); // NOTE: // Reported bandwidth doesn't always mean available bandwidth, e.g. when we don't push enough data. // In general, `VideoBandwidth` value is either: // * encoder's output bitrate if available bandwidth is more than required for encoder with current settings // * currently available bandwidth if encoder's output bitrate is higher than that // NOTE: // Bitrate depends on framerate as it's in bits per second(!): e.g. to calculate how much bits per encoded // frame is allowed encoder divides configured bitrate by framerate. // bitrate control const double BitrateProximityTolerance = CVarLiveStreamingBitrateProximityTolerance.GetValueOnAnyThread(); uint32 NewVideoBitrate = 0; uint32 NewVideoFramerate = 0; // - if queue is not empty we restrict bitrate to reported bandwidth; // - if queue is empty and reported bandwidth is close to current bitrate (encoder output is restricted // but potentially greater bandwidth is available) we increase bitrate by a constant factor. // we don't increase bitrate if encoder wasn't fulfilling available bandwidth if (!bQueueWasEmpty) { NewVideoBitrate = static_cast(VideoBandwidth * CVarLiveStreamingSpaceToEmptyQueue.GetValueOnAnyThread()); } else if (CurrentBitrate < VideoBandwidth * BitrateProximityTolerance) { // `VideoBandwidth` is often just a fluctuations of current encoder bitrate, so if it's too close to currently // configured bitrate let's be optimistic and try to stress network a little bit more, // mainly to increase bitrate recovery speed NewVideoBitrate = FMath::Max(static_cast(CurrentBitrate * CVarLiveStreamingNonCongestedBitrateIncrease.GetValueOnAnyThread()), VideoBandwidth); } // framerate control // by default we stream 60FPS or as much as the game manages // if available b/w is lower than configurable threshold (e.g. 15Mbps) we downscale to e.g. 30FPS // (configurable) and restore back to 60FPS if b/w improves const uint32 BitrateThresholdToSwitchFPS = static_cast(CVarLiveStreamingBitrateThresholdToSwitchFPS.GetValueOnAnyThread() * 1000 * 1000); // two values to avoid switching back and forth on bitrate fluctuations const uint32 BitrateThresholdDown = static_cast(BitrateThresholdToSwitchFPS * BitrateProximityTolerance); // switch down to 30FPS if bitrate is lower than that const uint32 BitrateThresholdUp = static_cast(BitrateThresholdToSwitchFPS / BitrateProximityTolerance); // switch up to 60FPS if bitrate is higher than that const uint32 DownscaledFPS = static_cast(CVarLiveStreamingDownscaledFPS.GetValueOnAnyThread()); if (NewVideoBitrate != 0) { if (CurrentFramerate > DownscaledFPS && NewVideoBitrate < BitrateThresholdDown && NewVideoBitrate < CurrentBitrate) // don't downscale if bitrate is growing { NewVideoFramerate = DownscaledFPS; } else if (CurrentFramerate < HardcodedVideoFPS && NewVideoBitrate > BitrateThresholdUp) { NewVideoFramerate = HardcodedVideoFPS; } } // a special case: // if we are on downscaled framerate and encoder doesn't produce enough data to probe higher b/w // (happens when currently set bitrate is more than enough for encoder on downscaled FPS and it's lower // than threshold to switch to higher framerate), we won't switch to higher framerate even on unlimited b/w. // so whenever we detect that encoder doesn't push hard enough on downscaled framerate try to switch to // higher framerate and let the logic above kick in if the assumption is wrong //if (!bChangeBitrate && !bChangeFramerate && bQueueWasEmpty && if (NewVideoBitrate == 0 && NewVideoFramerate == 0 && bQueueWasEmpty && CurrentFramerate < HardcodedVideoFPS && CurrentBitrate > VideoBandwidth * BitrateProximityTolerance) { NewVideoFramerate = HardcodedVideoFPS; UE_LOG(IbmLiveStreaming, Verbose, TEXT("framerate control special case: switching to %dFPS"), NewVideoFramerate); } if (NewVideoBitrate) { FGameplayMediaEncoder::Get()->SetVideoBitrate(NewVideoBitrate); } if (NewVideoFramerate) { FGameplayMediaEncoder::Get()->SetVideoFramerate(NewVideoFramerate); } UE_LOG(IbmLiveStreaming, Verbose, TEXT("reported b/w %.3f Mbps, video b/w %.3f, queue empty %d, configured bitrate %.3f, new bitrate %.3f, configured %dFPS, new %dFPS"), Bandwidth / 1000000.0, VideoBandwidth / 1000000.0, bQueueWasEmpty, CurrentBitrate / 1000000.0, NewVideoBitrate ? NewVideoBitrate / 1000000.0 : -1.0, CurrentFramerate, NewVideoFramerate ? NewVideoFramerate : -1); } void FIbmLiveStreaming::OnConnectionError(RTMPModuleConnect* Module, RTMPEvent Evt, void* RejectInfoObj) { reinterpret_cast(Module->customData)->OnConnectionErrorImpl(Module, Evt, RejectInfoObj); } void FIbmLiveStreaming::OnConnectionSuccess(RTMPModuleConnect* Module) { reinterpret_cast(Module->customData)->OnConnectionSuccessImpl(Module); } void FIbmLiveStreaming::OnStreamPublished(RTMPModuleBroadcaster* Module) { reinterpret_cast(Module->customData)->OnStreamPublishedImpl(Module); } void FIbmLiveStreaming::OnStreamDeleted(RTMPModuleBroadcaster* Module) { reinterpret_cast(Module->customData)->OnStreamDeletedImpl(Module); } void FIbmLiveStreaming::OnStreamError(RTMPModuleBroadcaster* Module) { reinterpret_cast(Module->customData)->OnStreamErrorImpl(Module); } void FIbmLiveStreaming::OnStopPublish(RTMPModuleBroadcaster* Module) { reinterpret_cast(Module->customData)->OnStopPublishImpl(Module); } void FIbmLiveStreaming::OnStreamBandwidthChanged(RTMPModuleBroadcaster* Module, unsigned long Bandwidth, int32 QueueWasEmpty) { reinterpret_cast(Module->customData)->OnStreamBandwidthChangedImpl(Module, Bandwidth, QueueWasEmpty != 0 ); } #endif // WITH_IBMRTMPINGEST