3544 lines
136 KiB
C++
3544 lines
136 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "ElectraPlayer.h"
|
|
#include "ElectraPlayerPrivate.h"
|
|
#include "ElectraPlayerPlatform.h"
|
|
#include "Misc/Optional.h"
|
|
|
|
#include "PlayerRuntimeGlobal.h"
|
|
#include "Player/AdaptiveStreamingPlayer.h"
|
|
#include "Player/AdaptivePlayerOptionKeynames.h"
|
|
#include "Player/IExternalDataReader.h"
|
|
#include "Renderer/RendererVideo.h"
|
|
#include "Renderer/RendererAudio.h"
|
|
#include "MediaMetaDataDecoderOutput.h"
|
|
#include "VideoDecoderResourceDelegate.h"
|
|
#include "Utilities/Utilities.h"
|
|
|
|
#include "CoreGlobals.h"
|
|
#include "Misc/ConfigCacheIni.h"
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
CSV_DEFINE_CATEGORY_MODULE(ELECTRAPLAYERRUNTIME_API, ElectraPlayer, false);
|
|
|
|
DECLARE_CYCLE_STAT(TEXT("FElectraPlayer::TickInput"), STAT_ElectraPlayer_ElectraPlayer_TickInput, 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")
|
|
|
|
|
|
#define USE_INTERNAL_PLAYBACK_STATE 1
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
#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;
|
|
};
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
/**
|
|
* Construction of new player
|
|
*/
|
|
FElectraPlayer::FElectraPlayer(const TSharedPtr<IElectraPlayerAdapterDelegate, ESPMode::ThreadSafe>& InAdapterDelegate,
|
|
FElectraPlayerSendAnalyticMetricsDelegate& InSendAnalyticMetricsDelegate,
|
|
FElectraPlayerSendAnalyticMetricsPerMinuteDelegate& InSendAnalyticMetricsPerMinuteDelegate,
|
|
FElectraPlayerReportVideoStreamingErrorDelegate& InReportVideoStreamingErrorDelegate,
|
|
FElectraPlayerReportSubtitlesMetricsDelegate& InReportSubtitlesFileMetricsDelegate)
|
|
: AdapterDelegate(InAdapterDelegate)
|
|
, SendAnalyticMetricsDelegate(InSendAnalyticMetricsDelegate)
|
|
, SendAnalyticMetricsPerMinuteDelegate(InSendAnalyticMetricsPerMinuteDelegate)
|
|
, ReportVideoStreamingErrorDelegate(InReportVideoStreamingErrorDelegate)
|
|
, ReportSubtitlesMetricsDelegate(InReportSubtitlesFileMetricsDelegate)
|
|
{
|
|
CSV_EVENT(ElectraPlayer, TEXT("Player Creation"));
|
|
|
|
WaitForPlayerDestroyedEvent = FPlatformProcess::GetSynchEventFromPool(true);
|
|
WaitForPlayerDestroyedEvent->Trigger();
|
|
|
|
AppTerminationHandler = MakeSharedTS<Electra::FApplicationTerminationHandler>();
|
|
AppTerminationHandler->Terminate = [this]() { CloseInternal(false); };
|
|
Electra::AddTerminationNotificationHandler(AppTerminationHandler);
|
|
|
|
FString OSMinor;
|
|
AnalyticsGPUType = InAdapterDelegate->GetVideoAdapterName().TrimStartAndEnd();
|
|
FPlatformMisc::GetOSVersions(AnalyticsOSVersion, OSMinor);
|
|
AnalyticsOSVersion.TrimStartAndEndInline();
|
|
|
|
SendAnalyticMetricsDelegate.AddRaw(this, &FElectraPlayer::SendAnalyticMetrics);
|
|
SendAnalyticMetricsPerMinuteDelegate.AddRaw(this, &FElectraPlayer::SendAnalyticMetricsPerMinute);
|
|
ReportVideoStreamingErrorDelegate.AddRaw(this, &FElectraPlayer::ReportVideoStreamingError);
|
|
ReportSubtitlesMetricsDelegate.AddRaw(this, &FElectraPlayer::ReportSubtitlesMetrics);
|
|
|
|
#if USE_INTERNAL_PLAYBACK_STATE
|
|
PlayerState.bUseInternal = true;
|
|
#endif
|
|
bAllowKillAfterCloseEvent = false;
|
|
bPlayerHasClosed = false;
|
|
bHasPendingError = false;
|
|
AnalyticsInstanceEventCount = 0;
|
|
NumQueuedAnalyticEvents = 0;
|
|
|
|
StaticResourceProvider = MakeShared<FAdaptiveStreamingPlayerResourceProvider, ESPMode::ThreadSafe>(AdapterDelegate);
|
|
VideoDecoderResourceDelegate = PlatformCreateVideoDecoderResourceDelegate(AdapterDelegate);
|
|
|
|
ClearToDefaultState();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
/**
|
|
* Cleanup destructor
|
|
*/
|
|
FElectraPlayer::~FElectraPlayer()
|
|
{
|
|
Electra::RemoveTerminationNotificationHandler(AppTerminationHandler);
|
|
AppTerminationHandler.Reset();
|
|
|
|
CloseInternal(false);
|
|
WaitForPlayerDestroyedEvent->Wait();
|
|
|
|
CSV_EVENT(ElectraPlayer, TEXT("Player Destruction"));
|
|
|
|
SendAnalyticMetricsDelegate.RemoveAll(this);
|
|
SendAnalyticMetricsPerMinuteDelegate.RemoveAll(this);
|
|
ReportVideoStreamingErrorDelegate.RemoveAll(this);
|
|
ReportSubtitlesMetricsDelegate.RemoveAll(this);
|
|
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] ~FElectraPlayer() finished."), InstanceID);
|
|
|
|
if (AsyncResourceReleaseNotification.IsValid())
|
|
{
|
|
AsyncResourceReleaseNotification->Signal(ResourceFlags_OutputBuffers);
|
|
}
|
|
|
|
FPlatformProcess::ReturnSynchEventToPool(WaitForPlayerDestroyedEvent);
|
|
}
|
|
|
|
|
|
void FElectraPlayer::ClearToDefaultState()
|
|
{
|
|
FScopeLock lock(&PlayerLock);
|
|
|
|
PlayerState.Reset();
|
|
NumTracksAudio = 0;
|
|
NumTracksVideo = 0;
|
|
NumTracksSubtitle = 0;
|
|
SelectedQuality = 0;
|
|
SelectedVideoTrackIndex = -1;
|
|
SelectedAudioTrackIndex = -1;
|
|
SelectedSubtitleTrackIndex = -1;
|
|
bVideoTrackIndexDirty = true;
|
|
bAudioTrackIndexDirty = true;
|
|
bSubtitleTrackIndexDirty = true;
|
|
bInitialSeekPerformed = false;
|
|
bDiscardOutputUntilCleanStart = false;
|
|
bIsFirstBuffering = true;
|
|
LastPresentedFrameDimension = FIntPoint::ZeroValue;
|
|
CurrentStreamMetadata.Reset();
|
|
CurrentlyActiveVideoStreamFormat.Reset();
|
|
DeferredPlayerEvents.Empty();
|
|
MediaUrl.Empty();
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
/**
|
|
* Open player
|
|
*/
|
|
bool FElectraPlayer::OpenInternal(const FString& Url, const FParamDict& InPlayerOptions, const FPlaystartOptions& InPlaystartOptions, EOpenType InOpenType)
|
|
{
|
|
static const FName KeyUniquePlayerID(TEXT("UniquePlayerID"));
|
|
|
|
LLM_SCOPE(ELLMTag::ElectraPlayer);
|
|
CSV_EVENT(ElectraPlayer, TEXT("Open"));
|
|
|
|
InstanceID = (uint32)InPlayerOptions.GetValue(KeyUniquePlayerID).SafeGetInt64(0);
|
|
|
|
// Open the provided URL as a media or a blob?
|
|
FString BlobParams;
|
|
bool bPreviousOpenLoadedBlob = PendingBlobRequest.IsValid();
|
|
PendingBlobRequest.Reset();
|
|
bool bCreateNewPlayer = (InOpenType == IElectraPlayerInterface::EOpenType::Media && !bPreviousOpenLoadedBlob) ||
|
|
(InOpenType == IElectraPlayerInterface::EOpenType::Blob);
|
|
if (bCreateNewPlayer)
|
|
{
|
|
CloseInternal(false);
|
|
}
|
|
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> NewPlayer = MoveTemp(CurrentPlayer);
|
|
|
|
// Clear out our work variables
|
|
ClearToDefaultState();
|
|
bAllowKillAfterCloseEvent = false;
|
|
bPlayerHasClosed = false;
|
|
bHasPendingError = false;
|
|
|
|
// Start statistics with a clean slate.
|
|
Statistics.Reset();
|
|
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 = FGuid::NewGuid().ToString(EGuidFormats::Digits);
|
|
UpdateAnalyticsCustomValues();
|
|
|
|
PlaystartOptions = InPlaystartOptions;
|
|
|
|
// Get a writable copy of the URL so we can sanitize it if necessary.
|
|
MediaUrl = Url;
|
|
MediaUrl.TrimStartAndEndInline();
|
|
|
|
if (!NewPlayer.IsValid())
|
|
{
|
|
FParamDict PlayerOptions(InPlayerOptions);
|
|
if (PlaystartOptions.ExternalDataReader.IsValid())
|
|
{
|
|
PlayerOptions.Set(Electra::OptionKeyUseExternalDataReader, FVariantValue(true));
|
|
StaticResourceProvider->SetExternalDataReader(PlaystartOptions.ExternalDataReader);
|
|
}
|
|
|
|
// Create a new empty player structure. This contains the actual player instance, its associated renderers and sample queues.
|
|
NewPlayer = MakeShared<FInternalPlayerImpl, ESPMode::ThreadSafe>();
|
|
|
|
// Create the renderers so we can pass them to the internal player.
|
|
// They get a pointer to ourselves which they will call On[Video|Audio]Decoded() and On[Video|Audio]Flush() on.
|
|
NewPlayer->RendererVideo = MakeShared<FElectraRendererVideo, ESPMode::ThreadSafe>(SharedThis(this));
|
|
NewPlayer->RendererAudio = MakeShared<FElectraRendererAudio, ESPMode::ThreadSafe>(SharedThis(this));
|
|
|
|
// Create the internal player and register ourselves as metrics receiver and static resource provider.
|
|
IAdaptiveStreamingPlayer::FCreateParam CreateParams;
|
|
CreateParams.VideoRenderer = NewPlayer->RendererVideo;
|
|
CreateParams.AudioRenderer = NewPlayer->RendererAudio;
|
|
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);
|
|
NewPlayer->AdaptivePlayer->SetVideoDecoderResourceDelegate(VideoDecoderResourceDelegate);
|
|
if (PlaystartOptions.ExternalDataCache.IsValid())
|
|
{
|
|
NewPlayer->AdaptivePlayer->SetPlayerDataCache(PlaystartOptions.ExternalDataCache);
|
|
}
|
|
|
|
// 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);
|
|
|
|
if (InOpenType == IElectraPlayerInterface::EOpenType::Blob)
|
|
{
|
|
const FName KeyBlob(TEXT("blobparams"));
|
|
if (PlayerOptions.HaveKey(KeyBlob))
|
|
{
|
|
BlobParams = PlayerOptions.GetValue(KeyBlob).SafeGetFString();
|
|
PlayerOptions.Remove(KeyBlob);
|
|
}
|
|
}
|
|
NewPlayer->AdaptivePlayer->Initialize(PlayerOptions);
|
|
}
|
|
|
|
if (InOpenType == IElectraPlayerInterface::EOpenType::Media)
|
|
{
|
|
// 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())
|
|
{
|
|
SetFrameAccurateSeekMode(bFrameAccurateSeeking.GetValue());
|
|
}
|
|
if (bEnableLooping.IsSet())
|
|
{
|
|
SetLooping(bEnableLooping.GetValue());
|
|
}
|
|
if (CurrentPlaybackRange.Start.IsSet() || CurrentPlaybackRange.End.IsSet())
|
|
{
|
|
SetPlaybackRange(CurrentPlaybackRange);
|
|
}
|
|
|
|
if (bPreviousOpenLoadedBlob)
|
|
{
|
|
CurrentPlayer->AdaptivePlayer->ModifyOptions(InPlayerOptions, Electra::FParamDict());
|
|
}
|
|
|
|
// Issue load of the playlist.
|
|
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] IMediaPlayer::Open(%s)"), InstanceID, *SanitizeMessage(MediaUrl));
|
|
CurrentPlayer->AdaptivePlayer->LoadManifest(MediaUrl);
|
|
}
|
|
else
|
|
{
|
|
PendingBlobRequest = MakeShared<FBlobRequest, ESPMode::ThreadSafe>();
|
|
if (!PendingBlobRequest->Request->SetFromJSON(BlobParams))
|
|
{
|
|
UE_LOG(LogElectraPlayer, Error, TEXT("[%u] IMediaPlayer::OpenBlob(%s) has bad JSON parameters"), InstanceID, *SanitizeMessage(MediaUrl));
|
|
PendingBlobRequest.Reset();
|
|
return false;
|
|
}
|
|
CurrentPlayer = MoveTemp(NewPlayer);
|
|
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] IMediaPlayer::OpenBlob(%s)"), InstanceID, *SanitizeMessage(MediaUrl));
|
|
PendingBlobRequest->Request->URL(MediaUrl).Callback().BindThreadSafeSP(PendingBlobRequest.ToSharedRef(), &FBlobRequest::OnBlobRequestComplete);
|
|
CurrentPlayer->AdaptivePlayer->LoadBlob(PendingBlobRequest->Request);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
/**
|
|
* Close / Shutdown player
|
|
*/
|
|
void FElectraPlayer::CloseInternal(bool bKillAfterClose)
|
|
{
|
|
LLM_SCOPE(ELLMTag::ElectraPlayer);
|
|
|
|
PlayerLock.Lock();
|
|
if (bPlayerHasClosed || !CurrentPlayer.IsValid())
|
|
{
|
|
PlayerLock.Unlock();
|
|
return;
|
|
}
|
|
bPlayerHasClosed = true;
|
|
WaitForPlayerDestroyedEvent->Reset();
|
|
PlayerLock.Unlock();
|
|
|
|
|
|
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] IMediaPlayer::Close()"), InstanceID);
|
|
CSV_EVENT(ElectraPlayer, TEXT("Close"));
|
|
|
|
/*
|
|
* Closing the player is a delicate procedure because there are several worker threads involved
|
|
* that we need to make sure will not report back to us or deliver any pending data while we
|
|
* are cleaning everything up.
|
|
*/
|
|
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> Player = CurrentPlayer;
|
|
|
|
// For all intents and purposes the player can be considered closed here now already.
|
|
PlayerState.State = EPlayerState::Closed;
|
|
MediaUrl = FString();
|
|
PlaystartOptions.TimeOffset.Reset();
|
|
PlaystartOptions.InitialAudioTrackAttributes.Reset();
|
|
CurrentPlaybackRange.Start.Reset();
|
|
CurrentPlaybackRange.End.Reset();
|
|
bFrameAccurateSeeking.Reset();
|
|
bEnableLooping.Reset();
|
|
|
|
// Next we detach ourselves from the renderers. This ensures we do not get any further data from them
|
|
// via OnVideoDecoded() and OnAudioDecoded(). It also means we do not get any calls to OnVideoFlush() and OnAudioFlush()
|
|
// and need to do this ourselves.
|
|
if (Player->RendererVideo.IsValid())
|
|
{
|
|
Player->RendererVideo->DetachPlayer();
|
|
}
|
|
if (Player->RendererAudio.IsValid())
|
|
{
|
|
Player->RendererAudio->DetachPlayer();
|
|
}
|
|
|
|
// Next up we clear out the sample queues.
|
|
// NOTE that it is important we use the On..Flush() methods here and not simply clear out the queues.
|
|
// The Flush() methods do more than that that is required to do and we don't need to duplicate that here.
|
|
// Most notably they make sure all pending samples from MediaSamples are cleared.
|
|
OnVideoFlush();
|
|
OnAudioFlush();
|
|
|
|
// Now that we should be clear of all samples and should also not be receiving any more we can tend
|
|
// to the actual media player shutdown.
|
|
check(Player->AdaptivePlayer.IsValid());
|
|
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();
|
|
}
|
|
|
|
// Unregister ourselves as the provider for static resources.
|
|
Player->AdaptivePlayer->SetStaticResourceProviderCallback(nullptr);
|
|
// Also unregister us from receiving further metric callbacks.
|
|
// NOTE: This means we will not be receiving the final ReportPlaybackStopped() event, but that is on purpose!
|
|
// The closing of the player will be handled asynchronously by a thread and we must not be notified on
|
|
// *anything* any more. It is *very possible* that this instance here will be destroyed before the
|
|
// player is and any callback would only cause a crash.
|
|
Player->AdaptivePlayer->RemoveMetricsReceiver(this);
|
|
}
|
|
|
|
// Clear any pending static resource requests now.
|
|
StaticResourceProvider->ClearPendingRequests();
|
|
|
|
// Pretend we got the playback stopped event via metrics, which we did not because we unregistered ourselves already.
|
|
HandlePlayerEventPlaybackStopped();
|
|
LogStatistics();
|
|
DeferredEvents.Enqueue(IElectraPlayerAdapterDelegate::EPlayerEvent::TracksChanged);
|
|
DeferredEvents.Enqueue(IElectraPlayerAdapterDelegate::EPlayerEvent::MediaClosed);
|
|
|
|
// Clear out the player instance now.
|
|
CurrentPlayer.Reset();
|
|
|
|
// Kick off asynchronous closing now.
|
|
FInternalPlayerImpl::DoCloseAsync(MoveTemp(Player), InstanceID, AsyncResourceReleaseNotification);
|
|
|
|
bAllowKillAfterCloseEvent = bKillAfterClose;
|
|
|
|
WaitForPlayerDestroyedEvent->Trigger();
|
|
}
|
|
|
|
void FElectraPlayer::FInternalPlayerImpl::DoCloseAsync(TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe>&& Player, uint32 PlayerID, TSharedPtr<FElectraPlayer::IAsyncResourceReleaseNotifyContainer, ESPMode::ThreadSafe> AsyncResourceReleaseNotification)
|
|
{
|
|
TSharedPtr<volatile bool> bClosedSig = MakeShared<volatile bool>(false);
|
|
|
|
TFunction<void()> CloseTask = [Player, PlayerID, bClosedSig, AsyncResourceReleaseNotification]()
|
|
{
|
|
double TimeCloseBegan = FPlatformTime::Seconds();
|
|
Player->AdaptivePlayer->Stop();
|
|
Player->AdaptivePlayer.Reset();
|
|
Player->RendererVideo.Reset();
|
|
Player->RendererAudio.Reset();
|
|
*bClosedSig = true;
|
|
double TimeCloseEnded = FPlatformTime::Seconds();
|
|
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] DoCloseAsync() finished after %.3f msec!"), PlayerID, (TimeCloseEnded - TimeCloseBegan) * 1000.0);
|
|
|
|
if (AsyncResourceReleaseNotification.IsValid())
|
|
{
|
|
AsyncResourceReleaseNotification->Signal(ResourceFlags_Decoder);
|
|
}
|
|
};
|
|
|
|
// Fallback to simple, sequential execution if the engine is already shutting down...
|
|
if (GIsRunning)
|
|
{
|
|
FMediaRunnable::EnqueueAsyncTask(MoveTemp(CloseTask));
|
|
}
|
|
else
|
|
{
|
|
CloseTask();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
float FElectraPlayer::FPlayerState::GetRate() const
|
|
{
|
|
return bUseInternal && IntendedPlayRate.IsSet() ? IntendedPlayRate.GetValue() : CurrentPlayRate;
|
|
}
|
|
|
|
FElectraPlayer::EPlayerState FElectraPlayer::FPlayerState::GetState() const
|
|
{
|
|
if (bUseInternal && IntendedPlayRate.IsSet() && (State == FElectraPlayer::EPlayerState::Playing || State == FElectraPlayer::EPlayerState::Paused || State == FElectraPlayer::EPlayerState::Stopped))
|
|
{
|
|
return IntendedPlayRate.GetValue() != 0.0f ? FElectraPlayer::EPlayerState::Playing : FElectraPlayer::EPlayerState::Paused;
|
|
}
|
|
return State;
|
|
}
|
|
|
|
FElectraPlayer::EPlayerStatus 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();
|
|
}
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
/**
|
|
* Suspends or resumes decoder instances.
|
|
*/
|
|
void FElectraPlayer::SuspendOrResumeDecoders(bool bSuspend, const Electra::FParamDict& InOptions)
|
|
{
|
|
PlatformSuspendOrResumeDecoders(bSuspend, InOptions);
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
/**
|
|
* Provides information about the time ranges that are currently available to the
|
|
* player and those that are being loaded.
|
|
*/
|
|
bool FElectraPlayer::GetStreamBufferInformation(IElectraPlayerInterface::FStreamBufferInfo& OutBufferInformation, EPlayerTrackType InTrackType) const
|
|
{
|
|
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> Player = CurrentPlayer;
|
|
if (Player.IsValid())
|
|
{
|
|
IAdaptiveStreamingPlayer::FStreamBufferInfo bi;
|
|
if (InTrackType == EPlayerTrackType::Video)
|
|
{
|
|
Player->AdaptivePlayer->QueryStreamBufferInfo(bi, Electra::EStreamType::Video);
|
|
}
|
|
else if (InTrackType == EPlayerTrackType::Audio)
|
|
{
|
|
Player->AdaptivePlayer->QueryStreamBufferInfo(bi, Electra::EStreamType::Audio);
|
|
}
|
|
if (bi.bIsBufferActive)
|
|
{
|
|
auto AddRanges = [](TArray<IElectraPlayerInterface::FStreamBufferInfo::FTimeRange>& OutRanges, const TArray<Electra::FTimeRange>& InRanges) -> void
|
|
{
|
|
for(int32 i=0; i<InRanges.Num(); ++i)
|
|
{
|
|
IElectraPlayerInterface::FStreamBufferInfo::FTimeRange& tv = OutRanges.Emplace_GetRef();
|
|
tv.Start.Time = InRanges[i].Start.GetAsTimespan();
|
|
tv.Start.SequenceIndex = InRanges[i].Start.GetSequenceIndex();
|
|
tv.End.Time = InRanges[i].End.GetAsTimespan();
|
|
tv.End.SequenceIndex = InRanges[i].End.GetSequenceIndex();
|
|
}
|
|
};
|
|
AddRanges(OutBufferInformation.TimeEnqueued, bi.TimeEnqueued);
|
|
AddRanges(OutBufferInformation.TimeAvailable, bi.TimeAvailable);
|
|
AddRanges(OutBufferInformation.TimeRequested, bi.TimeRequested);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
/**
|
|
*/
|
|
void FElectraPlayer::SetAsyncResourceReleaseNotification(IAsyncResourceReleaseNotifyContainer* InAsyncResourceReleaseNotification)
|
|
{
|
|
AsyncResourceReleaseNotification = TSharedPtr<IAsyncResourceReleaseNotifyContainer, ESPMode::ThreadSafe>(InAsyncResourceReleaseNotification);
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
/**
|
|
*/
|
|
void FElectraPlayer::Tick(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.
|
|
PlayerLock.Lock();
|
|
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> Player = CurrentPlayer;
|
|
if (!bPlayerHasClosed && Player.IsValid() && PlayerState.State != EPlayerState::Error)
|
|
{
|
|
if (Player->RendererVideo.IsValid())
|
|
{
|
|
Player->RendererVideo->TickInput(DeltaTime, Timecode);
|
|
}
|
|
|
|
// Handle static resource fetch requests.
|
|
StaticResourceProvider->ProcessPendingStaticResourceRequests();
|
|
|
|
// Check for blob loading completed
|
|
HandleBlobDownload();
|
|
|
|
// Check for option changes
|
|
TSharedPtr<IElectraPlayerAdapterDelegate, ESPMode::ThreadSafe> PinnedAdapterDelegate = AdapterDelegate.Pin();
|
|
if (PinnedAdapterDelegate.IsValid())
|
|
{
|
|
FVariantValue Value = PinnedAdapterDelegate->QueryOptions(IElectraPlayerAdapterDelegate::EOptionType::MaxVerticalStreamResolution);
|
|
if (Value.IsValid())
|
|
{
|
|
int64 NewVerticalStreamResolution = Value.GetInt64();
|
|
if (NewVerticalStreamResolution != PlaystartOptions.MaxVerticalStreamResolution.Get(0))
|
|
{
|
|
PlaystartOptions.MaxVerticalStreamResolution = NewVerticalStreamResolution;
|
|
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] Limiting max vertical resolution to %d"), InstanceID, (int32)NewVerticalStreamResolution);
|
|
Player->AdaptivePlayer->SetMaxResolution(0, (int32)NewVerticalStreamResolution);
|
|
}
|
|
}
|
|
Value = PinnedAdapterDelegate->QueryOptions(IElectraPlayerAdapterDelegate::EOptionType::MaxBandwidthForStreaming);
|
|
if (Value.IsValid())
|
|
{
|
|
int64 NewBandwidthForStreaming = Value.GetInt64();
|
|
if (NewBandwidthForStreaming != PlaystartOptions.MaxBandwidthForStreaming.Get(0))
|
|
{
|
|
PlaystartOptions.MaxBandwidthForStreaming = NewBandwidthForStreaming;
|
|
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] Limiting max streaming bandwidth to %d bps"), InstanceID, (int32)NewBandwidthForStreaming);
|
|
Player->AdaptivePlayer->SetBitrateCeiling((int32)NewBandwidthForStreaming);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process accumulated player events.
|
|
HandleDeferredPlayerEvents();
|
|
if (bHasPendingError)
|
|
{
|
|
bHasPendingError = false;
|
|
if (PlayerState.State == EPlayerState::Preparing)
|
|
{
|
|
DeferredEvents.Enqueue(IElectraPlayerAdapterDelegate::EPlayerEvent::MediaOpenFailed);
|
|
}
|
|
else if (PlayerState.State == EPlayerState::Playing)
|
|
{
|
|
DeferredEvents.Enqueue(IElectraPlayerAdapterDelegate::EPlayerEvent::MediaClosed);
|
|
}
|
|
|
|
CloseInternal(true);
|
|
PlayerState.State = EPlayerState::Error;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DeferredPlayerEvents.Empty();
|
|
}
|
|
PlayerLock.Unlock();
|
|
|
|
// Forward enqueued session events. We do this even with no current internal player to ensure all pending events are sent and none are lost.
|
|
TSharedPtr<IElectraPlayerAdapterDelegate, ESPMode::ThreadSafe> PinnedAdapterDelegate = AdapterDelegate.Pin();
|
|
if (PinnedAdapterDelegate.IsValid())
|
|
{
|
|
IElectraPlayerAdapterDelegate::EPlayerEvent Event;
|
|
while(DeferredEvents.Dequeue(Event))
|
|
{
|
|
PinnedAdapterDelegate->SendMediaEvent(Event);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FElectraPlayer::HandleBlobDownload()
|
|
{
|
|
if (PendingBlobRequest.IsValid() && PendingBlobRequest->bIsComplete && !PendingBlobRequest->bDispatched)
|
|
{
|
|
PendingBlobRequest->bDispatched = true;
|
|
if (!PendingBlobRequest->Request->GetWasCanceled())
|
|
{
|
|
int32 ErrCode = PendingBlobRequest->Request->GetError();
|
|
IElectraPlayerAdapterDelegate::EBlobResultType Result = ErrCode == 0 ? IElectraPlayerAdapterDelegate::EBlobResultType::Success :
|
|
ErrCode > 0 && ErrCode < 100 ? IElectraPlayerAdapterDelegate::EBlobResultType::TimedOut :
|
|
IElectraPlayerAdapterDelegate::EBlobResultType::HttpFailure;
|
|
TSharedPtr<TArray<uint8>, ESPMode::ThreadSafe> BlobData = MakeShared<TArray<uint8>, ESPMode::ThreadSafe>();
|
|
TSharedPtrTS<FWaitableBuffer> ResponseBuffer = PendingBlobRequest->Request->GetResponseBuffer();
|
|
if (ResponseBuffer.IsValid())
|
|
{
|
|
BlobData->Append((const uint8*)ResponseBuffer->GetLinearReadData(), ResponseBuffer->Num());
|
|
}
|
|
TSharedPtr<IElectraPlayerAdapterDelegate, ESPMode::ThreadSafe> PinnedAdapterDelegate = AdapterDelegate.Pin();
|
|
if (PinnedAdapterDelegate.IsValid())
|
|
{
|
|
PinnedAdapterDelegate->BlobReceived(BlobData, Result, ErrCode, nullptr);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
/**
|
|
* The video renderer is adding a buffer to the queue
|
|
*/
|
|
void FElectraPlayer::OnVideoDecoded(const FVideoDecoderOutputPtr& DecoderOutput, bool bDoNotRender)
|
|
{
|
|
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> Player = CurrentPlayer;
|
|
if (Player.IsValid())
|
|
{
|
|
if (PlayerState.State != EPlayerState::Closed)
|
|
{
|
|
if (DecoderOutput.IsValid())
|
|
{
|
|
if (!bDoNotRender)
|
|
{
|
|
PresentVideoFrame(DecoderOutput);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FElectraPlayer::OnVideoFlush()
|
|
{
|
|
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> Player = CurrentPlayer;
|
|
if (Player.IsValid())
|
|
{
|
|
TSharedPtr<IElectraPlayerAdapterDelegate, ESPMode::ThreadSafe> PinnedAdapterDelegate = AdapterDelegate.Pin();
|
|
if (PinnedAdapterDelegate.IsValid())
|
|
{
|
|
PinnedAdapterDelegate->OnVideoFlush();
|
|
}
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
/**
|
|
* The audio renderer is adding a buffer to the queue
|
|
*
|
|
*/
|
|
void FElectraPlayer::OnAudioDecoded(const IAudioDecoderOutputPtr& DecoderOutput)
|
|
{
|
|
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> Player = CurrentPlayer;
|
|
if (Player.IsValid())
|
|
{
|
|
if (PlayerState.State != EPlayerState::Closed)
|
|
{
|
|
PresentAudioFrame(DecoderOutput);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FElectraPlayer::OnAudioFlush()
|
|
{
|
|
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> Player = CurrentPlayer;
|
|
if (Player.IsValid())
|
|
{
|
|
TSharedPtr<IElectraPlayerAdapterDelegate, ESPMode::ThreadSafe> PinnedAdapterDelegate = AdapterDelegate.Pin();
|
|
if (PinnedAdapterDelegate.IsValid())
|
|
{
|
|
PinnedAdapterDelegate->OnAudioFlush();
|
|
}
|
|
}
|
|
}
|
|
|
|
void FElectraPlayer::OnSubtitleDecoded(ISubtitleDecoderOutputPtr DecoderOutput)
|
|
{
|
|
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> Player = CurrentPlayer;
|
|
if (Player.IsValid())
|
|
{
|
|
if (PlayerState.State != EPlayerState::Closed)
|
|
{
|
|
PresentSubtitle(DecoderOutput);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FElectraPlayer::OnSubtitleFlush()
|
|
{
|
|
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> Player = CurrentPlayer;
|
|
if (Player.IsValid())
|
|
{
|
|
TSharedPtr<IElectraPlayerAdapterDelegate, ESPMode::ThreadSafe> PinnedAdapterDelegate = AdapterDelegate.Pin();
|
|
if (PinnedAdapterDelegate.IsValid())
|
|
{
|
|
PinnedAdapterDelegate->OnSubtitleFlush();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
/**
|
|
* Check timeline and moves sample over to media FACADE sinks
|
|
*
|
|
* Returns true if sample was moved over, but DOES not remove the sample from player queue
|
|
*/
|
|
bool FElectraPlayer::PresentVideoFrame(const FVideoDecoderOutputPtr& InVideoFrame)
|
|
{
|
|
TSharedPtr<IElectraPlayerAdapterDelegate, ESPMode::ThreadSafe> PinnedAdapterDelegate = AdapterDelegate.Pin();
|
|
if (PinnedAdapterDelegate.IsValid())
|
|
{
|
|
PinnedAdapterDelegate->PresentVideoFrame(InVideoFrame);
|
|
LastPresentedFrameDimension = InVideoFrame->GetOutputDim();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
/**
|
|
* Check timeline and moves sample over to media FACADE sinks
|
|
*
|
|
* Returns true if sample was moved over, but DOES not remove the sample from player queue
|
|
*/
|
|
bool FElectraPlayer::PresentAudioFrame(const IAudioDecoderOutputPtr& DecoderOutput)
|
|
{
|
|
TSharedPtr<IElectraPlayerAdapterDelegate, ESPMode::ThreadSafe> PinnedAdapterDelegate = AdapterDelegate.Pin();
|
|
if (PinnedAdapterDelegate.IsValid())
|
|
{
|
|
PinnedAdapterDelegate->PresentAudioFrame(DecoderOutput);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
bool FElectraPlayer::PresentSubtitle(const ISubtitleDecoderOutputPtr& DecoderOutput)
|
|
{
|
|
TSharedPtr<IElectraPlayerAdapterDelegate, ESPMode::ThreadSafe> PinnedAdapterDelegate = AdapterDelegate.Pin();
|
|
if (PinnedAdapterDelegate.IsValid())
|
|
{
|
|
PinnedAdapterDelegate->PresentSubtitleSample(DecoderOutput);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
/**
|
|
* Attempt to drop any old frames from the presentation queue
|
|
*/
|
|
void FElectraPlayer::DropOldFramesFromPresentationQueue()
|
|
{
|
|
TSharedPtr<IElectraPlayerAdapterDelegate, ESPMode::ThreadSafe> PinnedAdapterDelegate = AdapterDelegate.Pin();
|
|
if (PinnedAdapterDelegate.IsValid())
|
|
{
|
|
// We ask the event sink (player facade) to trigger this, as we don't have good enough timing info
|
|
PinnedAdapterDelegate->SendMediaEvent(IElectraPlayerAdapterDelegate::EPlayerEvent::Internal_PurgeVideoSamplesHint);
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
/**
|
|
*
|
|
*/
|
|
bool FElectraPlayer::CanPresentVideoFrames(uint64 NumFrames)
|
|
{
|
|
DropOldFramesFromPresentationQueue();
|
|
TSharedPtr<IElectraPlayerAdapterDelegate, ESPMode::ThreadSafe> PinnedAdapterDelegate = AdapterDelegate.Pin();
|
|
if (PinnedAdapterDelegate.IsValid())
|
|
{
|
|
if (!bDiscardOutputUntilCleanStart)
|
|
{
|
|
return PinnedAdapterDelegate->CanReceiveVideoSamples(NumFrames);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool FElectraPlayer::CanPresentAudioFrames(uint64 NumFrames)
|
|
{
|
|
TSharedPtr<IElectraPlayerAdapterDelegate, ESPMode::ThreadSafe> PinnedAdapterDelegate = AdapterDelegate.Pin();
|
|
if (PinnedAdapterDelegate.IsValid())
|
|
{
|
|
if (!bDiscardOutputUntilCleanStart)
|
|
{
|
|
return PinnedAdapterDelegate->CanReceiveAudioSamples(NumFrames);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
FElectraPlayer::EPlayerState FElectraPlayer::GetState() const
|
|
{
|
|
return PlayerState.GetState();
|
|
}
|
|
|
|
FElectraPlayer::EPlayerStatus FElectraPlayer::GetStatus() const
|
|
{
|
|
return PlayerState.GetStatus();
|
|
}
|
|
|
|
bool FElectraPlayer::IsLooping() const
|
|
{
|
|
if (CurrentPlayer.Get())
|
|
{
|
|
IAdaptiveStreamingPlayer::FLoopState loopState;
|
|
CurrentPlayer->AdaptivePlayer->GetLoopState(loopState);
|
|
return loopState.bIsEnabled;
|
|
}
|
|
return bEnableLooping.IsSet() ? bEnableLooping.GetValue() : false;
|
|
}
|
|
|
|
bool FElectraPlayer::SetLooping(bool bLooping)
|
|
{
|
|
bEnableLooping = bLooping;
|
|
if (CurrentPlayer.Get())
|
|
{
|
|
IAdaptiveStreamingPlayer::FLoopParam loop;
|
|
loop.bEnableLooping = bLooping;
|
|
CurrentPlayer->AdaptivePlayer->SetLooping(loop);
|
|
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] IMediaPlayer::SetLooping(%s)"), InstanceID, bLooping?TEXT("true"):TEXT("false"));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
int32 FElectraPlayer::GetLoopCount() const
|
|
{
|
|
if (CurrentPlayer.Get())
|
|
{
|
|
IAdaptiveStreamingPlayer::FLoopState loopState;
|
|
CurrentPlayer->AdaptivePlayer->GetLoopState(loopState);
|
|
return (int32) loopState.Count;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
|
|
FTimespan FElectraPlayer::GetTime() const
|
|
{
|
|
if (CurrentPlayer.Get())
|
|
{
|
|
Electra::FTimeValue playerTime = CurrentPlayer->AdaptivePlayer->GetPlayPosition();
|
|
return playerTime.GetAsTimespan();
|
|
}
|
|
else
|
|
{
|
|
return FTimespan::Zero();
|
|
}
|
|
}
|
|
|
|
FTimespan FElectraPlayer::GetDuration() const
|
|
{
|
|
if (CurrentPlayer.Get())
|
|
{
|
|
Electra::FTimeValue playDuration = CurrentPlayer->AdaptivePlayer->GetDuration();
|
|
if (playDuration.IsValid())
|
|
{
|
|
return playDuration.IsInfinity() ? FTimespan::MaxValue() : playDuration.GetAsTimespan();
|
|
}
|
|
}
|
|
return FTimespan::Zero();
|
|
}
|
|
|
|
bool FElectraPlayer::IsLive() const
|
|
{
|
|
if (CurrentPlayer.Get())
|
|
{
|
|
Electra::FTimeValue playDuration = CurrentPlayer->AdaptivePlayer->GetDuration();
|
|
if (playDuration.IsValid())
|
|
{
|
|
return playDuration.IsInfinity();
|
|
}
|
|
}
|
|
// Default assumption is Live playback.
|
|
return true;
|
|
}
|
|
|
|
FTimespan FElectraPlayer::GetSeekableDuration() const
|
|
{
|
|
if (CurrentPlayer.Get())
|
|
{
|
|
Electra::FTimeRange seekRange;
|
|
CurrentPlayer->AdaptivePlayer->GetSeekableRange(seekRange);
|
|
if (seekRange.IsValid())
|
|
{
|
|
// By definition here this is always positive, even for Live streams where we intend
|
|
// to seek only backwards from the Live edge.
|
|
return FTimespan((seekRange.End - seekRange.Start).GetAsHNS());
|
|
}
|
|
}
|
|
return FTimespan::Zero();
|
|
}
|
|
|
|
|
|
TRangeSet<float> FElectraPlayer::GetSupportedRates(EPlayRateType InPlayRateType) const
|
|
{
|
|
TRangeSet<float> Res;
|
|
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> LockedPlayer = CurrentPlayer;
|
|
if (LockedPlayer.IsValid() && LockedPlayer->AdaptivePlayer.IsValid())
|
|
{
|
|
TArray<TRange<double>> SupportedRanges;
|
|
LockedPlayer->AdaptivePlayer->GetSupportedRates(InPlayRateType == IElectraPlayerInterface::EPlayRateType::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;
|
|
}
|
|
|
|
bool FElectraPlayer::SetRate(float Rate)
|
|
{
|
|
//UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] IMediaPlayer::SetRate(%.3f)"), InstanceID, Rate);
|
|
if (CurrentPlayer.Get())
|
|
{
|
|
// 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(Rate);
|
|
if (Rate <= 0.0f)
|
|
{
|
|
CurrentPlayer->AdaptivePlayer->Pause();
|
|
}
|
|
else
|
|
{
|
|
if (CurrentPlayer->AdaptivePlayer->IsPaused() || !CurrentPlayer->AdaptivePlayer->IsPlaying())
|
|
{
|
|
TriggerFirstSeekIfNecessary();
|
|
CurrentPlayer->AdaptivePlayer->Resume();
|
|
}
|
|
}
|
|
IAdaptiveStreamingPlayer::FTrickplayParams Params;
|
|
CurrentPlayer->AdaptivePlayer->SetPlayRate((double) Rate, Params);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
float FElectraPlayer::GetRate() const
|
|
{
|
|
return PlayerState.GetRate();
|
|
/*
|
|
if (PlayerState.bUseInternal)
|
|
{
|
|
return 0.0f;
|
|
}
|
|
else
|
|
{
|
|
return CurrentPlayer.Get() && CurrentPlayer->AdaptivePlayer->IsPlaying() ? 1.0f : 0.0f;
|
|
}
|
|
*/
|
|
}
|
|
|
|
|
|
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();
|
|
}
|
|
|
|
// Next, give a list of the seekable positions to the delegate and ask it if it wants to seek to one of them,
|
|
// overriding any potential time offset from above.
|
|
TSharedPtr<IElectraPlayerAdapterDelegate, ESPMode::ThreadSafe> PinnedAdapterDelegate = AdapterDelegate.Pin();
|
|
if (PinnedAdapterDelegate.IsValid())
|
|
{
|
|
TSharedPtr<TArray<FTimespan>, ESPMode::ThreadSafe> SeekablePositions = MakeShared<TArray<FTimespan>, ESPMode::ThreadSafe>();
|
|
// Check with the delegate if it wants to start somewhere else.
|
|
FVariantValue Result = PinnedAdapterDelegate->QueryOptions(IElectraPlayerAdapterDelegate::EOptionType::PlaystartPosFromSeekPositions, FVariantValue(SeekablePositions));
|
|
if (Result.IsValid())
|
|
{
|
|
check(Result.IsType(FVariantValue::EDataType::TypeInt64));
|
|
playParam.Time.SetFromHNS(Result.GetInt64());
|
|
}
|
|
}
|
|
|
|
// Trigger buffering at the intended start time.
|
|
CurrentPlayer->AdaptivePlayer->SeekTo(playParam);
|
|
}
|
|
}
|
|
|
|
|
|
void FElectraPlayer::CalculateTargetSeekTime(FTimespan& OutTargetTime, const FTimespan& InTime)
|
|
{
|
|
if (CurrentPlayer.Get())
|
|
{
|
|
Electra::FTimeValue newTime;
|
|
Electra::FTimeRange playRange;
|
|
newTime.SetFromHNS(InTime.GetTicks());
|
|
CurrentPlayer->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());
|
|
}
|
|
}
|
|
|
|
|
|
bool FElectraPlayer::Seek(const FTimespan& Time, const FSeekParam& Param)
|
|
{
|
|
if (CurrentPlayer.Get())
|
|
{
|
|
FTimespan Target;
|
|
CalculateTargetSeekTime(Target, Time);
|
|
Electra::IAdaptiveStreamingPlayer::FSeekParam seek;
|
|
if (Target != FTimespan::MaxValue())
|
|
{
|
|
seek.Time.SetFromTimespan(Target);
|
|
}
|
|
check(Param.SequenceIndex.IsSet());
|
|
seek.NewSequenceIndex = Param.SequenceIndex;
|
|
seek.StartingBitrate = Param.StartingBitrate;
|
|
bInitialSeekPerformed = true;
|
|
bDiscardOutputUntilCleanStart = true;
|
|
CurrentPlayer->AdaptivePlayer->SeekTo(seek);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void FElectraPlayer::SetFrameAccurateSeekMode(bool bEnableFrameAccuracy)
|
|
{
|
|
bFrameAccurateSeeking = bEnableFrameAccuracy;
|
|
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> LockedPlayer = CurrentPlayer;
|
|
if (LockedPlayer.IsValid() && LockedPlayer->AdaptivePlayer.IsValid())
|
|
{
|
|
LockedPlayer->AdaptivePlayer->EnableFrameAccurateSeeking(bFrameAccurateSeeking.GetValue());
|
|
}
|
|
}
|
|
|
|
void FElectraPlayer::SetPlaybackRange(const FPlaybackRange& InPlaybackRange)
|
|
{
|
|
CurrentPlaybackRange = InPlaybackRange;
|
|
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> LockedPlayer = CurrentPlayer;
|
|
if (LockedPlayer.IsValid() && LockedPlayer->AdaptivePlayer.IsValid())
|
|
{
|
|
// Ranges cannot be set on Live streams.
|
|
Electra::FTimeValue playDuration = CurrentPlayer->AdaptivePlayer->GetDuration();
|
|
if (playDuration.IsValid() && playDuration.IsInfinity())
|
|
{
|
|
return;
|
|
}
|
|
Electra::IAdaptiveStreamingPlayer::FPlaybackRange Range;
|
|
if (CurrentPlaybackRange.Start.IsSet())
|
|
{
|
|
Range.Start = Electra::FTimeValue().SetFromTimespan(CurrentPlaybackRange.Start.GetValue());
|
|
}
|
|
if (CurrentPlaybackRange.End.IsSet())
|
|
{
|
|
Range.End = Electra::FTimeValue().SetFromTimespan(CurrentPlaybackRange.End.GetValue());
|
|
}
|
|
LockedPlayer->AdaptivePlayer->SetPlaybackRange(Range);
|
|
}
|
|
}
|
|
|
|
void FElectraPlayer::GetPlaybackRange(FPlaybackRange& OutPlaybackRange) const
|
|
{
|
|
OutPlaybackRange = CurrentPlaybackRange;
|
|
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> LockedPlayer = CurrentPlayer;
|
|
if (LockedPlayer.IsValid() && LockedPlayer->AdaptivePlayer.IsValid())
|
|
{
|
|
Electra::IAdaptiveStreamingPlayer::FPlaybackRange Range;
|
|
LockedPlayer->AdaptivePlayer->GetPlaybackRange(Range);
|
|
if (Range.Start.IsSet())
|
|
{
|
|
OutPlaybackRange.Start = Range.Start.GetValue().GetAsTimespan();
|
|
}
|
|
else
|
|
{
|
|
OutPlaybackRange.Start.Reset();
|
|
}
|
|
if (Range.End.IsSet())
|
|
{
|
|
OutPlaybackRange.End = Range.End.GetValue().GetAsTimespan();
|
|
}
|
|
else
|
|
{
|
|
OutPlaybackRange.End.Reset();
|
|
}
|
|
}
|
|
}
|
|
|
|
TRange<FTimespan> FElectraPlayer::GetPlaybackRange(ETimeRangeType InRangeToGet) const
|
|
{
|
|
TRange<FTimespan> Range(FTimespan(0), FTimespan(0));
|
|
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> LockedPlayer = CurrentPlayer;
|
|
if (LockedPlayer.IsValid() && LockedPlayer->AdaptivePlayer.IsValid())
|
|
{
|
|
switch(InRangeToGet)
|
|
{
|
|
case IElectraPlayerInterface::ETimeRangeType::Absolute:
|
|
{
|
|
Electra::FTimeRange Timeline;
|
|
LockedPlayer->AdaptivePlayer->GetTimelineRange(Timeline);
|
|
if (Timeline.IsValid())
|
|
{
|
|
Range.SetLowerBound(Timeline.Start.GetAsTimespan());
|
|
Range.SetUpperBound(Timeline.End.GetAsTimespan());
|
|
}
|
|
else
|
|
{
|
|
Electra::FTimeValue playDuration = CurrentPlayer->AdaptivePlayer->GetDuration();
|
|
if (playDuration.IsValid())
|
|
{
|
|
Range.SetLowerBound(FTimespan(0));
|
|
Range.SetUpperBound(playDuration.IsInfinity() ? FTimespan::MaxValue() : playDuration.GetAsTimespan());
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case IElectraPlayerInterface::ETimeRangeType::Current:
|
|
{
|
|
Electra::IAdaptiveStreamingPlayer::FPlaybackRange Current;
|
|
LockedPlayer->AdaptivePlayer->GetPlaybackRange(Current);
|
|
if (Current.Start.IsSet() && Current.End.IsSet())
|
|
{
|
|
Range.SetLowerBound(Current.Start.GetValue().GetAsTimespan());
|
|
Range.SetUpperBound(Current.End.GetValue().GetAsTimespan());
|
|
}
|
|
else
|
|
{
|
|
return GetPlaybackRange(IElectraPlayerInterface::ETimeRangeType::Absolute);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return Range;
|
|
}
|
|
|
|
Electra::FVariantValue FElectraPlayer::GetMediaInfo(FName InInfoName) const
|
|
{
|
|
const TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> LockedPlayer = CurrentPlayer;
|
|
if (LockedPlayer.IsValid() && LockedPlayer->AdaptivePlayer.IsValid())
|
|
{
|
|
return LockedPlayer->AdaptivePlayer->GetMediaInfo(InInfoName);
|
|
}
|
|
return Electra::FVariantValue();
|
|
}
|
|
|
|
TSharedPtr<TMap<FString, TArray<TSharedPtr<Electra::IMediaStreamMetadata::IItem, ESPMode::ThreadSafe>>>, ESPMode::ThreadSafe> FElectraPlayer::GetMediaMetadata() const
|
|
{
|
|
return CurrentStreamMetadata;
|
|
}
|
|
|
|
|
|
TSharedPtr<Electra::FTrackMetadata, ESPMode::ThreadSafe> FElectraPlayer::GetTrackStreamMetadata(EPlayerTrackType TrackType, int32 TrackIndex) const
|
|
{
|
|
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> LockedPlayer = CurrentPlayer;
|
|
if (LockedPlayer.IsValid() && LockedPlayer->AdaptivePlayer.IsValid())
|
|
{
|
|
TArray<Electra::FTrackMetadata> TrackMetaData;
|
|
if (TrackType == EPlayerTrackType::Video)
|
|
{
|
|
LockedPlayer->AdaptivePlayer->GetTrackMetadata(TrackMetaData, Electra::EStreamType::Video);
|
|
}
|
|
else if (TrackType == EPlayerTrackType::Audio)
|
|
{
|
|
LockedPlayer->AdaptivePlayer->GetTrackMetadata(TrackMetaData, Electra::EStreamType::Audio);
|
|
}
|
|
else if (TrackType == EPlayerTrackType::Subtitle)
|
|
{
|
|
LockedPlayer->AdaptivePlayer->GetTrackMetadata(TrackMetaData, Electra::EStreamType::Subtitle);
|
|
}
|
|
if (TrackIndex >= 0 && TrackIndex < TrackMetaData.Num())
|
|
{
|
|
return MakeShared<Electra::FTrackMetadata, ESPMode::ThreadSafe>(TrackMetaData[TrackIndex]);
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
bool FElectraPlayer::GetAudioTrackFormat(int32 TrackIndex, int32 FormatIndex, FAudioTrackFormat& OutFormat) const
|
|
{
|
|
if (TrackIndex >= 0 && TrackIndex < NumTracksAudio && FormatIndex == 0)
|
|
{
|
|
TSharedPtr<Electra::FTrackMetadata, ESPMode::ThreadSafe> Meta = GetTrackStreamMetadata(EPlayerTrackType::Audio, TrackIndex);
|
|
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;
|
|
}
|
|
|
|
|
|
|
|
bool FElectraPlayer::GetVideoTrackFormat(int32 TrackIndex, int32 FormatIndex, FVideoTrackFormat& OutFormat) const
|
|
{
|
|
if (TrackIndex >= 0 && TrackIndex < NumTracksVideo && FormatIndex == 0)
|
|
{
|
|
TSharedPtr<Electra::FTrackMetadata, ESPMode::ThreadSafe> Meta = GetTrackStreamMetadata(EPlayerTrackType::Video, TrackIndex);
|
|
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;
|
|
}
|
|
|
|
|
|
int32 FElectraPlayer::GetNumVideoStreams(int32 TrackIndex) const
|
|
{
|
|
TSharedPtr<Electra::FTrackMetadata, ESPMode::ThreadSafe> Meta = GetTrackStreamMetadata(EPlayerTrackType::Video, TrackIndex);
|
|
return Meta.IsValid() ? Meta->StreamDetails.Num() : 0;
|
|
}
|
|
|
|
bool FElectraPlayer::GetVideoStreamFormat(FVideoStreamFormat& OutFormat, int32 InTrackIndex, int32 InStreamIndex) const
|
|
{
|
|
TSharedPtr<Electra::FTrackMetadata, ESPMode::ThreadSafe> Meta = GetTrackStreamMetadata(EPlayerTrackType::Video, InTrackIndex);
|
|
if (Meta.IsValid() && InStreamIndex < Meta->StreamDetails.Num())
|
|
{
|
|
const Electra::FStreamCodecInformation& ci = Meta->StreamDetails[InStreamIndex].CodecInformation;
|
|
OutFormat.Bitrate = Meta->StreamDetails[InStreamIndex].Bandwidth;
|
|
OutFormat.Resolution.X = ci.GetResolution().Width;
|
|
OutFormat.Resolution.Y = ci.GetResolution().Height;
|
|
OutFormat.FrameRate = (float)ci.GetFrameRate().GetAsDouble();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
bool FElectraPlayer::GetActiveVideoStreamFormat(FVideoStreamFormat& OutFormat) const
|
|
{
|
|
FScopeLock lock(&PlayerLock);
|
|
if (CurrentlyActiveVideoStreamFormat.IsSet())
|
|
{
|
|
OutFormat = CurrentlyActiveVideoStreamFormat.GetValue();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
int32 FElectraPlayer::GetNumTracks(EPlayerTrackType TrackType) const
|
|
{
|
|
if (TrackType == EPlayerTrackType::Audio)
|
|
{
|
|
return NumTracksAudio;
|
|
}
|
|
else if (TrackType == EPlayerTrackType::Video)
|
|
{
|
|
return NumTracksVideo;
|
|
}
|
|
else if (TrackType == EPlayerTrackType::Subtitle)
|
|
{
|
|
return NumTracksSubtitle;
|
|
}
|
|
// TODO: Implement missing track types.
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32 FElectraPlayer::GetNumTrackFormats(EPlayerTrackType TrackType, int32 TrackIndex) const
|
|
{
|
|
// Right now we only have a single format per track
|
|
if ((TrackType == EPlayerTrackType::Video && NumTracksVideo != 0) ||
|
|
(TrackType == EPlayerTrackType::Audio && NumTracksAudio != 0) ||
|
|
(TrackType == EPlayerTrackType::Subtitle && NumTracksSubtitle != 0))
|
|
{
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int32 FElectraPlayer::GetSelectedTrack(EPlayerTrackType TrackType) 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
|
|
{
|
|
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> LockedPlayer = CurrentPlayer;
|
|
if (LockedPlayer.IsValid() && LockedPlayer->AdaptivePlayer.IsValid())
|
|
{
|
|
if (LockedPlayer->AdaptivePlayer->IsTrackDeselected(InStreamType))
|
|
{
|
|
InOutSelectedIndex = -1;
|
|
InOutDirtyFlag = false;
|
|
}
|
|
else
|
|
{
|
|
Electra::FStreamSelectionAttributes Attributes;
|
|
LockedPlayer->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 (TrackType == EPlayerTrackType::Video)
|
|
{
|
|
return CheckAndReselectTrack(Electra::EStreamType::Video, bVideoTrackIndexDirty, SelectedVideoTrackIndex, NumTracksVideo);
|
|
}
|
|
else if (TrackType == EPlayerTrackType::Audio)
|
|
{
|
|
return CheckAndReselectTrack(Electra::EStreamType::Audio, bAudioTrackIndexDirty, SelectedAudioTrackIndex, NumTracksAudio);
|
|
}
|
|
else if (TrackType == EPlayerTrackType::Subtitle)
|
|
{
|
|
return CheckAndReselectTrack(Electra::EStreamType::Subtitle, bSubtitleTrackIndexDirty, SelectedSubtitleTrackIndex, NumTracksSubtitle);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
FText FElectraPlayer::GetTrackDisplayName(EPlayerTrackType TrackType, int32 TrackIndex) const
|
|
{
|
|
TSharedPtr<Electra::FTrackMetadata, ESPMode::ThreadSafe> Meta = GetTrackStreamMetadata(TrackType, TrackIndex);
|
|
if (Meta.IsValid())
|
|
{
|
|
if (TrackType == EPlayerTrackType::Video)
|
|
{
|
|
if (!Meta->Label.IsEmpty())
|
|
{
|
|
return FText::FromString(Meta->Label);
|
|
}
|
|
return FText::FromString(FString::Printf(TEXT("Video Track ID %s"), *Meta->ID));
|
|
}
|
|
else if (TrackType == EPlayerTrackType::Audio)
|
|
{
|
|
if (!Meta->Label.IsEmpty())
|
|
{
|
|
return FText::FromString(Meta->Label);
|
|
}
|
|
return FText::FromString(FString::Printf(TEXT("Audio Track ID %s"), *Meta->ID));
|
|
}
|
|
else if (TrackType == EPlayerTrackType::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(EPlayerTrackType TrackType, int32 TrackIndex) const
|
|
{
|
|
// Right now we only have a single format per track so we return format index 0 at all times.
|
|
return 0;
|
|
}
|
|
|
|
FString FElectraPlayer::GetTrackLanguage(EPlayerTrackType TrackType, int32 TrackIndex) const
|
|
{
|
|
TSharedPtr<Electra::FTrackMetadata, ESPMode::ThreadSafe> Meta = GetTrackStreamMetadata(TrackType, TrackIndex);
|
|
if (Meta.IsValid())
|
|
{
|
|
if (TrackType == EPlayerTrackType::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 TEXT("");
|
|
}
|
|
|
|
FString FElectraPlayer::GetTrackName(EPlayerTrackType TrackType, int32 TrackIndex) const
|
|
{
|
|
return TEXT("");
|
|
}
|
|
|
|
|
|
/**
|
|
* 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(EPlayerTrackType TrackType, int32 TrackIndex)
|
|
{
|
|
auto PerformSelection = [this, TrackType, TrackIndex](int32& OutSelectedTrackIndex, IElectraPlayerInterface::FStreamSelectionAttributes& OutSelectionAttributes) -> bool
|
|
{
|
|
Electra::EStreamType StreamType = TrackType == IElectraPlayerInterface::EPlayerTrackType::Video ? Electra::EStreamType::Video :
|
|
TrackType == IElectraPlayerInterface::EPlayerTrackType::Audio ? Electra::EStreamType::Audio :
|
|
TrackType == IElectraPlayerInterface::EPlayerTrackType::Subtitle ? Electra::EStreamType::Subtitle :
|
|
Electra::EStreamType::Unsupported;
|
|
// Select a track or deselect?
|
|
if (TrackIndex >= 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(TrackType, TrackIndex);
|
|
if (Meta.IsValid())
|
|
{
|
|
// Switch only when the track index has changed.
|
|
if (GetSelectedTrack(TrackType) != TrackIndex)
|
|
{
|
|
Electra::FStreamSelectionAttributes TrackAttributes;
|
|
TrackAttributes.OverrideIndex = TrackIndex;
|
|
|
|
OutSelectionAttributes.TrackIndexOverride = TrackIndex;
|
|
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> LockedPlayer = CurrentPlayer;
|
|
if (LockedPlayer.IsValid() && LockedPlayer->AdaptivePlayer.IsValid())
|
|
{
|
|
LockedPlayer->AdaptivePlayer->SelectTrackByAttributes(StreamType, TrackAttributes);
|
|
}
|
|
|
|
OutSelectedTrackIndex = TrackIndex;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Deselect track.
|
|
OutSelectionAttributes.TrackIndexOverride = -1;
|
|
OutSelectedTrackIndex = -1;
|
|
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> LockedPlayer = CurrentPlayer;
|
|
if (LockedPlayer.IsValid() && LockedPlayer->AdaptivePlayer.IsValid())
|
|
{
|
|
LockedPlayer->AdaptivePlayer->DeselectTrack(StreamType);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
if (TrackType == EPlayerTrackType::Video)
|
|
{
|
|
return PerformSelection(SelectedVideoTrackIndex, PlaystartOptions.InitialVideoTrackAttributes);
|
|
}
|
|
else if (TrackType == EPlayerTrackType::Audio)
|
|
{
|
|
return PerformSelection(SelectedAudioTrackIndex, PlaystartOptions.InitialAudioTrackAttributes);
|
|
}
|
|
else if (TrackType == EPlayerTrackType::Subtitle)
|
|
{
|
|
return PerformSelection(SelectedSubtitleTrackIndex, PlaystartOptions.InitialSubtitleTrackAttributes);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
void FElectraPlayer::OnMediaPlayerEventReceived(TSharedPtrTS<IAdaptiveStreamingPlayerAEMSEvent> InEvent, IAdaptiveStreamingPlayerAEMSReceiver::EDispatchMode InDispatchMode)
|
|
{
|
|
#if !UE_BUILD_SHIPPING
|
|
const TCHAR* const Origins[] = { TEXT("Playlist"), TEXT("Inband"), TEXT("TimedMetadata"), TEXT("???") };
|
|
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] %s event %s with \"%s\", \"%s\", \"%s\" PTS @ %.3f for %.3fs"), InstanceID,
|
|
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
|
|
|
|
TSharedPtr<IElectraPlayerAdapterDelegate, ESPMode::ThreadSafe> PinnedAdapterDelegate = AdapterDelegate.Pin();
|
|
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> LockedPlayer = CurrentPlayer;
|
|
if (PinnedAdapterDelegate.IsValid() && LockedPlayer.IsValid() && LockedPlayer->AdaptivePlayer.IsValid())
|
|
{
|
|
Electra::FTimeRange MediaTimeline;
|
|
LockedPlayer->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());
|
|
PinnedAdapterDelegate->PresentMetadataSample(Meta);
|
|
}
|
|
}
|
|
|
|
|
|
TSharedPtr<FElectraPlayer::FAnalyticsEvent> FElectraPlayer::CreateAnalyticsEvent(FString InEventName)
|
|
{
|
|
// 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();
|
|
FPlatformAtomics::InterlockedDecrement(&NumQueuedAnalyticEvents);
|
|
}
|
|
|
|
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();
|
|
TSharedPtr<IElectraPlayerAdapterDelegate, ESPMode::ThreadSafe> PinnedAdapterDelegate = AdapterDelegate.Pin();
|
|
if (PinnedAdapterDelegate.IsValid())
|
|
{
|
|
for(int32 nI=0, nIMax=UE_ARRAY_COUNT(AnalyticsCustomValues); nI<nIMax; ++nI)
|
|
{
|
|
FVariantValue Value = PinnedAdapterDelegate->QueryOptions(IElectraPlayerAdapterDelegate::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)
|
|
{
|
|
QueuedAnalyticEvents.Enqueue(InAnalyticEvent);
|
|
FPlatformAtomics::InterlockedIncrement(&NumQueuedAnalyticEvents);
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
/**
|
|
* State management information from media player...
|
|
*/
|
|
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:
|
|
{
|
|
bDiscardOutputUntilCleanStart = false;
|
|
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 | EPlayerStatus::Connecting;
|
|
|
|
PlayerState.State = EPlayerState::Preparing;
|
|
DeferredEvents.Enqueue(IElectraPlayerAdapterDelegate::EPlayerEvent::MediaConnecting);
|
|
|
|
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] Opening stream at \"%s\""), InstanceID, *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\""), InstanceID, *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()
|
|
{
|
|
PlayerState.Status = PlayerState.Status & ~EPlayerStatus::Connecting;
|
|
|
|
//PlayerState.Status = PlayerState.Status | EPlayerStatus::Buffering;
|
|
//DeferredEvents.Enqueue(EPlayerEvent::MediaBuffering);
|
|
|
|
// Player starts in paused mode. We need a SetRate() to start playback...
|
|
|
|
MediaStateOnPreparingFinished();
|
|
|
|
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] Received initial stream playlists"), InstanceID);
|
|
|
|
Electra::FTimeRange MediaTimeline;
|
|
Electra::FTimeValue MediaDuration;
|
|
CurrentPlayer->AdaptivePlayer->GetTimelineRange(MediaTimeline);
|
|
MediaDuration = CurrentPlayer->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;
|
|
CurrentPlayer->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"), InstanceID,
|
|
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;
|
|
CurrentPlayer->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"), InstanceID,
|
|
AudioStreamMetaData[0].StreamDetails[i].Bandwidth);
|
|
}
|
|
}
|
|
SelectedAudioTrackIndex = NumTracksAudio ? 0 : -1;
|
|
|
|
TArray<Electra::FTrackMetadata> SubtitleStreamMetaData;
|
|
CurrentPlayer->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.TrackIndexOverride;
|
|
CurrentPlayer->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.TrackIndexOverride;
|
|
CurrentPlayer->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.TrackIndexOverride;
|
|
CurrentPlayer->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()
|
|
{
|
|
TArray<Electra::FTrackMetadata> VideoStreamMetaData;
|
|
CurrentPlayer->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"), InstanceID,
|
|
VideoStreamMetaData[0].StreamDetails[i].CodecInformation.GetResolution().Width,
|
|
VideoStreamMetaData[0].StreamDetails[i].CodecInformation.GetResolution().Height,
|
|
VideoStreamMetaData[0].StreamDetails[i].Bandwidth);
|
|
}
|
|
}
|
|
|
|
TArray<Electra::FTrackMetadata> AudioStreamMetaData;
|
|
CurrentPlayer->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"), InstanceID,
|
|
AudioStreamMetaData[0].StreamDetails[i].Bandwidth);
|
|
}
|
|
}
|
|
|
|
TArray<Electra::FTrackMetadata> SubtitleStreamMetaData;
|
|
CurrentPlayer->AdaptivePlayer->GetTrackMetadata(SubtitleStreamMetaData, Electra::EStreamType::Subtitle);
|
|
NumTracksSubtitle = SubtitleStreamMetaData.Num();
|
|
|
|
bVideoTrackIndexDirty = true;
|
|
bAudioTrackIndexDirty = true;
|
|
bSubtitleTrackIndexDirty = true;
|
|
|
|
DeferredEvents.Enqueue(IElectraPlayerAdapterDelegate::EPlayerEvent::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"), InstanceID);
|
|
FScopeLock Lock(&StatisticsLock);
|
|
Statistics.AddMessageToHistory(TEXT("Obtained license key"));
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] License key error \"%s\""), InstanceID, *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)
|
|
{
|
|
DeferredEvents.Enqueue(IElectraPlayerAdapterDelegate::EPlayerEvent::Internal_VideoSamplesAvailable);
|
|
}
|
|
else if (DataAvailability.Availability == Electra::Metrics::FDataAvailabilityChange::EAvailability::DataNotAvailable)
|
|
{
|
|
DeferredEvents.Enqueue(IElectraPlayerAdapterDelegate::EPlayerEvent::Internal_VideoSamplesUnavailable);
|
|
}
|
|
}
|
|
else if (DataAvailability.StreamType == Electra::EStreamType::Audio)
|
|
{
|
|
if (DataAvailability.Availability == Electra::Metrics::FDataAvailabilityChange::EAvailability::DataAvailable)
|
|
{
|
|
DeferredEvents.Enqueue(IElectraPlayerAdapterDelegate::EPlayerEvent::Internal_AudioSamplesAvailable);
|
|
}
|
|
else if (DataAvailability.Availability == Electra::Metrics::FDataAvailabilityChange::EAvailability::DataNotAvailable)
|
|
{
|
|
DeferredEvents.Enqueue(IElectraPlayerAdapterDelegate::EPlayerEvent::Internal_AudioSamplesUnavailable);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FElectraPlayer::HandlePlayerEventBufferingStart(Electra::Metrics::EBufferingReason BufferingReason)
|
|
{
|
|
PlayerState.Status = PlayerState.Status | EPlayerStatus::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;
|
|
DeferredEvents.Enqueue(IElectraPlayerAdapterDelegate::EPlayerEvent::TracksChanged);
|
|
}
|
|
|
|
DeferredEvents.Enqueue(IElectraPlayerAdapterDelegate::EPlayerEvent::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"), InstanceID, *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 & ~EPlayerStatus::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"), InstanceID, Electra::Metrics::GetBufferingReasonString(BufferingReason), BufferingDuration);
|
|
Statistics.AddMessageToHistory(TEXT("Buffering ended"));
|
|
// Should we set the state (back?) to something or wait for the following play/pause event to set a new one?
|
|
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"), InstanceID, 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\""), InstanceID, 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"), InstanceID, 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)
|
|
{
|
|
// 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)"), InstanceID, 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)"), InstanceID, 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;
|
|
CurrentPlayer->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)"), InstanceID, 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)"), InstanceID, 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(&PlayerLock);
|
|
CurrentlyActiveVideoStreamFormat = fmt;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FElectraPlayer::HandlePlayerEventPrerollStart()
|
|
{
|
|
bDiscardOutputUntilCleanStart = false;
|
|
// Update statistics
|
|
FScopeLock Lock(&StatisticsLock);
|
|
Statistics.TimeAtPrerollBegin = FPlatformTime::Seconds();
|
|
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] Player starts prerolling to warm decoders and renderers"), InstanceID);
|
|
// Statistics.LastState = "Ready";
|
|
// FString CurrentState; // "Empty", "Opening", "Preparing", "Buffering", "Idle", "Ready", "Playing", "Paused", "Seeking", "Rebuffering", "Ended", "Closed"
|
|
|
|
// 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 & ~EPlayerStatus::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"), InstanceID);
|
|
Statistics.LastState = "Ready";
|
|
|
|
DeferredEvents.Enqueue(IElectraPlayerAdapterDelegate::EPlayerEvent::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 & ~EPlayerStatus::Buffering;
|
|
MediaStateOnPlay();
|
|
|
|
// Update statistics
|
|
FScopeLock Lock(&StatisticsLock);
|
|
double PlayPos = CurrentPlayer->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"), InstanceID, 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();
|
|
FScopeLock Lock(&StatisticsLock);
|
|
Statistics.LastState = "Paused";
|
|
double PlayPos = CurrentPlayer->AdaptivePlayer->GetPlayPosition().GetAsSeconds();
|
|
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] Playback paused at play position %.3f"), InstanceID, 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();
|
|
FScopeLock Lock(&StatisticsLock);
|
|
Statistics.LastState = "Playing";
|
|
double PlayPos = CurrentPlayer->AdaptivePlayer->GetPlayPosition().GetAsSeconds();
|
|
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] Playback resumed at play position %.3f"), InstanceID, 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();
|
|
double PlayPos = CurrentPlayer->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"), InstanceID, 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;
|
|
CurrentPlayer->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"), InstanceID, FromTime.GetAsSeconds(), ToNewTime.GetAsSeconds());
|
|
}
|
|
else if (TimejumpReason == Electra::Metrics::ETimeJumpReason::Looping)
|
|
{
|
|
++Statistics.NumTimesLooped;
|
|
Electra::IAdaptiveStreamingPlayer::FLoopState loopState;
|
|
CurrentPlayer->AdaptivePlayer->GetLoopState(loopState);
|
|
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] Looping (%d) from %.3f to %.3f"), InstanceID, 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();
|
|
double PlayPos = CurrentPlayer->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"), InstanceID, 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"), InstanceID);
|
|
bDiscardOutputUntilCleanStart = false;
|
|
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> NewMeta = InMetadata->GetMediaStreamMetadata();
|
|
CurrentStreamMetadata = MoveTemp(NewMeta);
|
|
DeferredEvents.Enqueue(IElectraPlayerAdapterDelegate::EPlayerEvent::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\""), InstanceID, *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"), InstanceID, *m);
|
|
FScopeLock Lock(&StatisticsLock);
|
|
Statistics.AddMessageToHistory(m);
|
|
break;
|
|
}
|
|
case Electra::IInfoLog::ELevel::Warning:
|
|
{
|
|
UE_LOG(LogElectraPlayer, Warning, TEXT("[%u] %s"), InstanceID, *m);
|
|
FScopeLock Lock(&StatisticsLock);
|
|
Statistics.AddMessageToHistory(m);
|
|
break;
|
|
}
|
|
case Electra::IInfoLog::ELevel::Info:
|
|
{
|
|
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] %s"), InstanceID, *m);
|
|
break;
|
|
}
|
|
case Electra::IInfoLog::ELevel::Verbose:
|
|
{
|
|
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] %s"), InstanceID, *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> LockedPlayer = CurrentPlayer;
|
|
if (!LockedPlayer.IsValid() || !LockedPlayer->AdaptivePlayer.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
double PlayPos = LockedPlayer->AdaptivePlayer->GetPlayPosition().GetAsSeconds();
|
|
Electra::FTimeRange MediaTimeline;
|
|
Electra::FTimeValue MediaDuration;
|
|
LockedPlayer->AdaptivePlayer->GetTimelineRange(MediaTimeline);
|
|
MediaDuration = LockedPlayer->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);
|
|
}
|
|
|
|
TSharedPtr<IElectraPlayerAdapterDelegate, ESPMode::ThreadSafe> PinnedAdapterDelegate = AdapterDelegate.Pin();
|
|
if (PinnedAdapterDelegate.IsValid())
|
|
{
|
|
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"
|
|
),
|
|
InstanceID,
|
|
*FString::Printf(TEXT("%s"), *AnalyticsOSVersion),
|
|
*PinnedAdapterDelegate->GetVideoAdapterName().TrimStartAndEnd(),
|
|
*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"), InstanceID);
|
|
// Try to fill in some of the blanks.
|
|
UpdatePlayEndStatistics();
|
|
}
|
|
|
|
UE_LOG(LogElectraPlayer, Verbose, TEXT("[%u] Submitting analytics"), InstanceID);
|
|
|
|
|
|
// 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));
|
|
FPlatformAtomics::InterlockedDecrement(&NumQueuedAnalyticEvents);
|
|
}
|
|
}
|
|
|
|
|
|
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)
|
|
{
|
|
/*
|
|
if (PlayerGuid != InPlayerGuid)
|
|
{
|
|
return;
|
|
}
|
|
*/
|
|
}
|
|
|
|
void FElectraPlayer::MediaStateOnPreparingFinished()
|
|
{
|
|
if (!ensure(PlayerState.State == EPlayerState::Preparing))
|
|
{
|
|
return;
|
|
}
|
|
|
|
CSV_EVENT(ElectraPlayer, TEXT("MediaStateOnPreparingFinished"));
|
|
|
|
PlayerState.State = EPlayerState::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.
|
|
DeferredEvents.Enqueue(IElectraPlayerAdapterDelegate::EPlayerEvent::MediaOpened);
|
|
}
|
|
|
|
bool FElectraPlayer::MediaStateOnPlay()
|
|
{
|
|
if (PlayerState.State != EPlayerState::Stopped && PlayerState.State != EPlayerState::Paused)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
CSV_EVENT(ElectraPlayer, TEXT("MediaStateOnPlay"));
|
|
|
|
double CurrentRate = 1.0;
|
|
TSharedPtr<FInternalPlayerImpl, ESPMode::ThreadSafe> LockedPlayer = CurrentPlayer;
|
|
if (LockedPlayer.IsValid() && LockedPlayer->AdaptivePlayer.IsValid())
|
|
{
|
|
CurrentRate = LockedPlayer->AdaptivePlayer->GetPlayRate();
|
|
}
|
|
|
|
PlayerState.State = EPlayerState::Playing;
|
|
PlayerState.SetPlayRateFromPlayer(CurrentRate);
|
|
|
|
DeferredEvents.Enqueue(IElectraPlayerAdapterDelegate::EPlayerEvent::PlaybackResumed);
|
|
return true;
|
|
}
|
|
|
|
bool FElectraPlayer::MediaStateOnPause()
|
|
{
|
|
if (PlayerState.State != EPlayerState::Playing)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
CSV_EVENT(ElectraPlayer, TEXT("MediaStateOnPause"));
|
|
|
|
PlayerState.State = EPlayerState::Paused;
|
|
PlayerState.SetPlayRateFromPlayer(0.0f);
|
|
|
|
DeferredEvents.Enqueue(IElectraPlayerAdapterDelegate::EPlayerEvent::PlaybackSuspended);
|
|
return true;
|
|
}
|
|
|
|
void FElectraPlayer::MediaStateOnEndReached()
|
|
{
|
|
CSV_EVENT(ElectraPlayer, TEXT("MediaStateOnEndReached"));
|
|
|
|
switch (PlayerState.State)
|
|
{
|
|
case EPlayerState::Preparing:
|
|
case EPlayerState::Playing:
|
|
case EPlayerState::Paused:
|
|
case EPlayerState::Stopped:
|
|
DeferredEvents.Enqueue(IElectraPlayerAdapterDelegate::EPlayerEvent::PlaybackEndReached);
|
|
break;
|
|
default:
|
|
// NOP
|
|
break;
|
|
}
|
|
PlayerState.State = EPlayerState::Stopped;
|
|
}
|
|
|
|
void FElectraPlayer::MediaStateOnSeekFinished()
|
|
{
|
|
CSV_EVENT(ElectraPlayer, TEXT("MediaStateOnSeekFinished"));
|
|
DeferredEvents.Enqueue(IElectraPlayerAdapterDelegate::EPlayerEvent::SeekCompleted);
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------------------------
|
|
|
|
FElectraPlayer::FAdaptiveStreamingPlayerResourceProvider::FAdaptiveStreamingPlayerResourceProvider(const TWeakPtr<IElectraPlayerAdapterDelegate, ESPMode::ThreadSafe>& InAdapterDelegate)
|
|
: AdapterDelegate(InAdapterDelegate)
|
|
{
|
|
}
|
|
|
|
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 ||
|
|
InOutRequest->GetResourceType() == Electra::IAdaptiveStreamingPlayerResourceRequest::EPlaybackResourceType::BinaryData);
|
|
if (InOutRequest->GetResourceType() == Electra::IAdaptiveStreamingPlayerResourceRequest::EPlaybackResourceType::Playlist)
|
|
{
|
|
TSharedPtr<IElectraPlayerAdapterDelegate, ESPMode::ThreadSafe> PinnedAdapterDelegate = AdapterDelegate.Pin();
|
|
if (PinnedAdapterDelegate.IsValid())
|
|
{
|
|
FVariantValue Value = PinnedAdapterDelegate->QueryOptions(IElectraPlayerAdapterDelegate::EOptionType::PlayListData, FVariantValue(InOutRequest->GetResourceURL()));
|
|
if (Value.IsValid())
|
|
{
|
|
FString PlaylistData = Value.GetFString();
|
|
// There needs to be a non-empty return that is also _not_ the default value we have provided!
|
|
// The latter being a quirk for a FN specific GetMediaOption() that takes the _default-value_ as the URL to look up the
|
|
// playlist contents for. When we are not in FN and have the standard engine version only we get the URL we pass in back
|
|
// since that's the default value when the key to look up has not been found.
|
|
if (!PlaylistData.IsEmpty() && PlaylistData != InOutRequest->GetResourceURL())
|
|
{
|
|
/*
|
|
UE_LOG(LogElectraPlayer, Log, TEXT("[%u] IMediaPlayer::Open: Source provided playlist data for '%s'"), InstanceID, *SanitizeMessage(InOutRequest->GetResourceURL()));
|
|
*/
|
|
|
|
// FString is Unicode but the HTTP response for a playlist is expected to be a UTF-8 string.
|
|
// Create a plain array from this.
|
|
TSharedPtr<TArray<uint8>, ESPMode::ThreadSafe> ResponseDataPtr = MakeShared<TArray<uint8>, ESPMode::ThreadSafe>((const uint8*)TCHAR_TO_UTF8(*PlaylistData), PlaylistData.Len());
|
|
// And put it into the request
|
|
InOutRequest->SetPlaybackData(ResponseDataPtr, 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (InOutRequest->GetResourceType() == Electra::IAdaptiveStreamingPlayerResourceRequest::EPlaybackResourceType::LicenseKey)
|
|
{
|
|
TSharedPtr<IElectraPlayerAdapterDelegate, ESPMode::ThreadSafe> PinnedAdapterDelegate = AdapterDelegate.Pin();
|
|
if (PinnedAdapterDelegate.IsValid())
|
|
{
|
|
FVariantValue Value = PinnedAdapterDelegate->QueryOptions(IElectraPlayerAdapterDelegate::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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (InOutRequest->GetResourceType() == Electra::IAdaptiveStreamingPlayerResourceRequest::EPlaybackResourceType::BinaryData)
|
|
{
|
|
TSharedPtr<IElectraPlayerExternalDataReader, ESPMode::ThreadSafe> dr = ExternalDataReader.Pin();
|
|
if (dr.IsValid())
|
|
{
|
|
Electra::IAdaptiveStreamingPlayerResourceRequest::FBinaryDataParams src = InOutRequest->GetBinaryDataParams();
|
|
IElectraPlayerExternalDataReader::FReadParam rp;
|
|
rp.URL = InOutRequest->GetResourceURL();
|
|
rp.AbsoluteFileOffset = src.AbsoluteFileOffset;
|
|
rp.NumBytesToRead = src.NumBytesToRead;
|
|
// Retain the request so it does not go out of scope.
|
|
rp.Custom = new TSharedPtr<Electra::IAdaptiveStreamingPlayerResourceRequest, ESPMode::ThreadSafe>(InOutRequest);
|
|
dr->ReadDataFromFile(rp, ExternalDataCompletedDelegate);
|
|
return;
|
|
}
|
|
}
|
|
InOutRequest->SignalDataReady();
|
|
}
|
|
}
|
|
|
|
void FElectraPlayer::FAdaptiveStreamingPlayerResourceProvider::ClearPendingRequests()
|
|
{
|
|
PendingStaticResourceRequests.Empty();
|
|
}
|
|
|
|
void FElectraPlayer::FAdaptiveStreamingPlayerResourceProvider::SetExternalDataReader(TWeakPtr<IElectraPlayerExternalDataReader, ESPMode::ThreadSafe> InExternalDataReader)
|
|
{
|
|
ExternalDataReader = MoveTemp(InExternalDataReader);
|
|
ExternalDataCompletedDelegate.BindStatic(&FElectraPlayer::FAdaptiveStreamingPlayerResourceProvider::OnExternalDataReadCompleted);
|
|
}
|
|
|
|
void FElectraPlayer::FAdaptiveStreamingPlayerResourceProvider::OnExternalDataReadCompleted(IElectraPlayerExternalDataReader::FResponseDataPtr InResponseData, int64 InTotalFileSize, const IElectraPlayerExternalDataReader::FReadParam& InFromRequestParams)
|
|
{
|
|
TSharedPtr<Electra::IAdaptiveStreamingPlayerResourceRequest, ESPMode::ThreadSafe>* rreq = reinterpret_cast<TSharedPtr<Electra::IAdaptiveStreamingPlayerResourceRequest, ESPMode::ThreadSafe>*>(InFromRequestParams.Custom);
|
|
TSharedPtr<Electra::IAdaptiveStreamingPlayerResourceRequest, ESPMode::ThreadSafe> Req(*rreq);
|
|
delete rreq;
|
|
if (Req.IsValid())
|
|
{
|
|
Req->SetPlaybackData(InResponseData, InTotalFileSize);
|
|
Req->SignalDataReady();
|
|
}
|
|
}
|
|
|
|
FElectraPlayer::FAdaptiveStreamingPlayerResourceProvider::~FAdaptiveStreamingPlayerResourceProvider()
|
|
{
|
|
ExternalDataCompletedDelegate.Unbind();
|
|
}
|
|
|
|
|
|
// ------------------------------------------------------------------------------------------------------------------
|
|
|
|
IElectraPlayerInterface* FElectraPlayerRuntimeFactory::CreatePlayer(const TSharedPtr<IElectraPlayerAdapterDelegate, ESPMode::ThreadSafe>& AdapterDelegate,
|
|
FElectraPlayerSendAnalyticMetricsDelegate& InSendAnalyticMetricsDelegate,
|
|
FElectraPlayerSendAnalyticMetricsPerMinuteDelegate& InSendAnalyticMetricsPerMinuteDelegate,
|
|
FElectraPlayerReportVideoStreamingErrorDelegate& InReportVideoStreamingErrorDelegate,
|
|
FElectraPlayerReportSubtitlesMetricsDelegate& InReportSubtitlesFileMetricsDelegate)
|
|
{
|
|
return new FElectraPlayer(AdapterDelegate, InSendAnalyticMetricsDelegate, InSendAnalyticMetricsPerMinuteDelegate, InReportVideoStreamingErrorDelegate, InReportSubtitlesFileMetricsDelegate);
|
|
}
|