Files
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

4038 lines
153 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "ElectraPlayer.h"
#include "ElectraPlayerPrivate.h"
#include "Misc/Optional.h"
#include "Misc/SecureHash.h"
#include "MediaSamples.h"
#include "MediaPlayerOptions.h"
#include "IMediaPlayerLifecycleManager.h"
#include "PlayerRuntimeGlobal.h"
#include "Player/AdaptiveStreamingPlayer.h"
#include "Player/AdaptivePlayerOptionKeynames.h"
#include "Player/ABRRules/ABROptionKeynames.h"
#include "Utilities/Utilities.h"
#include "Utilities/URLParser.h"
#include "Utilities/StringHelpers.h"
#include "MediaSubtitleDecoderOutput.h"
#include "MediaMetaDataDecoderOutput.h"
#include "IElectraSubtitleSample.h"
#include "IElectraMetadataSample.h"
#include "IMediaMetadataItem.h"
#include "ElectraDecodersPlatformResources.h"
#include "RHIGlobals.h"
//-----------------------------------------------------------------------------
CSV_DEFINE_CATEGORY_MODULE(ELECTRAPLAYERRUNTIME_API, ElectraPlayer, false);
DECLARE_CYCLE_STAT(TEXT("FElectraPlayer::TickInput"), STAT_ElectraPlayer_ElectraPlayer_TickInput, STATGROUP_ElectraPlayer);
DECLARE_CYCLE_STAT(TEXT("FElectraPlayer::StaticResourceRequest"), STAT_ElectraPlayer_ElectraPlayer_StaticResourceRequest, STATGROUP_ElectraPlayer);
DECLARE_CYCLE_STAT(TEXT("FElectraPlayer::PlayerEvents"), STAT_ElectraPlayer_ElectraPlayer_PlayerEvents, STATGROUP_ElectraPlayer);
DECLARE_CYCLE_STAT(TEXT("FElectraPlayer::QueryOptions"), STAT_ElectraPlayer_ElectraPlayer_QueryOptions, STATGROUP_ElectraPlayer);
//-----------------------------------------------------------------------------
// Prefix to use in querying for a custom analytic value through QueryOptions()
#define CUSTOM_ANALYTIC_METRIC_QUERYOPTION_KEY TEXT("ElectraCustomAnalytic")
// Prefix to use in the metric event to set the custom value.
#define CUSTOM_ANALYTIC_METRIC_KEYNAME TEXT("Custom")
namespace ElectraMediaOptions
{
static const FName GetSafeMediaOptions(TEXT("GetSafeMediaOptions"));
static const FName ContentID(TEXT("content_id"));
static const FName ElectraCMCDConfig(TEXT("ElectraCMCDConfig"));
static const FName ElectraNoPreloading(TEXT("ElectraNoPreloading"));
static const FName PlaylistProperties(TEXT("playlist_properties"));
static const FName ElectraInitialBitrate(TEXT("ElectraInitialBitrate"));
static const FName MaxElectraVerticalResolution(TEXT("MaxElectraVerticalResolution"));
static const FName MaxElectraVerticalResolutionOf60fpsVideos(TEXT("MaxElectraVerticalResolutionOf60fpsVideos"));
static const FName ElectraLivePresentationOffset(TEXT("ElectraLivePresentationOffset"));
static const FName ElectraThrowErrorWhenRebuffering(TEXT("ElectraThrowErrorWhenRebuffering"));
static const FName ElectraGetDenyStreamCode(TEXT("ElectraGetDenyStreamCode"));
static const FName MaxResolutionForMediaStreaming(TEXT("MaxResolutionForMediaStreaming"));
static const FName ElectraMaxStreamingBandwidth(TEXT("ElectraMaxStreamingBandwidth"));
static const FName Mimetype(TEXT("mimetype"));
static const FName CodecOptions[] = { TEXT("excluded_codecs_video"), TEXT("excluded_codecs_audio"), TEXT("excluded_codecs_subtitles"), TEXT("preferred_codecs_video"), TEXT("preferred_codecs_audio"),TEXT("preferred_codecs_subtitles") };
static const FName ElectraGetPlaylistData(TEXT("ElectraGetPlaylistData"));
static const FName ElectraGetLicenseKeyData(TEXT("ElectraGetLicenseKeyData"));
static const FName ElectraGetPlaystartPosFromSeekPositions(TEXT("ElectraGetPlaystartPosFromSeekPositions"));
enum class EOptionType
{
MaxVerticalStreamResolution = 0,
MaxBandwidthForStreaming,
PlayListData,
LicenseKeyData,
CustomAnalyticsMetric,
PlaystartPosFromSeekPositions
};
static Electra::FVariantValue GetOptionValue(TWeakPtr<IElectraSafeMediaOptionInterface, ESPMode::ThreadSafe> InFromOptions, EOptionType InWhich, const Electra::FVariantValue& DefaultValue = Electra::FVariantValue())
{
Electra::FVariantValue Value;
TSharedPtr<IElectraSafeMediaOptionInterface, ESPMode::ThreadSafe> SafeOptions = InFromOptions.Pin();
if (SafeOptions.IsValid())
{
IElectraSafeMediaOptionInterface::FScopedLock SafeLock(SafeOptions);
IMediaOptions *Options = SafeOptions->GetMediaOptionInterface();
if (Options)
{
switch(InWhich)
{
default:
{
break;
}
case EOptionType::MaxVerticalStreamResolution:
{
return FVariantValue((int64)Options->GetMediaOption(MaxResolutionForMediaStreaming, (int64)0));
}
case EOptionType::MaxBandwidthForStreaming:
{
return FVariantValue((int64)Options->GetMediaOption(ElectraMaxStreamingBandwidth, (int64)0));
}
case EOptionType::PlayListData:
{
if (Options->HasMediaOption(ElectraGetPlaylistData))
{
check(DefaultValue.IsType(FVariantValue::EDataType::TypeFString));
return FVariantValue(Options->GetMediaOption(ElectraGetPlaylistData, DefaultValue.GetFString()));
}
break;
}
case EOptionType::LicenseKeyData:
{
if (Options->HasMediaOption(ElectraGetLicenseKeyData))
{
check(DefaultValue.IsType(FVariantValue::EDataType::TypeFString));
return FVariantValue(Options->GetMediaOption(ElectraGetLicenseKeyData, DefaultValue.GetFString()));
}
break;
}
case EOptionType::CustomAnalyticsMetric:
{
check(DefaultValue.IsType(FVariantValue::EDataType::TypeFString));
if (DefaultValue.IsType(FVariantValue::EDataType::TypeFString))
{
FName OptionKey(*DefaultValue.GetFString());
if (Options->HasMediaOption(OptionKey))
{
return FVariantValue(Options->GetMediaOption(OptionKey, FString()));
}
}
break;
}
case EOptionType::PlaystartPosFromSeekPositions:
{
if (Options->HasMediaOption(ElectraGetPlaystartPosFromSeekPositions))
{
check(DefaultValue.IsType(FVariantValue::EDataType::TypeSharedPointer));
TSharedPtr<TArray<FTimespan>, ESPMode::ThreadSafe> PosArray = DefaultValue.GetSharedPointer<TArray<FTimespan>>();
if (PosArray.IsValid())
{
TSharedPtr<FElectraSeekablePositions, ESPMode::ThreadSafe> Res = StaticCastSharedPtr<FElectraSeekablePositions, IMediaOptions::FDataContainer, ESPMode::ThreadSafe>(Options->GetMediaOption(ElectraGetPlaystartPosFromSeekPositions, MakeShared<FElectraSeekablePositions, ESPMode::ThreadSafe>(*PosArray)));
if (Res.IsValid() && Res->Data.Num())
{
return FVariantValue(int64(Res->Data[0].GetTicks())); // return HNS
}
}
return FVariantValue();
}
break;
}
}
}
}
return Value;
}
}
//-----------------------------------------------------------------------------
#if UE_BUILD_SHIPPING
#define HIDE_URLS_FROM_LOG 1
#else
#define HIDE_URLS_FROM_LOG 0
#endif
static FString SanitizeMessage(FString InMessage)
{
#if !HIDE_URLS_FROM_LOG
return MoveTemp(InMessage);
#else
int32 searchPos = 0;
while(1)
{
static FString SchemeStr(TEXT("://"));
static FString DotDotDotStr(TEXT("..."));
static FString TermChars(TEXT("'\",; "));
int32 schemePos = InMessage.Find(SchemeStr, ESearchCase::IgnoreCase, ESearchDir::FromStart, searchPos);
if (schemePos != INDEX_NONE)
{
schemePos += SchemeStr.Len();
// There may be a generic user message following a potential URL that we do not want to clobber.
// We search for any next character that tends to end a URL in a user message, like one of ['",; ]
int32 EndPos = InMessage.Len();
int32 Start = schemePos;
while(Start < EndPos)
{
int32 pos;
if (TermChars.FindChar(InMessage[Start], pos))
{
break;
}
++Start;
}
InMessage.RemoveAt(schemePos, Start-schemePos);
InMessage.InsertAt(schemePos, DotDotDotStr);
searchPos = schemePos + SchemeStr.Len();
}
else
{
break;
}
}
return InMessage;
#endif
}
//-----------------------------------------------------------------------------
class FMetaDataDecoderOutput : public IMetaDataDecoderOutput
{
public:
virtual ~FMetaDataDecoderOutput() = default;
const void* GetData() override { return Data.GetData(); }
FTimespan GetDuration() const override { return Duration; }
uint32 GetSize() const override { return (uint32) Data.Num(); }
FDecoderTimeStamp GetTime() const override { return PresentationTime; }
EOrigin GetOrigin() const override { return Origin; }
EDispatchedMode GetDispatchedMode() const override { return DispatchedMode; }
const FString& GetSchemeIdUri() const override { return SchemeIdUri; }
const FString& GetValue() const override { return Value; }
const FString& GetID() const override { return ID; }
TOptional<FDecoderTimeStamp> GetTrackBaseTime() const override { return TrackBaseTime; }
void SetTime(FDecoderTimeStamp& InTime) override { PresentationTime = InTime; }
TArray<uint8> Data;
FDecoderTimeStamp PresentationTime;
FTimespan Duration;
EOrigin Origin;
EDispatchedMode DispatchedMode;
FString SchemeIdUri;
FString Value;
FString ID;
TOptional<FDecoderTimeStamp> TrackBaseTime;
};
class FElectraSubtitleSample : public IElectraSubtitleSample
{
public:
FGuid GetGUID() const override { return IElectraSubtitleSample::GetSampleTypeGUID(); }
FMediaTimeStamp GetTime() const override { FDecoderTimeStamp ts = Subtitle->GetTime(); return FMediaTimeStamp(ts.Time, ts.SequenceIndex); }
FTimespan GetDuration() const override { return Subtitle->GetDuration(); }
TOptional<FVector2D> GetPosition() const override { return TOptional<FVector2D>(); }
EMediaOverlaySampleType GetType() const override { return EMediaOverlaySampleType::Subtitle; }
FText GetText() const override
{
FUTF8ToTCHAR cnv((const ANSICHAR*)Subtitle->GetData().GetData(), Subtitle->GetData().Num());
FString UTF8Text = FString::ConstructFromPtrSize(cnv.Get(), cnv.Length());
return FText::FromString(UTF8Text);
}
ISubtitleDecoderOutputPtr Subtitle;
};
class FElectraBinarySample : public IElectraBinarySample
{
public:
~FElectraBinarySample() = default;
const void* GetData() override { return Metadata->GetData(); }
uint32 GetSize() const override { return Metadata->GetSize(); }
FGuid GetGUID() const override { return IElectraBinarySample::GetSampleTypeGUID(); }
const FString& GetSchemeIdUri() const override { return Metadata->GetSchemeIdUri(); }
const FString& GetValue() const override { return Metadata->GetValue(); }
const FString& GetID() const override { return Metadata->GetID(); }
EDispatchedMode GetDispatchedMode() const override
{
switch(Metadata->GetDispatchedMode())
{
default:
case IMetaDataDecoderOutput::EDispatchedMode::OnReceive:
{
return FElectraBinarySample::EDispatchedMode::OnReceive;
}
case IMetaDataDecoderOutput::EDispatchedMode::OnStart:
{
return FElectraBinarySample::EDispatchedMode::OnStart;
}
}
}
EOrigin GetOrigin() const override
{
switch(Metadata->GetOrigin())
{
default:
case IMetaDataDecoderOutput::EOrigin::TimedMetadata:
{
return FElectraBinarySample::EOrigin::TimedMetadata;
}
case IMetaDataDecoderOutput::EOrigin::EventStream:
{
return FElectraBinarySample::EOrigin::EventStream;
}
case IMetaDataDecoderOutput::EOrigin::InbandEventStream:
{
return FElectraBinarySample::EOrigin::InbandEventStream;
}
}
}
FMediaTimeStamp GetTime() const override
{
FDecoderTimeStamp ts = Metadata->GetTime();
return FMediaTimeStamp(ts.Time, ts.SequenceIndex);
}
FTimespan GetDuration() const override
{
FTimespan Duration = Metadata->GetDuration();
// A zero duration might cause the metadata sample fall through the cracks later
// so set it to a short 1ms instead.
if (Duration.IsZero())
{
Duration = FTimespan::FromMilliseconds(1);
}
return Duration;
}
TOptional<FMediaTimeStamp> GetTrackBaseTime() const override
{
TOptional<FMediaTimeStamp> ms;
TOptional<FDecoderTimeStamp> ts = Metadata->GetTime();
if (ts.IsSet())
{
ms = FMediaTimeStamp(ts.GetValue().Time, ts.GetValue().SequenceIndex);
}
return ms;
}
IMetaDataDecoderOutputPtr Metadata;
};
class FStreamMetadataItem : public IMediaMetadataItem
{
public:
FStreamMetadataItem(const TSharedPtr<Electra::IMediaStreamMetadata::IItem, ESPMode::ThreadSafe>& InItem) : Item(InItem.ToSharedRef())
{ }
virtual ~FStreamMetadataItem() = default;
const FString& GetLanguageCode() const override { return Item->GetLanguageCode(); }
const FString& GetMimeType() const override { return Item->GetMimeType(); }
const FVariant& GetValue() const override { return Item->GetValue(); }
private:
TSharedRef<Electra::IMediaStreamMetadata::IItem, ESPMode::ThreadSafe> Item;
};
//-----------------------------------------------------------------------------
std::atomic<uint32> FElectraPlayer::NextPlayerUniqueID { 0 };
//-----------------------------------------------------------------------------
/**
* Construction of new player
*/
FElectraPlayer::FElectraPlayer(IMediaEventSink& InEventSink,
FElectraPlayerSendAnalyticMetricsDelegate& InSendAnalyticMetricsDelegate,
FElectraPlayerSendAnalyticMetricsPerMinuteDelegate& InSendAnalyticMetricsPerMinuteDelegate,
FElectraPlayerReportVideoStreamingErrorDelegate& InReportVideoStreamingErrorDelegate,
FElectraPlayerReportSubtitlesMetricsDelegate& InReportSubtitlesFileMetricsDelegate)
: EventSink(&InEventSink)
, SendAnalyticMetricsDelegate(InSendAnalyticMetricsDelegate)
, SendAnalyticMetricsPerMinuteDelegate(InSendAnalyticMetricsPerMinuteDelegate)
, ReportVideoStreamingErrorDelegate(InReportVideoStreamingErrorDelegate)
, ReportSubtitlesMetricsDelegate(InReportSubtitlesFileMetricsDelegate)
{
CSV_EVENT(ElectraPlayer, TEXT("Player Creation"));
OutputAudioSamplePool = MakeShareable(new FElectraAudioSamplePool);
OutputTexturePool = MakeShareable(new FElectraTextureSamplePool);
#if PLATFORM_ANDROID
OutputTexturePoolDecoderID = FElectraDecodersPlatformResources::RegisterOutputTexturePool(OutputTexturePool);
#endif
EmptyMediaSamples.Reset(new FMediaSamples);
CurrentPlaybackRange = TRange<FTimespan>::Empty();
AppTerminationHandler = MakeSharedTS<Electra::FApplicationTerminationHandler>();
AppTerminationHandler->Terminate = [this]() { CloseInternal(); };
Electra::AddTerminationNotificationHandler(AppTerminationHandler);
SendAnalyticMetricsDelegate.AddRaw(this, &FElectraPlayer::SendAnalyticMetrics);
SendAnalyticMetricsPerMinuteDelegate.AddRaw(this, &FElectraPlayer::SendAnalyticMetricsPerMinute);
ReportVideoStreamingErrorDelegate.AddRaw(this, &FElectraPlayer::ReportVideoStreamingError);
ReportSubtitlesMetricsDelegate.AddRaw(this, &FElectraPlayer::ReportSubtitlesMetrics);
FString OSMinor;
AnalyticsGPUType = GRHIAdapterName.TrimStartAndEnd();
FPlatformMisc::GetOSVersions(AnalyticsOSVersion, OSMinor);
AnalyticsOSVersion.TrimStartAndEndInline();
AnalyticsInstanceEventCount = 0;
NumQueuedAnalyticEvents = 0;
ClearToDefaultState();
bHasPendingError = false;
bHasClosedDueToError = false;
}
//-----------------------------------------------------------------------------
/**
* Cleanup destructor
*/
FElectraPlayer::~FElectraPlayer()
{
CSV_EVENT(ElectraPlayer, TEXT("Player Destruction"));
Electra::RemoveTerminationNotificationHandler(AppTerminationHandler);
AppTerminationHandler.Reset();
CloseInternal();
#if PLATFORM_ANDROID
FElectraDecodersPlatformResources::UnregisterOutputTexturePool(OutputTexturePoolDecoderID);
#endif
SendAnalyticMetricsDelegate.RemoveAll(this);
SendAnalyticMetricsPerMinuteDelegate.RemoveAll(this);
ReportVideoStreamingErrorDelegate.RemoveAll(this);
ReportSubtitlesMetricsDelegate.RemoveAll(this);
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] ~FElectraPlayer() finished."), PlayerUniqueID);
if (AsyncResourceReleaseNotification.IsValid())
{
AsyncResourceReleaseNotification->Signal(IMediaPlayerLifecycleManagerDelegate::ResourceFlags_OutputBuffers);
}
}
void FElectraPlayer::ClearToDefaultState()
{
PlayerState.Reset();
NumTracksAudio = 0;
NumTracksVideo = 0;
NumTracksSubtitle = 0;
SelectedQuality = 0;
SelectedVideoTrackIndex = -1;
SelectedAudioTrackIndex = -1;
SelectedSubtitleTrackIndex = -1;
bVideoTrackIndexDirty = true;
bAudioTrackIndexDirty = true;
bSubtitleTrackIndexDirty = true;
bInitialSeekPerformed = false;
bIsFirstBuffering = true;
LastPresentedFrameDimension = FIntPoint::ZeroValue;
CurrentStreamMetadata.Reset();
CurrentlyActiveVideoStreamFormat.Reset();
DeferredPlayerEvents.Empty();
MediaUrl.Empty();
}
void FElectraPlayer::SendMediaSinkEvent(EMediaEvent InEventToSend)
{
FScopeLock lock(&ParentObjectLock);
if (EventSink)
{
EventSink->ReceiveMediaEvent(InEventToSend);
}
}
bool FElectraPlayer::OpenInternal(const FString& InUrl, const IMediaOptions* InOptions, const FMediaPlayerOptions* InPlayerOptions)
{
LLM_SCOPE(ELLMTag::ElectraPlayer);
CSV_EVENT(ElectraPlayer, TEXT("Open"));
CloseInternal();
ClearToDefaultState();
FGuid SessionID = FGuid::NewGuid();
PlayerUniqueID = ++NextPlayerUniqueID;
MediaSamplesLock.Lock();
MediaSamples.Reset(new FMediaSamples);
MediaSamplesLock.Unlock();
// Check that we have a valid option interface.
if (InOptions == nullptr)
{
UE_LOG(LogElectraPlayer, Error, TEXT("[%u] IMediaPlayer::Open: Options == nullptr"), PlayerUniqueID);
SendMediaSinkEvent(EMediaEvent::MediaOpenFailed);
return false;
}
// Get the safe option interface to poll for changes during playback.
ParentObjectLock.Lock();
OptionInterface = StaticCastSharedPtr<IElectraSafeMediaOptionInterface>(InOptions->GetMediaOption(ElectraMediaOptions::GetSafeMediaOptions, TSharedPtr<IElectraSafeMediaOptionInterface, ESPMode::ThreadSafe>()));
ParentObjectLock.Unlock();
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] IMediaPlayer::Open"), PlayerUniqueID);
// Create the static resource provider using the safe media option interface.
StaticResourceProvider = MakeShared<FAdaptiveStreamingPlayerResourceProvider, ESPMode::ThreadSafe>(OptionInterface);
// Prepare the start options
PlaystartOptions.Reset();
FName Environment;
if (InPlayerOptions)
{
if (InPlayerOptions->Loop != EMediaPlayerOptionBooleanOverride::UseMediaPlayerSetting)
{
bEnableLooping = InPlayerOptions->Loop == EMediaPlayerOptionBooleanOverride::Enabled;
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] IMediaPlayer::Open: Setting initial loop state to %s"), PlayerUniqueID, bEnableLooping ? TEXT("enabled") : TEXT("disabled"));
}
if (InPlayerOptions->SeekTimeType != EMediaPlayerOptionSeekTimeType::Ignored)
{
PlaystartOptions.TimeOffset = InPlayerOptions->SeekTime;
}
if (InPlayerOptions->TrackSelection == EMediaPlayerOptionTrackSelectMode::UseLanguageCodes)
{
if (InPlayerOptions->TracksByLanguage.Video.Len())
{
PlaystartOptions.InitialVideoTrackAttributes.Language_RFC4647 = InPlayerOptions->TracksByLanguage.Video;
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] IMediaPlayer::Open: Asking for initial video language \"%s\""), PlayerUniqueID, *InPlayerOptions->TracksByLanguage.Video);
}
if (InPlayerOptions->TracksByLanguage.Audio.Len())
{
PlaystartOptions.InitialAudioTrackAttributes.Language_RFC4647 = InPlayerOptions->TracksByLanguage.Audio;
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] IMediaPlayer::Open: Asking for initial audio language \"%s\""), PlayerUniqueID, *InPlayerOptions->TracksByLanguage.Audio);
}
if (InPlayerOptions->TracksByLanguage.Subtitle.Len())
{
PlaystartOptions.InitialSubtitleTrackAttributes.Language_RFC4647 = InPlayerOptions->TracksByLanguage.Subtitle;
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] IMediaPlayer::Open: Asking for initial subtitle language \"%s\""), PlayerUniqueID, *InPlayerOptions->TracksByLanguage.Subtitle);
}
}
else if (InPlayerOptions->TrackSelection == EMediaPlayerOptionTrackSelectMode::UseTrackOptionIndices)
{
PlaystartOptions.InitialVideoTrackAttributes.OverrideIndex = InPlayerOptions->Tracks.Video;
PlaystartOptions.InitialAudioTrackAttributes.OverrideIndex = InPlayerOptions->Tracks.Audio;
PlaystartOptions.InitialSubtitleTrackAttributes.OverrideIndex = InPlayerOptions->Tracks.Subtitle;
}
const FVariant* Env = InPlayerOptions->InternalCustomOptions.Find(MediaPlayerOptionValues::Environment());
Environment = Env ? Env->GetValue<FName>() : Environment;
}
bool bNoPreloading = InOptions->GetMediaOption(ElectraMediaOptions::ElectraNoPreloading, (bool)false);
if (bNoPreloading)
{
PlaystartOptions.bDoNotPreload = true;
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] IMediaPlayer::Open: No preloading after opening media"), PlayerUniqueID);
}
// Set up options to initialize the internal player with.
Electra::FParamDict PlayerOptions;
PlayerOptions.Set(Electra::OptionKeyOutputTexturePoolID, FVariantValue(OutputTexturePoolDecoderID));
// Set the session ID to be the same we will use for analytics here (see below)
PlayerOptions.Set(Electra::OptionKeySessionID, Electra::FVariantValue(SessionID.ToString(EGuidFormats::DigitsWithHyphensLower)));
// Ask for content ID. If none is provided we construct one from the URL.
FString ContentID = InOptions->GetMediaOption(ElectraMediaOptions::ContentID, FString());
if (ContentID.IsEmpty())
{
FString UrlToHash(InUrl.TrimStartAndEnd());
Electra::FURL_RFC3986 up;
if (up.Parse(UrlToHash))
{
UrlToHash = up.GetHost() + up.GetPath(false, false);
}
ContentID = FMD5::HashAnsiString(*UrlToHash);
}
// Set the content ID.
PlayerOptions.Set(Electra::OptionKeyContentID, Electra::FVariantValue(ContentID));
for(auto &CodecOption : ElectraMediaOptions::CodecOptions)
{
FString Value = InOptions->GetMediaOption(CodecOption, FString());
if (Value.Len())
{
PlayerOptions.Set(CodecOption, Electra::FVariantValue(Value));
}
}
// Required playlist properties?
FString PlaylistProperties = InOptions->GetMediaOption(ElectraMediaOptions::PlaylistProperties, FString());
if (PlaylistProperties.Len())
{
PlayerOptions.Set(Electra::OptionKeyPlaylistProperties, Electra::FVariantValue(PlaylistProperties));
}
// CMCD configuration
FString CMCDConfiguration = InOptions->GetMediaOption(ElectraMediaOptions::ElectraCMCDConfig, FString());
if (CMCDConfiguration.Len())
{
PlayerOptions.Set(Electra::OptionKeyCMCDConfiguration, Electra::FVariantValue(CMCDConfiguration));
}
// Timecode parsing?
if (InPlayerOptions && InPlayerOptions->InternalCustomOptions.Find(MediaPlayerOptionValues::ParseTimecodeInfo()))
{
PlayerOptions.Set(Electra::OptionKeyParseTimecodeInfo, FVariantValue());
}
// Check for one-time initialization options that can't be changed during playback.
int64 InitialStreamBitrate = InOptions->GetMediaOption(ElectraMediaOptions::ElectraInitialBitrate, (int64)-1);
if (InitialStreamBitrate > 0)
{
PlayerOptions.Set(Electra::OptionKeyInitialBitrate, Electra::FVariantValue(InitialStreamBitrate));
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] IMediaPlayer::Open: Using initial bitrate of %d bits/second"), PlayerUniqueID, (int32)InitialStreamBitrate);
}
FString MediaMimeType = InOptions->GetMediaOption(ElectraMediaOptions::Mimetype, FString());
if (MediaMimeType.Len())
{
PlayerOptions.Set(Electra::OptionKeyMimeType, Electra::FVariantValue(MediaMimeType));
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] IMediaPlayer::Open: Setting media mime type to \"%s\""), PlayerUniqueID, *MediaMimeType);
}
int64 MaxVerticalHeight = InOptions->GetMediaOption(ElectraMediaOptions::MaxElectraVerticalResolution, (int64)-1);
if (MaxVerticalHeight > 0)
{
PlayerOptions.Set(Electra::OptionKeyMaxVerticalResolution, Electra::FVariantValue(MaxVerticalHeight));
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] IMediaPlayer::Open: Limiting vertical resolution to %d for all streams"), PlayerUniqueID, (int32)MaxVerticalHeight);
}
int64 MaxVerticalHeightAt60 = InOptions->GetMediaOption(ElectraMediaOptions::MaxElectraVerticalResolutionOf60fpsVideos, (int64)-1);
if (MaxVerticalHeightAt60 > 0)
{
PlayerOptions.Set(Electra::OptionKeyMaxVerticalResolutionAbove30fps, Electra::FVariantValue(MaxVerticalHeightAt60));
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] IMediaPlayer::Open: Limiting vertical resolution to %d for streams >30fps"), PlayerUniqueID, (int32)MaxVerticalHeightAt60);
}
double LiveEdgeDistanceForNormalPresentation = InOptions->GetMediaOption(ElectraMediaOptions::ElectraLivePresentationOffset, (double)-1.0);
if (LiveEdgeDistanceForNormalPresentation > 0.0)
{
PlayerOptions.Set(Electra::OptionKeyLiveSeekableEndOffset, Electra::FVariantValue(Electra::FTimeValue().SetFromSeconds(LiveEdgeDistanceForNormalPresentation)));
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] IMediaPlayer::Open: Setting distance to live edge for normal presentations to %.3f seconds"), PlayerUniqueID, LiveEdgeDistanceForNormalPresentation);
}
bool bThrowErrorWhenRebuffering = InOptions->GetMediaOption(ElectraMediaOptions::ElectraThrowErrorWhenRebuffering, (bool)false);
if (bThrowErrorWhenRebuffering)
{
PlayerOptions.Set(Electra::OptionThrowErrorWhenRebuffering, Electra::FVariantValue(bThrowErrorWhenRebuffering));
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] IMediaPlayer::Open: Throw playback error when rebuffering"), PlayerUniqueID);
}
FString CDNHTTPStatusDenyStream = InOptions->GetMediaOption(ElectraMediaOptions::ElectraGetDenyStreamCode, FString());
if (CDNHTTPStatusDenyStream.Len())
{
int32 HTTPStatus = -1;
LexFromString(HTTPStatus, *CDNHTTPStatusDenyStream);
if (HTTPStatus > 0 && HTTPStatus < 1000)
{
PlayerOptions.Set(Electra::ABR::OptionKeyABR_CDNSegmentDenyHTTPStatus, Electra::FVariantValue((int64)HTTPStatus));
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] IMediaPlayer::Open: CDN HTTP status %d will deny a stream permanently"), PlayerUniqueID, HTTPStatus);
}
}
// Check if there is an environment specified in which this player is used.
// Certain optimization settings apply for dedicated environments.
if (Environment == MediaPlayerOptionValues::Environment_Preview() || Environment == MediaPlayerOptionValues::Environment_Sequencer())
{
PlayerOptions.Set(Electra::OptionKeyWorkerThreads, Electra::FVariantValue(FString(TEXT("worker"))));
}
// Check for options that can be changed during playback and apply them at startup already.
// If a media source supports the MaxResolutionForMediaStreaming option then we can override the max resolution.
int64 DefaultValue = 0;
int64 MaxVerticalStreamResolution = InOptions->GetMediaOption(ElectraMediaOptions::MaxResolutionForMediaStreaming, DefaultValue);
if (MaxVerticalStreamResolution != 0)
{
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] IMediaPlayer::Open: Limiting max resolution to %d"), PlayerUniqueID, (int32)MaxVerticalStreamResolution);
PlaystartOptions.MaxVerticalStreamResolution = (int32)MaxVerticalStreamResolution;
}
int64 MaxBandwidthForStreaming = InOptions->GetMediaOption(ElectraMediaOptions::ElectraMaxStreamingBandwidth, (int64)0);
if (MaxBandwidthForStreaming > 0)
{
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] Limiting max streaming bandwidth to %d bps"), PlayerUniqueID, (int32)MaxBandwidthForStreaming);
PlaystartOptions.MaxBandwidthForStreaming = (int32)MaxBandwidthForStreaming;
}
AnalyticsInstanceEventCount = 0;
QueuedAnalyticEvents.Empty();
NumQueuedAnalyticEvents = 0;
// Create a guid string for the analytics. We do this here and not in the constructor in case the same instance is used over again.
AnalyticsInstanceGuid = SessionID.ToString(EGuidFormats::Digits);
UpdateAnalyticsCustomValues();
// Start statistics with a clean slate.
Statistics.Reset();
// Get a writable copy of the URL so we can sanitize it if necessary.
MediaUrl = InUrl.TrimStartAndEnd();
bHasPendingError = false;
bHasClosedDueToError = false;
// Create a new empty player structure. This contains the actual player instance, its associated renderers and sample queues.
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> NewPlayer = MakeShared<FInternalPlayerImpl, ESPMode::ThreadSafe>();
// Create the output handlers for audio and video.
NewPlayer->AudioOutputHandler = MakeShared<FOutputHandlerAudio, ESPMode::ThreadSafe>();
NewPlayer->AudioOutputHandler->SetOutputAudioSamplePool(OutputAudioSamplePool);
NewPlayer->AudioOutputHandler->CanOutputQueueReceiveDelegate().BindLambda([Player=NewPlayer.ToWeakPtr(), This=this](bool& bOutCanReceive, int32 InNumSamples)
{
bOutCanReceive = false;
if (auto Pl = Player.Pin())
{
bOutCanReceive = This->CanPresentAudioFrames(InNumSamples);
}
});
NewPlayer->AudioOutputHandler->OutputQueueReceiveSampleDelegate().BindLambda([Player=NewPlayer.ToWeakPtr(), This=this](FElectraAudioSamplePtr InSample)
{
if (auto Pl = Player.Pin())
{
This->OnAudioDecoded(InSample);
}
});
NewPlayer->AudioOutputHandler->OutputQueueFlushSamplesDelegate().BindLambda([Player=NewPlayer.ToWeakPtr(), This=this]()
{
if (auto Pl = Player.Pin())
{
This->OnAudioFlush();
}
});
NewPlayer->VideoOutputHandler = MakeShared<FOutputHandlerVideo, ESPMode::ThreadSafe>();
NewPlayer->VideoOutputHandler->SetOutputTexturePool(OutputTexturePool);
NewPlayer->VideoOutputHandler->CanOutputQueueReceiveDelegate().BindLambda([Player=NewPlayer.ToWeakPtr(), This=this](bool& bOutCanReceive, int32 InNumSamples)
{
bOutCanReceive = false;
if (auto Pl = Player.Pin())
{
bOutCanReceive = This->CanPresentVideoFrames(InNumSamples);
}
});
NewPlayer->VideoOutputHandler->OutputQueueReceiveSampleDelegate().BindLambda([Player=NewPlayer.ToWeakPtr(), This=this](FElectraTextureSamplePtr InSample)
{
if (auto Pl = Player.Pin())
{
This->OnVideoDecoded(InSample);
}
});
NewPlayer->VideoOutputHandler->OutputQueueFlushSamplesDelegate().BindLambda([Player=NewPlayer.ToWeakPtr(), This=this]()
{
if (auto Pl = Player.Pin())
{
This->OnVideoFlush();
}
});
// Create the internal player and register ourselves as metrics receiver and static resource provider.
IAdaptiveStreamingPlayer::FCreateParam CreateParams;
CreateParams.VideoOutputHandler = NewPlayer->VideoOutputHandler;
CreateParams.AudioOutputHandler = NewPlayer->AudioOutputHandler;
CreateParams.ExternalPlayerGUID = PlayerGuid;
FString WorkerThreadOption = PlayerOptions.GetValue(Electra::OptionKeyWorkerThreads).SafeGetFString(TEXT("shared"));
CreateParams.WorkerThreads = WorkerThreadOption.Equals(TEXT("worker"), ESearchCase::IgnoreCase) ? IAdaptiveStreamingPlayer::FCreateParam::EWorkerThreads::DedicatedWorker :
WorkerThreadOption.Equals(TEXT("worker_and_events"), ESearchCase::IgnoreCase) ? IAdaptiveStreamingPlayer::FCreateParam::EWorkerThreads::DedicatedWorkerAndEventDispatch :
IAdaptiveStreamingPlayer::FCreateParam::EWorkerThreads::Shared;
NewPlayer->AdaptivePlayer = IAdaptiveStreamingPlayer::Create(CreateParams);
NewPlayer->AdaptivePlayer->AddMetricsReceiver(this);
NewPlayer->AdaptivePlayer->SetStaticResourceProviderCallback(StaticResourceProvider);
// Create the subtitle receiver and register it with the player.
MediaPlayerSubtitleReceiver = MakeSharedTS<FSubtitleEventReceiver>();
MediaPlayerSubtitleReceiver->GetSubtitleReceivedDelegate().BindRaw(this, &FElectraPlayer::OnSubtitleDecoded);
MediaPlayerSubtitleReceiver->GetSubtitleFlushDelegate().BindRaw(this, &FElectraPlayer::OnSubtitleFlush);
NewPlayer->AdaptivePlayer->AddSubtitleReceiver(MediaPlayerSubtitleReceiver);
// Create a new media player event receiver and register it to receive all non player internal events as soon as they are received.
MediaPlayerEventReceiver = MakeSharedTS<FAEMSEventReceiver>();
MediaPlayerEventReceiver->GetEventReceivedDelegate().BindRaw(this, &FElectraPlayer::OnMediaPlayerEventReceived);
NewPlayer->AdaptivePlayer->AddAEMSReceiver(MediaPlayerEventReceiver, TEXT("*"), TEXT(""), IAdaptiveStreamingPlayerAEMSReceiver::EDispatchMode::OnReceive);
NewPlayer->AdaptivePlayer->Initialize(PlayerOptions);
// Check for options that can be changed during playback and apply them at startup already.
// If a media source supports the MaxResolutionForMediaStreaming option then we can override the max resolution.
if (PlaystartOptions.MaxVerticalStreamResolution.IsSet())
{
NewPlayer->AdaptivePlayer->SetMaxResolution(0, PlaystartOptions.MaxVerticalStreamResolution.GetValue());
}
if (PlaystartOptions.MaxBandwidthForStreaming.IsSet())
{
NewPlayer->AdaptivePlayer->SetBitrateCeiling(PlaystartOptions.MaxBandwidthForStreaming.GetValue());
}
// Set the player member variable to the new player so we can use our internal configuration methods on the new player.
CurrentPlayer = MoveTemp(NewPlayer);
// Apply options that may have been set prior to calling Open().
// Set these only if they have defined values as to not override what might have been set in the PlayerOptions.
if (bFrameAccurateSeeking.IsSet())
{
NewPlayer->AdaptivePlayer->EnableFrameAccurateSeeking(bFrameAccurateSeeking.GetValue());
}
if (bEnableLooping.IsSet())
{
SetLooping(bEnableLooping.GetValue());
}
if (!CurrentPlaybackRange.IsEmpty())
{
SetPlaybackTimeRange(CurrentPlaybackRange);
}
// Issue load of the playlist.
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] IMediaPlayer::Open(%s)"), PlayerUniqueID, *SanitizeMessage(MediaUrl));
CurrentPlayer->AdaptivePlayer->LoadManifest(MediaUrl);
return true;
}
//-----------------------------------------------------------------------------
/**
* Close / Shutdown player
*/
void FElectraPlayer::CloseInternal()
{
LLM_SCOPE(ELLMTag::ElectraPlayer);
ParentObjectLock.Lock();
OptionInterface.Reset();
ParentObjectLock.Unlock();
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> Player = CurrentPlayer;
if (!Player.IsValid())
{
return;
}
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] IMediaPlayer::Close()"), PlayerUniqueID);
CSV_EVENT(ElectraPlayer, TEXT("Close"));
// For all intents and purposes the player can be considered closed here now already.
PlayerState.State = EMediaState::Closed;
MediaUrl.Empty();
PlaystartOptions.TimeOffset.Reset();
PlaystartOptions.InitialAudioTrackAttributes.Reset();
CurrentPlaybackRange = TRange<FTimespan>::Empty();
bFrameAccurateSeeking.Reset();
bEnableLooping.Reset();
// Detach the output handlers so we do not receive any new output.
if (Player->AudioOutputHandler.IsValid())
{
Player->AudioOutputHandler->DetachPlayer();
}
if (Player->VideoOutputHandler.IsValid())
{
Player->VideoOutputHandler->DetachPlayer();
}
// Detach ourselves from receiving any player events.
if (Player->AdaptivePlayer.IsValid())
{
if (MediaPlayerEventReceiver.IsValid())
{
MediaPlayerEventReceiver->GetEventReceivedDelegate().Unbind();
Player->AdaptivePlayer->RemoveAEMSReceiver(MediaPlayerEventReceiver, TEXT("*"), TEXT(""), IAdaptiveStreamingPlayerAEMSReceiver::EDispatchMode::OnStart);
MediaPlayerEventReceiver.Reset();
}
if (MediaPlayerSubtitleReceiver.IsValid())
{
Player->AdaptivePlayer->RemoveSubtitleReceiver(MediaPlayerSubtitleReceiver);
MediaPlayerSubtitleReceiver->GetSubtitleReceivedDelegate().Unbind();
MediaPlayerSubtitleReceiver->GetSubtitleFlushDelegate().Unbind();
MediaPlayerSubtitleReceiver.Reset();
}
Player->AdaptivePlayer->SetStaticResourceProviderCallback(nullptr);
Player->AdaptivePlayer->RemoveMetricsReceiver(this);
}
// Clear any pending static resource requests now.
StaticResourceProvider->ClearPendingRequests();
HandlePlayerEventPlaybackStopped();
LogStatistics();
// Enqueue the last media events. They may get sent in TickInternal() as long as we are still getting ticked.
DeferredMediaEvents.Enqueue(EMediaEvent::TracksChanged);
DeferredMediaEvents.Enqueue(EMediaEvent::MediaClosed);
// Clear out the player instance now.
CurrentPlayer.Reset();
// Swap out the media sample queue.
MediaSamplesLock.Lock();
Player->MediaSamplesToDelete = MoveTemp(MediaSamples);
MediaSamplesLock.Unlock();
// Kick off asynchronous closing now.
FInternalPlayerImpl::DoCloseAsync(MoveTemp(Player), PlayerUniqueID, AsyncResourceReleaseNotification);
}
void FElectraPlayer::FInternalPlayerImpl::DoCloseAsync(TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe>&& InPlayer, uint32 InPlayerID, TSharedPtr<IAsyncResourceReleaseNotification, ESPMode::ThreadSafe> InAsyncResourceReleaseNotification)
{
TFunction<void()> CloseTask = [InPlayer, InPlayerID, InAsyncResourceReleaseNotification]()
{
double TimeCloseBegan = FPlatformTime::Seconds();
InPlayer->AdaptivePlayer->Stop();
InPlayer->AdaptivePlayer.Reset();
InPlayer->AudioOutputHandler.Reset();
InPlayer->VideoOutputHandler.Reset();
InPlayer->MediaSamplesToDelete.Reset();
double TimeCloseEnded = FPlatformTime::Seconds();
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] DoCloseAsync() finished after %.3f msec!"), InPlayerID, (TimeCloseEnded - TimeCloseBegan) * 1000.0);
if (InAsyncResourceReleaseNotification.IsValid())
{
InAsyncResourceReleaseNotification->Signal(IMediaPlayerLifecycleManagerDelegate::ResourceFlags_Decoder);
}
};
if (GIsRunning)
{
FMediaRunnable::EnqueueAsyncTask(MoveTemp(CloseTask));
}
else
{
CloseTask();
}
}
float FElectraPlayer::FPlayerState::GetRate() const
{
return IntendedPlayRate.IsSet() ? IntendedPlayRate.GetValue() : CurrentPlayRate;
}
EMediaState FElectraPlayer::FPlayerState::GetState() const
{
if (IntendedPlayRate.IsSet() && (State == EMediaState::Playing || State == EMediaState::Paused || State == EMediaState::Stopped))
{
return IntendedPlayRate.GetValue() != 0.0f ? EMediaState::Playing : EMediaState::Paused;
}
return State;
}
EMediaStatus FElectraPlayer::FPlayerState::GetStatus() const
{
return Status;
}
void FElectraPlayer::FPlayerState::SetIntendedPlayRate(float InIntendedRate)
{
IntendedPlayRate = InIntendedRate;
}
void FElectraPlayer::FPlayerState::SetPlayRateFromPlayer(float InCurrentPlayerPlayRate)
{
CurrentPlayRate = InCurrentPlayerPlayRate;
// If reverse playback is selected even though it is not supported, leave it set as such.
if (IntendedPlayRate.IsSet() && IntendedPlayRate.GetValue() >= 0.0f)
{
IntendedPlayRate.Reset();
}
}
void FElectraPlayer::TickInternal(FTimespan DeltaTime, FTimespan Timecode)
{
LLM_SCOPE(ELLMTag::ElectraPlayer);
SCOPE_CYCLE_COUNTER(STAT_ElectraPlayer_ElectraPlayer_TickInput);
CSV_SCOPED_TIMING_STAT(ElectraPlayer, TickInput);
// Handle the internal player, if we have one.
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> Player = CurrentPlayer;
if (Player.IsValid() && PlayerState.State != EMediaState::Error)
{
{
// Handle static resource fetch requests.
SCOPE_CYCLE_COUNTER(STAT_ElectraPlayer_ElectraPlayer_StaticResourceRequest);
CSV_SCOPED_TIMING_STAT(ElectraPlayer, StaticResourceRequest);
StaticResourceProvider->ProcessPendingStaticResourceRequests();
}
{
// Check for option changes
SCOPE_CYCLE_COUNTER(STAT_ElectraPlayer_ElectraPlayer_QueryOptions);
CSV_SCOPED_TIMING_STAT(ElectraPlayer, QueryOptions);
FVariantValue VerticalResoLimit = ElectraMediaOptions::GetOptionValue(OptionInterface, ElectraMediaOptions::EOptionType::MaxVerticalStreamResolution, FVariantValue());
if (VerticalResoLimit.IsValid())
{
int64 NewVerticalStreamResolution = VerticalResoLimit.GetInt64();
if (NewVerticalStreamResolution != PlaystartOptions.MaxVerticalStreamResolution.Get(0))
{
PlaystartOptions.MaxVerticalStreamResolution = NewVerticalStreamResolution;
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] Limiting max vertical resolution to %d"), PlayerUniqueID, (int32)NewVerticalStreamResolution);
Player->AdaptivePlayer->SetMaxResolution(0, (int32)NewVerticalStreamResolution);
}
}
FVariantValue BandwidthLimit = ElectraMediaOptions::GetOptionValue(OptionInterface, ElectraMediaOptions::EOptionType::MaxBandwidthForStreaming, FVariantValue());
if (BandwidthLimit.IsValid())
{
int64 NewBandwidthForStreaming = BandwidthLimit.GetInt64();
if (NewBandwidthForStreaming != PlaystartOptions.MaxBandwidthForStreaming.Get(0))
{
PlaystartOptions.MaxBandwidthForStreaming = NewBandwidthForStreaming;
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] Limiting max streaming bandwidth to %d bps"), PlayerUniqueID, (int32)NewBandwidthForStreaming);
Player->AdaptivePlayer->SetBitrateCeiling((int32)NewBandwidthForStreaming);
}
}
}
{
// Process accumulated player events.
SCOPE_CYCLE_COUNTER(STAT_ElectraPlayer_ElectraPlayer_PlayerEvents);
CSV_SCOPED_TIMING_STAT(ElectraPlayer, PlayerEvents);
HandleDeferredPlayerEvents();
if (bHasPendingError)
{
bHasPendingError = false;
if (PlayerState.State == EMediaState::Preparing)
{
DeferredMediaEvents.Enqueue(EMediaEvent::MediaOpenFailed);
}
else if (PlayerState.State == EMediaState::Playing)
{
DeferredMediaEvents.Enqueue(EMediaEvent::MediaClosed);
}
bHasClosedDueToError = true;
CloseInternal();
PlayerState.State = EMediaState::Error;
}
}
}
else
{
DeferredPlayerEvents.Empty();
}
// Forward enqueued media events. We do this even with no current internal player to ensure all pending events are sent and none are lost.
EMediaEvent Event;
while(DeferredMediaEvents.Dequeue(Event))
{
SendMediaSinkEvent(Event);
}
}
bool FElectraPlayer::CanPresentVideoFrames(uint64 InNumFrames)
{
SendMediaSinkEvent(EMediaEvent::Internal_PurgeVideoSamplesHint);
FScopeLock SampleLock(&MediaSamplesLock);
return MediaSamples.IsValid() && MediaSamples->CanReceiveVideoSamples(InNumFrames);
}
void FElectraPlayer::OnVideoDecoded(FElectraTextureSamplePtr InSample)
{
//UE_LOG(LogElectraPlayer, VeryVerbose, TEXT("[%u] OnVideoDecoded(%#.4f, %d,%d)"), PlayerUniqueID, InSample->GetTime().GetTime().GetTotalSeconds(), InSample->GetTime().GetSequenceIndex(), InSample->GetTime().GetLoopIndex());
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> Player = CurrentPlayer;
if (Player.IsValid() && PlayerState.State != EMediaState::Closed)
{
LastPresentedFrameDimension = InSample->GetOutputDim();
FScopeLock SampleLock(&MediaSamplesLock);
if (MediaSamples.IsValid())
{
MediaSamples->AddVideo(InSample.ToSharedRef());
}
}
}
void FElectraPlayer::OnVideoFlush()
{
//UE_LOG(LogElectraPlayer, VeryVerbose, TEXT("[%u] OnVideoFlush()"), PlayerUniqueID);
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> Player = CurrentPlayer;
if (Player.IsValid())
{
FScopeLock SampleLock(&MediaSamplesLock);
if (MediaSamples.IsValid())
{
TRange<FTimespan> AllTime(FTimespan::MinValue(), FTimespan::MaxValue());
TSharedPtr<IMediaTextureSample, ESPMode::ThreadSafe> FlushSample;
while(MediaSamples->FetchVideo(AllTime, FlushSample))
{ }
}
}
}
bool FElectraPlayer::CanPresentAudioFrames(uint64 InNumFrames)
{
FScopeLock lock(&MediaSamplesLock);
return MediaSamples.IsValid() && MediaSamples->CanReceiveAudioSamples(InNumFrames);
}
void FElectraPlayer::OnAudioDecoded(FElectraAudioSamplePtr InSample)
{
//UE_LOG(LogElectraPlayer, VeryVerbose, TEXT("[%u] OnAudioDecoded(%#.4f, %d,%d)"), PlayerUniqueID, InSample->GetTime().Time.GetTotalSeconds(), (int32)InSample->GetTime().SequenceIndex, (int32)(InSample->GetTime().SequenceIndex >> 32));
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> Player = CurrentPlayer;
if (Player.IsValid() && InSample.IsValid() && PlayerState.State != EMediaState::Closed)
{
FScopeLock SampleLock(&MediaSamplesLock);
if (MediaSamples.IsValid())
{
MediaSamples->AddAudio(InSample.ToSharedRef());
}
}
}
void FElectraPlayer::OnAudioFlush()
{
//UE_LOG(LogElectraPlayer, VeryVerbose, TEXT("[%u] OnAudioFlush()"), PlayerUniqueID);
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> Player = CurrentPlayer;
if (Player.IsValid())
{
FScopeLock SampleLock(&MediaSamplesLock);
if (MediaSamples.IsValid())
{
TRange<FTimespan> AllTime(FTimespan::MinValue(), FTimespan::MaxValue());
TSharedPtr<IMediaAudioSample, ESPMode::ThreadSafe> FlushSample;
while(MediaSamples->FetchAudio(AllTime, FlushSample))
{ }
}
}
}
void FElectraPlayer::OnSubtitleDecoded(ISubtitleDecoderOutputPtr InDecoderOutput)
{
//UE_LOG(LogElectraPlayer, VeryVerbose, TEXT("[%u] OnSubtitleDecoded(%#.4f, %d,%d)"), PlayerUniqueID, InDecoderOutput->GetTime().Time.GetTotalSeconds(), (int32)InDecoderOutput->GetTime().SequenceIndex, (int32)(InDecoderOutput->GetTime().SequenceIndex >> 32));
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> Player = CurrentPlayer;
if (Player.IsValid() && InDecoderOutput.IsValid() && PlayerState.State != EMediaState::Closed)
{
FScopeLock SampleLock(&MediaSamplesLock);
if (MediaSamples.IsValid())
{
TSharedRef<FElectraSubtitleSample, ESPMode::ThreadSafe> SubtitleSample = MakeShared<FElectraSubtitleSample, ESPMode::ThreadSafe>();
SubtitleSample->Subtitle = InDecoderOutput;
MediaSamples->AddSubtitle(SubtitleSample);
}
}
}
void FElectraPlayer::OnSubtitleFlush()
{
//UE_LOG(LogElectraPlayer, VeryVerbose, TEXT("[%u] OnSubtitleFlush()"), PlayerUniqueID);
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> Player = CurrentPlayer;
if (Player.IsValid())
{
FScopeLock SampleLock(&MediaSamplesLock);
if (MediaSamples.IsValid())
{
TRange<FTimespan> AllTime(FTimespan::MinValue(), FTimespan::MaxValue());
TSharedPtr<IMediaOverlaySample, ESPMode::ThreadSafe> FlushSample;
while(MediaSamples->FetchSubtitle(AllTime, FlushSample))
{ }
}
}
}
bool FElectraPlayer::IsLive()
{
auto Player = CurrentPlayer;
if (Player.IsValid())
{
Electra::FTimeValue Dur = Player->AdaptivePlayer->GetDuration();
if (Dur.IsValid())
{
return Dur.IsInfinity();
}
}
// Default assumption is Live playback.
return true;
}
void FElectraPlayer::TriggerFirstSeekIfNecessary()
{
if (!bInitialSeekPerformed)
{
bInitialSeekPerformed = true;
// Set up the initial playback position
IAdaptiveStreamingPlayer::FSeekParam playParam;
// First we look at any potential time offset specified in the playstart options.
if (PlaystartOptions.TimeOffset.IsSet())
{
FTimespan Target;
CalculateTargetSeekTime(Target, PlaystartOptions.TimeOffset.GetValue());
playParam.Time.SetFromHNS(Target.GetTicks());
}
else
{
// Do not set a start time, let the player pick one.
//playParam.Time.SetToZero();
}
// Check with the media options if it wants to start somewhere else.
TSharedPtr<TArray<FTimespan>, ESPMode::ThreadSafe> SeekablePositions = MakeShared<TArray<FTimespan>, ESPMode::ThreadSafe>();
FVariantValue PlaystartPos = ElectraMediaOptions::GetOptionValue(OptionInterface, ElectraMediaOptions::EOptionType::PlaystartPosFromSeekPositions, FVariantValue(SeekablePositions));
if (PlaystartPos.IsValid())
{
check(PlaystartPos.IsType(FVariantValue::EDataType::TypeInt64));
playParam.Time.SetFromHNS(PlaystartPos.GetInt64());
}
// Trigger buffering at the intended start time.
auto Player = CurrentPlayer;
if (Player.IsValid())
{
// If the media is flagged as a replay event then the start time is also implicitly provided and we must not set one.
IAdaptiveStreamingPlayer::FEventReplayState evtrs;
Player->AdaptivePlayer->GetReplayEventState(evtrs);
if (evtrs.bIsReplayEvent)
{
playParam.Time.SetToInvalid();
}
Player->AdaptivePlayer->SeekTo(playParam);
}
}
}
void FElectraPlayer::CalculateTargetSeekTime(FTimespan& OutTargetTime, const FTimespan& InTime)
{
auto Player = CurrentPlayer;
if (Player.IsValid())
{
Electra::FTimeValue newTime;
Electra::FTimeRange playRange;
newTime.SetFromHNS(InTime.GetTicks());
Player->AdaptivePlayer->GetSeekableRange(playRange);
// Seek semantics are different for VoD and Live.
// For VoD we assume the timeline to be from [0 .. duration) and not offset to what may have been an original airdate in UTC, and the seek time
// needs to fall into that range.
// For Live the timeline is assumed to be UTC wallclock time in [UTC-DVRwindow .. UTC) and the seek time is an offset BACKWARDS from the UTC Live edge
// into content already aired.
if (IsLive())
{
// If the target is maximum we treat it as going to the Live edge.
if (InTime == FTimespan::MaxValue())
{
OutTargetTime = InTime;
return;
}
// In case the seek time has been given as a negative number we negate it.
if (newTime.GetAsHNS() < 0)
{
newTime = Electra::FTimeValue::GetZero() - newTime;
}
// We want to go that far back from the Live edge.
newTime = playRange.End - newTime;
// Need to clamp this to the beginning of the timeline.
if (newTime < playRange.Start)
{
newTime = playRange.Start;
}
}
else
{
// For VoD we clamp the time into the timeline only when it would fall off the beginning.
// We purposely allow to seek outside the duration which will trigger an 'ended' event.
// This is to make sure that a game event during which a VoD asset is played and synchronized
// to the beginning of the event itself will not play the last n seconds for people who have
// joined the event when it is already over.
if (newTime < playRange.Start)
{
newTime = playRange.Start;
}
/*
else if (newTime > playRange.End)
{
newTime = playRange.End;
}
*/
}
OutTargetTime = FTimespan(newTime.GetAsHNS());
}
}
TSharedPtr<Electra::FTrackMetadata, ESPMode::ThreadSafe> FElectraPlayer::GetTrackStreamMetadata(EMediaTrackType InTrackType, int32 InTrackIndex) const
{
auto Player = CurrentPlayer;
if (!Player.IsValid())
{
return nullptr;
}
TArray<Electra::FTrackMetadata> TrackMetaData;
if (InTrackType == EMediaTrackType::Video)
{
Player->AdaptivePlayer->GetTrackMetadata(TrackMetaData, Electra::EStreamType::Video);
}
else if (InTrackType == EMediaTrackType::Audio)
{
Player->AdaptivePlayer->GetTrackMetadata(TrackMetaData, Electra::EStreamType::Audio);
}
else if (InTrackType == EMediaTrackType::Subtitle)
{
Player->AdaptivePlayer->GetTrackMetadata(TrackMetaData, Electra::EStreamType::Subtitle);
}
if (InTrackIndex >= 0 && InTrackIndex < TrackMetaData.Num())
{
return MakeShared<Electra::FTrackMetadata, ESPMode::ThreadSafe>(TrackMetaData[InTrackIndex]);
}
return nullptr;
}
void FElectraPlayer::HandleDeferredPlayerEvents()
{
TSharedPtrTS<FPlayerMetricEventBase> Event;
while(DeferredPlayerEvents.Dequeue(Event))
{
switch(Event->Type)
{
case FPlayerMetricEventBase::EType::OpenSource:
{
FPlayerMetricEvent_OpenSource* Ev = static_cast<FPlayerMetricEvent_OpenSource*>(Event.Get());
HandlePlayerEventOpenSource(Ev->URL);
break;
}
case FPlayerMetricEventBase::EType::ReceivedMainPlaylist:
{
FPlayerMetricEvent_ReceivedMainPlaylist* Ev = static_cast<FPlayerMetricEvent_ReceivedMainPlaylist*>(Event.Get());
HandlePlayerEventReceivedMainPlaylist(Ev->EffectiveURL);
break;
}
case FPlayerMetricEventBase::EType::ReceivedPlaylists:
{
HandlePlayerEventReceivedPlaylists();
break;
}
case FPlayerMetricEventBase::EType::TracksChanged:
{
HandlePlayerEventTracksChanged();
break;
}
case FPlayerMetricEventBase::EType::PlaylistDownload:
{
FPlayerMetricEvent_PlaylistDownload* Ev = static_cast<FPlayerMetricEvent_PlaylistDownload*>(Event.Get());
HandlePlayerEventPlaylistDownload(Ev->PlaylistDownloadStats);
break;
}
case FPlayerMetricEventBase::EType::CleanStart:
{
break;
}
case FPlayerMetricEventBase::EType::BufferingStart:
{
FPlayerMetricEvent_BufferingStart* Ev = static_cast<FPlayerMetricEvent_BufferingStart*>(Event.Get());
HandlePlayerEventBufferingStart(Ev->BufferingReason);
break;
}
case FPlayerMetricEventBase::EType::BufferingEnd:
{
FPlayerMetricEvent_BufferingEnd* Ev = static_cast<FPlayerMetricEvent_BufferingEnd*>(Event.Get());
HandlePlayerEventBufferingEnd(Ev->BufferingReason);
break;
}
case FPlayerMetricEventBase::EType::Bandwidth:
{
FPlayerMetricEvent_Bandwidth* Ev = static_cast<FPlayerMetricEvent_Bandwidth*>(Event.Get());
HandlePlayerEventBandwidth(Ev->EffectiveBps, Ev->ThroughputBps, Ev->LatencyInSeconds);
break;
}
case FPlayerMetricEventBase::EType::BufferUtilization:
{
FPlayerMetricEvent_BufferUtilization* Ev = static_cast<FPlayerMetricEvent_BufferUtilization*>(Event.Get());
HandlePlayerEventBufferUtilization(Ev->BufferStats);
break;
}
case FPlayerMetricEventBase::EType::SegmentDownload:
{
FPlayerMetricEvent_SegmentDownload* Ev = static_cast<FPlayerMetricEvent_SegmentDownload*>(Event.Get());
HandlePlayerEventSegmentDownload(Ev->SegmentDownloadStats);
break;
}
case FPlayerMetricEventBase::EType::LicenseKey:
{
FPlayerMetricEvent_LicenseKey* Ev = static_cast<FPlayerMetricEvent_LicenseKey*>(Event.Get());
HandlePlayerEventLicenseKey(Ev->LicenseKeyStats);
break;
}
case FPlayerMetricEventBase::EType::DataAvailabilityChange:
{
FPlayerMetricEvent_DataAvailabilityChange* Ev = static_cast<FPlayerMetricEvent_DataAvailabilityChange*>(Event.Get());
HandlePlayerEventDataAvailabilityChange(Ev->DataAvailability);
break;
}
case FPlayerMetricEventBase::EType::VideoQualityChange:
{
FPlayerMetricEvent_VideoQualityChange* Ev = static_cast<FPlayerMetricEvent_VideoQualityChange*>(Event.Get());
HandlePlayerEventVideoQualityChange(Ev->NewBitrate, Ev->PreviousBitrate, Ev->bIsDrasticDownswitch);
break;
}
case FPlayerMetricEventBase::EType::AudioQualityChange:
{
FPlayerMetricEvent_AudioQualityChange* Ev = static_cast<FPlayerMetricEvent_AudioQualityChange*>(Event.Get());
HandlePlayerEventAudioQualityChange(Ev->NewBitrate, Ev->PreviousBitrate, Ev->bIsDrasticDownswitch);
break;
}
case FPlayerMetricEventBase::EType::CodecFormatChange:
{
FPlayerMetricEvent_CodecFormatChange* Ev = static_cast<FPlayerMetricEvent_CodecFormatChange*>(Event.Get());
HandlePlayerEventCodecFormatChange(Ev->NewDecodingFormat);
break;
}
case FPlayerMetricEventBase::EType::PrerollStart:
{
HandlePlayerEventPrerollStart();
break;
}
case FPlayerMetricEventBase::EType::PrerollEnd:
{
HandlePlayerEventPrerollEnd();
break;
}
case FPlayerMetricEventBase::EType::PlaybackStart:
{
HandlePlayerEventPlaybackStart();
break;
}
case FPlayerMetricEventBase::EType::PlaybackPaused:
{
HandlePlayerEventPlaybackPaused();
break;
}
case FPlayerMetricEventBase::EType::PlaybackResumed:
{
HandlePlayerEventPlaybackResumed();
break;
}
case FPlayerMetricEventBase::EType::PlaybackEnded:
{
HandlePlayerEventPlaybackEnded();
break;
}
case FPlayerMetricEventBase::EType::JumpInPlayPosition:
{
FPlayerMetricEvent_JumpInPlayPosition* Ev = static_cast<FPlayerMetricEvent_JumpInPlayPosition*>(Event.Get());
HandlePlayerEventJumpInPlayPosition(Ev->ToNewTime, Ev->FromTime, Ev->TimejumpReason);
break;
}
case FPlayerMetricEventBase::EType::PlaybackStopped:
{
HandlePlayerEventPlaybackStopped();
break;
}
case FPlayerMetricEventBase::EType::SeekCompleted:
{
HandlePlayerEventSeekCompleted();
break;
}
case FPlayerMetricEventBase::EType::MediaMetadataChanged:
{
FPlayerMetricEvent_MediaMetadataChange* Ev = static_cast<FPlayerMetricEvent_MediaMetadataChange*>(Event.Get());
HandlePlayerMediaMetadataChanged(Ev->NewMetadata);
break;
}
case FPlayerMetricEventBase::EType::Error:
{
FPlayerMetricEvent_Error* Ev = static_cast<FPlayerMetricEvent_Error*>(Event.Get());
HandlePlayerEventError(Ev->ErrorReason);
break;
}
case FPlayerMetricEventBase::EType::LogMessage:
{
FPlayerMetricEvent_LogMessage* Ev = static_cast<FPlayerMetricEvent_LogMessage*>(Event.Get());
HandlePlayerEventLogMessage(Ev->LogLevel, Ev->LogMessage, Ev->PlayerWallclockMilliseconds);
break;
}
case FPlayerMetricEventBase::EType::DroppedVideoFrame:
{
HandlePlayerEventDroppedVideoFrame();
break;
}
case FPlayerMetricEventBase::EType::DroppedAudioFrame:
{
HandlePlayerEventDroppedAudioFrame();
break;
}
default:
{
break;
}
}
}
}
void FElectraPlayer::HandlePlayerEventOpenSource(const FString& URL)
{
PlayerState.Status = PlayerState.Status | EMediaStatus::Connecting;
PlayerState.State = EMediaState::Preparing;
DeferredMediaEvents.Enqueue(EMediaEvent::MediaConnecting);
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] Opening stream at \"%s\""), PlayerUniqueID, *SanitizeMessage(URL));
// Update statistics
FScopeLock Lock(&StatisticsLock);
Statistics.AddMessageToHistory(TEXT("Opening stream"));
Statistics.InitialURL = URL;
Statistics.TimeAtOpen = FPlatformTime::Seconds();
Statistics.LastState = "Opening";
// Enqueue an "OpenSource" event.
static const FString kEventNameElectraOpenSource(TEXT("Electra.OpenSource"));
if (Electra::IsAnalyticsEventEnabled(kEventNameElectraOpenSource))
{
TSharedPtr<FAnalyticsEvent> AnalyticEvent = CreateAnalyticsEvent(kEventNameElectraOpenSource);
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("URL"), *URL));
EnqueueAnalyticsEvent(AnalyticEvent);
}
}
void FElectraPlayer::HandlePlayerEventReceivedMainPlaylist(const FString& EffectiveURL)
{
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] Received main playlist from \"%s\""), PlayerUniqueID, *SanitizeMessage(EffectiveURL));
// Update statistics
FScopeLock Lock(&StatisticsLock);
Statistics.AddMessageToHistory(TEXT("Got main playlist"));
// Note the time it took to get the main playlist
Statistics.TimeToLoadMainPlaylist = FPlatformTime::Seconds() - Statistics.TimeAtOpen;
Statistics.LastState = "Preparing";
// Enqueue a "MainPlaylist" event.
static const FString kEventNameElectraMainPlaylist(TEXT("Electra.MainPlaylist"));
if (Electra::IsAnalyticsEventEnabled(kEventNameElectraMainPlaylist))
{
TSharedPtr<FAnalyticsEvent> AnalyticEvent = CreateAnalyticsEvent(kEventNameElectraMainPlaylist);
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("URL"), *EffectiveURL));
EnqueueAnalyticsEvent(AnalyticEvent);
}
}
void FElectraPlayer::HandlePlayerEventReceivedPlaylists()
{
auto Player = CurrentPlayer;
if (!Player.IsValid())
{
return;
}
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] Received initial stream playlists"), PlayerUniqueID);
PlayerState.Status = PlayerState.Status & ~EMediaStatus::Connecting;
MediaStateOnPreparingFinished();
Electra::FTimeRange MediaTimeline;
Electra::FTimeValue MediaDuration;
Player->AdaptivePlayer->GetTimelineRange(MediaTimeline);
MediaDuration = Player->AdaptivePlayer->GetDuration();
// Update statistics
StatisticsLock.Lock();
Statistics.AddMessageToHistory(TEXT("Got initial playlists"));
// Note the time it took to get the stream playlist
Statistics.TimeToLoadStreamPlaylists = FPlatformTime::Seconds() - Statistics.TimeAtOpen;
Statistics.LastState = "Idle";
// Establish the timeline and duration.
Statistics.MediaTimelineAtStart = MediaTimeline;
Statistics.MediaTimelineAtEnd = MediaTimeline;
Statistics.MediaDuration = MediaDuration.IsInfinity() ? -1.0 : MediaDuration.GetAsSeconds();
Statistics.VideoQualityPercentages.Empty();
Statistics.AudioQualityPercentages.Empty();
Statistics.VideoSegmentBitratesStreamed.Empty();
Statistics.AudioSegmentBitratesStreamed.Empty();
Statistics.NumVideoSegmentsStreamed = 0;
Statistics.NumAudioSegmentsStreamed = 0;
StatisticsLock.Unlock();
// Get the video bitrates and populate our number of segments per bitrate map.
TArray<Electra::FTrackMetadata> VideoStreamMetaData;
Player->AdaptivePlayer->GetTrackMetadata(VideoStreamMetaData, Electra::EStreamType::Video);
NumTracksVideo = VideoStreamMetaData.Num();
if (NumTracksVideo)
{
for(int32 i=0; i<VideoStreamMetaData[0].StreamDetails.Num(); ++i)
{
StatisticsLock.Lock();
Statistics.VideoSegmentBitratesStreamed.Add(VideoStreamMetaData[0].StreamDetails[i].Bandwidth, 0);
Statistics.VideoQualityPercentages.Add(VideoStreamMetaData[0].StreamDetails[i].Bandwidth, 0);
StatisticsLock.Unlock();
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] Found %d * %d video stream at bitrate %d"), PlayerUniqueID,
VideoStreamMetaData[0].StreamDetails[i].CodecInformation.GetResolution().Width,
VideoStreamMetaData[0].StreamDetails[i].CodecInformation.GetResolution().Height,
VideoStreamMetaData[0].StreamDetails[i].Bandwidth);
}
}
SelectedVideoTrackIndex = NumTracksVideo ? 0 : -1;
// Get the audio bitrates and populate our number of segments per bitrate map.
TArray<Electra::FTrackMetadata> AudioStreamMetaData;
Player->AdaptivePlayer->GetTrackMetadata(AudioStreamMetaData, Electra::EStreamType::Audio);
NumTracksAudio = AudioStreamMetaData.Num();
if (NumTracksAudio)
{
for(int32 i=0; i<AudioStreamMetaData[0].StreamDetails.Num(); ++i)
{
StatisticsLock.Lock();
Statistics.AudioSegmentBitratesStreamed.Add(AudioStreamMetaData[0].StreamDetails[i].Bandwidth, 0);
Statistics.AudioQualityPercentages.Add(AudioStreamMetaData[0].StreamDetails[i].Bandwidth, 0);
StatisticsLock.Unlock();
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] Found audio stream at bitrate %d"), PlayerUniqueID,
AudioStreamMetaData[0].StreamDetails[i].Bandwidth);
}
}
SelectedAudioTrackIndex = NumTracksAudio ? 0 : -1;
TArray<Electra::FTrackMetadata> SubtitleStreamMetaData;
Player->AdaptivePlayer->GetTrackMetadata(SubtitleStreamMetaData, Electra::EStreamType::Subtitle);
NumTracksSubtitle = SubtitleStreamMetaData.Num();
// Set the initial video track selection attributes.
Electra::FStreamSelectionAttributes InitialVideoAttributes;
InitialVideoAttributes.Kind = PlaystartOptions.InitialVideoTrackAttributes.Kind;
InitialVideoAttributes.Language_RFC4647 = PlaystartOptions.InitialVideoTrackAttributes.Language_RFC4647;
InitialVideoAttributes.OverrideIndex = PlaystartOptions.InitialVideoTrackAttributes.OverrideIndex;
Player->AdaptivePlayer->SetInitialStreamAttributes(Electra::EStreamType::Video, InitialVideoAttributes);
// Set the initial audio track selection attributes.
Electra::FStreamSelectionAttributes InitialAudioAttributes;
InitialAudioAttributes.Kind = PlaystartOptions.InitialAudioTrackAttributes.Kind;
InitialAudioAttributes.Language_RFC4647 = PlaystartOptions.InitialAudioTrackAttributes.Language_RFC4647;
InitialAudioAttributes.OverrideIndex = PlaystartOptions.InitialAudioTrackAttributes.OverrideIndex;
Player->AdaptivePlayer->SetInitialStreamAttributes(Electra::EStreamType::Audio, InitialAudioAttributes);
// Set the initial subtitle track selection attributes.
Electra::FStreamSelectionAttributes InitialSubtitleAttributes;
InitialSubtitleAttributes.Kind = PlaystartOptions.InitialSubtitleTrackAttributes.Kind;
InitialSubtitleAttributes.Language_RFC4647 = PlaystartOptions.InitialSubtitleTrackAttributes.Language_RFC4647;
InitialSubtitleAttributes.OverrideIndex = PlaystartOptions.InitialSubtitleTrackAttributes.OverrideIndex;
Player->AdaptivePlayer->SetInitialStreamAttributes(Electra::EStreamType::Subtitle, InitialSubtitleAttributes);
// Enqueue a "PlaylistsLoaded" event.
static const FString kEventNameElectraPlaylistLoaded(TEXT("Electra.PlaylistsLoaded"));
if (Electra::IsAnalyticsEventEnabled(kEventNameElectraPlaylistLoaded))
{
TSharedPtr<FAnalyticsEvent> AnalyticEvent = CreateAnalyticsEvent(kEventNameElectraPlaylistLoaded);
EnqueueAnalyticsEvent(AnalyticEvent);
}
// Trigger preloading unless forbidden.
if (PlaystartOptions.bDoNotPreload == false)
{
TriggerFirstSeekIfNecessary();
}
}
void FElectraPlayer::HandlePlayerEventTracksChanged()
{
auto Player = CurrentPlayer;
if (Player.IsValid())
{
TArray<Electra::FTrackMetadata> VideoStreamMetaData;
Player->AdaptivePlayer->GetTrackMetadata(VideoStreamMetaData, Electra::EStreamType::Video);
NumTracksVideo = VideoStreamMetaData.Num();
if (NumTracksVideo)
{
for(int32 i=0; i<VideoStreamMetaData[0].StreamDetails.Num(); ++i)
{
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] Found %d * %d video stream at bitrate %d"), PlayerUniqueID,
VideoStreamMetaData[0].StreamDetails[i].CodecInformation.GetResolution().Width,
VideoStreamMetaData[0].StreamDetails[i].CodecInformation.GetResolution().Height,
VideoStreamMetaData[0].StreamDetails[i].Bandwidth);
}
}
TArray<Electra::FTrackMetadata> AudioStreamMetaData;
Player->AdaptivePlayer->GetTrackMetadata(AudioStreamMetaData, Electra::EStreamType::Audio);
NumTracksAudio = AudioStreamMetaData.Num();
if (NumTracksAudio)
{
for(int32 i=0; i<AudioStreamMetaData[0].StreamDetails.Num(); ++i)
{
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] Found audio stream at bitrate %d"), PlayerUniqueID,
AudioStreamMetaData[0].StreamDetails[i].Bandwidth);
}
}
TArray<Electra::FTrackMetadata> SubtitleStreamMetaData;
Player->AdaptivePlayer->GetTrackMetadata(SubtitleStreamMetaData, Electra::EStreamType::Subtitle);
NumTracksSubtitle = SubtitleStreamMetaData.Num();
bVideoTrackIndexDirty = true;
bAudioTrackIndexDirty = true;
bSubtitleTrackIndexDirty = true;
DeferredMediaEvents.Enqueue(EMediaEvent::TracksChanged);
}
}
void FElectraPlayer::HandlePlayerEventPlaylistDownload(const Electra::Metrics::FPlaylistDownloadStats& PlaylistDownloadStats)
{
// To reduce the number of playlist events during a Live presentation we will only report the initial playlist load
// and later on only failed loads but not successful ones.
bool bReport = PlaylistDownloadStats.LoadType == Electra::Playlist::ELoadType::Initial || !PlaylistDownloadStats.bWasSuccessful;
if (bReport)
{
static const FString kEventNameElectraPlaylistDownload(TEXT("Electra.PlaylistDownload"));
if (Electra::IsAnalyticsEventEnabled(kEventNameElectraPlaylistDownload))
{
// Enqueue a "PlaylistDownload" event.
TSharedPtr<FAnalyticsEvent> AnalyticEvent = CreateAnalyticsEvent(kEventNameElectraPlaylistDownload);
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("URL"), *PlaylistDownloadStats.Url.URL));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("Failure"), *PlaylistDownloadStats.FailureReason));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("ListType"), Electra::Playlist::GetPlaylistTypeString(PlaylistDownloadStats.ListType)));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("LoadType"), Electra::Playlist::GetPlaylistLoadTypeString(PlaylistDownloadStats.LoadType)));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("HTTPStatus"), PlaylistDownloadStats.HTTPStatusCode));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("Retry"), PlaylistDownloadStats.RetryNumber));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("bSuccess"), PlaylistDownloadStats.bWasSuccessful));
EnqueueAnalyticsEvent(AnalyticEvent);
}
}
// If unsuccessful keep track of the type of error.
if (!PlaylistDownloadStats.bWasSuccessful && !PlaylistDownloadStats.bWasAborted)
{
FScopeLock Lock(&StatisticsLock);
if (PlaylistDownloadStats.HTTPStatusCode == 404)
{
++Statistics.NumErr404;
}
else if (PlaylistDownloadStats.HTTPStatusCode >= 400 && PlaylistDownloadStats.HTTPStatusCode < 500)
{
++Statistics.NumErr4xx;
}
else if (PlaylistDownloadStats.HTTPStatusCode >= 500 && PlaylistDownloadStats.HTTPStatusCode < 600)
{
++Statistics.NumErr5xx;
}
else if (PlaylistDownloadStats.bDidTimeout)
{
++Statistics.NumErrTimeouts;
}
else
{
++Statistics.NumErrConnDrops;
}
}
}
void FElectraPlayer::HandlePlayerEventLicenseKey(const Electra::Metrics::FLicenseKeyStats& LicenseKeyStats)
{
// TBD
if (LicenseKeyStats.bWasSuccessful)
{
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] License key obtained"), PlayerUniqueID);
FScopeLock Lock(&StatisticsLock);
Statistics.AddMessageToHistory(TEXT("Obtained license key"));
}
else
{
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] License key error \"%s\""), PlayerUniqueID, *LicenseKeyStats.FailureReason);
FScopeLock Lock(&StatisticsLock);
Statistics.AddMessageToHistory(TEXT("License key error"));
}
}
void FElectraPlayer::HandlePlayerEventDataAvailabilityChange(const Electra::Metrics::FDataAvailabilityChange& DataAvailability)
{
// Pass this event up to the media player facade. We do not act on this here right now.
if (DataAvailability.StreamType == Electra::EStreamType::Video)
{
if (DataAvailability.Availability == Electra::Metrics::FDataAvailabilityChange::EAvailability::DataAvailable)
{
DeferredMediaEvents.Enqueue(EMediaEvent::Internal_VideoSamplesAvailable);
}
else if (DataAvailability.Availability == Electra::Metrics::FDataAvailabilityChange::EAvailability::DataNotAvailable)
{
DeferredMediaEvents.Enqueue(EMediaEvent::Internal_VideoSamplesUnavailable);
}
}
else if (DataAvailability.StreamType == Electra::EStreamType::Audio)
{
if (DataAvailability.Availability == Electra::Metrics::FDataAvailabilityChange::EAvailability::DataAvailable)
{
DeferredMediaEvents.Enqueue(EMediaEvent::Internal_AudioSamplesAvailable);
}
else if (DataAvailability.Availability == Electra::Metrics::FDataAvailabilityChange::EAvailability::DataNotAvailable)
{
DeferredMediaEvents.Enqueue(EMediaEvent::Internal_AudioSamplesUnavailable);
}
}
}
void FElectraPlayer::HandlePlayerEventBufferingStart(Electra::Metrics::EBufferingReason BufferingReason)
{
PlayerState.Status = PlayerState.Status | EMediaStatus::Buffering;
// In case a seek was performed right away the reason would be `Seeking`, but we want to
// track it as `Initial` for statistics reasons and to make sure we won't miss sending `TracksChanged`.
if (bIsFirstBuffering)
{
BufferingReason = Electra::Metrics::EBufferingReason::Initial;
}
// Send TracksChanged on the initial buffering event. Prior to that we do not know where in the stream
// playback will begin and what tracks are available there.
if (BufferingReason == Electra::Metrics::EBufferingReason::Initial)
{
// Mark the track indices as dirty in order to get the current active ones again.
// This is necessary since the player may have made a different selection given the
// initial track preferences we gave it.
bVideoTrackIndexDirty = true;
bAudioTrackIndexDirty = true;
bSubtitleTrackIndexDirty = true;
DeferredMediaEvents.Enqueue(EMediaEvent::TracksChanged);
}
DeferredMediaEvents.Enqueue(EMediaEvent::MediaBuffering);
// Update statistics
FScopeLock Lock(&StatisticsLock);
Statistics.TimeAtBufferingBegin = FPlatformTime::Seconds();
switch(BufferingReason)
{
case Electra::Metrics::EBufferingReason::Initial:
{
Statistics.bIsInitiallyDownloading = true;
Statistics.LastState = "Buffering";
break;
}
case Electra::Metrics::EBufferingReason::Seeking:
{
Statistics.LastState = "Seeking";
break;
}
case Electra::Metrics::EBufferingReason::Rebuffering:
{
++Statistics.NumTimesRebuffered;
Statistics.LastState = "Rebuffering";
break;
}
}
// Enqueue a "BufferingStart" event.
static const FString kEventNameElectraBufferingStart(TEXT("Electra.BufferingStart"));
if (Electra::IsAnalyticsEventEnabled(kEventNameElectraBufferingStart))
{
TSharedPtr<FAnalyticsEvent> AnalyticEvent = CreateAnalyticsEvent(kEventNameElectraBufferingStart);
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("Type"), Electra::Metrics::GetBufferingReasonString(BufferingReason)));
EnqueueAnalyticsEvent(AnalyticEvent);
}
FString Msg = FString::Printf(TEXT("%s buffering starts"), Electra::Metrics::GetBufferingReasonString(BufferingReason));
Statistics.AddMessageToHistory(Msg);
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] %s"), PlayerUniqueID, *Msg);
CSV_EVENT(ElectraPlayer, TEXT("Buffering starts"));
}
void FElectraPlayer::HandlePlayerEventBufferingEnd(Electra::Metrics::EBufferingReason BufferingReason)
{
// Note: While this event signals the end of buffering the player will now immediately transition into the pre-rolling
// state from which a playback start is not quite possible yet and would incur a slight delay until it is.
// To avoid this we keep the state as buffering until the pre-rolling phase has also completed.
//PlayerState.Status = PlayerState.Status & ~EMediaStatus::Buffering;
// In case a seek was performed right away the reason would be `Seeking`, but we want to track it as `Initial` for statistics.
if (bIsFirstBuffering)
{
BufferingReason = Electra::Metrics::EBufferingReason::Initial;
bIsFirstBuffering = false;
}
// Update statistics
FScopeLock Lock(&StatisticsLock);
double BufferingDuration = FPlatformTime::Seconds() - Statistics.TimeAtBufferingBegin;
switch(BufferingReason)
{
case Electra::Metrics::EBufferingReason::Initial:
{
Statistics.InitialBufferingDuration = BufferingDuration;
break;
}
case Electra::Metrics::EBufferingReason::Seeking:
{
// End of seek buffering is not relevant here.
break;
}
case Electra::Metrics::EBufferingReason::Rebuffering:
{
if (BufferingDuration > Statistics.LongestRebufferingDuration)
{
Statistics.LongestRebufferingDuration = BufferingDuration;
}
Statistics.TotalRebufferingDuration += BufferingDuration;
break;
}
}
// Enqueue a "BufferingEnd" event.
static const FString kEventNameElectraBufferingEnd(TEXT("Electra.BufferingEnd"));
if (Electra::IsAnalyticsEventEnabled(kEventNameElectraBufferingEnd))
{
TSharedPtr<FAnalyticsEvent> AnalyticEvent = CreateAnalyticsEvent(kEventNameElectraBufferingEnd);
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("Type"), Electra::Metrics::GetBufferingReasonString(BufferingReason)));
EnqueueAnalyticsEvent(AnalyticEvent);
}
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] %s buffering ended after %.3fs"), PlayerUniqueID, Electra::Metrics::GetBufferingReasonString(BufferingReason), BufferingDuration);
Statistics.AddMessageToHistory(TEXT("Buffering ended"));
Statistics.LastState = "Ready";
CSV_EVENT(ElectraPlayer, TEXT("Buffering ends"));
}
void FElectraPlayer::HandlePlayerEventBandwidth(int64 EffectiveBps, int64 ThroughputBps, double LatencyInSeconds)
{
// FScopeLock Lock(&StatisticsLock);
UE_LOG(LogElectraPlayer, VeryVerbose, TEXT("[%u] Observed bandwidth of %lld Kbps; throughput = %lld Kbps; latency = %.3fs"), PlayerUniqueID, EffectiveBps/1000, ThroughputBps/1000, LatencyInSeconds);
}
void FElectraPlayer::HandlePlayerEventBufferUtilization(const Electra::Metrics::FBufferStats& BufferStats)
{
// FScopeLock Lock(&StatisticsLock);
}
void FElectraPlayer::HandlePlayerEventSegmentDownload(const Electra::Metrics::FSegmentDownloadStats& SegmentDownloadStats)
{
// Cached responses are not actual network traffic, so we ignore them.
if (SegmentDownloadStats.bIsCachedResponse)
{
return;
}
// Update statistics
FScopeLock Lock(&StatisticsLock);
if (SegmentDownloadStats.StreamType == Electra::EStreamType::Video)
{
Statistics.NumVideoDatabytesStreamed += SegmentDownloadStats.NumBytesDownloaded;
if (!Statistics.VideoSegmentBitratesStreamed.Contains(SegmentDownloadStats.Bitrate))
{
Statistics.VideoSegmentBitratesStreamed.Add(SegmentDownloadStats.Bitrate, 0);
}
++Statistics.VideoSegmentBitratesStreamed[SegmentDownloadStats.Bitrate];
++Statistics.NumVideoSegmentsStreamed;
if (!Statistics.VideoQualityPercentages.Contains(SegmentDownloadStats.Bitrate))
{
Statistics.VideoQualityPercentages.Add(SegmentDownloadStats.Bitrate, 0);
}
for(auto& It : Statistics.VideoQualityPercentages)
{
const uint32 NumAt = Statistics.VideoSegmentBitratesStreamed[It.Key];
const int32 AsPercentage = FMath::RoundToInt(100.0 * (double)NumAt / (double)Statistics.NumVideoSegmentsStreamed);
It.Value = AsPercentage;
}
if (Statistics.bIsInitiallyDownloading)
{
Statistics.InitialBufferingBandwidth.AddSample(8*SegmentDownloadStats.NumBytesDownloaded / (SegmentDownloadStats.TimeToDownload > 0.0 ? SegmentDownloadStats.TimeToDownload : 1.0), SegmentDownloadStats.TimeToFirstByte);
if (Statistics.InitialBufferingDuration > 0.0)
{
Statistics.bIsInitiallyDownloading = false;
}
}
}
else if (SegmentDownloadStats.StreamType == Electra::EStreamType::Audio)
{
Statistics.NumAudioDatabytesStreamed += SegmentDownloadStats.NumBytesDownloaded;
if (!Statistics.AudioSegmentBitratesStreamed.Contains(SegmentDownloadStats.Bitrate))
{
Statistics.AudioSegmentBitratesStreamed.Add(SegmentDownloadStats.Bitrate, 0);
}
++Statistics.AudioSegmentBitratesStreamed[SegmentDownloadStats.Bitrate];
++Statistics.NumAudioSegmentsStreamed;
if (!Statistics.AudioQualityPercentages.Contains(SegmentDownloadStats.Bitrate))
{
Statistics.AudioQualityPercentages.Add(SegmentDownloadStats.Bitrate, 0);
}
for(auto& It : Statistics.AudioQualityPercentages)
{
const uint32 NumAt = Statistics.AudioSegmentBitratesStreamed[It.Key];
const int32 AsPercentage = FMath::RoundToInt(100.0 * (double)NumAt / (double)Statistics.NumAudioSegmentsStreamed);
It.Value = AsPercentage;
}
if (Statistics.bIsInitiallyDownloading && NumTracksVideo == 0) // Do this just for audio-only presentations.
{
Statistics.InitialBufferingBandwidth.AddSample(8*SegmentDownloadStats.NumBytesDownloaded / (SegmentDownloadStats.TimeToDownload > 0.0 ? SegmentDownloadStats.TimeToDownload : 1.0), SegmentDownloadStats.TimeToFirstByte);
if (Statistics.InitialBufferingDuration > 0.0)
{
Statistics.bIsInitiallyDownloading = false;
}
}
}
if (SegmentDownloadStats.bWasSuccessful)
{
UE_LOG(LogElectraPlayer, VeryVerbose, TEXT("[%u] Downloaded %s segment at bitrate %d: Playback time = %.3fs, duration = %.3fs, download time = %.3fs, URL=%s \"%s\""), PlayerUniqueID, Electra::GetStreamTypeName(SegmentDownloadStats.StreamType), SegmentDownloadStats.Bitrate, SegmentDownloadStats.PresentationTime, SegmentDownloadStats.Duration, SegmentDownloadStats.TimeToDownload, *SegmentDownloadStats.Range, *SanitizeMessage(SegmentDownloadStats.URL.URL));
}
else if (SegmentDownloadStats.bWasAborted)
{
++Statistics.NumSegmentDownloadsAborted;
}
if (!SegmentDownloadStats.bWasSuccessful || SegmentDownloadStats.RetryNumber)
{
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] %s segment download issue (%s): retry:%d, success:%d, aborted:%d, filler:%d"), PlayerUniqueID, Electra::Metrics::GetSegmentTypeString(SegmentDownloadStats.SegmentType), *SegmentDownloadStats.FailureReason, SegmentDownloadStats.RetryNumber, SegmentDownloadStats.bWasSuccessful, SegmentDownloadStats.bWasAborted, SegmentDownloadStats.bInsertedFillerData);
if (SegmentDownloadStats.FailureReason.Len())
{
FString Msg;
if (!SegmentDownloadStats.bWasAborted)
{
Msg = FString::Printf(TEXT("%s segment download issue on representation %s, bitrate %d, retry %d: %s"), Electra::Metrics::GetSegmentTypeString(SegmentDownloadStats.SegmentType),
*SegmentDownloadStats.RepresentationID, SegmentDownloadStats.Bitrate, SegmentDownloadStats.RetryNumber, *SegmentDownloadStats.FailureReason);
}
else
{
Msg = FString::Printf(TEXT("%s segment download issue on representation %s, bitrate %d, aborted: %s"), Electra::Metrics::GetSegmentTypeString(SegmentDownloadStats.SegmentType),
*SegmentDownloadStats.RepresentationID, SegmentDownloadStats.Bitrate, *SegmentDownloadStats.FailureReason);
}
Statistics.AddMessageToHistory(Msg);
}
static const FString kEventNameElectraSegmentIssue(TEXT("Electra.SegmentIssue"));
if (Electra::IsAnalyticsEventEnabled(kEventNameElectraSegmentIssue))
{
// Enqueue a "SegmentIssue" event.
TSharedPtr<FAnalyticsEvent> AnalyticEvent = CreateAnalyticsEvent(kEventNameElectraSegmentIssue);
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("URL"), *SegmentDownloadStats.URL.URL));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("Failure"), *SegmentDownloadStats.FailureReason));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("SegmentType"), Electra::Metrics::GetSegmentTypeString(SegmentDownloadStats.SegmentType)));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("HTTPStatus"), SegmentDownloadStats.HTTPStatusCode));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("Retry"), SegmentDownloadStats.RetryNumber));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("bSuccess"), SegmentDownloadStats.bWasSuccessful));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("TimeToFirstByte"), SegmentDownloadStats.TimeToFirstByte));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("ByteSize"), SegmentDownloadStats.ByteSize));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("NumBytesDownloaded"), SegmentDownloadStats.NumBytesDownloaded));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("bWasAborted"), SegmentDownloadStats.bWasAborted));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("bDidTimeout"), SegmentDownloadStats.bDidTimeout));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("bParseFailure"), SegmentDownloadStats.bParseFailure));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("bInsertedFillerData"), SegmentDownloadStats.bInsertedFillerData));
EnqueueAnalyticsEvent(AnalyticEvent);
}
}
if (!SegmentDownloadStats.bWasSuccessful && !SegmentDownloadStats.bWasAborted)
{
if (SegmentDownloadStats.HTTPStatusCode == 404)
{
++Statistics.NumErr404;
}
else if (SegmentDownloadStats.HTTPStatusCode >= 400 && SegmentDownloadStats.HTTPStatusCode < 500)
{
++Statistics.NumErr4xx;
}
else if (SegmentDownloadStats.HTTPStatusCode >= 500 && SegmentDownloadStats.HTTPStatusCode < 600)
{
++Statistics.NumErr5xx;
}
else if (SegmentDownloadStats.bDidTimeout)
{
++Statistics.NumErrTimeouts;
}
else if (SegmentDownloadStats.bParseFailure)
{
++Statistics.NumErrOther;
}
else
{
++Statistics.NumErrConnDrops;
}
}
}
void FElectraPlayer::HandlePlayerEventVideoQualityChange(int32 NewBitrate, int32 PreviousBitrate, bool bIsDrasticDownswitch)
{
auto Player = CurrentPlayer;
if (!Player.IsValid())
{
return;
}
// Update statistics
FScopeLock Lock(&StatisticsLock);
if (PreviousBitrate == 0)
{
Statistics.InitialVideoStreamBitrate = NewBitrate;
}
else
{
if (bIsDrasticDownswitch)
{
++Statistics.NumVideoQualityDrasticDownswitches;
}
if (NewBitrate > PreviousBitrate)
{
++Statistics.NumVideoQualityUpswitches;
}
else
{
++Statistics.NumVideoQualityDownswitches;
}
}
if (bIsDrasticDownswitch)
{
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] Player switched video quality drastically down to %d bps from %d bps. %d upswitches, %d downswitches (%d drastic ones)"), PlayerUniqueID, NewBitrate, PreviousBitrate, Statistics.NumVideoQualityUpswitches, Statistics.NumVideoQualityDownswitches, Statistics.NumVideoQualityDrasticDownswitches);
}
else
{
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] Player switched video quality to %d bps from %d bps. %d upswitches, %d downswitches (%d drastic ones)"), PlayerUniqueID, NewBitrate, PreviousBitrate, Statistics.NumVideoQualityUpswitches, Statistics.NumVideoQualityDownswitches, Statistics.NumVideoQualityDrasticDownswitches);
}
int32 prvWidth = Statistics.CurrentlyActiveResolutionWidth;
int32 prvHeight = Statistics.CurrentlyActiveResolutionHeight;
// Get the current playlist URL
TArray<Electra::FTrackMetadata> VideoStreamMetaData;
Player->AdaptivePlayer->GetTrackMetadata(VideoStreamMetaData, Electra::EStreamType::Video);
if (VideoStreamMetaData.Num())
{
for(int32 i=0; i<VideoStreamMetaData[0].StreamDetails.Num(); ++i)
{
if (VideoStreamMetaData[0].StreamDetails[i].Bandwidth == NewBitrate)
{
SelectedQuality = i;
Statistics.CurrentlyActivePlaylistURL = VideoStreamMetaData[0].ID;
Statistics.CurrentlyActiveResolutionWidth = VideoStreamMetaData[0].StreamDetails[i].CodecInformation.GetResolution().Width;
Statistics.CurrentlyActiveResolutionHeight = VideoStreamMetaData[0].StreamDetails[i].CodecInformation.GetResolution().Height;
break;
}
}
}
// Enqueue a "VideoQualityChange" event.
static const FString kEventNameElectraVideoQualityChange(TEXT("Electra.VideoQualityChange"));
if (Electra::IsAnalyticsEventEnabled(kEventNameElectraVideoQualityChange))
{
TSharedPtr<FAnalyticsEvent> AnalyticEvent = CreateAnalyticsEvent(kEventNameElectraVideoQualityChange);
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("OldBitrate"), PreviousBitrate));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("NewBitrate"), NewBitrate));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("bIsDrasticDownswitch"), bIsDrasticDownswitch));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("OldResolution"), *FString::Printf(TEXT("%d*%d"), prvWidth, prvHeight)));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("NewResolution"), *FString::Printf(TEXT("%d*%d"), Statistics.CurrentlyActiveResolutionWidth, Statistics.CurrentlyActiveResolutionHeight)));
EnqueueAnalyticsEvent(AnalyticEvent);
}
Statistics.AddMessageToHistory(FString::Printf(TEXT("Video bitrate change from %d to %d"), PreviousBitrate, NewBitrate));
CSV_EVENT(ElectraPlayer, TEXT("VideoQualityChange %d -> %d"), PreviousBitrate, NewBitrate);
}
void FElectraPlayer::HandlePlayerEventAudioQualityChange(int32 NewBitrate, int32 PreviousBitrate, bool bIsDrasticDownswitch)
{
// Update statistics
FScopeLock Lock(&StatisticsLock);
if (PreviousBitrate == 0)
{
Statistics.InitialAudioStreamBitrate = NewBitrate;
}
else
{
if (bIsDrasticDownswitch)
{
++Statistics.NumAudioQualityDrasticDownswitches;
}
if (NewBitrate > PreviousBitrate)
{
++Statistics.NumAudioQualityUpswitches;
}
else
{
++Statistics.NumAudioQualityDownswitches;
}
}
if (bIsDrasticDownswitch)
{
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] Player switched audio quality drastically down to %d bps from %d bps. %d upswitches, %d downswitches (%d drastic ones)"), PlayerUniqueID, NewBitrate, PreviousBitrate, Statistics.NumAudioQualityUpswitches, Statistics.NumAudioQualityDownswitches, Statistics.NumAudioQualityDrasticDownswitches);
}
else
{
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] Player switched audio quality to %d bps from %d bps. %d upswitches, %d downswitches (%d drastic ones)"), PlayerUniqueID, NewBitrate, PreviousBitrate, Statistics.NumAudioQualityUpswitches, Statistics.NumAudioQualityDownswitches, Statistics.NumAudioQualityDrasticDownswitches);
}
// Enqueue a "AudioQualityChange" event.
static const FString kEventNameElectraAudioQualityChange(TEXT("Electra.AudioQualityChange"));
if (Electra::IsAnalyticsEventEnabled(kEventNameElectraAudioQualityChange))
{
TSharedPtr<FAnalyticsEvent> AnalyticEvent = CreateAnalyticsEvent(kEventNameElectraAudioQualityChange);
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("OldBitrate"), PreviousBitrate));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("NewBitrate"), NewBitrate));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("bIsDrasticDownswitch"), bIsDrasticDownswitch));
EnqueueAnalyticsEvent(AnalyticEvent);
}
Statistics.AddMessageToHistory(FString::Printf(TEXT("Audio bitrate change from %d to %d"), PreviousBitrate, NewBitrate));
CSV_EVENT(ElectraPlayer, TEXT("AudioQualityChange %d -> %d"), PreviousBitrate, NewBitrate);
}
void FElectraPlayer::HandlePlayerEventCodecFormatChange(const Electra::FStreamCodecInformation& NewDecodingFormat)
{
if (NewDecodingFormat.IsVideoCodec())
{
FVideoStreamFormat fmt;
fmt.Bitrate = NewDecodingFormat.GetBitrate();
fmt.Resolution.X = NewDecodingFormat.GetResolution().Width;
fmt.Resolution.Y = NewDecodingFormat.GetResolution().Height;
fmt.FrameRate = NewDecodingFormat.GetFrameRate().IsValid() ? NewDecodingFormat.GetFrameRate().GetAsDouble() : 0.0;
{
FScopeLock lock(&VideoFormatLock);
CurrentlyActiveVideoStreamFormat = fmt;
}
}
}
void FElectraPlayer::HandlePlayerEventPrerollStart()
{
// Update statistics
FScopeLock Lock(&StatisticsLock);
Statistics.TimeAtPrerollBegin = FPlatformTime::Seconds();
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] Player starts prerolling to warm decoders and renderers"), PlayerUniqueID);
// Enqueue a "PrerollStart" event.
static const FString kEventNameElectraPrerollStart(TEXT("Electra.PrerollStart"));
if (Electra::IsAnalyticsEventEnabled(kEventNameElectraPrerollStart))
{
TSharedPtr<FAnalyticsEvent> AnalyticEvent = CreateAnalyticsEvent(kEventNameElectraPrerollStart);
EnqueueAnalyticsEvent(AnalyticEvent);
}
}
void FElectraPlayer::HandlePlayerEventPrerollEnd()
{
// Note: See comments in ReportBufferingEnd()
// Preroll follows at the end of buffering and we keep the buffering state until preroll has finished as well.
PlayerState.Status = PlayerState.Status & ~EMediaStatus::Buffering;
// Update statistics
FScopeLock Lock(&StatisticsLock);
if (Statistics.TimeForInitialPreroll < 0.0)
{
Statistics.TimeForInitialPreroll = FPlatformTime::Seconds() - Statistics.TimeAtPrerollBegin;
}
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] Player prerolling complete"), PlayerUniqueID);
Statistics.LastState = "Ready";
DeferredMediaEvents.Enqueue(EMediaEvent::MediaBufferingComplete);
// Enqueue a "PrerollEnd" event.
static const FString kEventNameElectraPrerollEnd(TEXT("Electra.PrerollEnd"));
if (Electra::IsAnalyticsEventEnabled(kEventNameElectraPrerollEnd))
{
TSharedPtr<FAnalyticsEvent> AnalyticEvent = CreateAnalyticsEvent(kEventNameElectraPrerollEnd);
EnqueueAnalyticsEvent(AnalyticEvent);
}
}
void FElectraPlayer::HandlePlayerEventPlaybackStart()
{
PlayerState.Status = PlayerState.Status & ~EMediaStatus::Buffering;
MediaStateOnPlay();
auto Player = CurrentPlayer;
if (Player.IsValid())
{
// Update statistics
FScopeLock Lock(&StatisticsLock);
double PlayPos = Player->AdaptivePlayer->GetPlayPosition().GetAsSeconds();
if (Statistics.PlayPosAtStart < 0.0)
{
Statistics.PlayPosAtStart = PlayPos;
}
Statistics.LastState = "Playing";
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] Playback started at play position %.3f"), PlayerUniqueID, PlayPos);
Statistics.AddMessageToHistory(TEXT("Playback started"));
// Enqueue a "Start" event.
static const FString kEventNameElectraStart(TEXT("Electra.Start"));
if (Electra::IsAnalyticsEventEnabled(kEventNameElectraStart))
{
TSharedPtr<FAnalyticsEvent> AnalyticEvent = CreateAnalyticsEvent(kEventNameElectraStart);
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("PlayPos"), PlayPos));
EnqueueAnalyticsEvent(AnalyticEvent);
}
}
}
void FElectraPlayer::HandlePlayerEventPlaybackPaused()
{
MediaStateOnPause();
auto Player = CurrentPlayer;
if (Player.IsValid())
{
FScopeLock Lock(&StatisticsLock);
Statistics.LastState = "Paused";
double PlayPos = Player->AdaptivePlayer->GetPlayPosition().GetAsSeconds();
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] Playback paused at play position %.3f"), PlayerUniqueID, PlayPos);
Statistics.AddMessageToHistory(TEXT("Playback paused"));
// Enqueue a "Pause" event.
static const FString kEventNameElectraPause(TEXT("Electra.Pause"));
if (Electra::IsAnalyticsEventEnabled(kEventNameElectraPause))
{
TSharedPtr<FAnalyticsEvent> AnalyticEvent = CreateAnalyticsEvent(kEventNameElectraPause);
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("PlayPos"), PlayPos));
EnqueueAnalyticsEvent(AnalyticEvent);
}
}
}
void FElectraPlayer::HandlePlayerEventPlaybackResumed()
{
MediaStateOnPlay();
auto Player = CurrentPlayer;
if (Player.IsValid())
{
FScopeLock Lock(&StatisticsLock);
Statistics.LastState = "Playing";
double PlayPos = Player->AdaptivePlayer->GetPlayPosition().GetAsSeconds();
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] Playback resumed at play position %.3f"), PlayerUniqueID, PlayPos);
Statistics.AddMessageToHistory(TEXT("Playback resumed"));
// Enqueue a "Resume" event.
static const FString kEventNameElectraResume(TEXT("Electra.Resume"));
if (Electra::IsAnalyticsEventEnabled(kEventNameElectraResume))
{
TSharedPtr<FAnalyticsEvent> AnalyticEvent = CreateAnalyticsEvent(kEventNameElectraResume);
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("PlayPos"), PlayPos));
EnqueueAnalyticsEvent(AnalyticEvent);
}
}
}
void FElectraPlayer::HandlePlayerEventPlaybackEnded()
{
UpdatePlayEndStatistics();
auto Player = CurrentPlayer;
if (Player.IsValid())
{
double PlayPos = Player->AdaptivePlayer->GetPlayPosition().GetAsSeconds();
// Update statistics
FScopeLock Lock(&StatisticsLock);
Statistics.LastState = "Ended";
Statistics.bDidPlaybackEnd = true;
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] Playback reached end at play position %.3f"), PlayerUniqueID, PlayPos);
Statistics.AddMessageToHistory(TEXT("Playback ended"));
// Enqueue an "End" event.
static const FString kEventNameElectraEnd(TEXT("Electra.End"));
if (Electra::IsAnalyticsEventEnabled(kEventNameElectraEnd))
{
TSharedPtr<FAnalyticsEvent> AnalyticEvent = CreateAnalyticsEvent(kEventNameElectraEnd);
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("PlayPos"), PlayPos));
EnqueueAnalyticsEvent(AnalyticEvent);
}
MediaStateOnEndReached();
}
}
void FElectraPlayer::HandlePlayerEventJumpInPlayPosition(const Electra::FTimeValue& ToNewTime, const Electra::FTimeValue& FromTime, Electra::Metrics::ETimeJumpReason TimejumpReason)
{
Electra::FTimeRange MediaTimeline;
auto Player = CurrentPlayer;
if (Player.IsValid())
{
Player->AdaptivePlayer->GetTimelineRange(MediaTimeline);
// Update statistics
FScopeLock Lock(&StatisticsLock);
if (TimejumpReason == Electra::Metrics::ETimeJumpReason::UserSeek)
{
if (ToNewTime > FromTime)
{
++Statistics.NumTimesForwarded;
}
else if (ToNewTime < FromTime)
{
++Statistics.NumTimesRewound;
}
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] Jump in play position from %.3f to %.3f"), PlayerUniqueID, FromTime.GetAsSeconds(), ToNewTime.GetAsSeconds());
}
else if (TimejumpReason == Electra::Metrics::ETimeJumpReason::Looping)
{
++Statistics.NumTimesLooped;
Electra::IAdaptiveStreamingPlayer::FLoopState loopState;
Player->AdaptivePlayer->GetLoopState(loopState);
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] Looping (%d) from %.3f to %.3f"), PlayerUniqueID, loopState.Count, FromTime.GetAsSeconds(), ToNewTime.GetAsSeconds());
Statistics.AddMessageToHistory(TEXT("Looped"));
}
// Enqueue a "PositionJump" event.
static const FString kEventNameElectraPositionJump(TEXT("Electra.PositionJump"));
if (Electra::IsAnalyticsEventEnabled(kEventNameElectraPositionJump))
{
TSharedPtr<FAnalyticsEvent> AnalyticEvent = CreateAnalyticsEvent(kEventNameElectraPositionJump);
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("From"), FromTime.GetAsSeconds()));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("To"), ToNewTime.GetAsSeconds()));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("Cause"), Electra::Metrics::GetTimejumpReasonString(TimejumpReason)));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("MediaTimeline.Start"), MediaTimeline.Start.GetAsSeconds(-1.0)));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("MediaTimeline.End"), MediaTimeline.End.GetAsSeconds(-1.0)));
EnqueueAnalyticsEvent(AnalyticEvent);
}
}
}
void FElectraPlayer::HandlePlayerEventPlaybackStopped()
{
UpdatePlayEndStatistics();
auto Player = CurrentPlayer;
if (Player.IsValid())
{
double PlayPos = Player->AdaptivePlayer->GetPlayPosition().GetAsSeconds();
// Update statistics
FScopeLock Lock(&StatisticsLock);
Statistics.bDidPlaybackEnd = true;
// Note: we do not change Statistics.LastState since we want to keep the state the player was in when it got closed.
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] Playback stopped. Last play position %.3f"), PlayerUniqueID, PlayPos);
Statistics.AddMessageToHistory(TEXT("Stopped"));
// Enqueue a "Stop" event.
static const FString kEventNameElectraStop(TEXT("Electra.Stop"));
if (Electra::IsAnalyticsEventEnabled(kEventNameElectraStop))
{
TSharedPtr<FAnalyticsEvent> AnalyticEvent = CreateAnalyticsEvent(kEventNameElectraStop);
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("PlayPos"), PlayPos));
EnqueueAnalyticsEvent(AnalyticEvent);
}
}
}
void FElectraPlayer::HandlePlayerEventSeekCompleted()
{
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] Seek completed"), PlayerUniqueID);
MediaStateOnSeekFinished();
}
void FElectraPlayer::HandlePlayerMediaMetadataChanged(const TSharedPtrTS<Electra::UtilsMP4::FMetadataParser>& InMetadata)
{
if (InMetadata.IsValid())
{
TSharedPtr<TMap<FString, TArray<TSharedPtr<Electra::IMediaStreamMetadata::IItem, ESPMode::ThreadSafe>>>, ESPMode::ThreadSafe> PlayerMeta = InMetadata->GetMediaStreamMetadata();
if (PlayerMeta.IsValid())
{
TSharedPtr<TMap<FString, TArray<TUniquePtr<IMediaMetadataItem>>>, ESPMode::ThreadSafe> NewMeta(new TMap<FString, TArray<TUniquePtr<IMediaMetadataItem>>>);
for(auto& PlayerMetaItem : *PlayerMeta)
{
TArray<TUniquePtr<IMediaMetadataItem>>& NewItemList = NewMeta->Emplace(PlayerMetaItem.Key);
for(auto& PlayerMetaListItem : PlayerMetaItem.Value)
{
if (PlayerMetaListItem.IsValid())
{
NewItemList.Emplace(MakeUnique<FStreamMetadataItem>(PlayerMetaListItem));
}
}
}
CurrentStreamMetadata = MoveTemp(NewMeta);
DeferredMediaEvents.Enqueue(EMediaEvent::MetadataChanged);
}
}
}
void FElectraPlayer::HandlePlayerEventError(const FString& ErrorReason)
{
bHasPendingError = true;
// Update statistics
FScopeLock Lock(&StatisticsLock);
// If there is already an error do not overwrite it. First come, first serve!
if (Statistics.LastError.Len() == 0)
{
Statistics.LastError = ErrorReason;
}
// Note: we do not change Statistics.LastState to something like 'error' because we want to know the state the player was in when it errored.
UE_LOG(LogElectraPlayer, Error, TEXT("[%u] ReportError: \"%s\""), PlayerUniqueID, *SanitizeMessage(ErrorReason));
Statistics.AddMessageToHistory(FString::Printf(TEXT("Error: %s"), *SanitizeMessage(ErrorReason)));
FString MessageHistory;
for(auto &msg : Statistics.MessageHistoryBuffer)
{
MessageHistory.Append(FString::Printf(TEXT("%8.3f: %s"), msg.TimeSinceStart, *msg.Message));
MessageHistory.Append(TEXT("<br>"));
}
// Enqueue an "Error" event.
static const FString kEventNameElectraError(TEXT("Electra.Error"));
if (Electra::IsAnalyticsEventEnabled(kEventNameElectraError))
{
TSharedPtr<FAnalyticsEvent> AnalyticEvent = CreateAnalyticsEvent(kEventNameElectraError);
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("Reason"), *ErrorReason));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("LastState"), *Statistics.LastState));
AnalyticEvent->ParamArray.Add(FAnalyticsEventAttribute(TEXT("MessageHistory"), MessageHistory));
EnqueueAnalyticsEvent(AnalyticEvent);
}
}
void FElectraPlayer::HandlePlayerEventLogMessage(Electra::IInfoLog::ELevel InLogLevel, const FString& InLogMessage, int64 InPlayerWallclockMilliseconds)
{
FString m(SanitizeMessage(InLogMessage));
switch(InLogLevel)
{
case Electra::IInfoLog::ELevel::Error:
{
UE_LOG(LogElectraPlayer, Error, TEXT("[%u] %s"), PlayerUniqueID, *m);
FScopeLock Lock(&StatisticsLock);
Statistics.AddMessageToHistory(m);
break;
}
case Electra::IInfoLog::ELevel::Warning:
{
UE_LOG(LogElectraPlayer, Warning, TEXT("[%u] %s"), PlayerUniqueID, *m);
FScopeLock Lock(&StatisticsLock);
Statistics.AddMessageToHistory(m);
break;
}
case Electra::IInfoLog::ELevel::Info:
{
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] %s"), PlayerUniqueID, *m);
break;
}
case Electra::IInfoLog::ELevel::Verbose:
{
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] %s"), PlayerUniqueID, *m);
break;
}
}
}
void FElectraPlayer::HandlePlayerEventDroppedVideoFrame()
{
}
void FElectraPlayer::HandlePlayerEventDroppedAudioFrame()
{
}
void FElectraPlayer::FStatistics::AddMessageToHistory(FString InMessage)
{
if (MessageHistoryBuffer.Num() >= 20)
{
MessageHistoryBuffer.RemoveAt(0);
}
double Now = FPlatformTime::Seconds();
FStatistics::FHistoryEntry he;
he.Message = MoveTemp(InMessage);
he.TimeSinceStart = TimeAtOpen < 0.0 ? 0.0 : Now - TimeAtOpen;
MessageHistoryBuffer.Emplace(MoveTemp(he));
}
void FElectraPlayer::UpdatePlayEndStatistics()
{
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> Player = CurrentPlayer;
if (!Player.IsValid() || !Player->AdaptivePlayer.IsValid())
{
return;
}
double PlayPos = Player->AdaptivePlayer->GetPlayPosition().GetAsSeconds();
Electra::FTimeRange MediaTimeline;
Electra::FTimeValue MediaDuration;
Player->AdaptivePlayer->GetTimelineRange(MediaTimeline);
MediaDuration = Player->AdaptivePlayer->GetDuration();
// Update statistics
FScopeLock Lock(&StatisticsLock);
if (Statistics.PlayPosAtStart >= 0.0 && Statistics.PlayPosAtEnd < 0.0)
{
Statistics.PlayPosAtEnd = PlayPos;
}
// Update the media timeline end.
Statistics.MediaTimelineAtEnd = MediaTimeline;
// Also re-set the duration in case it changed dynamically.
Statistics.MediaDuration = MediaDuration.IsInfinity() ? -1.0 : MediaDuration.GetAsSeconds();
}
void FElectraPlayer::LogStatistics()
{
FString VideoSegsPercentage;
FString AudioSegsPercentage;
FScopeLock Lock(&StatisticsLock);
int32 Idx=0;
for(auto& It : Statistics.VideoQualityPercentages)
{
VideoSegsPercentage += FString::Printf(TEXT("%d/%d: %d%%\n"), Idx++, It.Key, It.Value);
}
Idx=0;
for(auto& It : Statistics.AudioQualityPercentages)
{
AudioSegsPercentage += FString::Printf(TEXT("%d/%d: %d%%\n"), Idx++, It.Key, It.Value);
}
UE_LOG(LogElectraPlayer, Verbose, TEXT(
"[%u] Electra player statistics:\n"\
"OS: %s\n"\
"GPU Adapter: %s\n"
"URL: %s\n"\
"Time after main playlist loaded: %.3fs\n"\
"Time after stream playlists loaded: %.3fs\n"\
"Time for initial buffering: %.3fs\n"\
"Initial video stream bitrate: %d bps\n"\
"Initial audio stream bitrate: %d bps\n"\
"Initial buffering bandwidth bps: %.3f\n"\
"Initial buffering latency: %.3fs\n"\
"Time for initial preroll: %.3fs\n"\
"Number of times moved forward: %d\n"\
"Number of times moved backward: %d\n"\
"Number of times looped: %d\n"\
"Number of times rebuffered: %d\n"\
"Total time spent rebuffering: %.3fs\n"\
"Longest rebuffering time: %.3fs\n"\
"First media timeline start: %.3fs\n"\
"First media timeline end: %.3fs\n"\
"Last media timeline start: %.3fs\n"\
"Last media timeline end: %.3fs\n"\
"Media duration: %.3fs\n"\
"Play position at start: %.3fs\n"\
"Play position at end: %.3fs\n"\
"Number of video quality upswitches: %d\n"\
"Number of video quality downswitches: %d\n"\
"Number of video drastic downswitches: %d\n"\
"Number of audio quality upswitches: %d\n"\
"Number of audio quality downswitches: %d\n"\
"Number of audio drastic downswitches: %d\n"\
"Bytes of video data streamed: %lld\n"\
"Bytes of audio data streamed: %lld\n"\
"Video quality percentage:\n%s"\
"Audio quality percentage:\n%s"\
"Currently active playlist URL: %s\n"\
"Currently active resolution: %d * %d\n" \
"Current state: %s\n" \
"404 errors: %u\n" \
"4xx errors: %u\n" \
"5xx errors: %u\n" \
"Timeouts: %u\n" \
"Connection failures: %u\n" \
"Other failures: %u\n" \
"Last issue: %s\n"
),
PlayerUniqueID,
*FString::Printf(TEXT("%s"), *AnalyticsOSVersion),
*AnalyticsGPUType,
*SanitizeMessage(Statistics.InitialURL),
Statistics.TimeToLoadMainPlaylist,
Statistics.TimeToLoadStreamPlaylists,
Statistics.InitialBufferingDuration,
Statistics.InitialVideoStreamBitrate,
Statistics.InitialAudioStreamBitrate,
Statistics.InitialBufferingBandwidth.GetAverageBandwidth(),
Statistics.InitialBufferingBandwidth.GetAverageLatency(),
Statistics.TimeForInitialPreroll,
Statistics.NumTimesForwarded,
Statistics.NumTimesRewound,
Statistics.NumTimesLooped,
Statistics.NumTimesRebuffered,
Statistics.TotalRebufferingDuration,
Statistics.LongestRebufferingDuration,
Statistics.MediaTimelineAtStart.Start.GetAsSeconds(-1.0),
Statistics.MediaTimelineAtStart.End.GetAsSeconds(-1.0),
Statistics.MediaTimelineAtEnd.Start.GetAsSeconds(-1.0),
Statistics.MediaTimelineAtEnd.End.GetAsSeconds(-1.0),
Statistics.MediaDuration,
Statistics.PlayPosAtStart,
Statistics.PlayPosAtEnd,
Statistics.NumVideoQualityUpswitches,
Statistics.NumVideoQualityDownswitches,
Statistics.NumVideoQualityDrasticDownswitches,
Statistics.NumAudioQualityUpswitches,
Statistics.NumAudioQualityDownswitches,
Statistics.NumAudioQualityDrasticDownswitches,
(long long int)Statistics.NumVideoDatabytesStreamed,
(long long int)Statistics.NumAudioDatabytesStreamed,
*VideoSegsPercentage,
*AudioSegsPercentage,
*SanitizeMessage(Statistics.CurrentlyActivePlaylistURL),
Statistics.CurrentlyActiveResolutionWidth,
Statistics.CurrentlyActiveResolutionHeight,
*Statistics.LastState,
Statistics.NumErr404,
Statistics.NumErr4xx,
Statistics.NumErr5xx,
Statistics.NumErrTimeouts,
Statistics.NumErrConnDrops,
Statistics.NumErrOther,
*SanitizeMessage(Statistics.LastError)
);
if (Statistics.LastError.Len())
{
FString MessageHistory;
for(auto &msg : Statistics.MessageHistoryBuffer)
{
MessageHistory.Append(FString::Printf(TEXT("%8.3f: %s"), msg.TimeSinceStart, *msg.Message));
MessageHistory.Append(TEXT("\n"));
}
UE_LOG(LogElectraPlayer, Verbose, TEXT("Most recent log messages:\n%s"), *MessageHistory);
}
}
void FElectraPlayer::SendAnalyticMetrics(const TSharedPtr<IAnalyticsProviderET>& AnalyticsProvider, const FGuid& InPlayerGuid)
{
if (PlayerGuid != InPlayerGuid)
{
return;
}
if (!AnalyticsProvider.IsValid())
{
return;
}
if (!Statistics.bDidPlaybackEnd)
{
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] Submitting analytics during playback, some data may be incomplete"), PlayerUniqueID);
// Try to fill in some of the blanks.
UpdatePlayEndStatistics();
}
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] Submitting analytics"), PlayerUniqueID);
// First emit all enqueued events before sending the final one.
SendPendingAnalyticMetrics(AnalyticsProvider);
TArray<FAnalyticsEventAttribute> ParamArray;
UpdateAnalyticsCustomValues();
AddCommonAnalyticsAttributes(ParamArray);
StatisticsLock.Lock();
FString MessageHistory;
for(auto &msg : Statistics.MessageHistoryBuffer)
{
MessageHistory.Append(FString::Printf(TEXT("%8.3f: %s"), msg.TimeSinceStart, *msg.Message));
MessageHistory.Append(TEXT("<br>"));
}
ParamArray.Add(FAnalyticsEventAttribute(TEXT("URL"), Statistics.InitialURL));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("LastState"), Statistics.LastState));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("MessageHistory"), MessageHistory));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("LastError"), Statistics.LastError));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("FinalVideoResolution"), FString::Printf(TEXT("%d*%d"), Statistics.CurrentlyActiveResolutionWidth, Statistics.CurrentlyActiveResolutionHeight)));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("TimeElapsedToMainPlaylist"), Statistics.TimeToLoadMainPlaylist));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("TimeElapsedToPlaylists"), Statistics.TimeToLoadStreamPlaylists));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("InitialAvgBufferingBandwidth"), Statistics.InitialBufferingBandwidth.GetAverageBandwidth()));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("InitialAvgBufferingLatency"), Statistics.InitialBufferingBandwidth.GetAverageLatency()));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("InitialVideoBitrate"), Statistics.InitialVideoStreamBitrate));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("InitialAudioBitrate"), Statistics.InitialAudioStreamBitrate));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("InitialBufferingDuration"), Statistics.InitialBufferingDuration));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("InitialPrerollDuration"), Statistics.TimeForInitialPreroll));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("TimeElapsedUntilReady"), Statistics.TimeForInitialPreroll + Statistics.TimeAtPrerollBegin - Statistics.TimeAtOpen));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("MediaTimeline.First.Start"), Statistics.MediaTimelineAtStart.Start.GetAsSeconds(-1.0)));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("MediaTimeline.First.End"), Statistics.MediaTimelineAtStart.End.GetAsSeconds(-1.0)));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("MediaTimeline.Last.Start"), Statistics.MediaTimelineAtEnd.Start.GetAsSeconds(-1.0)));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("MediaTimeline.Last.End"), Statistics.MediaTimelineAtEnd.End.GetAsSeconds(-1.0)));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("MediaDuration"), Statistics.MediaDuration));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("PlayPosAtStart"), Statistics.PlayPosAtStart));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("PlayPosAtEnd"), Statistics.PlayPosAtEnd));
// FIXME: the difference is pointless as it does not tell how long playback was really performed for unless we are tracking an uninterrupted playback of a Live session.
ParamArray.Add(FAnalyticsEventAttribute(TEXT("PlaybackDuration"), Statistics.PlayPosAtEnd >= 0.0 ? Statistics.PlayPosAtEnd - Statistics.PlayPosAtStart : 0.0));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("NumTimesMovedForward"), (uint32) Statistics.NumTimesForwarded));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("NumTimesMovedBackward"), (uint32) Statistics.NumTimesRewound));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("NumTimesLooped"), (uint32) Statistics.NumTimesLooped));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("AbortedSegmentDownloads"), (uint32) Statistics.NumSegmentDownloadsAborted));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("NumQualityUpswitches"), (uint32) Statistics.NumVideoQualityUpswitches));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("NumQualityDownswitches"), (uint32) Statistics.NumVideoQualityDownswitches));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("NumQualityDrasticDownswitches"), (uint32) Statistics.NumVideoQualityDrasticDownswitches));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("AudioQualityUpswitches"), (uint32) Statistics.NumAudioQualityUpswitches));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("AudioQualityDownswitches"), (uint32) Statistics.NumAudioQualityDownswitches));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("AudioQualityDrasticDownswitches"), (uint32) Statistics.NumAudioQualityDrasticDownswitches));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("Rebuffering.Num"), (uint32)Statistics.NumTimesRebuffered));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("Rebuffering.AvgDuration"), Statistics.NumTimesRebuffered > 0 ? Statistics.TotalRebufferingDuration / Statistics.NumTimesRebuffered : 0.0));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("Rebuffering.MaxDuration"), Statistics.LongestRebufferingDuration));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("NumBytesStreamedAudio"), (double) Statistics.NumAudioDatabytesStreamed));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("NumBytesStreamedVideo"), (double) Statistics.NumVideoDatabytesStreamed));
FString SegsPerStream;
for(const TPair<int32, uint32>& pair : Statistics.VideoSegmentBitratesStreamed)
{
SegsPerStream += FString::Printf(TEXT("%d:%u;"), pair.Key, pair.Value);
}
ParamArray.Add(FAnalyticsEventAttribute(TEXT("VideoSegmentFetchStats"), *SegsPerStream));
SegsPerStream.Empty();
for(const TPair<int32, uint32>& pair : Statistics.AudioSegmentBitratesStreamed)
{
SegsPerStream += FString::Printf(TEXT("%d:%u;"), pair.Key, pair.Value);
}
ParamArray.Add(FAnalyticsEventAttribute(TEXT("AudioSegmentFetchStats"), *SegsPerStream));
// Quality buckets by percentage
int32 qbIdx = 0;
for(auto &qbIt : (Statistics.NumVideoSegmentsStreamed ? Statistics.VideoQualityPercentages : Statistics.AudioQualityPercentages))
{
ParamArray.Add(FAnalyticsEventAttribute(FString::Printf(TEXT("qp%d"), qbIdx++), (int32) qbIt.Value));
}
ParamArray.Add(FAnalyticsEventAttribute(TEXT("Num404"), (uint32) Statistics.NumErr404));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("Num4xx"), (uint32) Statistics.NumErr4xx));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("Num5xx"), (uint32) Statistics.NumErr5xx));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("NumTimeouts"), (uint32) Statistics.NumErrTimeouts));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("NumConnDrops"), (uint32) Statistics.NumErrConnDrops));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("NumErrOther"), (uint32) Statistics.NumErrOther));
StatisticsLock.Unlock();
AnalyticsProvider->RecordEvent(TEXT("Electra.FinalMetrics"), MoveTemp(ParamArray));
}
void FElectraPlayer::SendAnalyticMetricsPerMinute(const TSharedPtr<IAnalyticsProviderET>& AnalyticsProvider)
{
SendPendingAnalyticMetrics(AnalyticsProvider);
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> Player = CurrentPlayer;
if (Player.Get() && Player->AdaptivePlayer->IsPlaying())
{
TArray<FAnalyticsEventAttribute> ParamArray;
UpdateAnalyticsCustomValues();
AddCommonAnalyticsAttributes(ParamArray);
StatisticsLock.Lock();
ParamArray.Add(FAnalyticsEventAttribute(TEXT("URL"), Statistics.CurrentlyActivePlaylistURL));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("VideoResolution"), FString::Printf(TEXT("%d*%d"), Statistics.CurrentlyActiveResolutionWidth, Statistics.CurrentlyActiveResolutionHeight)));
StatisticsLock.Unlock();
AnalyticsProvider->RecordEvent(TEXT("Electra.PerMinuteMetrics"), MoveTemp(ParamArray));
}
}
void FElectraPlayer::SendPendingAnalyticMetrics(const TSharedPtr<IAnalyticsProviderET>& AnalyticsProvider)
{
FScopeLock Lock(&StatisticsLock);
TSharedPtr<FAnalyticsEvent> AnalyticEvent;
while(QueuedAnalyticEvents.Dequeue(AnalyticEvent))
{
AnalyticsProvider->RecordEvent(*AnalyticEvent->EventName, MoveTemp(AnalyticEvent->ParamArray));
}
NumQueuedAnalyticEvents = 0;
}
void FElectraPlayer::ReportVideoStreamingError(const FGuid& InPlayerGuid, const FString& LastError)
{
if (PlayerGuid != InPlayerGuid)
{
return;
}
FScopeLock Lock(&StatisticsLock);
// Only replace a blank string with a non-blank string. We want to preserve
// existing last error messages, as they will be the root of the problem.
if (LastError.Len() > 0 && Statistics.LastError.Len() == 0)
{
Statistics.LastError = LastError;
}
}
void FElectraPlayer::ReportSubtitlesMetrics(const FGuid& InPlayerGuid, const FString& URL, double ResponseTime, const FString& LastError)
{
}
void FElectraPlayer::MediaStateOnPreparingFinished()
{
if (!ensure(PlayerState.State == EMediaState::Preparing))
{
return;
}
CSV_EVENT(ElectraPlayer, TEXT("MediaStateOnPreparingFinished"));
PlayerState.State = EMediaState::Stopped;
// Only report MediaOpened here and *not* TracksChanged as well.
// We do not know where playback will start at and what tracks are available at that point.
DeferredMediaEvents.Enqueue(EMediaEvent::MediaOpened);
}
bool FElectraPlayer::MediaStateOnPlay()
{
if (PlayerState.State != EMediaState::Stopped && PlayerState.State != EMediaState::Paused)
{
return false;
}
CSV_EVENT(ElectraPlayer, TEXT("MediaStateOnPlay"));
double CurrentRate = 1.0;
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> Player = CurrentPlayer;
if (Player.IsValid() && Player->AdaptivePlayer.IsValid())
{
CurrentRate = Player->AdaptivePlayer->GetPlayRate();
}
PlayerState.State = EMediaState::Playing;
PlayerState.SetPlayRateFromPlayer(CurrentRate);
DeferredMediaEvents.Enqueue(EMediaEvent::PlaybackResumed);
return true;
}
bool FElectraPlayer::MediaStateOnPause()
{
if (PlayerState.State != EMediaState::Playing)
{
return false;
}
CSV_EVENT(ElectraPlayer, TEXT("MediaStateOnPause"));
PlayerState.State = EMediaState::Paused;
PlayerState.SetPlayRateFromPlayer(0.0f);
DeferredMediaEvents.Enqueue(EMediaEvent::PlaybackSuspended);
return true;
}
void FElectraPlayer::MediaStateOnEndReached()
{
CSV_EVENT(ElectraPlayer, TEXT("MediaStateOnEndReached"));
switch(PlayerState.State)
{
case EMediaState::Preparing:
case EMediaState::Playing:
case EMediaState::Paused:
case EMediaState::Stopped:
{
DeferredMediaEvents.Enqueue(EMediaEvent::PlaybackEndReached);
break;
}
default:
{
break;
}
}
PlayerState.State = EMediaState::Stopped;
}
void FElectraPlayer::MediaStateOnSeekFinished()
{
CSV_EVENT(ElectraPlayer, TEXT("MediaStateOnSeekFinished"));
DeferredMediaEvents.Enqueue(EMediaEvent::SeekCompleted);
}
// ------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------------------------
TSharedPtr<FElectraPlayer::FAnalyticsEvent> FElectraPlayer::CreateAnalyticsEvent(FString InEventName)
{
TSharedPtr<FAnalyticsEvent> Ev = MakeShared<FAnalyticsEvent>();
Ev->EventName = MoveTemp(InEventName);
AddCommonAnalyticsAttributes(Ev->ParamArray);
return Ev;
}
void FElectraPlayer::AddCommonAnalyticsAttributes(TArray<FAnalyticsEventAttribute>& InOutParamArray)
{
InOutParamArray.Add(FAnalyticsEventAttribute(TEXT("SessionId"), AnalyticsInstanceGuid));
InOutParamArray.Add(FAnalyticsEventAttribute(TEXT("EventNum"), AnalyticsInstanceEventCount));
InOutParamArray.Add(FAnalyticsEventAttribute(TEXT("Utc"), static_cast<double>(FDateTime::UtcNow().ToUnixTimestamp())));
InOutParamArray.Add(FAnalyticsEventAttribute(TEXT("OS"), FString::Printf(TEXT("%s"), *AnalyticsOSVersion)));
InOutParamArray.Add(FAnalyticsEventAttribute(TEXT("GPUAdapter"), AnalyticsGPUType));
++AnalyticsInstanceEventCount;
StatisticsLock.Lock();
for(int32 nI=0, nIMax=UE_ARRAY_COUNT(AnalyticsCustomValues); nI<nIMax; ++nI)
{
if (AnalyticsCustomValues[nI].Len())
{
InOutParamArray.Add(FAnalyticsEventAttribute(FString::Printf(TEXT("%s%d"), CUSTOM_ANALYTIC_METRIC_KEYNAME, nI), AnalyticsCustomValues[nI]));
}
}
StatisticsLock.Unlock();
}
void FElectraPlayer::UpdateAnalyticsCustomValues()
{
StatisticsLock.Lock();
for(int32 nI=0, nIMax=UE_ARRAY_COUNT(AnalyticsCustomValues); nI<nIMax; ++nI)
{
FVariantValue Value = ElectraMediaOptions::GetOptionValue(OptionInterface, ElectraMediaOptions::EOptionType::CustomAnalyticsMetric, FVariantValue(FString::Printf(TEXT("%s%d"), CUSTOM_ANALYTIC_METRIC_QUERYOPTION_KEY, nI)));
if (Value.IsValid() && Value.GetDataType() == FVariantValue::EDataType::TypeFString)
{
AnalyticsCustomValues[nI] = Value.GetFString();
}
}
StatisticsLock.Unlock();
}
void FElectraPlayer::EnqueueAnalyticsEvent(TSharedPtr<FAnalyticsEvent> InAnalyticEvent)
{
FScopeLock Lock(&StatisticsLock);
// Since analytics are popped from the outside only we check if we have accumulated a lot without them having been retrieved.
// To prevent those from growing beyond leap and bounds we limit ourselves to 100.
while(NumQueuedAnalyticEvents > 100)
{
QueuedAnalyticEvents.Pop();
--NumQueuedAnalyticEvents;
}
QueuedAnalyticEvents.Enqueue(InAnalyticEvent);
++NumQueuedAnalyticEvents;
}
// ------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------------------------
void FElectraPlayer::FAdaptiveStreamingPlayerResourceProvider::ProvideStaticPlaybackDataForURL(TSharedPtr<Electra::IAdaptiveStreamingPlayerResourceRequest, ESPMode::ThreadSafe> InOutRequest)
{
check(InOutRequest.IsValid());
PendingStaticResourceRequests.Enqueue(InOutRequest);
}
void FElectraPlayer::FAdaptiveStreamingPlayerResourceProvider::ProcessPendingStaticResourceRequests()
{
TSharedPtr<Electra::IAdaptiveStreamingPlayerResourceRequest, ESPMode::ThreadSafe> InOutRequest;
while(PendingStaticResourceRequests.Dequeue(InOutRequest))
{
check(InOutRequest->GetResourceType() == Electra::IAdaptiveStreamingPlayerResourceRequest::EPlaybackResourceType::Empty ||
InOutRequest->GetResourceType() == Electra::IAdaptiveStreamingPlayerResourceRequest::EPlaybackResourceType::Playlist ||
InOutRequest->GetResourceType() == Electra::IAdaptiveStreamingPlayerResourceRequest::EPlaybackResourceType::LicenseKey);
if (InOutRequest->GetResourceType() == Electra::IAdaptiveStreamingPlayerResourceRequest::EPlaybackResourceType::Playlist)
{
FVariantValue Value = ElectraMediaOptions::GetOptionValue(OptionInterface, ElectraMediaOptions::EOptionType::PlayListData, FVariantValue(InOutRequest->GetResourceURL()));
if (Value.IsValid())
{
FString PlaylistData = Value.GetFString();
if (!PlaylistData.IsEmpty() && PlaylistData != InOutRequest->GetResourceURL())
{
TSharedPtr<TArray<uint8>, ESPMode::ThreadSafe> ResponseDataPtr = MakeShared<TArray<uint8>, ESPMode::ThreadSafe>();
Electra::StringHelpers::StringToArray(*ResponseDataPtr, PlaylistData);
InOutRequest->SetPlaybackData(ResponseDataPtr, 0);
}
}
}
else if (InOutRequest->GetResourceType() == Electra::IAdaptiveStreamingPlayerResourceRequest::EPlaybackResourceType::LicenseKey)
{
FVariantValue Value = ElectraMediaOptions::GetOptionValue(OptionInterface, ElectraMediaOptions::EOptionType::LicenseKeyData, FVariantValue(InOutRequest->GetResourceURL()));
if (Value.IsValid())
{
FString LicenseKeyData = Value.GetFString();
if (!LicenseKeyData.IsEmpty() && LicenseKeyData != InOutRequest->GetResourceURL())
{
TArray<uint8> BinKey;
BinKey.AddUninitialized(LicenseKeyData.Len());
BinKey.SetNum(HexToBytes(LicenseKeyData, BinKey.GetData()));
TSharedPtr<TArray<uint8>, ESPMode::ThreadSafe> ResponseDataPtr = MakeShared<TArray<uint8>, ESPMode::ThreadSafe>(BinKey);
InOutRequest->SetPlaybackData(ResponseDataPtr, 0);
}
}
}
InOutRequest->SignalDataReady();
}
}
void FElectraPlayer::FAdaptiveStreamingPlayerResourceProvider::ClearPendingRequests()
{
PendingStaticResourceRequests.Empty();
}
// ------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------------------------
void FElectraPlayer::OnMediaPlayerEventReceived(TSharedPtrTS<IAdaptiveStreamingPlayerAEMSEvent> InEvent, IAdaptiveStreamingPlayerAEMSReceiver::EDispatchMode InDispatchMode)
{
#if !UE_BUILD_SHIPPING
const TCHAR* const Origins[] = { TEXT("Playlist"), TEXT("Inband"), TEXT("TimedMetadata"), TEXT("n/a") };
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] %s event %s with \"%s\", \"%s\", \"%s\" PTS @ %.3f for %.3fs"), PlayerUniqueID,
Origins[Electra::Utils::Min((int32)InEvent->GetOrigin(), (int32)UE_ARRAY_COUNT(Origins)-1)],
InDispatchMode==IAdaptiveStreamingPlayerAEMSReceiver::EDispatchMode::OnReceive?TEXT("received"):TEXT("started"),
*InEvent->GetSchemeIdUri(), *InEvent->GetValue(), *InEvent->GetID(),
InEvent->GetPresentationTime().GetAsSeconds(), InEvent->GetDuration().GetAsSeconds());
#endif
auto Player = CurrentPlayer;
if (Player.IsValid())
{
Electra::FTimeRange MediaTimeline;
Player->AdaptivePlayer->GetTimelineRange(MediaTimeline);
// Create a binary media sample of our extended format and pass it up.
TSharedPtr<FMetaDataDecoderOutput, ESPMode::ThreadSafe> Meta = MakeShared<FMetaDataDecoderOutput, ESPMode::ThreadSafe>();
switch(InDispatchMode)
{
default:
case IAdaptiveStreamingPlayerAEMSReceiver::EDispatchMode::OnReceive:
{
Meta->DispatchedMode = FMetaDataDecoderOutput::EDispatchedMode::OnReceive;
break;
}
case IAdaptiveStreamingPlayerAEMSReceiver::EDispatchMode::OnStart:
{
Meta->DispatchedMode = FMetaDataDecoderOutput::EDispatchedMode::OnStart;
break;
}
}
switch(InEvent->GetOrigin())
{
default:
case IAdaptiveStreamingPlayerAEMSEvent::EOrigin::TimedMetadata:
{
Meta->Origin = FMetaDataDecoderOutput::EOrigin::TimedMetadata;
break;
}
case IAdaptiveStreamingPlayerAEMSEvent::EOrigin::EventStream:
{
Meta->Origin = FMetaDataDecoderOutput::EOrigin::EventStream;
break;
}
case IAdaptiveStreamingPlayerAEMSEvent::EOrigin::InbandEventStream:
{
Meta->Origin = FMetaDataDecoderOutput::EOrigin::InbandEventStream;
break;
}
}
Meta->Data = InEvent->GetMessageData();
Meta->SchemeIdUri = InEvent->GetSchemeIdUri();
Meta->Value = InEvent->GetValue();
Meta->ID = InEvent->GetID(),
Meta->Duration = InEvent->GetDuration().GetAsTimespan();
Meta->PresentationTime = FDecoderTimeStamp(InEvent->GetPresentationTime().GetAsTimespan(), 0);
// Set the current timeline start as the metadata track's zero point. This is only useful if the timeline does not
// actually change over time. The use of the base time is therefore tied to knowledge by the using code that the
// timeline will be fixed.
Meta->TrackBaseTime = FDecoderTimeStamp(MediaTimeline.Start.GetAsTimespan(), MediaTimeline.Start.GetSequenceIndex());
FScopeLock SampleLock(&MediaSamplesLock);
TSharedRef<FElectraBinarySample, ESPMode::ThreadSafe> MetaDataSample = MakeShared<FElectraBinarySample, ESPMode::ThreadSafe>();
MetaDataSample->Metadata = Meta;
MediaSamples->AddMetadata(MetaDataSample);
}
}
// ------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------------------------
void FElectraPlayer::Close()
{
CloseInternal();
}
FString FElectraPlayer::GetInfo() const
{
return FString();
}
FGuid FElectraPlayer::GetPlayerPluginGUID() const
{
// Same GUID as used in the player factory.
static FGuid PlayerPluginGUID(0x94ee3f80, 0x8e604292, 0xb4d24dd5, 0xfdade1c2);
return PlayerPluginGUID;
}
IMediaSamples& FElectraPlayer::GetSamples()
{
FScopeLock lock(&MediaSamplesLock);
return MediaSamples.IsValid() ? *MediaSamples : *EmptyMediaSamples;
}
FString FElectraPlayer::GetStats() const
{
return FString();
}
FString FElectraPlayer::GetUrl() const
{
return MediaUrl;
}
bool FElectraPlayer::Open(const FString& InUrl, const IMediaOptions* InOptions)
{
return Open(InUrl, InOptions, nullptr);
}
bool FElectraPlayer::Open(const TSharedRef<FArchive, ESPMode::ThreadSafe>& InArchive, const FString& InOriginalUrl, const IMediaOptions* InOptions)
{
// Opening from an archive is not supported.
return false;
}
bool FElectraPlayer::Open(const FString& InUrl, const IMediaOptions* InOptions, const FMediaPlayerOptions* InPlayerOptions)
{
return OpenInternal(InUrl, InOptions, InPlayerOptions);
}
FVariant FElectraPlayer::GetMediaInfo(FName InInfoName) const
{
const TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> Player = CurrentPlayer;
return Player.IsValid() ? Player->AdaptivePlayer->GetMediaInfo(InInfoName).ToFVariant() : FVariant();
}
TSharedPtr<TMap<FString, TArray<TUniquePtr<IMediaMetadataItem>>>, ESPMode::ThreadSafe> FElectraPlayer::GetMediaMetadata() const
{
return CurrentStreamMetadata;
}
void FElectraPlayer::SetGuid(const FGuid& InGuid)
{
PlayerGuid = InGuid;
}
void FElectraPlayer::TickInput(FTimespan InDeltaTime, FTimespan InTimecode)
{
TickInternal(InDeltaTime, InTimecode);
}
bool FElectraPlayer::FlushOnSeekStarted() const
{
return false;
}
bool FElectraPlayer::FlushOnSeekCompleted() const
{
return false;
}
bool FElectraPlayer::GetPlayerFeatureFlag(EFeatureFlag InFlag) const
{
switch(InFlag)
{
case EFeatureFlag::AllowShutdownOnClose:
{
return bHasClosedDueToError;
}
case EFeatureFlag::UsePlaybackTimingV2:
{
return true;
}
case EFeatureFlag::PlayerUsesInternalFlushOnSeek:
{
return true;
}
case EFeatureFlag::IsTrackSwitchSeamless:
{
return true;
}
case EFeatureFlag::PlayerSelectsDefaultTracks:
{
return true;
}
default:
{
return IMediaPlayer::GetPlayerFeatureFlag(InFlag);
}
}
}
bool FElectraPlayer::SetAsyncResourceReleaseNotification(IAsyncResourceReleaseNotificationRef InAsyncDestructNotification)
{
AsyncResourceReleaseNotification = InAsyncDestructNotification;
return true;
}
uint32 FElectraPlayer::GetNewResourcesOnOpen() const
{
return IMediaPlayerLifecycleManagerDelegate::ResourceFlags_Decoder;
}
bool FElectraPlayer::QueryCacheState(EMediaCacheState InState, TRangeSet<FTimespan>& OutTimeRanges) const
{
// Note: The data of time ranges returned here will not actually get "cached" as
// it is always only transient. We thus report the ranges only for `Loaded` and `Loading`,
// but never for `Cached`!
switch(InState)
{
case EMediaCacheState::Loaded:
case EMediaCacheState::Loading:
case EMediaCacheState::Pending:
{
// When asked to provide what's already loaded we look at what we have in the sample queue
// and add that to the result. These samples have already left the player but are ready
// for use.
if (InState == EMediaCacheState::Loaded)
{
TArray<TRange<FMediaTimeStamp>> QueuedRange;
FScopeLock SampleLock(&MediaSamplesLock);
if (MediaSamples.IsValid() && MediaSamples->PeekVideoSampleTimeRanges(QueuedRange) && QueuedRange.Num())
{
OutTimeRanges.Add(TRange<FTimespan>(QueuedRange[0].GetLowerBoundValue().Time, QueuedRange.Last().GetUpperBoundValue().Time));
}
}
auto Player = CurrentPlayer;
if (Player.IsValid())
{
IAdaptiveStreamingPlayer::FStreamBufferInfo bi;
// Query video first.
Player->AdaptivePlayer->QueryStreamBufferInfo(bi, Electra::EStreamType::Video);
// If that is not active query audio.
if (!bi.bIsBufferActive)
{
Player->AdaptivePlayer->QueryStreamBufferInfo(bi, Electra::EStreamType::Audio);
}
if (bi.bIsBufferActive)
{
auto AddRanges = [](TRangeSet<FTimespan>& OutRanges, const TArray<Electra::FTimeRange>& InRanges) -> void
{
for(int32 i=0; i<InRanges.Num(); ++i)
{
OutRanges.Add(TRange<FTimespan>(InRanges[i].Start.GetAsTimespan(), InRanges[i].End.GetAsTimespan()));
}
};
switch(InState)
{
case EMediaCacheState::Loaded:
{
AddRanges(OutTimeRanges, bi.TimeEnqueued);
break;
}
case EMediaCacheState::Loading:
{
AddRanges(OutTimeRanges, bi.TimeAvailable);
break;
}
case EMediaCacheState::Pending:
{
AddRanges(OutTimeRanges, bi.TimeRequested);
break;
}
}
}
}
return true;
}
}
return false;
}
bool FElectraPlayer::CanControl(EMediaControl InControl) const
{
const EMediaState CurrentState = GetState();
if (InControl == EMediaControl::BlockOnFetch)
{
return CurrentState == EMediaState::Playing || CurrentState == EMediaState::Paused;
}
else if (InControl == EMediaControl::Pause)
{
return CurrentState == EMediaState::Playing;
}
else if (InControl == EMediaControl::Resume)
{
return CurrentState == EMediaState::Paused || CurrentState == EMediaState::Stopped;
}
else if (InControl == EMediaControl::Seek || InControl == EMediaControl::Scrub)
{
return CurrentState == EMediaState::Playing || CurrentState == EMediaState::Paused || CurrentState == EMediaState::Stopped;
}
else if (InControl == EMediaControl::PlaybackRange)
{
return true;
}
return false;
}
FTimespan FElectraPlayer::GetDuration() const
{
auto Player = CurrentPlayer;
if (!Player.IsValid())
{
return FTimespan::Zero();
}
Electra::FTimeValue Dur = Player->AdaptivePlayer->GetDuration();
return Dur.IsValid() ? (Dur.IsInfinity() ? FTimespan::MaxValue() : Dur.GetAsTimespan()) : FTimespan();
}
float FElectraPlayer::GetRate() const
{
return PlayerState.GetRate();
}
EMediaState FElectraPlayer::GetState() const
{
return PlayerState.GetState();
}
EMediaStatus FElectraPlayer::GetStatus() const
{
return PlayerState.GetStatus();
}
TRangeSet<float> FElectraPlayer::GetSupportedRates(EMediaRateThinning InThinning) const
{
TRangeSet<float> Res;
auto Player = CurrentPlayer;
if (!Player.IsValid())
{
return Res;
}
TArray<TRange<double>> SupportedRanges;
Player->AdaptivePlayer->GetSupportedRates(InThinning == EMediaRateThinning::Unthinned ? IAdaptiveStreamingPlayer::EPlaybackRateType::Unthinned : IAdaptiveStreamingPlayer::EPlaybackRateType::Thinned).GetRanges(SupportedRanges);
for(auto &Rate : SupportedRanges)
{
TRange<float> r;
if (Rate.HasLowerBound())
{
r.SetLowerBound(TRange<float>::BoundsType::Inclusive((float) Rate.GetLowerBoundValue()));
}
if (Rate.HasUpperBound())
{
r.SetUpperBound(TRange<float>::BoundsType::Inclusive((float) Rate.GetUpperBoundValue()));
}
Res.Add(r);
}
return Res;
}
FTimespan FElectraPlayer::GetTime() const
{
auto Player = CurrentPlayer;
if (!Player.IsValid())
{
return FTimespan::Zero();
}
Electra::FTimeValue playerTime = Player->AdaptivePlayer->GetPlayPosition();
return playerTime.GetAsTimespan();
}
bool FElectraPlayer::IsLooping() const
{
auto Player = CurrentPlayer;
if (Player.IsValid())
{
IAdaptiveStreamingPlayer::FLoopState loopState;
Player->AdaptivePlayer->GetLoopState(loopState);
return loopState.bIsEnabled;
}
return bEnableLooping.Get(false);
}
bool FElectraPlayer::SetLooping(bool bInLooping)
{
bEnableLooping = bInLooping;
auto Player = CurrentPlayer;
if (!Player.IsValid())
{
return false;
}
IAdaptiveStreamingPlayer::FLoopParam loop;
loop.bEnableLooping = bEnableLooping.GetValue();
Player->AdaptivePlayer->SetLooping(loop);
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] IMediaPlayer::SetLooping(%s)"), PlayerUniqueID, bEnableLooping.GetValue()?TEXT("true"):TEXT("false"));
return true;
}
bool FElectraPlayer::SetRate(float InRate)
{
auto Player = CurrentPlayer;
if (!Player.IsValid())
{
return false;
}
// Set the intended rate, which *may* be set negative. This is not supported and we put the adaptive player into pause
// if this happens, but we keep the intended rate set nevertheless.
PlayerState.SetIntendedPlayRate(InRate);
if (InRate <= 0.0f)
{
Player->AdaptivePlayer->Pause();
}
else
{
if (Player->AdaptivePlayer->IsPaused() || !Player->AdaptivePlayer->IsPlaying())
{
TriggerFirstSeekIfNecessary();
Player->AdaptivePlayer->Resume();
}
}
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] IMediaControls::SetRate(%.3f)"), PlayerUniqueID, InRate);
CSV_EVENT(ElectraPlayer, TEXT("Setting Rate"));
IAdaptiveStreamingPlayer::FTrickplayParams Params;
Player->AdaptivePlayer->SetPlayRate((double) InRate, Params);
return true;
}
bool FElectraPlayer::Seek(const FTimespan& InNewTime, const FMediaSeekParams& InAdditionalParams)
{
auto Player = CurrentPlayer;
if (!Player.IsValid())
{
return false;
}
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] IMediaControls::Seek() to %#.4f (%s)"), PlayerUniqueID, InNewTime.GetTotalSeconds(), *InNewTime.ToString(TEXT("%h:%m:%s.%f")));
CSV_EVENT(ElectraPlayer, TEXT("Seeking"));
FTimespan Target;
CalculateTargetSeekTime(Target, InNewTime);
Electra::IAdaptiveStreamingPlayer::FSeekParam seek;
if (Target != FTimespan::MaxValue())
{
seek.Time.SetFromTimespan(Target);
}
check(InAdditionalParams.NewSequenceIndex.IsSet());
seek.NewSequenceIndex = InAdditionalParams.NewSequenceIndex;
bInitialSeekPerformed = true;
Player->AdaptivePlayer->SeekTo(seek);
return true;
}
TRange<FTimespan> FElectraPlayer::GetPlaybackTimeRange(EMediaTimeRangeType InRangeToGet) const
{
TRange<FTimespan> Range(CurrentPlaybackRange);
auto Player = CurrentPlayer;
if (Player.IsValid())
{
switch(InRangeToGet)
{
case EMediaTimeRangeType::Absolute:
{
Electra::FTimeRange Timeline;
Player->AdaptivePlayer->GetTimelineRange(Timeline);
if (Timeline.IsValid())
{
Range.SetLowerBound(Timeline.Start.GetAsTimespan());
Range.SetUpperBound(Timeline.End.GetAsTimespan());
}
else
{
Electra::FTimeValue Dur = Player->AdaptivePlayer->GetDuration();
if (Dur.IsValid())
{
Range.SetLowerBound(FTimespan(0));
Range.SetUpperBound(Dur.IsInfinity() ? FTimespan::MaxValue() : Dur.GetAsTimespan());
}
}
break;
}
case EMediaTimeRangeType::Current:
{
Electra::IAdaptiveStreamingPlayer::FPlaybackRange Current;
Player->AdaptivePlayer->GetPlaybackRange(Current);
if (Current.Start.IsSet() && Current.End.IsSet())
{
Range.SetLowerBound(Current.Start.GetValue().GetAsTimespan());
Range.SetUpperBound(Current.End.GetValue().GetAsTimespan());
}
else
{
return GetPlaybackTimeRange(EMediaTimeRangeType::Absolute);
}
break;
}
}
}
return Range;
}
bool FElectraPlayer::SetPlaybackTimeRange(const TRange<FTimespan>& InTimeRange)
{
CurrentPlaybackRange = InTimeRange;
auto Player = CurrentPlayer;
if (Player.IsValid())
{
// Ranges cannot be set on Live streams.
Electra::FTimeValue Dur = Player->AdaptivePlayer->GetDuration();
if (Dur.IsValid() && Dur.IsInfinity())
{
return false;
}
if (!CurrentPlaybackRange.IsEmpty())
{
Electra::IAdaptiveStreamingPlayer::FPlaybackRange Range;
if (CurrentPlaybackRange.HasLowerBound())
{
Range.Start = Electra::FTimeValue().SetFromTimespan(CurrentPlaybackRange.GetLowerBoundValue());
}
if (CurrentPlaybackRange.HasUpperBound())
{
Range.End = Electra::FTimeValue().SetFromTimespan(CurrentPlaybackRange.GetUpperBoundValue());
}
Player->AdaptivePlayer->SetPlaybackRange(Range);
if (Range.Start.IsSet() && Range.End.IsSet())
{
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] IMediaControls::SetPlaybackTimeRange(%#.4f - %#.4f)"), PlayerUniqueID, Range.Start.GetValue().GetAsSeconds(), Range.End.GetValue().GetAsSeconds());
}
else if (Range.Start.IsSet())
{
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] IMediaControls::SetPlaybackTimeRange(%#.4f - n/a)"), PlayerUniqueID, Range.Start.GetValue().GetAsSeconds());
}
else if (Range.End.IsSet())
{
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] IMediaControls::SetPlaybackTimeRange(n/a - %#.4f)"), PlayerUniqueID, Range.End.GetValue().GetAsSeconds());
}
}
}
return true;
}
bool FElectraPlayer::GetAudioTrackFormat(int32 InTrackIndex, int32 InFormatIndex, FMediaAudioTrackFormat& OutFormat) const
{
if (InTrackIndex >= 0 && InTrackIndex < NumTracksAudio && InFormatIndex == 0)
{
TSharedPtr<Electra::FTrackMetadata, ESPMode::ThreadSafe> Meta = GetTrackStreamMetadata(EMediaTrackType::Audio, InTrackIndex);
if (Meta.IsValid())
{
const Electra::FStreamCodecInformation& ci = Meta->HighestBandwidthCodec;
OutFormat.BitsPerSample = 16;
OutFormat.NumChannels = (uint32)ci.GetNumberOfChannels();
OutFormat.SampleRate = (uint32)ci.GetSamplingRate();
OutFormat.TypeName = ci.GetHumanReadableCodecName();
return true;
}
}
return false;
}
int32 FElectraPlayer::GetNumTracks(EMediaTrackType InTrackType) const
{
if (InTrackType == EMediaTrackType::Audio)
{
return NumTracksAudio;
}
else if (InTrackType == EMediaTrackType::Video)
{
return NumTracksVideo;
}
else if (InTrackType == EMediaTrackType::Subtitle)
{
return NumTracksSubtitle;
}
return 0;
}
int32 FElectraPlayer::GetNumTrackFormats(EMediaTrackType InTrackType, int32 InTrackIndex) const
{
// Right now we only have a single format per track
if ((InTrackType == EMediaTrackType::Video && NumTracksVideo != 0) ||
(InTrackType == EMediaTrackType::Audio && NumTracksAudio != 0) ||
(InTrackType == EMediaTrackType::Subtitle && NumTracksSubtitle != 0))
{
return 1;
}
return 0;
}
int32 FElectraPlayer::GetSelectedTrack(EMediaTrackType InTrackType) const
{
/*
To reduce the overhead of this function we check for the track the underlying player has
actually selected only when we were told the tracks changed.
It is possible that the underlying player changes the track automatically as playback progresses.
For instance, when playing a DASH stream consisting of several periods the player needs to re-select
the audio stream when transitioning from one period into the next, which may change the index of
the selected track.
*/
auto CheckAndReselectTrack = [this](Electra::EStreamType InStreamType, bool& InOutDirtyFlag, int32& InOutSelectedIndex, int32 InNumTracks) -> int32
{
if (InOutDirtyFlag)
{
if (InNumTracks == 0)
{
InOutSelectedIndex = -1;
}
else
{
auto Player = CurrentPlayer;
if (Player.IsValid())
{
if (Player->AdaptivePlayer->IsTrackDeselected(InStreamType))
{
InOutSelectedIndex = -1;
InOutDirtyFlag = false;
}
else
{
Electra::FStreamSelectionAttributes Attributes;
Player->AdaptivePlayer->GetSelectedTrackAttributes(Attributes, InStreamType);
if (Attributes.OverrideIndex.IsSet())
{
InOutSelectedIndex = Attributes.OverrideIndex.GetValue();
InOutDirtyFlag = false;
}
}
}
}
}
return InOutSelectedIndex;
};
// Electra does not have caption or metadata tracks, handle only video, audio and subtitles.
if (InTrackType == EMediaTrackType::Video)
{
return CheckAndReselectTrack(Electra::EStreamType::Video, bVideoTrackIndexDirty, SelectedVideoTrackIndex, NumTracksVideo);
}
else if (InTrackType == EMediaTrackType::Audio)
{
return CheckAndReselectTrack(Electra::EStreamType::Audio, bAudioTrackIndexDirty, SelectedAudioTrackIndex, NumTracksAudio);
}
else if (InTrackType == EMediaTrackType::Subtitle)
{
return CheckAndReselectTrack(Electra::EStreamType::Subtitle, bSubtitleTrackIndexDirty, SelectedSubtitleTrackIndex, NumTracksSubtitle);
}
return -1;
}
FText FElectraPlayer::GetTrackDisplayName(EMediaTrackType InTrackType, int32 InTrackIndex) const
{
TSharedPtr<Electra::FTrackMetadata, ESPMode::ThreadSafe> Meta = GetTrackStreamMetadata(InTrackType, InTrackIndex);
if (Meta.IsValid())
{
if (InTrackType == EMediaTrackType::Video)
{
if (!Meta->Label.IsEmpty())
{
return FText::FromString(Meta->Label);
}
return FText::FromString(FString::Printf(TEXT("Video Track ID %s"), *Meta->ID));
}
else if (InTrackType == EMediaTrackType::Audio)
{
if (!Meta->Label.IsEmpty())
{
return FText::FromString(Meta->Label);
}
return FText::FromString(FString::Printf(TEXT("Audio Track ID %s"), *Meta->ID));
}
else if (InTrackType == EMediaTrackType::Subtitle)
{
FString Name;
if (!Meta->Label.IsEmpty())
{
Name = FString::Printf(TEXT("%s (%s)"), *Meta->Label, *Meta->HighestBandwidthCodec.GetCodecSpecifierRFC6381());
}
else
{
Name = FString::Printf(TEXT("Subtitle Track ID %s (%s)"), *Meta->ID, *Meta->HighestBandwidthCodec.GetCodecSpecifierRFC6381());
}
return FText::FromString(Name);
}
}
return FText();
}
int32 FElectraPlayer::GetTrackFormat(EMediaTrackType InTrackType, int32 InTrackIndex) const
{
if ((InTrackType == EMediaTrackType::Audio && NumTracksAudio > 0) ||
(InTrackType == EMediaTrackType::Video && NumTracksVideo > 0) ||
(InTrackType == EMediaTrackType::Subtitle && NumTracksSubtitle > 0))
{
// Right now we only have a single format per track so we return format index 0 at all times.
return 0;
}
return -1;
}
FString FElectraPlayer::GetTrackLanguage(EMediaTrackType InTrackType, int32 InTrackIndex) const
{
TSharedPtr<Electra::FTrackMetadata, ESPMode::ThreadSafe> Meta = GetTrackStreamMetadata(InTrackType, InTrackIndex);
if (Meta.IsValid())
{
if (InTrackType == EMediaTrackType::Audio)
{
// Audio does not need to include the script tag (but video does as it could include burned in subtitles)
return Meta->LanguageTagRFC5646.Get(true, false, true, false, false, false);
}
else
{
return Meta->LanguageTagRFC5646.Get(true, true, true, false, false, false);
}
}
return FString();
}
FString FElectraPlayer::GetTrackName(EMediaTrackType InTrackType, int32 InTrackIndex) const
{
return FString();
}
bool FElectraPlayer::GetVideoTrackFormat(int32 InTrackIndex, int32 InFormatIndex, FMediaVideoTrackFormat& OutFormat) const
{
if (InTrackIndex >= 0 && InTrackIndex < NumTracksVideo && InFormatIndex == 0)
{
TSharedPtr<Electra::FTrackMetadata, ESPMode::ThreadSafe> Meta = GetTrackStreamMetadata(EMediaTrackType::Video, InTrackIndex);
if (Meta.IsValid())
{
const Electra::FStreamCodecInformation& ci = Meta->HighestBandwidthCodec;
OutFormat.Dim.X = ci.GetResolution().Width;
OutFormat.Dim.Y = ci.GetResolution().Height;
OutFormat.FrameRate = (float)ci.GetFrameRate().GetAsDouble();
OutFormat.FrameRates = TRange<float>{ OutFormat.FrameRate };
OutFormat.TypeName = ci.GetHumanReadableCodecName();
return true;
}
}
return false;
}
/**
* Selects a specified track for playback.
*
* Note:
* There is currently no concept of selecting a track based on metadata, only by index.
* The idea being that before selecting a track by index the application needs to check
* the metadata beforehand (eg. call GetTrackLanguage()) to figure out the index of the
* track it wants to play.
*
* The underlying player however needs to select tracks based on metadata alone instead
* of an index in case the track layout changes dynamically during playback.
* For example, a part of the presentation could have both English and French audio,
* followed by a part (say, an advertisement) that only has English audio, followed
* by the continued regular part that has both. Without any user intervention the
* player needs to automatically switch from French to English and back to French, or
* index 1 -> 0 -> 1 (assuming French was the starting language of choice).
* Indices are therefore meaningless to the underlying player.
*
* SelectTrack() is currently called implicitly by FMediaPlayerFacade::SelectDefaultTracks()
* when EMediaEvent::TracksChanged is received. This is why this event is NOT sent out
* in HandlePlayerEventTracksChanged() when the underlying player notifies us about a
* change in track layout.
* Other than the very first track selection made by the facade this method should only
* be called from a direct user interaction.
*/
bool FElectraPlayer::SelectTrack(EMediaTrackType InTrackType, int32 InTrackIndex)
{
auto PerformSelection = [this, InTrackType, InTrackIndex](int32& OutSelectedTrackIndex, Electra::FStreamSelectionAttributes& OutSelectionAttributes) -> bool
{
Electra::EStreamType StreamType = InTrackType == EMediaTrackType::Video ? Electra::EStreamType::Video :
InTrackType == EMediaTrackType::Audio ? Electra::EStreamType::Audio :
InTrackType == EMediaTrackType::Subtitle ? Electra::EStreamType::Subtitle :
Electra::EStreamType::Unsupported;
// Select a track or deselect?
if (InTrackIndex >= 0)
{
// Check if the track index exists by checking the presence of the track metadata.
// If for some reason the index is not valid the selection will not be changed.
TSharedPtr<Electra::FTrackMetadata, ESPMode::ThreadSafe> Meta = GetTrackStreamMetadata(InTrackType, InTrackIndex);
if (Meta.IsValid())
{
// Switch only when the track index has changed.
if (GetSelectedTrack(InTrackType) != InTrackIndex)
{
Electra::FStreamSelectionAttributes TrackAttributes;
TrackAttributes.OverrideIndex = InTrackIndex;
OutSelectionAttributes.OverrideIndex = InTrackIndex;
if (!Meta->Kind.IsEmpty())
{
TrackAttributes.Kind = Meta->Kind;
OutSelectionAttributes.Kind = Meta->Kind;
}
TrackAttributes.Language_RFC4647 = Meta->LanguageTagRFC5646.Get(true, true, true, false, false, false);
OutSelectionAttributes.Language_RFC4647 = TrackAttributes.Language_RFC4647;
TrackAttributes.Codec = Meta->HighestBandwidthCodec.GetCodecName();
OutSelectionAttributes.Codec = Meta->HighestBandwidthCodec.GetCodecName();
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> Player = CurrentPlayer;
if (Player.IsValid() && Player->AdaptivePlayer.IsValid())
{
Player->AdaptivePlayer->SelectTrackByAttributes(StreamType, TrackAttributes);
}
OutSelectedTrackIndex = InTrackIndex;
}
return true;
}
}
else
{
// Deselect track.
OutSelectionAttributes.OverrideIndex = -1;
OutSelectedTrackIndex = -1;
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> Player = CurrentPlayer;
if (Player.IsValid() && Player->AdaptivePlayer.IsValid())
{
Player->AdaptivePlayer->DeselectTrack(StreamType);
}
return true;
}
return false;
};
if (InTrackType == EMediaTrackType::Video)
{
return PerformSelection(SelectedVideoTrackIndex, PlaystartOptions.InitialVideoTrackAttributes);
}
else if (InTrackType == EMediaTrackType::Audio)
{
return PerformSelection(SelectedAudioTrackIndex, PlaystartOptions.InitialAudioTrackAttributes);
}
else if (InTrackType == EMediaTrackType::Subtitle)
{
return PerformSelection(SelectedSubtitleTrackIndex, PlaystartOptions.InitialSubtitleTrackAttributes);
}
return false;
}
bool FElectraPlayer::SetTrackFormat(EMediaTrackType InTrackType, int32 InTrackIndex, int32 InFormatIndex)
{
return false;
}
bool FElectraPlayer::SetVideoTrackFrameRate(int32 InTrackIndex, int32 InFormatIndex, float InFrameRate)
{
return false;
}
void FElectraPlayer::ReportOpenSource(const FString& InURL)
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEvent_OpenSource>(InURL));
}
void FElectraPlayer::ReportReceivedMainPlaylist(const FString& InEffectiveURL)
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEvent_ReceivedMainPlaylist>(InEffectiveURL));
}
void FElectraPlayer::ReportReceivedPlaylists()
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEventBase>(FPlayerMetricEventBase::EType::ReceivedPlaylists));
}
void FElectraPlayer::ReportTracksChanged()
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEventBase>(FPlayerMetricEventBase::EType::TracksChanged));
}
void FElectraPlayer::ReportPlaylistDownload(const Metrics::FPlaylistDownloadStats& InPlaylistDownloadStats)
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEvent_PlaylistDownload>(InPlaylistDownloadStats));
}
void FElectraPlayer::ReportCleanStart()
{
/*DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEventBase>(FPlayerMetricEventBase::EType::CleanStart));*/
}
void FElectraPlayer::ReportBufferingStart(Metrics::EBufferingReason InBufferingReason)
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEvent_BufferingStart>(InBufferingReason));
}
void FElectraPlayer::ReportBufferingEnd(Metrics::EBufferingReason InBufferingReason)
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEvent_BufferingEnd>(InBufferingReason));
}
void FElectraPlayer::ReportBandwidth(int64 InEffectiveBps, int64 InThroughputBps, double InLatencyInSeconds)
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEvent_Bandwidth>(InEffectiveBps, InThroughputBps, InLatencyInSeconds));
}
void FElectraPlayer::ReportBufferUtilization(const Metrics::FBufferStats& InBufferStats)
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEvent_BufferUtilization>(InBufferStats));
}
void FElectraPlayer::ReportSegmentDownload(const Metrics::FSegmentDownloadStats& InSegmentDownloadStats)
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEvent_SegmentDownload>(InSegmentDownloadStats));
}
void FElectraPlayer::ReportLicenseKey(const Metrics::FLicenseKeyStats& InLicenseKeyStats)
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEvent_LicenseKey>(InLicenseKeyStats));
}
void FElectraPlayer::ReportDataAvailabilityChange(const Metrics::FDataAvailabilityChange& InDataAvailability)
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEvent_DataAvailabilityChange>(InDataAvailability));
}
void FElectraPlayer::ReportVideoQualityChange(int32 InNewBitrate, int32 InPreviousBitrate, bool bInIsDrasticDownswitch)
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEvent_VideoQualityChange>(InNewBitrate, InPreviousBitrate, bInIsDrasticDownswitch));
}
void FElectraPlayer::ReportAudioQualityChange(int32 InNewBitrate, int32 InPreviousBitrate, bool bInIsDrasticDownswitch)
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEvent_AudioQualityChange>(InNewBitrate, InPreviousBitrate, bInIsDrasticDownswitch));
}
void FElectraPlayer::ReportDecodingFormatChange(const FStreamCodecInformation& InNewDecodingFormat)
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEvent_CodecFormatChange>(InNewDecodingFormat));
}
void FElectraPlayer::ReportPrerollStart()
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEventBase>(FPlayerMetricEventBase::EType::PrerollStart));
}
void FElectraPlayer::ReportPrerollEnd()
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEventBase>(FPlayerMetricEventBase::EType::PrerollEnd));
}
void FElectraPlayer::ReportPlaybackStart()
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEventBase>(FPlayerMetricEventBase::EType::PlaybackStart));
}
void FElectraPlayer::ReportPlaybackPaused()
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEventBase>(FPlayerMetricEventBase::EType::PlaybackPaused));
}
void FElectraPlayer::ReportPlaybackResumed()
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEventBase>(FPlayerMetricEventBase::EType::PlaybackResumed));
}
void FElectraPlayer::ReportPlaybackEnded()
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEventBase>(FPlayerMetricEventBase::EType::PlaybackEnded));
}
void FElectraPlayer::ReportJumpInPlayPosition(const FTimeValue& InToNewTime, const FTimeValue& InFromTime, Metrics::ETimeJumpReason InTimejumpReason)
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEvent_JumpInPlayPosition>(InToNewTime, InFromTime, InTimejumpReason));
}
void FElectraPlayer::ReportPlaybackStopped()
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEventBase>(FPlayerMetricEventBase::EType::PlaybackStopped));
}
void FElectraPlayer::ReportSeekCompleted()
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEventBase>(FPlayerMetricEventBase::EType::SeekCompleted));
}
void FElectraPlayer::ReportMediaMetadataChanged(TSharedPtrTS<Electra::UtilsMP4::FMetadataParser> InMetadata)
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEvent_MediaMetadataChange>(InMetadata));
}
void FElectraPlayer::ReportError(const FString& InErrorReason)
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEvent_Error>(InErrorReason));
}
void FElectraPlayer::ReportLogMessage(IInfoLog::ELevel InLogLevel, const FString& InLogMessage, int64 InPlayerWallclockMilliseconds)
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEvent_LogMessage>(InLogLevel, InLogMessage, InPlayerWallclockMilliseconds));
}
void FElectraPlayer::ReportDroppedVideoFrame()
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEventBase>(FPlayerMetricEventBase::EType::DroppedVideoFrame));
}
void FElectraPlayer::ReportDroppedAudioFrame()
{
DeferredPlayerEvents.Enqueue(MakeSharedTS<FPlayerMetricEventBase>(FPlayerMetricEventBase::EType::DroppedAudioFrame));
}
// ------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------------------------
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> FElectraPlayerRuntimeFactory::CreatePlayer(IMediaEventSink& InEventSink,
FElectraPlayerSendAnalyticMetricsDelegate& InSendAnalyticMetricsDelegate,
FElectraPlayerSendAnalyticMetricsPerMinuteDelegate& InSendAnalyticMetricsPerMinuteDelegate,
FElectraPlayerReportVideoStreamingErrorDelegate& InReportVideoStreamingErrorDelegate,
FElectraPlayerReportSubtitlesMetricsDelegate& InReportSubtitlesFileMetricsDelegate)
{
TSharedPtr<FElectraPlayer, ESPMode::ThreadSafe> NewPlayer = MakeShared<FElectraPlayer, ESPMode::ThreadSafe>(InEventSink, InSendAnalyticMetricsDelegate, InSendAnalyticMetricsPerMinuteDelegate, InReportVideoStreamingErrorDelegate, InReportSubtitlesFileMetricsDelegate);
return NewPlayer;
}