2019-12-26 14:45:42 -05:00
// Copyright Epic Games, Inc. All Rights Reserved.
2019-02-27 11:57:17 -05:00
# 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 < FString > CVarLiveStreamingClientId (
TEXT ( " LiveStreaming.ClientId " ) ,
TEXT ( " 76898b5ebb31c697023fccb47a3e7a7eb6aacb83 " ) ,
TEXT ( " IBMCloudVideo ClientId to use for live streaming " ) ) ;
static TAutoConsoleVariable < FString > CVarLiveStreamingClientSecret (
TEXT ( " LiveStreaming.ClientSecret " ) ,
TEXT ( " 11b3d99c37f85943224d710f037f2c5e8c8fe673 " ) ,
TEXT ( " IBMCloudVideo ClientSecret to use for live streaming " ) ) ;
static TAutoConsoleVariable < FString > CVarLiveStreamingChannel (
TEXT ( " LiveStreaming.Channel " ) ,
TEXT ( " 23619107 " ) ,
TEXT ( " IBMCloudVideo Channel to use for live streaming " ) ) ;
static TAutoConsoleVariable < float > CVarLiveStreamingMaxBitrate (
TEXT ( " LiveStreaming.MaxBitrate " ) ,
30 ,
TEXT ( " LiveStreaming: max allowed bitrate, in Mbps " ) ) ;
static TAutoConsoleVariable < float > 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 < float > CVarLiveStreamingNonCongestedBitrateIncrease (
TEXT ( " LiveStreaming.NonCongestedBitrateIncrease " ) ,
1.2 ,
TEXT ( " LiveStreaming: rate of growth of non-congested bitrate " ) ) ;
static TAutoConsoleVariable < float > CVarLiveStreamingBitrateThresholdToSwitchFPS (
TEXT ( " LiveStreaming.BitrateThresholdToSwitchFPS " ) ,
15 ,
TEXT ( " LiveStreaming: bitrate threshold to switch to lower FPS, in Mbps " ) ) ;
static TAutoConsoleVariable < int32 > CVarLiveStreamingDownscaledFPS (
TEXT ( " LiveStreaming.DownscaledFPS " ) ,
30 ,
TEXT ( " LiveStreaming: framerate to switch if poor uplink is detected " ) ) ;
static TAutoConsoleVariable < float > 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 < uint8 > FIbmLiveStreaming : : FFormData : : FStringToUint8 ( const FString & InString )
{
TArray < uint8 > OutBytes ;
// Handle empty strings
if ( InString . Len ( ) > 0 )
{
FTCHARToUTF8 Converted ( * InString ) ; // Convert to UTF8
OutBytes . Append ( reinterpret_cast < const uint8 * > ( Converted . Get ( ) ) , Converted . Length ( ) ) ;
}
return OutBytes ;
}
2021-04-29 19:32:06 -04:00
TSharedRef < IHttpRequest > FIbmLiveStreaming : : FFormData : : CreateHttpPostRequestImpl ( const FString & URL )
2019-02-27 11:57:17 -05:00
{
2021-04-29 19:32:06 -04:00
TSharedRef < IHttpRequest > HttpRequest = FHttpModule : : Get ( ) . CreateRequest ( ) ;
2019-02-27 11:57:17 -05:00
TArray < uint8 > 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 < int > ( State . Load ( ) ) , static_cast < int > ( 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 < int > ( State . Load ( ) ) , static_cast < int > ( ExpectedStateMin ) , static_cast < int > ( 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 < IMFMediaType > 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 " ) ) ;
2021-04-29 19:32:06 -04:00
TSharedRef < IHttpRequest > HttpRequest = FormData . CreateHttpPostRequest ( " https://www.ustream.tv/oauth2/token " , this , & FIbmLiveStreaming : : OnProcessRequestComplete ) ;
2019-02-27 11:57:17 -05:00
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 < FJsonObject > & 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 < FJsonObject > & JsonObject , const FString & FieldName , const TSharedPtr < FJsonObject > * & 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 < TJsonReader < TCHAR > > JsonReader = TJsonReaderFactory < TCHAR > : : Create ( Content ) ;
TSharedPtr < FJsonObject > 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
//
2021-04-29 19:32:06 -04:00
TSharedRef < IHttpRequest > HttpRequest = FHttpModule : : Get ( ) . CreateRequest ( ) ;
2019-02-27 11:57:17 -05:00
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 < FJsonObject > * 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 < FString > 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 < int32 > CVarLiveStreamingBwMeasureWithEmptyQueue (
TEXT ( " LiveStreaming.bwMeasureWithEmptyQueue " ) ,
1000 ,
TEXT ( " LiveStreaming: interval of reporting measured bandwdith when send queue is empty, in msecs " ) ) ;
TAutoConsoleVariable < int32 > 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 < uint8 > MakeArrayViewFromRange ( const uint8 * Begin , const uint8 * End )
{
return TArrayView < uint8 > ( const_cast < uint8 * > ( Begin ) , static_cast < int32 > ( 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 < int > ( End - Begin ) ;
check ( ( Pkt - > offset + Len ) < = Pkt - > length ) ;
FMemory : : Memcpy ( Pkt - > data + Pkt - > offset , Begin , Len ) ;
Pkt - > offset + = Len ;
}
}
RawData * FIbmLiveStreaming : : GetAvccHeader ( const TArrayView < uint8 > & 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 < uint8 > 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 < uint8 > 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 < const uint16 & > ( 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 < const uint16 & > ( 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 < uint8 > & 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 < int32 > ( 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 < uint8 > & 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 < uint8 > & 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 < uint32 > ( ( Sample . GetTime ( ) - LiveStreamStartTimespan ) . GetTotalMilliseconds ( ) ) ;
TRefCountPtr < IMFMediaBuffer > MediaBuffer = nullptr ;
verify ( SUCCEEDED ( const_cast < IMFSample * > ( 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 < uint8 > 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 < IMFMediaType > 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 < uint32 > ( 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 < uint32 > ( 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 < uint32 > ( CVarLiveStreamingBitrateThresholdToSwitchFPS . GetValueOnAnyThread ( ) * 1000 * 1000 ) ;
// two values to avoid switching back and forth on bitrate fluctuations
const uint32 BitrateThresholdDown = static_cast < uint32 > ( BitrateThresholdToSwitchFPS * BitrateProximityTolerance ) ; // switch down to 30FPS if bitrate is lower than that
const uint32 BitrateThresholdUp = static_cast < uint32 > ( BitrateThresholdToSwitchFPS / BitrateProximityTolerance ) ; // switch up to 60FPS if bitrate is higher than that
const uint32 DownscaledFPS = static_cast < uint32 > ( 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 < FIbmLiveStreaming * > ( Module - > customData ) - > OnConnectionErrorImpl ( Module , Evt , RejectInfoObj ) ;
}
void FIbmLiveStreaming : : OnConnectionSuccess ( RTMPModuleConnect * Module )
{
reinterpret_cast < FIbmLiveStreaming * > ( Module - > customData ) - > OnConnectionSuccessImpl ( Module ) ;
}
void FIbmLiveStreaming : : OnStreamPublished ( RTMPModuleBroadcaster * Module )
{
reinterpret_cast < FIbmLiveStreaming * > ( Module - > customData ) - > OnStreamPublishedImpl ( Module ) ;
}
void FIbmLiveStreaming : : OnStreamDeleted ( RTMPModuleBroadcaster * Module )
{
reinterpret_cast < FIbmLiveStreaming * > ( Module - > customData ) - > OnStreamDeletedImpl ( Module ) ;
}
void FIbmLiveStreaming : : OnStreamError ( RTMPModuleBroadcaster * Module )
{
reinterpret_cast < FIbmLiveStreaming * > ( Module - > customData ) - > OnStreamErrorImpl ( Module ) ;
}
void FIbmLiveStreaming : : OnStopPublish ( RTMPModuleBroadcaster * Module )
{
reinterpret_cast < FIbmLiveStreaming * > ( Module - > customData ) - > OnStopPublishImpl ( Module ) ;
}
void FIbmLiveStreaming : : OnStreamBandwidthChanged ( RTMPModuleBroadcaster * Module , unsigned long Bandwidth , int32 QueueWasEmpty )
{
reinterpret_cast < FIbmLiveStreaming * > ( Module - > customData ) - > OnStreamBandwidthChangedImpl ( Module , Bandwidth , QueueWasEmpty ! = 0 ) ;
}
# endif // WITH_IBMRTMPINGEST