// 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 GetTrackBaseTime() const override { return TrackBaseTime; } void SetTime(FDecoderTimeStamp& InTime) override { PresentationTime = InTime; } TArray Data; FDecoderTimeStamp PresentationTime; FTimespan Duration; EOrigin Origin; EDispatchedMode DispatchedMode; FString SchemeIdUri; FString Value; FString ID; TOptional TrackBaseTime; }; //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- /** * Construction of new player */ FElectraPlayer::FElectraPlayer(const TSharedPtr& 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(); 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(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 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(); // 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(SharedThis(this)); NewPlayer->RendererAudio = MakeShared(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(); 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(); 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(); 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 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&& Player, uint32 PlayerID, TSharedPtr AsyncResourceReleaseNotification) { TSharedPtr bClosedSig = MakeShared(false); TFunction 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 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& OutRanges, const TArray& InRanges) -> void { for(int32 i=0; i(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 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 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 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, ESPMode::ThreadSafe> BlobData = MakeShared, ESPMode::ThreadSafe>(); TSharedPtrTS ResponseBuffer = PendingBlobRequest->Request->GetResponseBuffer(); if (ResponseBuffer.IsValid()) { BlobData->Append((const uint8*)ResponseBuffer->GetLinearReadData(), ResponseBuffer->Num()); } TSharedPtr 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 Player = CurrentPlayer; if (Player.IsValid()) { if (PlayerState.State != EPlayerState::Closed) { if (DecoderOutput.IsValid()) { if (!bDoNotRender) { PresentVideoFrame(DecoderOutput); } } } } } void FElectraPlayer::OnVideoFlush() { TSharedPtr Player = CurrentPlayer; if (Player.IsValid()) { TSharedPtr 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 Player = CurrentPlayer; if (Player.IsValid()) { if (PlayerState.State != EPlayerState::Closed) { PresentAudioFrame(DecoderOutput); } } } void FElectraPlayer::OnAudioFlush() { TSharedPtr Player = CurrentPlayer; if (Player.IsValid()) { TSharedPtr PinnedAdapterDelegate = AdapterDelegate.Pin(); if (PinnedAdapterDelegate.IsValid()) { PinnedAdapterDelegate->OnAudioFlush(); } } } void FElectraPlayer::OnSubtitleDecoded(ISubtitleDecoderOutputPtr DecoderOutput) { TSharedPtr Player = CurrentPlayer; if (Player.IsValid()) { if (PlayerState.State != EPlayerState::Closed) { PresentSubtitle(DecoderOutput); } } } void FElectraPlayer::OnSubtitleFlush() { TSharedPtr Player = CurrentPlayer; if (Player.IsValid()) { TSharedPtr 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 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 PinnedAdapterDelegate = AdapterDelegate.Pin(); if (PinnedAdapterDelegate.IsValid()) { PinnedAdapterDelegate->PresentAudioFrame(DecoderOutput); } return true; } bool FElectraPlayer::PresentSubtitle(const ISubtitleDecoderOutputPtr& DecoderOutput) { TSharedPtr 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 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 PinnedAdapterDelegate = AdapterDelegate.Pin(); if (PinnedAdapterDelegate.IsValid()) { if (!bDiscardOutputUntilCleanStart) { return PinnedAdapterDelegate->CanReceiveVideoSamples(NumFrames); } } return false; } bool FElectraPlayer::CanPresentAudioFrames(uint64 NumFrames) { TSharedPtr 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 FElectraPlayer::GetSupportedRates(EPlayRateType InPlayRateType) const { TRangeSet Res; TSharedPtr LockedPlayer = CurrentPlayer; if (LockedPlayer.IsValid() && LockedPlayer->AdaptivePlayer.IsValid()) { TArray> SupportedRanges; LockedPlayer->AdaptivePlayer->GetSupportedRates(InPlayRateType == IElectraPlayerInterface::EPlayRateType::Unthinned ? IAdaptiveStreamingPlayer::EPlaybackRateType::Unthinned : IAdaptiveStreamingPlayer::EPlaybackRateType::Thinned).GetRanges(SupportedRanges); for(auto &Rate : SupportedRanges) { TRange r; if (Rate.HasLowerBound()) { r.SetLowerBound(TRange::BoundsType::Inclusive((float) Rate.GetLowerBoundValue())); } if (Rate.HasUpperBound()) { r.SetUpperBound(TRange::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 PinnedAdapterDelegate = AdapterDelegate.Pin(); if (PinnedAdapterDelegate.IsValid()) { TSharedPtr, ESPMode::ThreadSafe> SeekablePositions = MakeShared, 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 LockedPlayer = CurrentPlayer; if (LockedPlayer.IsValid() && LockedPlayer->AdaptivePlayer.IsValid()) { LockedPlayer->AdaptivePlayer->EnableFrameAccurateSeeking(bFrameAccurateSeeking.GetValue()); } } void FElectraPlayer::SetPlaybackRange(const FPlaybackRange& InPlaybackRange) { CurrentPlaybackRange = InPlaybackRange; TSharedPtr 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 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 FElectraPlayer::GetPlaybackRange(ETimeRangeType InRangeToGet) const { TRange Range(FTimespan(0), FTimespan(0)); TSharedPtr 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 LockedPlayer = CurrentPlayer; if (LockedPlayer.IsValid() && LockedPlayer->AdaptivePlayer.IsValid()) { return LockedPlayer->AdaptivePlayer->GetMediaInfo(InInfoName); } return Electra::FVariantValue(); } TSharedPtr>>, ESPMode::ThreadSafe> FElectraPlayer::GetMediaMetadata() const { return CurrentStreamMetadata; } TSharedPtr FElectraPlayer::GetTrackStreamMetadata(EPlayerTrackType TrackType, int32 TrackIndex) const { TSharedPtr LockedPlayer = CurrentPlayer; if (LockedPlayer.IsValid() && LockedPlayer->AdaptivePlayer.IsValid()) { TArray 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(TrackMetaData[TrackIndex]); } } return nullptr; } bool FElectraPlayer::GetAudioTrackFormat(int32 TrackIndex, int32 FormatIndex, FAudioTrackFormat& OutFormat) const { if (TrackIndex >= 0 && TrackIndex < NumTracksAudio && FormatIndex == 0) { TSharedPtr 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 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{ OutFormat.FrameRate }; OutFormat.TypeName = ci.GetHumanReadableCodecName(); return true; } } return false; } int32 FElectraPlayer::GetNumVideoStreams(int32 TrackIndex) const { TSharedPtr Meta = GetTrackStreamMetadata(EPlayerTrackType::Video, TrackIndex); return Meta.IsValid() ? Meta->StreamDetails.Num() : 0; } bool FElectraPlayer::GetVideoStreamFormat(FVideoStreamFormat& OutFormat, int32 InTrackIndex, int32 InStreamIndex) const { TSharedPtr 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 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 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 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 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 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 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 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 PinnedAdapterDelegate = AdapterDelegate.Pin(); TSharedPtr 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 Meta = MakeShared(); 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::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 Ev = MakeShared(); Ev->EventName = MoveTemp(InEventName); AddCommonAnalyticsAttributes(Ev->ParamArray); return Ev; } void FElectraPlayer::AddCommonAnalyticsAttributes(TArray& InOutParamArray) { InOutParamArray.Add(FAnalyticsEventAttribute(TEXT("SessionId"), AnalyticsInstanceGuid)); InOutParamArray.Add(FAnalyticsEventAttribute(TEXT("EventNum"), AnalyticsInstanceEventCount)); InOutParamArray.Add(FAnalyticsEventAttribute(TEXT("Utc"), static_cast(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 PinnedAdapterDelegate = AdapterDelegate.Pin(); if (PinnedAdapterDelegate.IsValid()) { for(int32 nI=0, nIMax=UE_ARRAY_COUNT(AnalyticsCustomValues); nIQueryOptions(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 InAnalyticEvent) { QueuedAnalyticEvents.Enqueue(InAnalyticEvent); FPlatformAtomics::InterlockedIncrement(&NumQueuedAnalyticEvents); } //----------------------------------------------------------------------------- /** * State management information from media player... */ void FElectraPlayer::HandleDeferredPlayerEvents() { TSharedPtrTS Event; while(DeferredPlayerEvents.Dequeue(Event)) { switch(Event->Type) { case FPlayerMetricEventBase::EType::OpenSource: { FPlayerMetricEvent_OpenSource* Ev = static_cast(Event.Get()); HandlePlayerEventOpenSource(Ev->URL); break; } case FPlayerMetricEventBase::EType::ReceivedMainPlaylist: { FPlayerMetricEvent_ReceivedMainPlaylist* Ev = static_cast(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(Event.Get()); HandlePlayerEventPlaylistDownload(Ev->PlaylistDownloadStats); break; } case FPlayerMetricEventBase::EType::CleanStart: { bDiscardOutputUntilCleanStart = false; break; } case FPlayerMetricEventBase::EType::BufferingStart: { FPlayerMetricEvent_BufferingStart* Ev = static_cast(Event.Get()); HandlePlayerEventBufferingStart(Ev->BufferingReason); break; } case FPlayerMetricEventBase::EType::BufferingEnd: { FPlayerMetricEvent_BufferingEnd* Ev = static_cast(Event.Get()); HandlePlayerEventBufferingEnd(Ev->BufferingReason); break; } case FPlayerMetricEventBase::EType::Bandwidth: { FPlayerMetricEvent_Bandwidth* Ev = static_cast(Event.Get()); HandlePlayerEventBandwidth(Ev->EffectiveBps, Ev->ThroughputBps, Ev->LatencyInSeconds); break; } case FPlayerMetricEventBase::EType::BufferUtilization: { FPlayerMetricEvent_BufferUtilization* Ev = static_cast(Event.Get()); HandlePlayerEventBufferUtilization(Ev->BufferStats); break; } case FPlayerMetricEventBase::EType::SegmentDownload: { FPlayerMetricEvent_SegmentDownload* Ev = static_cast(Event.Get()); HandlePlayerEventSegmentDownload(Ev->SegmentDownloadStats); break; } case FPlayerMetricEventBase::EType::LicenseKey: { FPlayerMetricEvent_LicenseKey* Ev = static_cast(Event.Get()); HandlePlayerEventLicenseKey(Ev->LicenseKeyStats); break; } case FPlayerMetricEventBase::EType::DataAvailabilityChange: { FPlayerMetricEvent_DataAvailabilityChange* Ev = static_cast(Event.Get()); HandlePlayerEventDataAvailabilityChange(Ev->DataAvailability); break; } case FPlayerMetricEventBase::EType::VideoQualityChange: { FPlayerMetricEvent_VideoQualityChange* Ev = static_cast(Event.Get()); HandlePlayerEventVideoQualityChange(Ev->NewBitrate, Ev->PreviousBitrate, Ev->bIsDrasticDownswitch); break; } case FPlayerMetricEventBase::EType::AudioQualityChange: { FPlayerMetricEvent_AudioQualityChange* Ev = static_cast(Event.Get()); HandlePlayerEventAudioQualityChange(Ev->NewBitrate, Ev->PreviousBitrate, Ev->bIsDrasticDownswitch); break; } case FPlayerMetricEventBase::EType::CodecFormatChange: { FPlayerMetricEvent_CodecFormatChange* Ev = static_cast(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(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(Event.Get()); HandlePlayerMediaMetadataChanged(Ev->NewMetadata); break; } case FPlayerMetricEventBase::EType::Error: { FPlayerMetricEvent_Error* Ev = static_cast(Event.Get()); HandlePlayerEventError(Ev->ErrorReason); break; } case FPlayerMetricEventBase::EType::LogMessage: { FPlayerMetricEvent_LogMessage* Ev = static_cast(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 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 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 VideoStreamMetaData; CurrentPlayer->AdaptivePlayer->GetTrackMetadata(VideoStreamMetaData, Electra::EStreamType::Video); NumTracksVideo = VideoStreamMetaData.Num(); if (NumTracksVideo) { for(int32 i=0; i AudioStreamMetaData; CurrentPlayer->AdaptivePlayer->GetTrackMetadata(AudioStreamMetaData, Electra::EStreamType::Audio); NumTracksAudio = AudioStreamMetaData.Num(); if (NumTracksAudio) { for(int32 i=0; i 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 AnalyticEvent = CreateAnalyticsEvent(kEventNameElectraPlaylistLoaded); EnqueueAnalyticsEvent(AnalyticEvent); } // Trigger preloading unless forbidden. if (PlaystartOptions.bDoNotPreload == false) { TriggerFirstSeekIfNecessary(); } } void FElectraPlayer::HandlePlayerEventTracksChanged() { TArray VideoStreamMetaData; CurrentPlayer->AdaptivePlayer->GetTrackMetadata(VideoStreamMetaData, Electra::EStreamType::Video); NumTracksVideo = VideoStreamMetaData.Num(); if (NumTracksVideo) { for(int32 i=0; i AudioStreamMetaData; CurrentPlayer->AdaptivePlayer->GetTrackMetadata(AudioStreamMetaData, Electra::EStreamType::Audio); NumTracksAudio = AudioStreamMetaData.Num(); if (NumTracksAudio) { for(int32 i=0; i 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 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 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 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 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 VideoStreamMetaData; CurrentPlayer->AdaptivePlayer->GetTrackMetadata(VideoStreamMetaData, Electra::EStreamType::Video); if (VideoStreamMetaData.Num()) { for(int32 i=0; i 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 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 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 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 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 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 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 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 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 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& InMetadata) { if (InMetadata.IsValid()) { TSharedPtr>>, 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("
")); } // Enqueue an "Error" event. static const FString kEventNameElectraError(TEXT("Electra.Error")); if (Electra::IsAnalyticsEventEnabled(kEventNameElectraError)) { TSharedPtr 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 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 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& 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 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("
")); } 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& pair : Statistics.VideoSegmentBitratesStreamed) { SegsPerStream += FString::Printf(TEXT("%d:%u;"), pair.Key, pair.Value); } ParamArray.Add(FAnalyticsEventAttribute(TEXT("VideoSegmentFetchStats"), *SegsPerStream)); SegsPerStream.Empty(); for(const TPair& 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& AnalyticsProvider) { SendPendingAnalyticMetrics(AnalyticsProvider); TSharedPtr Player = CurrentPlayer; if (Player.Get() && Player->AdaptivePlayer->IsPlaying()) { TArray 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& AnalyticsProvider) { FScopeLock Lock(&StatisticsLock); TSharedPtr 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 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& InAdapterDelegate) : AdapterDelegate(InAdapterDelegate) { } void FElectraPlayer::FAdaptiveStreamingPlayerResourceProvider::ProvideStaticPlaybackDataForURL(TSharedPtr InOutRequest) { check(InOutRequest.IsValid()); PendingStaticResourceRequests.Enqueue(InOutRequest); } void FElectraPlayer::FAdaptiveStreamingPlayerResourceProvider::ProcessPendingStaticResourceRequests() { TSharedPtr 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 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, ESPMode::ThreadSafe> ResponseDataPtr = MakeShared, 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 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 BinKey; BinKey.AddUninitialized(LicenseKeyData.Len()); BinKey.SetNum(HexToBytes(LicenseKeyData, BinKey.GetData())); TSharedPtr, ESPMode::ThreadSafe> ResponseDataPtr = MakeShared, ESPMode::ThreadSafe>(BinKey); InOutRequest->SetPlaybackData(ResponseDataPtr, 0); } } } } else if (InOutRequest->GetResourceType() == Electra::IAdaptiveStreamingPlayerResourceRequest::EPlaybackResourceType::BinaryData) { TSharedPtr 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(InOutRequest); dr->ReadDataFromFile(rp, ExternalDataCompletedDelegate); return; } } InOutRequest->SignalDataReady(); } } void FElectraPlayer::FAdaptiveStreamingPlayerResourceProvider::ClearPendingRequests() { PendingStaticResourceRequests.Empty(); } void FElectraPlayer::FAdaptiveStreamingPlayerResourceProvider::SetExternalDataReader(TWeakPtr InExternalDataReader) { ExternalDataReader = MoveTemp(InExternalDataReader); ExternalDataCompletedDelegate.BindStatic(&FElectraPlayer::FAdaptiveStreamingPlayerResourceProvider::OnExternalDataReadCompleted); } void FElectraPlayer::FAdaptiveStreamingPlayerResourceProvider::OnExternalDataReadCompleted(IElectraPlayerExternalDataReader::FResponseDataPtr InResponseData, int64 InTotalFileSize, const IElectraPlayerExternalDataReader::FReadParam& InFromRequestParams) { TSharedPtr* rreq = reinterpret_cast*>(InFromRequestParams.Custom); TSharedPtr Req(*rreq); delete rreq; if (Req.IsValid()) { Req->SetPlaybackData(InResponseData, InTotalFileSize); Req->SignalDataReady(); } } FElectraPlayer::FAdaptiveStreamingPlayerResourceProvider::~FAdaptiveStreamingPlayerResourceProvider() { ExternalDataCompletedDelegate.Unbind(); } // ------------------------------------------------------------------------------------------------------------------ IElectraPlayerInterface* FElectraPlayerRuntimeFactory::CreatePlayer(const TSharedPtr& AdapterDelegate, FElectraPlayerSendAnalyticMetricsDelegate& InSendAnalyticMetricsDelegate, FElectraPlayerSendAnalyticMetricsPerMinuteDelegate& InSendAnalyticMetricsPerMinuteDelegate, FElectraPlayerReportVideoStreamingErrorDelegate& InReportVideoStreamingErrorDelegate, FElectraPlayerReportSubtitlesMetricsDelegate& InReportSubtitlesFileMetricsDelegate) { return new FElectraPlayer(AdapterDelegate, InSendAnalyticMetricsDelegate, InSendAnalyticMetricsPerMinuteDelegate, InReportVideoStreamingErrorDelegate, InReportSubtitlesFileMetricsDelegate); }