You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
3412 lines
105 KiB
C++
3412 lines
105 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "MediaPlayerFacade.h"
|
|
#include "MediaUtilsPrivate.h"
|
|
|
|
#include "HAL/PlatformMath.h"
|
|
#include "HAL/PlatformProcess.h"
|
|
#include "IMediaCache.h"
|
|
#include "IMediaControls.h"
|
|
#include "IMediaModule.h"
|
|
#include "IMediaOptions.h"
|
|
#include "IMediaPlayer.h"
|
|
#include "IMediaPlayerFactory.h"
|
|
#include "IMediaSamples.h"
|
|
#include "IMediaAudioSample.h"
|
|
#include "IMediaTextureSample.h"
|
|
#include "IMediaOverlaySample.h"
|
|
#include "IMediaTracks.h"
|
|
#include "IMediaView.h"
|
|
#include "IMediaTicker.h"
|
|
#include "MediaPlayerOptions.h"
|
|
#include "Math/NumericLimits.h"
|
|
#include "Misc/CoreMisc.h"
|
|
#include "Misc/ScopeLock.h"
|
|
#include "Modules/ModuleManager.h"
|
|
|
|
#include "MediaHelpers.h"
|
|
#include "MediaSampleCache.h"
|
|
#include "MediaSampleQueueDepths.h"
|
|
#include "MediaSampleQueue.h"
|
|
|
|
#include "Async/Async.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#define MEDIAPLAYERFACADE_DISABLE_BLOCKING 0
|
|
#define MEDIAPLAYERFACADE_TRACE_SINKOVERFLOWS 0
|
|
|
|
|
|
/** Time spent in media player facade closing media. */
|
|
DECLARE_CYCLE_STAT(TEXT("MediaUtils MediaPlayerFacade Close"), STAT_MediaUtils_FacadeClose, STATGROUP_Media);
|
|
|
|
/** Time spent in media player facade opening media. */
|
|
DECLARE_CYCLE_STAT(TEXT("MediaUtils MediaPlayerFacade Open"), STAT_MediaUtils_FacadeOpen, STATGROUP_Media);
|
|
|
|
/** Time spent in media player facade event processing. */
|
|
DECLARE_CYCLE_STAT(TEXT("MediaUtils MediaPlayerFacade ProcessEvent"), STAT_MediaUtils_FacadeProcessEvent, STATGROUP_Media);
|
|
|
|
/** Time spent in media player facade fetch tick. */
|
|
DECLARE_CYCLE_STAT(TEXT("MediaUtils MediaPlayerFacade TickFetch"), STAT_MediaUtils_FacadeTickFetch, STATGROUP_Media);
|
|
|
|
/** Time spent in media player facade input tick. */
|
|
DECLARE_CYCLE_STAT(TEXT("MediaUtils MediaPlayerFacade TickInput"), STAT_MediaUtils_FacadeTickInput, STATGROUP_Media);
|
|
|
|
/** Time spent in media player facade output tick. */
|
|
DECLARE_CYCLE_STAT(TEXT("MediaUtils MediaPlayerFacade TickOutput"), STAT_MediaUtils_FacadeTickOutput, STATGROUP_Media);
|
|
|
|
/** Time spent in media player facade high frequency tick. */
|
|
DECLARE_CYCLE_STAT(TEXT("MediaUtils MediaPlayerFacade TickTickable"), STAT_MediaUtils_FacadeTickTickable, STATGROUP_Media);
|
|
|
|
/** Player time on main thread during last fetch tick. */
|
|
DECLARE_FLOAT_COUNTER_STAT(TEXT("MediaPlayerFacade PlaybackTime"), STAT_MediaUtils_FacadeTime, STATGROUP_Media);
|
|
|
|
/** Number of video samples currently in the queue. */
|
|
DECLARE_DWORD_COUNTER_STAT(TEXT("MediaPlayerFacade NumVideoSamples"), STAT_MediaUtils_FacadeNumVideoSamples, STATGROUP_Media);
|
|
|
|
/** Number of audio samples currently in the queue. */
|
|
DECLARE_DWORD_COUNTER_STAT(TEXT("MediaPlayerFacade NumAudioSamples"), STAT_MediaUtils_FacadeNumAudioSamples, STATGROUP_Media);
|
|
|
|
/** Number of purged video samples */
|
|
DECLARE_DWORD_COUNTER_STAT(TEXT("MediaPlayerFacade NumPurgedVideoSamples"), STAT_MediaUtils_FacadeNumPurgedVideoSamples, STATGROUP_Media);
|
|
DECLARE_DWORD_ACCUMULATOR_STAT(TEXT("MediaPlayerFacade TotalPurgedVideoSamples"), STAT_MediaUtils_FacadeTotalPurgedVideoSamples, STATGROUP_Media);
|
|
|
|
/** Number of purged subtitle samples */
|
|
DECLARE_DWORD_COUNTER_STAT(TEXT("MediaPlayerFacade NumPurgedSubtitleSamples"), STAT_MediaUtils_FacadeNumPurgedSubtitleSamples, STATGROUP_Media);
|
|
DECLARE_DWORD_ACCUMULATOR_STAT(TEXT("MediaPlayerFacade TotalPurgedSubtitleSamples"), STAT_MediaUtils_FacadeTotalPurgedSubtitleSamples, STATGROUP_Media);
|
|
|
|
/** Number of purged caption samples */
|
|
DECLARE_DWORD_COUNTER_STAT(TEXT("MediaPlayerFacade NumPurgedCaptionSamples"), STAT_MediaUtils_FacadeNumPurgedCaptionSamples, STATGROUP_Media);
|
|
DECLARE_DWORD_ACCUMULATOR_STAT(TEXT("MediaPlayerFacade TotalPurgedCaptionSamples"), STAT_MediaUtils_FacadeTotalPurgedCaptionSamples, STATGROUP_Media);
|
|
|
|
/* Some constants
|
|
*****************************************************************************/
|
|
|
|
static const double kMaxTimeSinceFrameStart = 0.300; // max seconds we allow between the start of the frame and the player facade timing computations (to catch suspended apps & debugging)
|
|
static const double kMaxTimeSinceAudioTimeSampling = 0.250; // max seconds we allow to have passed between the last audio timing sampling and the player facade timing computations (to catch suspended apps & debugging - some platforms do update audio at a farily low rate: hence the big tollerance)
|
|
static const double kOutdatedVideoSamplesTolerance = 0.080; // seconds video samples are allowed to be "too old" to stay in the player's output queue despite of calculations indicating they need to go
|
|
static const double kOutdatedSubtitleSamplesTolerance = 0.050; // seconds subtitle samples are allowed to be "too old" to stay in the player's output queue despite of calculations indicating they need to go
|
|
static const double kOutdatedSamplePurgeRange = 1.0; // milliseconds for pseudo DT timespan used with async purging of outdated video samples
|
|
static const int32 kMinFramesInVideoQueueToPurge = 3; // we only consider purging any old frames from the video queue if more than these are present (to not kill a slow playback entirely)
|
|
static const int32 kMinFramesInSubtitleQueueToPurge = 3; // we only consider purging any old frames from the subtitle queue if more than these are present (to not kill a slow playback entirely)
|
|
static const int32 kMinFramesInCaptionQueueToPurge = 3; // we only consider purging any old frames from the caption queue if more than these are present (to not kill a slow playback entirely)
|
|
|
|
/* Local helpers
|
|
*****************************************************************************/
|
|
|
|
namespace MediaPlayerFacade
|
|
{
|
|
const FTimespan AudioPreroll = FTimespan::FromSeconds(1.0);
|
|
const FTimespan MetadataPreroll = FTimespan::FromSeconds(1.0);
|
|
}
|
|
|
|
static FTimespan WrappedModulo(FTimespan Time, FTimespan Duration)
|
|
{
|
|
return (Time >= FTimespan::Zero()) ? (Time % Duration) : (Duration + (Time % Duration));
|
|
}
|
|
|
|
/* FMediaPlayerFacade structors
|
|
*****************************************************************************/
|
|
|
|
FMediaPlayerFacade::FMediaPlayerFacade(TWeakObjectPtr<UMediaPlayer> InMediaPlayer)
|
|
: TimeDelay(FTimespan::Zero())
|
|
, BlockOnRange(this)
|
|
, Cache(new FMediaSampleCache)
|
|
, LastRate(0.0f)
|
|
, CurrentRate(0.0f)
|
|
, bHaveActiveAudio(false)
|
|
, VideoSampleAvailability(-1)
|
|
, AudioSampleAvailability(-1)
|
|
, bIsSinkFlushPending(false)
|
|
, bAreEventsSafeForAnyThread(false)
|
|
, MediaPlayer(InMediaPlayer)
|
|
{
|
|
BlockOnRangeDisabled = false;
|
|
|
|
MediaModule = FModuleManager::LoadModulePtr<IMediaModule>("Media");
|
|
bDidRecentPlayerHaveError = false;
|
|
|
|
ResetTracks();
|
|
}
|
|
|
|
|
|
FMediaPlayerFacade::~FMediaPlayerFacade()
|
|
{
|
|
FMediaSampleSinkEventData Data;
|
|
Data.Detached.MediaPlayer = MediaPlayer.Get();
|
|
SendSinkEvent(EMediaSampleSinkEvent::Detached, Data);
|
|
|
|
if (Player.IsValid())
|
|
{
|
|
{
|
|
FScopeLock Lock(&CriticalSection);
|
|
Player->Close();
|
|
}
|
|
NotifyLifetimeManagerDelegate_PlayerClosed();
|
|
|
|
DestroyPlayer();
|
|
}
|
|
|
|
delete Cache;
|
|
Cache = nullptr;
|
|
}
|
|
|
|
|
|
/* FMediaPlayerFacade interface
|
|
*****************************************************************************/
|
|
|
|
void FMediaPlayerFacade::AddAudioSampleSink(const TSharedRef<FMediaAudioSampleSink, ESPMode::ThreadSafe>& SampleSink)
|
|
{
|
|
FScopeLock Lock(&CriticalSection);
|
|
AudioSampleSinks.Add(SampleSink);
|
|
PrimaryAudioSink = AudioSampleSinks.GetPrimaryAudioSink();
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::AddCaptionSampleSink(const TSharedRef<FMediaOverlaySampleSink, ESPMode::ThreadSafe>& SampleSink)
|
|
{
|
|
CaptionSampleSinks.Add(SampleSink);
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::AddMetadataSampleSink(const TSharedRef<FMediaBinarySampleSink, ESPMode::ThreadSafe>& SampleSink)
|
|
{
|
|
FScopeLock Lock(&CriticalSection);
|
|
MetadataSampleSinks.Add(SampleSink);
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::AddSubtitleSampleSink(const TSharedRef<FMediaOverlaySampleSink, ESPMode::ThreadSafe>& SampleSink)
|
|
{
|
|
SubtitleSampleSinks.Add(SampleSink);
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::AddVideoSampleSink(const TSharedRef<FMediaTextureSampleSink, ESPMode::ThreadSafe>& SampleSink)
|
|
{
|
|
VideoSampleSinks.Add(SampleSink);
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::CanPause() const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
return CurrentPlayer->GetControls().CanControl(EMediaControl::Pause);
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::CanPlayUrl(const FString& Url, const IMediaOptions* Options)
|
|
{
|
|
if (MediaModule == nullptr)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const FString RunningPlatformName(FPlatformProperties::IniPlatformName());
|
|
const TArray<IMediaPlayerFactory*>& PlayerFactories = MediaModule->GetPlayerFactories();
|
|
|
|
for (IMediaPlayerFactory* Factory : PlayerFactories)
|
|
{
|
|
if (Factory->SupportsPlatform(RunningPlatformName) && Factory->CanPlayUrl(Url, Options))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::CanResume() const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
return CurrentPlayer->GetControls().CanControl(EMediaControl::Resume);
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::CanScrub() const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
return CurrentPlayer->GetControls().CanControl(EMediaControl::Scrub);
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::CanSeek() const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
return CurrentPlayer->GetControls().CanControl(EMediaControl::Seek);
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::Close()
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_MediaUtils_FacadeClose);
|
|
|
|
if (CurrentUrl.IsEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (CurrentPlayer.IsValid())
|
|
{
|
|
{
|
|
FScopeLock Lock(&CriticalSection);
|
|
CurrentPlayer->Close();
|
|
}
|
|
NotifyLifetimeManagerDelegate_PlayerClosed();
|
|
}
|
|
|
|
BlockOnRange.Reset();
|
|
|
|
Cache->Empty();
|
|
CurrentUrl.Empty();
|
|
LastRate = 0.0f;
|
|
CurrentRate = 0.0f;
|
|
|
|
bHaveActiveAudio = false;
|
|
VideoSampleAvailability = -1;
|
|
AudioSampleAvailability = -1;
|
|
bIsSinkFlushPending = false;
|
|
bDidRecentPlayerHaveError = false;
|
|
|
|
Flush();
|
|
}
|
|
|
|
|
|
uint32 FMediaPlayerFacade::GetAudioTrackChannels(int32 TrackIndex, int32 FormatIndex) const
|
|
{
|
|
FMediaAudioTrackFormat Format;
|
|
return GetAudioTrackFormat(TrackIndex, FormatIndex, Format) ? Format.NumChannels : 0;
|
|
}
|
|
|
|
|
|
uint32 FMediaPlayerFacade::GetAudioTrackSampleRate(int32 TrackIndex, int32 FormatIndex) const
|
|
{
|
|
FMediaAudioTrackFormat Format;
|
|
return GetAudioTrackFormat(TrackIndex, FormatIndex, Format) ? Format.SampleRate : 0;
|
|
}
|
|
|
|
|
|
FString FMediaPlayerFacade::GetAudioTrackType(int32 TrackIndex, int32 FormatIndex) const
|
|
{
|
|
FMediaAudioTrackFormat Format;
|
|
return GetAudioTrackFormat(TrackIndex, FormatIndex, Format) ? Format.TypeName : FString();
|
|
}
|
|
|
|
|
|
FTimespan FMediaPlayerFacade::GetDuration() const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return FTimespan::Zero();
|
|
}
|
|
return CurrentPlayer->GetControls().GetDuration();
|
|
}
|
|
|
|
|
|
const FGuid& FMediaPlayerFacade::GetGuid()
|
|
{
|
|
return PlayerGuid;
|
|
}
|
|
|
|
|
|
FString FMediaPlayerFacade::GetInfo() const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return FString();
|
|
}
|
|
return CurrentPlayer->GetInfo();
|
|
}
|
|
|
|
|
|
FVariant FMediaPlayerFacade::GetMediaInfo(FName InfoName) const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return FVariant();
|
|
}
|
|
return CurrentPlayer->GetMediaInfo(InfoName);
|
|
}
|
|
|
|
|
|
FText FMediaPlayerFacade::GetMediaName() const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return FText::GetEmpty();
|
|
}
|
|
return CurrentPlayer->GetMediaName();
|
|
}
|
|
|
|
|
|
TSharedPtr<TMap<FString, TArray<TUniquePtr<IMediaMetadataItem>>>, ESPMode::ThreadSafe> FMediaPlayerFacade::GetMediaMetadata() const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return nullptr;
|
|
}
|
|
return CurrentPlayer->GetMediaMetadata();
|
|
}
|
|
|
|
|
|
int32 FMediaPlayerFacade::GetNumTracks(EMediaTrackType TrackType) const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return 0;
|
|
}
|
|
return CurrentPlayer->GetTracks().GetNumTracks(TrackType);
|
|
}
|
|
|
|
|
|
int32 FMediaPlayerFacade::GetNumTrackFormats(EMediaTrackType TrackType, int32 TrackIndex) const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return 0;
|
|
}
|
|
return CurrentPlayer->GetTracks().GetNumTrackFormats(TrackType, TrackIndex);
|
|
}
|
|
|
|
|
|
FName FMediaPlayerFacade::GetPlayerName() const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return NAME_None;
|
|
}
|
|
return MediaModule->GetPlayerFactory(CurrentPlayer->GetPlayerPluginGUID())->GetPlayerName();
|
|
}
|
|
|
|
|
|
float FMediaPlayerFacade::GetRate() const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return 0.0f;
|
|
}
|
|
return CurrentPlayer->GetControls().GetRate();
|
|
}
|
|
|
|
|
|
FString FMediaPlayerFacade::GetStats() const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return FString();
|
|
}
|
|
return CurrentPlayer->GetStats();
|
|
}
|
|
|
|
|
|
TRangeSet<float> FMediaPlayerFacade::GetSupportedRates(bool Unthinned) const
|
|
{
|
|
const EMediaRateThinning Thinning = Unthinned ? EMediaRateThinning::Unthinned : EMediaRateThinning::Thinned;
|
|
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return TRangeSet<float>();
|
|
}
|
|
return CurrentPlayer->GetControls().GetSupportedRates(Thinning);
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::HaveVideoPlayback() const
|
|
{
|
|
return VideoSampleSinks.Num() && (GetSelectedTrack(EMediaTrackType::Video) != INDEX_NONE);
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::HaveAudioPlayback() const
|
|
{
|
|
return PrimaryAudioSink.IsValid() && (GetSelectedTrack(EMediaTrackType::Audio) != INDEX_NONE);
|
|
}
|
|
|
|
|
|
FTimespan FMediaPlayerFacade::GetTime() const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return FTimespan::Zero(); // no media opened
|
|
}
|
|
|
|
FTimespan Result;
|
|
|
|
if (CurrentPlayer->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::UsePlaybackTimingV2))
|
|
{
|
|
// New style: framework controls timing - we use GetTimeStamp() and return the legacy part of the value
|
|
FMediaTimeStamp TimeStamp = GetTimeStamp();
|
|
return TimeStamp.IsValid() ? TimeStamp.Time : FTimespan::Zero();
|
|
}
|
|
else
|
|
{
|
|
// Old style: ask the player for timing
|
|
Result = CurrentPlayer->GetControls().GetTime() - TimeDelay;
|
|
if (Result.GetTicks() < 0)
|
|
{
|
|
Result = FTimespan::Zero();
|
|
}
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
|
|
FMediaTimeStamp FMediaPlayerFacade::GetTimeStamp() const
|
|
{
|
|
return GetTimeStampInternal(false);
|
|
}
|
|
|
|
|
|
FMediaTimeStamp FMediaPlayerFacade::GetDisplayTimeStamp() const
|
|
{
|
|
return GetTimeStampInternal(true);
|
|
}
|
|
|
|
FMediaTimeStamp FMediaPlayerFacade::GetTimeStampInternal(bool bForDisplay) const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return FMediaTimeStamp();
|
|
}
|
|
|
|
FScopeLock Lock(&LastTimeValuesCS);
|
|
|
|
if (!CurrentPlayer->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::UsePlaybackTimingV2))
|
|
{
|
|
// Make sure we can return values for V1 players...
|
|
return FMediaTimeStamp(GetTime());
|
|
}
|
|
|
|
// Check if the value is for display purposes. If so: do we seek right now?
|
|
if (bForDisplay && SeekTargetTime.IsValid())
|
|
{
|
|
return SeekTargetTime;
|
|
}
|
|
|
|
// Check if there are video samples present or presence is unknown.
|
|
// Only when we know for sure that there are none because the existing video stream has ended do we set this to false.
|
|
bool bHaveVideoSamples = VideoSampleAvailability != 0;
|
|
|
|
if (HaveVideoPlayback() && bHaveVideoSamples)
|
|
{
|
|
/*
|
|
Returning the precise time of the sample returned during TickFetch()
|
|
*/
|
|
return bForDisplay ? CurrentFrameVideoDisplayTimeStamp : CurrentFrameVideoTimeStamp;
|
|
}
|
|
else if (HaveAudioPlayback())
|
|
{
|
|
/*
|
|
We grab the last processed audio sample timestamp when it gets passed out to the sink(s) and keep it
|
|
as "the value" for the frame (on the gamethread) -- an approximation, but better then having it return
|
|
new values each time its called in one and the same frame...
|
|
*/
|
|
return CurrentFrameAudioTimeStamp;
|
|
}
|
|
|
|
// we assume video and/or audio to be present in any stream we play - otherwise: no time info
|
|
// (at least for now)
|
|
return FMediaTimeStamp();
|
|
}
|
|
|
|
|
|
FText FMediaPlayerFacade::GetTrackDisplayName(EMediaTrackType TrackType, int32 TrackIndex) const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return FText::GetEmpty();
|
|
}
|
|
return CurrentPlayer->GetTracks().GetTrackDisplayName((EMediaTrackType)TrackType, TrackIndex);
|
|
}
|
|
|
|
|
|
int32 FMediaPlayerFacade::GetTrackFormat(EMediaTrackType TrackType, int32 TrackIndex) const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return INDEX_NONE;
|
|
}
|
|
return CurrentPlayer->GetTracks().GetTrackFormat((EMediaTrackType)TrackType, TrackIndex);
|
|
}
|
|
|
|
|
|
FString FMediaPlayerFacade::GetTrackLanguage(EMediaTrackType TrackType, int32 TrackIndex) const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return FString();
|
|
}
|
|
return CurrentPlayer->GetTracks().GetTrackLanguage((EMediaTrackType)TrackType, TrackIndex);
|
|
}
|
|
|
|
|
|
float FMediaPlayerFacade::GetVideoTrackAspectRatio(int32 TrackIndex, int32 FormatIndex) const
|
|
{
|
|
FMediaVideoTrackFormat Format;
|
|
return (GetVideoTrackFormat(TrackIndex, FormatIndex, Format) && (Format.Dim.Y != 0)) ? ((float)(Format.Dim.X) / (float)Format.Dim.Y) : 0.0f;
|
|
}
|
|
|
|
|
|
FIntPoint FMediaPlayerFacade::GetVideoTrackDimensions(int32 TrackIndex, int32 FormatIndex) const
|
|
{
|
|
FMediaVideoTrackFormat Format;
|
|
return GetVideoTrackFormat(TrackIndex, FormatIndex, Format) ? Format.Dim : FIntPoint::ZeroValue;
|
|
}
|
|
|
|
|
|
float FMediaPlayerFacade::GetVideoTrackFrameRate(int32 TrackIndex, int32 FormatIndex) const
|
|
{
|
|
FMediaVideoTrackFormat Format;
|
|
return GetVideoTrackFormat(TrackIndex, FormatIndex, Format) ? Format.FrameRate : 0.0f;
|
|
}
|
|
|
|
|
|
TRange<float> FMediaPlayerFacade::GetVideoTrackFrameRates(int32 TrackIndex, int32 FormatIndex) const
|
|
{
|
|
FMediaVideoTrackFormat Format;
|
|
return GetVideoTrackFormat(TrackIndex, FormatIndex, Format) ? Format.FrameRates : TRange<float>::Empty();
|
|
}
|
|
|
|
|
|
FString FMediaPlayerFacade::GetVideoTrackType(int32 TrackIndex, int32 FormatIndex) const
|
|
{
|
|
FMediaVideoTrackFormat Format;
|
|
return GetVideoTrackFormat(TrackIndex, FormatIndex, Format) ? Format.TypeName : FString();
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::GetViewField(float& OutHorizontal, float& OutVertical) const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
return CurrentPlayer->GetView().GetViewField(OutHorizontal, OutVertical);
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::GetViewOrientation(FQuat& OutOrientation) const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
return CurrentPlayer->GetView().GetViewOrientation(OutOrientation);
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::HasError() const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return bDidRecentPlayerHaveError;
|
|
}
|
|
return (CurrentPlayer->GetControls().GetState() == EMediaState::Error);
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::IsBuffering() const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
return EnumHasAnyFlags(CurrentPlayer->GetControls().GetStatus(), EMediaStatus::Buffering);
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::IsConnecting() const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
return EnumHasAnyFlags(CurrentPlayer->GetControls().GetStatus(), EMediaStatus::Connecting);
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::IsLooping() const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
return CurrentPlayer->GetControls().IsLooping();
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::IsPaused() const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
return (CurrentPlayer->GetControls().GetState() == EMediaState::Paused);
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::IsPlaying() const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
return (CurrentPlayer->GetControls().GetState() == EMediaState::Playing);
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::IsPreparing() const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
return (CurrentPlayer->GetControls().GetState() == EMediaState::Preparing);
|
|
}
|
|
|
|
bool FMediaPlayerFacade::IsClosed() const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
return (CurrentPlayer->GetControls().GetState() == EMediaState::Closed);
|
|
}
|
|
|
|
bool FMediaPlayerFacade::IsReady() const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
EMediaState State = CurrentPlayer->GetControls().GetState();
|
|
return ((State != EMediaState::Closed) &&
|
|
(State != EMediaState::Error) &&
|
|
(State != EMediaState::Preparing));
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
class FMediaPlayerLifecycleManagerDelegateOpenRequest : public IMediaPlayerLifecycleManagerDelegate::IOpenRequest
|
|
{
|
|
public:
|
|
FMediaPlayerLifecycleManagerDelegateOpenRequest(const FString& InUrl, const IMediaOptions* InOptions, const FMediaPlayerOptions* InPlayerOptions, IMediaPlayerFactory* InPlayerFactory, TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> InReusedPlayer, bool bInWillCreatePlayer, uint32 InWillUseNewResources)
|
|
: Url(InUrl), Options(InOptions), PlayerFactory(InPlayerFactory), ReusedPlayer(InReusedPlayer), bWillCreatePlayer(bInWillCreatePlayer), NewResources(InWillUseNewResources)
|
|
{
|
|
if (InPlayerOptions)
|
|
{
|
|
PlayerOptions = *InPlayerOptions;
|
|
}
|
|
}
|
|
|
|
virtual const FString& GetUrl() const override
|
|
{
|
|
return Url;
|
|
}
|
|
|
|
virtual const IMediaOptions* GetOptions() const override
|
|
{
|
|
return Options;
|
|
}
|
|
|
|
virtual const FMediaPlayerOptions* GetPlayerOptions() const override
|
|
{
|
|
return PlayerOptions.IsSet() ? &PlayerOptions.GetValue() : nullptr;
|
|
}
|
|
|
|
virtual IMediaPlayerFactory* GetPlayerFactory() const override
|
|
{
|
|
return PlayerFactory;
|
|
}
|
|
|
|
virtual bool WillCreateNewPlayer() const
|
|
{
|
|
return bWillCreatePlayer;
|
|
}
|
|
|
|
virtual bool WillUseNewResources(uint32 ResourceFlags) const
|
|
{
|
|
return !!(NewResources & ResourceFlags);
|
|
}
|
|
|
|
FString Url;
|
|
const IMediaOptions* Options;
|
|
TOptional<FMediaPlayerOptions> PlayerOptions;
|
|
IMediaPlayerFactory* PlayerFactory;
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> ReusedPlayer;
|
|
bool bWillCreatePlayer;
|
|
uint32 NewResources;
|
|
};
|
|
|
|
class FMediaPlayerLifecycleManagerDelegateControl : public IMediaPlayerLifecycleManagerDelegate::IControl, public TSharedFromThis<FMediaPlayerLifecycleManagerDelegateControl, ESPMode::ThreadSafe>
|
|
{
|
|
public:
|
|
FMediaPlayerLifecycleManagerDelegateControl(TWeakPtr<FMediaPlayerFacade, ESPMode::ThreadSafe> InFacade) : Facade(InFacade), InstanceID(~0), SubmittedRequest(false) {}
|
|
|
|
virtual ~FMediaPlayerLifecycleManagerDelegateControl()
|
|
{
|
|
if (!SubmittedRequest)
|
|
{
|
|
if (TSharedPtr<FMediaPlayerFacade, ESPMode::ThreadSafe> PinnedFacade = Facade.Pin())
|
|
{
|
|
PinnedFacade->ReceiveMediaEvent(EMediaEvent::MediaOpenFailed);
|
|
}
|
|
}
|
|
}
|
|
|
|
virtual bool SubmitOpenRequest(IMediaPlayerLifecycleManagerDelegate::IOpenRequestRef&& OpenRequest) override
|
|
{
|
|
if (TSharedPtr<FMediaPlayerFacade, ESPMode::ThreadSafe> PinnedFacade = Facade.Pin())
|
|
{
|
|
const FMediaPlayerLifecycleManagerDelegateOpenRequest* OR = static_cast<const FMediaPlayerLifecycleManagerDelegateOpenRequest*>(OpenRequest.Get());
|
|
if (PinnedFacade->ContinueOpen(AsShared(), OR->Url, OR->Options, OR->PlayerOptions.IsSet() ? &OR->PlayerOptions.GetValue() : nullptr, OR->PlayerFactory, OR->ReusedPlayer, OR->bWillCreatePlayer, InstanceID))
|
|
{
|
|
SubmittedRequest = true;
|
|
}
|
|
//note: we return "true" in all cases in which we were able to get to call "ContinueOpen". Failures in here will be messaged to the delegate using the OnMediaPlayerCreateFailed() method
|
|
// (returning true here allows for capturing an unlikely early death of the facade while protecting us from double-handling the failure of the creation in the delegate)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
virtual TSharedPtr<FMediaPlayerFacade, ESPMode::ThreadSafe> GetFacade() const override
|
|
{
|
|
return Facade.Pin();
|
|
}
|
|
|
|
virtual uint64 GetMediaPlayerInstanceID() const override
|
|
{
|
|
return InstanceID;
|
|
}
|
|
|
|
void SetInstanceID(uint64 InInstanceID)
|
|
{
|
|
InstanceID = InInstanceID;
|
|
}
|
|
|
|
void Reset()
|
|
{
|
|
SubmittedRequest = true;
|
|
}
|
|
|
|
private:
|
|
TWeakPtr<FMediaPlayerFacade, ESPMode::ThreadSafe> Facade;
|
|
uint64 InstanceID;
|
|
bool SubmittedRequest;
|
|
};
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
bool FMediaPlayerFacade::NotifyLifetimeManagerDelegate_PlayerOpen(IMediaPlayerLifecycleManagerDelegate::IControlRef& NewLifecycleManagerDelegateControl, const FString& Url, const IMediaOptions* Options, const FMediaPlayerOptions* PlayerOptions, IMediaPlayerFactory* PlayerFactory, bool bWillCreatePlayer, uint32 WillUseNewResources, uint64 NewPlayerInstanceID)
|
|
{
|
|
check(IsInGameThread() || IsInSlateThread());
|
|
|
|
if (IMediaPlayerLifecycleManagerDelegate* Delegate = MediaModule->GetPlayerLifecycleManagerDelegate())
|
|
{
|
|
NewLifecycleManagerDelegateControl = MakeShared<FMediaPlayerLifecycleManagerDelegateControl, ESPMode::ThreadSafe>(AsShared());
|
|
if (NewLifecycleManagerDelegateControl.IsValid())
|
|
{
|
|
// Set instance ID we will use for a new player if we get the go-ahead to create it (old ID if player is about to be reused)
|
|
static_cast<FMediaPlayerLifecycleManagerDelegateControl*>(NewLifecycleManagerDelegateControl.Get())->SetInstanceID(NewPlayerInstanceID);
|
|
|
|
IMediaPlayerLifecycleManagerDelegate::IOpenRequestRef OpenRequest(new FMediaPlayerLifecycleManagerDelegateOpenRequest(Url, Options, PlayerOptions, PlayerFactory, !bWillCreatePlayer ? Player : TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe>(), bWillCreatePlayer, WillUseNewResources));
|
|
if (OpenRequest.IsValid())
|
|
{
|
|
if (Delegate->OnMediaPlayerOpen(NewLifecycleManagerDelegateControl, OpenRequest))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
static_cast<FMediaPlayerLifecycleManagerDelegateControl*>(NewLifecycleManagerDelegateControl.Get())->Reset();
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool FMediaPlayerFacade::NotifyLifetimeManagerDelegate_PlayerCreated()
|
|
{
|
|
check(IsInGameThread() || IsInSlateThread());
|
|
check(Player.IsValid());
|
|
|
|
if (LifecycleManagerDelegateControl.IsValid())
|
|
{
|
|
if (IMediaPlayerLifecycleManagerDelegate* Delegate = MediaModule->GetPlayerLifecycleManagerDelegate())
|
|
{
|
|
Delegate->OnMediaPlayerCreated(LifecycleManagerDelegateControl);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool FMediaPlayerFacade::NotifyLifetimeManagerDelegate_PlayerCreateFailed()
|
|
{
|
|
check(IsInGameThread() || IsInSlateThread());
|
|
|
|
if (LifecycleManagerDelegateControl.IsValid())
|
|
{
|
|
if (IMediaPlayerLifecycleManagerDelegate* Delegate = MediaModule->GetPlayerLifecycleManagerDelegate())
|
|
{
|
|
Delegate->OnMediaPlayerCreateFailed(LifecycleManagerDelegateControl);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool FMediaPlayerFacade::NotifyLifetimeManagerDelegate_PlayerClosed()
|
|
{
|
|
check(IsInGameThread() || IsInSlateThread());
|
|
|
|
if (LifecycleManagerDelegateControl.IsValid())
|
|
{
|
|
if (IMediaPlayerLifecycleManagerDelegate* Delegate = MediaModule->GetPlayerLifecycleManagerDelegate())
|
|
{
|
|
Delegate->OnMediaPlayerClosed(LifecycleManagerDelegateControl);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool FMediaPlayerFacade::NotifyLifetimeManagerDelegate_PlayerDestroyed()
|
|
{
|
|
check(IsInGameThread() || IsInSlateThread());
|
|
|
|
if (LifecycleManagerDelegateControl.IsValid())
|
|
{
|
|
if (IMediaPlayerLifecycleManagerDelegate* Delegate = MediaModule->GetPlayerLifecycleManagerDelegate())
|
|
{
|
|
Delegate->OnMediaPlayerDestroyed(LifecycleManagerDelegateControl);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool FMediaPlayerFacade::NotifyLifetimeManagerDelegate_PlayerResourcesReleased(uint32 ResourceFlags)
|
|
{
|
|
check(IsInGameThread() || IsInSlateThread());
|
|
|
|
if (LifecycleManagerDelegateControl.IsValid())
|
|
{
|
|
if (IMediaPlayerLifecycleManagerDelegate* Delegate = MediaModule->GetPlayerLifecycleManagerDelegate())
|
|
{
|
|
Delegate->OnMediaPlayerResourcesReleased(LifecycleManagerDelegateControl, ResourceFlags);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
void FMediaPlayerFacade::DestroyPlayer()
|
|
{
|
|
FScopeLock Lock(&CriticalSection);
|
|
|
|
if (!Player.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
Player.Reset();
|
|
NotifyLifetimeManagerDelegate_PlayerDestroyed();
|
|
if (!PlayerUsesResourceReleaseNotification)
|
|
{
|
|
NotifyLifetimeManagerDelegate_PlayerResourcesReleased(IMediaPlayerLifecycleManagerDelegate::ResourceFlags_All);
|
|
}
|
|
}
|
|
|
|
bool FMediaPlayerFacade::Open(const FString& Url, const IMediaOptions* Options, const FMediaPlayerOptions* PlayerOptions)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_MediaUtils_FacadeOpen);
|
|
|
|
ActivePlayerOptions.Reset();
|
|
|
|
if (IsRunningDedicatedServer())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Close();
|
|
|
|
if (Url.IsEmpty())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
check(MediaModule);
|
|
|
|
// find a player factory for the intended playback
|
|
IMediaPlayerFactory* PlayerFactory = GetPlayerFactoryForUrl(Url, Options);
|
|
if (PlayerFactory == nullptr)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
IMediaPlayerFactory* OldFactory(Player.IsValid() ? MediaModule->GetPlayerFactory(Player->GetPlayerPluginGUID()) : nullptr);
|
|
|
|
bool bWillCreatePlayer = (!Player.IsValid() || PlayerFactory != OldFactory);
|
|
uint64 NewPlayerInstanceID;
|
|
uint32 WillUseNewResources;
|
|
|
|
if (bWillCreatePlayer)
|
|
{
|
|
NewPlayerInstanceID = MediaModule->CreateMediaPlayerInstanceID();
|
|
WillUseNewResources = IMediaPlayerLifecycleManagerDelegate::ResourceFlags_All; // as we create a new player we assume all resources a newly created in any case
|
|
}
|
|
else
|
|
{
|
|
check(Player.IsValid());
|
|
NewPlayerInstanceID = PlayerInstanceID;
|
|
WillUseNewResources = Player->GetNewResourcesOnOpen(); // ask player what resources it will create again even if it already exists
|
|
}
|
|
|
|
IMediaPlayerLifecycleManagerDelegate::IControlRef NewLifecycleManagerDelegateControl;
|
|
if (FMediaPlayerFacade::NotifyLifetimeManagerDelegate_PlayerOpen(NewLifecycleManagerDelegateControl, Url, Options, PlayerOptions, PlayerFactory, bWillCreatePlayer, WillUseNewResources, NewPlayerInstanceID))
|
|
{
|
|
// Assume all is well: the delegate will either (have) submit(ted) the request or not -- in any case we need to assume the best -> "true"
|
|
return true;
|
|
}
|
|
|
|
// We did not notify successfully or the delegate will not submit the request in its own. Do so here...
|
|
return ContinueOpen(NewLifecycleManagerDelegateControl, Url, Options, PlayerOptions, PlayerFactory, Player, bWillCreatePlayer, NewPlayerInstanceID);
|
|
}
|
|
|
|
bool FMediaPlayerFacade::ContinueOpen(IMediaPlayerLifecycleManagerDelegate::IControlRef NewLifecycleManagerDelegateControl, const FString& Url, const IMediaOptions* Options, const FMediaPlayerOptions* PlayerOptions, IMediaPlayerFactory* PlayerFactory, TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> ReusedPlayer, bool bCreateNewPlayer, uint64 NewPlayerInstanceID)
|
|
{
|
|
// Create or reuse player
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> NewPlayer(bCreateNewPlayer ? PlayerFactory->CreatePlayer(*this) : ReusedPlayer);
|
|
|
|
// Continue initialization ---------------------------------------
|
|
|
|
if (NewPlayer != Player)
|
|
{
|
|
DestroyPlayer();
|
|
|
|
class FAsyncResourceReleaseNotification : public IMediaPlayer::IAsyncResourceReleaseNotification
|
|
{
|
|
public:
|
|
FAsyncResourceReleaseNotification(IMediaPlayerLifecycleManagerDelegate::IControlRef InDelegateControl) : DelegateControl(InDelegateControl) {}
|
|
|
|
virtual void Signal(uint32 ResourceFlags) override
|
|
{
|
|
TFunction<void()> NotifyTask = [TargetDelegateControl = DelegateControl, ResourceFlags]()
|
|
{
|
|
// Get MediaModule & check if it is already unloaded...
|
|
IMediaModule* TargetMediaModule = FModuleManager::GetModulePtr<IMediaModule>("Media");
|
|
if (TargetMediaModule)
|
|
{
|
|
// Delegate still there?
|
|
if (IMediaPlayerLifecycleManagerDelegate* Delegate = TargetMediaModule->GetPlayerLifecycleManagerDelegate())
|
|
{
|
|
// Notify it!
|
|
Delegate->OnMediaPlayerResourcesReleased(TargetDelegateControl, ResourceFlags);
|
|
}
|
|
}
|
|
};
|
|
Async(EAsyncExecution::TaskGraphMainThread, NotifyTask);
|
|
};
|
|
|
|
IMediaModule* MediaModule;
|
|
IMediaPlayerLifecycleManagerDelegate::IControlRef DelegateControl;
|
|
};
|
|
|
|
FScopeLock Lock(&CriticalSection);
|
|
Player = NewPlayer;
|
|
PlayerInstanceID = NewPlayerInstanceID;
|
|
LifecycleManagerDelegateControl = NewLifecycleManagerDelegateControl;
|
|
PlayerUsesResourceReleaseNotification = LifecycleManagerDelegateControl.IsValid() ? Player->SetAsyncResourceReleaseNotification(TSharedRef<IMediaPlayer::IAsyncResourceReleaseNotification, ESPMode::ThreadSafe>(new FAsyncResourceReleaseNotification(LifecycleManagerDelegateControl))) : false;
|
|
}
|
|
else
|
|
{
|
|
LifecycleManagerDelegateControl = NewLifecycleManagerDelegateControl;
|
|
}
|
|
|
|
if (!Player.IsValid())
|
|
{
|
|
NotifyLifetimeManagerDelegate_PlayerCreateFailed();
|
|
// Make sure we don't get called from the "tickable" thread anymore - no need as we have no player
|
|
MediaModule->GetTicker().RemoveTickable(AsShared());
|
|
return false;
|
|
}
|
|
|
|
// Make sure we get ticked on the "tickable" thread
|
|
// (this will not re-add us, should we already be registered)
|
|
MediaModule->GetTicker().AddTickable(AsShared());
|
|
|
|
// update the Guid
|
|
Player->SetGuid(PlayerGuid);
|
|
|
|
CurrentUrl = Url;
|
|
|
|
if (PlayerOptions)
|
|
{
|
|
ActivePlayerOptions = *PlayerOptions;
|
|
}
|
|
|
|
// open the new media source
|
|
if (!Player->Open(Url, Options, PlayerOptions))
|
|
{
|
|
NotifyLifetimeManagerDelegate_PlayerCreateFailed();
|
|
CurrentUrl.Empty();
|
|
ActivePlayerOptions.Reset();
|
|
|
|
return false;
|
|
}
|
|
|
|
{
|
|
FScopeLock Lock(&LastTimeValuesCS);
|
|
|
|
BlockOnRangeDisabled = false;
|
|
BlockOnRange.OnFlush();
|
|
LastVideoSampleProcessedTimeRange = TRange<FMediaTimeStamp>::Empty();
|
|
LastAudioSampleProcessedTime.Invalidate();
|
|
CurrentFrameVideoTimeStamp.Invalidate();
|
|
CurrentFrameVideoDisplayTimeStamp.Invalidate();
|
|
CurrentFrameAudioTimeStamp.Invalidate();
|
|
|
|
NextEstVideoTimeAtFrameStart.Invalidate();
|
|
SeekTargetTime.Invalidate();
|
|
SeekIndex = 0;
|
|
}
|
|
|
|
ResetTracks();
|
|
|
|
if (bCreateNewPlayer)
|
|
{
|
|
NotifyLifetimeManagerDelegate_PlayerCreated();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::QueryCacheState(EMediaTrackType TrackType, EMediaCacheState State, TRangeSet<FTimespan>& OutTimeRanges) const
|
|
{
|
|
if (!Player.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (State == EMediaCacheState::Cached)
|
|
{
|
|
if (TrackType == EMediaTrackType::Audio)
|
|
{
|
|
Cache->GetCachedAudioSampleRanges(OutTimeRanges);
|
|
}
|
|
else if (TrackType == EMediaTrackType::Video)
|
|
{
|
|
Cache->GetCachedVideoSampleRanges(OutTimeRanges);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (TrackType == EMediaTrackType::Video)
|
|
{
|
|
Player->GetCache().QueryCacheState(State, OutTimeRanges);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::Seek(const FTimespan& InTime)
|
|
{
|
|
auto CurrentPlayer = Player;
|
|
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FTimespan Duration = CurrentPlayer->GetControls().GetDuration();
|
|
if (Duration == FTimespan::Zero())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FTimespan Time;
|
|
if (CurrentPlayer->GetControls().IsLooping())
|
|
{
|
|
Time = WrappedModulo(InTime, Duration);
|
|
}
|
|
else
|
|
{
|
|
Time = FTimespan(FMath::Clamp(InTime.GetTicks(), (int64)0L, Duration.GetTicks()));
|
|
}
|
|
|
|
if (!CurrentPlayer->GetControls().Seek(Time))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FScopeLock Lock(&CriticalSection);
|
|
|
|
// V2 timing player?
|
|
if (CurrentPlayer->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::UsePlaybackTimingV2))
|
|
{
|
|
// Yes. Flush only the facade side of the system as needed for seeks
|
|
// (the player is expected to flush its internal queues as needed itself)
|
|
check(CurrentPlayer->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::PlayerUsesInternalFlushOnSeek));
|
|
Flush(true, true);
|
|
}
|
|
else
|
|
{
|
|
// No. Flush as requested...
|
|
if (CurrentPlayer->FlushOnSeekStarted())
|
|
{
|
|
Flush(CurrentPlayer->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::PlayerUsesInternalFlushOnSeek), false);
|
|
}
|
|
}
|
|
|
|
SeekTargetTime = FMediaTimeStamp(Time, FMediaTimeStamp::MakeSequenceIndex(SeekIndex, 0));
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::SetBlockOnTime(const FTimespan& Time)
|
|
{
|
|
#if !MEDIAPLAYERFACADE_DISABLE_BLOCKING
|
|
if (!Player.IsValid() || !Player->GetControls().CanControl(EMediaControl::BlockOnFetch))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Time == FTimespan::MinValue())
|
|
{
|
|
BlockOnRange.SetRange(TRange<FTimespan>::Empty());
|
|
Player->GetControls().SetBlockingPlaybackHint(false);
|
|
}
|
|
else
|
|
{
|
|
TRange<FTimespan> Range;
|
|
Range.Inclusive(Time, Time);
|
|
BlockOnRange.SetRange(Range);
|
|
Player->GetControls().SetBlockingPlaybackHint(true);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::SetBlockOnTimeRange(const TRange<FTimespan>& TimeRange)
|
|
{
|
|
#if !MEDIAPLAYERFACADE_DISABLE_BLOCKING
|
|
BlockOnRange.SetRange(TimeRange);
|
|
#endif
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::FBlockOnRange::OnFlush()
|
|
{
|
|
LastTimeRange = TRange<FTimespan>::Empty();
|
|
OnBlockPrimaryIndex = 0;
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::FBlockOnRange::OnSeek(int32 PrimaryIndex)
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Facade->Player);
|
|
check(CurrentPlayer.IsValid());
|
|
|
|
LastTimeRange = TRange<FTimespan>::Empty();
|
|
OnBlockPrimaryIndex = PrimaryIndex;
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::FBlockOnRange::SetRange(const TRange<FTimespan>& NewRange)
|
|
{
|
|
if (CurrentTimeRange != NewRange)
|
|
{
|
|
CurrentTimeRange = NewRange;
|
|
RangeIsDirty = true;
|
|
}
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::FBlockOnRange::IsSet() const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Facade->Player);
|
|
check(CurrentPlayer.IsValid());
|
|
|
|
if (!RangeIsDirty)
|
|
{
|
|
return !BlockOnRange.IsEmpty();
|
|
}
|
|
return (!CurrentTimeRange.IsEmpty() && CurrentPlayer->GetControls().CanControl(EMediaControl::BlockOnFetch));
|
|
}
|
|
|
|
|
|
const TRange<FMediaTimeStamp>& FMediaPlayerFacade::FBlockOnRange::GetRange() const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Facade->Player);
|
|
check(CurrentPlayer.IsValid());
|
|
|
|
if (!RangeIsDirty)
|
|
{
|
|
return BlockOnRange;
|
|
}
|
|
|
|
// If the range is empty or the player can't support blocked playback: reset everything & return empty block range...
|
|
if (CurrentTimeRange.IsEmpty() || !CurrentPlayer->GetControls().CanControl(EMediaControl::BlockOnFetch))
|
|
{
|
|
LastTimeRange = TRange<FTimespan>::Empty();
|
|
BlockOnRange = TRange<FMediaTimeStamp>::Empty();
|
|
CurrentPlayer->GetControls().SetBlockingPlaybackHint(false);
|
|
return BlockOnRange;
|
|
}
|
|
|
|
EMediaState PlayerState = CurrentPlayer->GetControls().GetState();
|
|
if (PlayerState != EMediaState::Paused && PlayerState != EMediaState::Playing)
|
|
{
|
|
// Return an empty range. Note that the "isSet()" method will still report a set block - so all code will remain in "external clock" mode,
|
|
// but no samples will be requested (and no actual blocking should take place)
|
|
static auto EmptyRange(TRange<FMediaTimeStamp>::Empty());
|
|
return EmptyRange;
|
|
}
|
|
|
|
FTimespan Duration(CurrentPlayer->GetControls().GetDuration());
|
|
FTimespan Start(CurrentTimeRange.GetLowerBoundValue());
|
|
FTimespan End(CurrentTimeRange.GetUpperBoundValue());
|
|
|
|
auto SetBlockOnRange = BlockOnRange;
|
|
|
|
/*
|
|
* On the synthesized sequence and loop index values:
|
|
* - We track seeks and hence can insert the proper seek index easily, although the user does not provide it / does not need to track it
|
|
* - The loop index gets somewhat of a special treatment:
|
|
* -- With tools like Sequencer a blocked range my speed along the timeline quite quickly and if the player is configured as looping it would be expected that the video loops while this is done
|
|
* -- We hence could loop multiple times within a single update interval
|
|
* -- Still we treat any loop (aka: a jump "backwards" without an explicit seek) as a single loop iteration
|
|
* (this is easier for any player to work with and provides the same visual results)
|
|
*/
|
|
|
|
if (!CurrentPlayer->GetControls().IsLooping())
|
|
{
|
|
int64 SequenceIndex = FMediaTimeStamp::MakeSequenceIndex(OnBlockPrimaryIndex, 0);
|
|
BlockOnRange = TRange<FMediaTimeStamp>(FMediaTimeStamp(Start, SequenceIndex), FMediaTimeStamp(End, SequenceIndex));
|
|
}
|
|
else
|
|
{
|
|
// If this would be called very early in the player's startup after open() we would not yet be known... that would be fatal
|
|
/*
|
|
Should this actually happen in real-life applications, we could move the computations here into an accessor method used internally, so that this would be done
|
|
only if data is processed, which would also mean: we know the duration!
|
|
(Exception: live playback! --> but we would not allow blocking there anyway! (makes no sense as real life use case))
|
|
*/
|
|
check(!Duration.IsZero());
|
|
if (Duration.IsZero())
|
|
{
|
|
// Catch if this is called to early and reset blocking...
|
|
BlockOnRange = TRange<FMediaTimeStamp>::Empty();
|
|
CurrentPlayer->GetControls().SetBlockingPlaybackHint(false);
|
|
return BlockOnRange;
|
|
}
|
|
|
|
|
|
float Rate = Facade->GetUnpausedRate();
|
|
|
|
// Modulo on the time to get it into media's range
|
|
// (assumes zero-start-time)
|
|
Start = WrappedModulo(Start, Duration);
|
|
End = WrappedModulo(End, Duration);
|
|
|
|
int64 LoopIdxS = FMath::FloorToInt(CurrentTimeRange.GetLowerBoundValue().GetTotalSeconds() / Duration.GetTotalSeconds());
|
|
int64 LoopIdxE = FMath::FloorToInt(CurrentTimeRange.GetUpperBoundValue().GetTotalSeconds() / Duration.GetTotalSeconds());
|
|
|
|
int32 EndLoopIndex = LoopIdxE - LoopIdxS;
|
|
|
|
// Assemble final blocking range
|
|
auto SeqIndexStart = FMediaTimeStamp::MakeSequenceIndex(OnBlockPrimaryIndex, 0);
|
|
auto SeqIndexEnd = FMediaTimeStamp::MakeSequenceIndex(OnBlockPrimaryIndex, EndLoopIndex);
|
|
BlockOnRange = TRange<FMediaTimeStamp>(FMediaTimeStamp(Start, SeqIndexStart), FMediaTimeStamp(End, SeqIndexEnd));
|
|
check(!BlockOnRange.IsEmpty());
|
|
}
|
|
|
|
// Does the new range overlap with the last one we set?
|
|
if (!SetBlockOnRange.IsEmpty() && SetBlockOnRange.Overlaps(BlockOnRange))
|
|
{
|
|
// Yes, make sure the new range is setup so that it is a "correct" progression given the current playback direction...
|
|
// (this may go so far as to undo any updates if the "new" one does not adds any range in the playback direction)
|
|
if (CurrentPlayer->GetControls().GetRate() >= 0.0f)
|
|
{
|
|
BlockOnRange = TRange<FMediaTimeStamp>(std::max(BlockOnRange.GetLowerBoundValue(), SetBlockOnRange.GetLowerBoundValue()), std::max(BlockOnRange.GetUpperBoundValue(), SetBlockOnRange.GetUpperBoundValue()));
|
|
}
|
|
else
|
|
{
|
|
BlockOnRange = TRange<FMediaTimeStamp>(std::min(BlockOnRange.GetLowerBoundValue(), SetBlockOnRange.GetLowerBoundValue()), std::min(BlockOnRange.GetUpperBoundValue(), SetBlockOnRange.GetUpperBoundValue()));
|
|
}
|
|
}
|
|
|
|
CurrentPlayer->GetControls().SetBlockingPlaybackHint(!BlockOnRange.IsEmpty());
|
|
|
|
LastTimeRange = CurrentTimeRange;
|
|
RangeIsDirty = false;
|
|
|
|
return BlockOnRange;
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::SetCacheWindow(FTimespan Ahead, FTimespan Behind)
|
|
{
|
|
Cache->SetCacheWindow(Ahead, Behind);
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::SetGuid(FGuid& Guid)
|
|
{
|
|
PlayerGuid = Guid;
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::SetLooping(bool Looping)
|
|
{
|
|
return Player.IsValid() && Player->GetControls().SetLooping(Looping);
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::SetMediaOptions(const IMediaOptions* Options)
|
|
{
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::SetRate(float Rate)
|
|
{
|
|
// Enter CS as we change the rate which we read on the tickable thread
|
|
FScopeLock Lock(&CriticalSection);
|
|
|
|
if (!Player.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Is this new rate supported?
|
|
bool bRateOk = true;
|
|
if (Rate != 0.0f && !(Player->GetControls().GetSupportedRates(EMediaRateThinning::Thinned).Contains(Rate) || Player->GetControls().GetSupportedRates(EMediaRateThinning::Unthinned).Contains(Rate)))
|
|
{
|
|
// Pause player instead...
|
|
// (some players may do this as a reaction to the illegal rate anyways - but we need to track the state properly!)
|
|
Rate = 0.0f;
|
|
bRateOk = false;
|
|
}
|
|
|
|
// Attempt to set the rate...
|
|
if (!Player->GetControls().SetRate(Rate))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
|
|
// Any change?
|
|
if (CurrentRate == Rate)
|
|
{
|
|
// no change - just return with ok status
|
|
return bRateOk;
|
|
}
|
|
|
|
// Notify sinks of rate change
|
|
FMediaSampleSinkEventData Data;
|
|
Data.PlaybackRateChanged.PlaybackRate = Rate;
|
|
SendSinkEvent(EMediaSampleSinkEvent::PlaybackRateChanged, Data);
|
|
|
|
if ((LastRate * Rate) < 0.0f)
|
|
{
|
|
Flush(); // direction change
|
|
}
|
|
else
|
|
{
|
|
if (Rate == 0.0f)
|
|
{
|
|
// Invalidate audio time on entering pause mode...
|
|
if (TSharedPtr< FMediaAudioSampleSink, ESPMode::ThreadSafe> AudioSink = PrimaryAudioSink.Pin())
|
|
{
|
|
AudioSink->InvalidateAudioTime();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Track last "unpaused" rate we set
|
|
if (Rate != 0.0)
|
|
{
|
|
LastRate = Rate;
|
|
}
|
|
CurrentRate = Rate;
|
|
|
|
return bRateOk;
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::SetNativeVolume(float Volume)
|
|
{
|
|
if (!Player.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return Player->SetNativeVolume(Volume);
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::SetTrackFormat(EMediaTrackType TrackType, int32 TrackIndex, int32 FormatIndex)
|
|
{
|
|
return Player.IsValid() ? Player->GetTracks().SetTrackFormat((EMediaTrackType)TrackType, TrackIndex, FormatIndex) : false;
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::SetVideoTrackFrameRate(int32 TrackIndex, int32 FormatIndex, float FrameRate)
|
|
{
|
|
return Player.IsValid() ? Player->GetTracks().SetVideoTrackFrameRate(TrackIndex, FormatIndex, FrameRate) : false;
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::SetViewField(float Horizontal, float Vertical, bool Absolute)
|
|
{
|
|
return Player.IsValid() && Player->GetView().SetViewField(Horizontal, Vertical, Absolute);
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::SetViewOrientation(const FQuat& Orientation, bool Absolute)
|
|
{
|
|
return Player.IsValid() && Player->GetView().SetViewOrientation(Orientation, Absolute);
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::SupportsRate(float Rate, bool Unthinned) const
|
|
{
|
|
EMediaRateThinning Thinning = Unthinned ? EMediaRateThinning::Unthinned : EMediaRateThinning::Thinned;
|
|
return Player.IsValid() && Player->GetControls().GetSupportedRates(Thinning).Contains(Rate);
|
|
}
|
|
|
|
void FMediaPlayerFacade::SetLastAudioRenderedSampleTime(FTimespan SampleTime)
|
|
{
|
|
FScopeLock Lock(&LastTimeValuesCS);
|
|
LastAudioRenderedSampleTime.TimeStamp = FMediaTimeStamp(SampleTime);
|
|
LastAudioRenderedSampleTime.SampledAtTime = FPlatformTime::Seconds();
|
|
}
|
|
|
|
FTimespan FMediaPlayerFacade::GetLastAudioRenderedSampleTime() const
|
|
{
|
|
FScopeLock Lock(&LastTimeValuesCS);
|
|
return LastAudioRenderedSampleTime.TimeStamp.Time;
|
|
}
|
|
|
|
void FMediaPlayerFacade::SetAreEventsSafeForAnyThread(bool bInAreEventsSafeForAnyThread)
|
|
{
|
|
bAreEventsSafeForAnyThread = bInAreEventsSafeForAnyThread;
|
|
}
|
|
|
|
/* FMediaPlayerFacade implementation
|
|
*****************************************************************************/
|
|
|
|
bool FMediaPlayerFacade::BlockOnFetch() const
|
|
{
|
|
check(Player.IsValid());
|
|
|
|
const TRange<FMediaTimeStamp> BR(GetAdjustedBlockOnRange());
|
|
|
|
if (BR.IsEmpty() || !Player->GetControls().CanControl(EMediaControl::BlockOnFetch) || BlockOnRangeDisabled || bHaveActiveAudio)
|
|
{
|
|
return false; // no blocking requested / not supported / audio present
|
|
}
|
|
|
|
if (Player->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::UsePlaybackTimingV2))
|
|
{
|
|
//
|
|
// V2 blocking logic
|
|
//
|
|
|
|
// note: with V2 timing we only get here if any current sample is no longer considered "valid" and we didn't so far get a new one that would be
|
|
// --> we do not need to check the actual range here; we only check for exceptions, where we can proceed although we don't have the sample...
|
|
|
|
// The next checks make only sense if the player is done preparing...
|
|
if (!IsPreparing())
|
|
{
|
|
// Looping off?
|
|
if (!Player->GetControls().IsLooping())
|
|
{
|
|
// Yes. Is the sample outside the media's range?
|
|
// (note: this assumes the media starts at time ZERO - this will not be the case at all times (e.g. life playback) -- for now we assume a player will flagged blocked playback as invalid in that case!)
|
|
if (BR.GetUpperBoundValue() < FMediaTimeStamp(FTimespan::Zero(), BR.GetUpperBoundValue().SequenceIndex) || Player->GetControls().GetDuration() <= BR.GetLowerBoundValue().Time)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
// Paused?
|
|
if (CurrentRate == 0.0f)
|
|
{
|
|
// Yes. If we get here the current sample did not satisfy the set range. So only a incoming seek could resolved this...
|
|
if (!SeekTargetTime.IsValid())
|
|
{
|
|
// No, so don't block. This range is impossible to satisfy...
|
|
return false;
|
|
}
|
|
// Would the seek target probably fit the range?
|
|
if (!BR.Overlaps(TRange<FMediaTimeStamp>(SeekTargetTime, SeekTargetTime + BR.Size<FMediaTimeStamp>().Time)))
|
|
{
|
|
// No, so don't block. This range is impossible to satisfy...
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Block until sample arrives!
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// V1 blocking logic
|
|
//
|
|
|
|
if (IsPreparing())
|
|
{
|
|
return true; // block on media opening
|
|
}
|
|
|
|
if (!IsPlaying())
|
|
{
|
|
// no blocking if we are not playing (e.g. paused)
|
|
return false;
|
|
}
|
|
|
|
if (CurrentRate < 0.0f)
|
|
{
|
|
return false; // block only in forward play
|
|
}
|
|
|
|
const bool VideoReady = (VideoSampleSinks.Num() == 0) || (BR.GetUpperBoundValue().Time < NextVideoSampleTime);
|
|
|
|
if (VideoReady)
|
|
{
|
|
return false; // video is ready
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::Flush(bool bExcludePlayer, bool bOnSeek)
|
|
{
|
|
UE_LOG(LogMediaUtils, Verbose, TEXT("PlayerFacade %p: Flushing sinks"), this);
|
|
|
|
FScopeLock Lock(&CriticalSection);
|
|
|
|
auto RawMediaPlayer = MediaPlayer.Get();
|
|
AudioSampleSinks.Flush(RawMediaPlayer);
|
|
CaptionSampleSinks.Flush(RawMediaPlayer);
|
|
MetadataSampleSinks.Flush(RawMediaPlayer);
|
|
SubtitleSampleSinks.Flush(RawMediaPlayer);
|
|
VideoSampleSinks.Flush(RawMediaPlayer);
|
|
|
|
if (Player.IsValid() && !bExcludePlayer)
|
|
{
|
|
Player->GetSamples().FlushSamples();
|
|
}
|
|
|
|
LastAudioRenderedSampleTime.Invalidate();
|
|
if (bOnSeek)
|
|
{
|
|
SeekIndex += (GetUnpausedRate() < 0.0f) ? -1 : 1;
|
|
BlockOnRange.OnSeek(SeekIndex);
|
|
}
|
|
else
|
|
{
|
|
BlockOnRange.OnFlush();
|
|
SeekIndex = 0;
|
|
}
|
|
|
|
// Logically we have no old sample anymore
|
|
// (as in: we will start asking for a new one until we get one - even with a rate of zero, if we had a non-zero one ever before)
|
|
LastVideoSampleProcessedTimeRange = TRange<FMediaTimeStamp>::Empty();
|
|
|
|
// Invalidate next video time to fetch (none-audio case)
|
|
NextEstVideoTimeAtFrameStart.Invalidate();
|
|
// ...and seek target
|
|
SeekTargetTime.Invalidate();
|
|
|
|
// V1 only
|
|
NextVideoSampleTime = FTimespan::MinValue();
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::SendSinkEvent(EMediaSampleSinkEvent Event, const FMediaSampleSinkEventData& Data)
|
|
{
|
|
{
|
|
FScopeLock Lock(&CriticalSection);
|
|
|
|
AudioSampleSinks.ReceiveEvent(Event, Data);
|
|
MetadataSampleSinks.ReceiveEvent(Event, Data);
|
|
}
|
|
|
|
CaptionSampleSinks.ReceiveEvent(Event, Data);
|
|
SubtitleSampleSinks.ReceiveEvent(Event, Data);
|
|
VideoSampleSinks.ReceiveEvent(Event, Data);
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::GetAudioTrackFormat(int32 TrackIndex, int32 FormatIndex, FMediaAudioTrackFormat& OutFormat) const
|
|
{
|
|
if (TrackIndex == INDEX_NONE)
|
|
{
|
|
TrackIndex = GetSelectedTrack(EMediaTrackType::Audio);
|
|
}
|
|
|
|
if (FormatIndex == INDEX_NONE)
|
|
{
|
|
FormatIndex = GetTrackFormat(EMediaTrackType::Audio, TrackIndex);
|
|
}
|
|
|
|
return (Player.IsValid() && Player->GetTracks().GetAudioTrackFormat(TrackIndex, FormatIndex, OutFormat));
|
|
}
|
|
|
|
|
|
IMediaPlayerFactory* FMediaPlayerFacade::GetPlayerFactoryForUrl(const FString& Url, const IMediaOptions* Options) const
|
|
{
|
|
FName PlayerName;
|
|
|
|
if (DesiredPlayerName != NAME_None)
|
|
{
|
|
PlayerName = DesiredPlayerName;
|
|
}
|
|
else if (Options != nullptr)
|
|
{
|
|
PlayerName = Options->GetDesiredPlayerName();
|
|
}
|
|
else
|
|
{
|
|
PlayerName = NAME_None;
|
|
}
|
|
|
|
if (MediaModule == nullptr)
|
|
{
|
|
UE_LOG(LogMediaUtils, Error, TEXT("Failed to load Media module"));
|
|
return nullptr;
|
|
}
|
|
|
|
//
|
|
// Reuse existing player if explicitly requested name matches
|
|
//
|
|
if (Player.IsValid())
|
|
{
|
|
IMediaPlayerFactory* CurrentFactory = MediaModule->GetPlayerFactory(Player->GetPlayerPluginGUID());
|
|
if (PlayerName == CurrentFactory->GetPlayerName())
|
|
{
|
|
return CurrentFactory;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Try to create explicitly requested player
|
|
//
|
|
if (PlayerName != NAME_None)
|
|
{
|
|
IMediaPlayerFactory* Factory = MediaModule->GetPlayerFactory(PlayerName);
|
|
|
|
if (Factory == nullptr)
|
|
{
|
|
UE_LOG(LogMediaUtils, Error, TEXT("Could not find desired player %s for %s"), *PlayerName.ToString(), *Url);
|
|
}
|
|
|
|
return Factory;
|
|
}
|
|
|
|
|
|
|
|
//
|
|
// Try to find a fitting player with no explicit name given
|
|
//
|
|
|
|
|
|
// Can any existing player play the URL?
|
|
if (Player.IsValid())
|
|
{
|
|
IMediaPlayerFactory* Factory = MediaModule->GetPlayerFactory(Player->GetPlayerPluginGUID());
|
|
|
|
if ((Factory != nullptr) && Factory->CanPlayUrl(Url, Options))
|
|
{
|
|
// Yes...
|
|
return Factory;
|
|
}
|
|
}
|
|
|
|
// Try to auto-select new player...
|
|
const FString RunningPlatformName(FPlatformProperties::IniPlatformName());
|
|
const TArray<IMediaPlayerFactory*>& PlayerFactories = MediaModule->GetPlayerFactories();
|
|
|
|
for (IMediaPlayerFactory* Factory : PlayerFactories)
|
|
{
|
|
if (!Factory->SupportsPlatform(RunningPlatformName) || !Factory->CanPlayUrl(Url, Options))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
return Factory;
|
|
}
|
|
|
|
//
|
|
// No suitable player found!
|
|
//
|
|
if (PlayerFactories.Num() > 0)
|
|
{
|
|
UE_LOG(LogMediaUtils, Error, TEXT("Cannot play %s, because none of the enabled media player plug-ins support it:"), *Url);
|
|
|
|
for (IMediaPlayerFactory* Factory : PlayerFactories)
|
|
{
|
|
if (Factory->SupportsPlatform(RunningPlatformName))
|
|
{
|
|
UE_LOG(LogMediaUtils, Log, TEXT("| %s (URI scheme or file extension not supported)"), *Factory->GetPlayerName().ToString());
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogMediaUtils, Log, TEXT("| %s (only available on %s, but not on %s)"), *Factory->GetPlayerName().ToString(), *FString::Join(Factory->GetSupportedPlatforms(), TEXT(", ")), *RunningPlatformName);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogMediaUtils, Error, TEXT("Cannot play %s: no media player plug-ins are installed and enabled in this project"), *Url);
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::GetVideoTrackFormat(int32 TrackIndex, int32 FormatIndex, FMediaVideoTrackFormat& OutFormat) const
|
|
{
|
|
if (TrackIndex == INDEX_NONE)
|
|
{
|
|
TrackIndex = GetSelectedTrack(EMediaTrackType::Video);
|
|
}
|
|
|
|
if (FormatIndex == INDEX_NONE)
|
|
{
|
|
FormatIndex = GetTrackFormat(EMediaTrackType::Video, TrackIndex);
|
|
}
|
|
|
|
return (Player.IsValid() && Player->GetTracks().GetVideoTrackFormat(TrackIndex, FormatIndex, OutFormat));
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::ProcessEvent(EMediaEvent Event, bool bIsBroadcastAllowed)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_MediaUtils_FacadeProcessEvent);
|
|
|
|
if ((Event == EMediaEvent::MediaOpened) || (Event == EMediaEvent::MediaOpenFailed))
|
|
{
|
|
if (Event == EMediaEvent::MediaOpenFailed)
|
|
{
|
|
CurrentUrl.Empty();
|
|
}
|
|
|
|
const FString MediaInfo = Player.IsValid() ? Player->GetInfo() : TEXT("");
|
|
|
|
if (MediaInfo.IsEmpty())
|
|
{
|
|
UE_LOG(LogMediaUtils, Verbose, TEXT("PlayerFacade %p: Media Info: n/a"), this);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogMediaUtils, Verbose, TEXT("PlayerFacade %p: Media Info:\n%s"), this, *MediaInfo);
|
|
}
|
|
}
|
|
else if (Event == EMediaEvent::TracksChanged)
|
|
{
|
|
SelectDefaultTracks();
|
|
|
|
if (Player.IsValid() && !Player->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::UsePlaybackTimingV2))
|
|
{
|
|
// Execute flush for older players only
|
|
Flush();
|
|
}
|
|
}
|
|
else if (Event == EMediaEvent::SeekCompleted)
|
|
{
|
|
// We only consider flushing on seek completion if there is a V1 timing player...
|
|
if (Player.IsValid() && !Player->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::UsePlaybackTimingV2))
|
|
{
|
|
// Does the player want this?
|
|
if (Player->FlushOnSeekCompleted())
|
|
{
|
|
Flush(Player->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::PlayerUsesInternalFlushOnSeek), true);
|
|
}
|
|
}
|
|
}
|
|
else if (Event == EMediaEvent::MediaClosed)
|
|
{
|
|
// Player still closed?
|
|
if (CurrentUrl.IsEmpty())
|
|
{
|
|
// Yes, this also means: if we still have a player, it's still the one this event originated from
|
|
FMediaSampleSinkEventData Data;
|
|
SendSinkEvent(EMediaSampleSinkEvent::MediaClosed, Data);
|
|
|
|
// If player allows: close it down all the way right now
|
|
if (Player.IsValid() && Player->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::AllowShutdownOnClose))
|
|
{
|
|
bDidRecentPlayerHaveError = HasError();
|
|
DestroyPlayer();
|
|
}
|
|
|
|
// Stop issuing audio thread ticks until we open the player again
|
|
MediaModule->GetTicker().RemoveTickable(AsShared());
|
|
}
|
|
}
|
|
else if (Event == EMediaEvent::PlaybackEndReached)
|
|
{
|
|
if (Player.IsValid() && !Player->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::UsePlaybackTimingV2))
|
|
{
|
|
// Execute flush for older players only
|
|
Flush();
|
|
}
|
|
FMediaSampleSinkEventData Data;
|
|
SendSinkEvent(EMediaSampleSinkEvent::PlaybackEndReached, Data);
|
|
}
|
|
|
|
if (bIsBroadcastAllowed)
|
|
{
|
|
MediaEvent.Broadcast(Event);
|
|
}
|
|
else
|
|
{
|
|
QueuedEventBroadcasts.Enqueue(Event);
|
|
}
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::ResetTracks()
|
|
{
|
|
for (int32 Idx = 0; Idx < (int32)EMediaTrackType::Num; ++Idx)
|
|
{
|
|
TrackSelection.UserSelection[Idx] = -1;
|
|
TrackSelection.PlayerSelection[Idx] = -1;
|
|
}
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::SelectDefaultTracks()
|
|
{
|
|
// See if the player has selected appropriate default tracks.
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (CurrentPlayer.IsValid() && CurrentPlayer->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::PlayerSelectsDefaultTracks))
|
|
{
|
|
ResetTracks();
|
|
// Get what the player has selected as user defaults.
|
|
// The TrackSelection.PlayerSelection[...] will be updated in UpdateTrackSelectionWithPlayer()
|
|
// where the existence of sinks is checked for.
|
|
IMediaTracks& Tracks = CurrentPlayer->GetTracks();
|
|
for(int32 Idx=0; Idx<(int32)EMediaTrackType::Num; ++Idx)
|
|
{
|
|
TrackSelection.UserSelection[Idx] = Tracks.GetSelectedTrack((EMediaTrackType)Idx);
|
|
}
|
|
// If overrides are set, use them.
|
|
if (ActivePlayerOptions.IsSet())
|
|
{
|
|
FMediaPlayerTrackOptions TrackOptions;
|
|
TrackOptions = ActivePlayerOptions.GetValue().Tracks;
|
|
TrackSelection.UserSelection[(int32)EMediaTrackType::Audio] = TrackOptions.Audio;
|
|
TrackSelection.UserSelection[(int32)EMediaTrackType::Caption] = TrackOptions.Caption;
|
|
TrackSelection.UserSelection[(int32)EMediaTrackType::Metadata] = TrackOptions.Metadata;
|
|
TrackSelection.UserSelection[(int32)EMediaTrackType::Subtitle] = TrackOptions.Subtitle;
|
|
TrackSelection.UserSelection[(int32)EMediaTrackType::Video] = TrackOptions.Video;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FMediaPlayerTrackOptions TrackOptions;
|
|
if (ActivePlayerOptions.IsSet())
|
|
{
|
|
TrackOptions = ActivePlayerOptions.GetValue().Tracks;
|
|
}
|
|
|
|
TrackSelection.UserSelection[(int32)EMediaTrackType::Audio] = TrackOptions.Audio;
|
|
TrackSelection.UserSelection[(int32)EMediaTrackType::Caption] = TrackOptions.Caption;
|
|
TrackSelection.UserSelection[(int32)EMediaTrackType::Metadata] = TrackOptions.Metadata;
|
|
TrackSelection.UserSelection[(int32)EMediaTrackType::Subtitle] = TrackOptions.Subtitle;
|
|
TrackSelection.UserSelection[(int32)EMediaTrackType::Video] = TrackOptions.Video;
|
|
}
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::SelectTrack(EMediaTrackType TrackType, int32 TrackIndex)
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (CurrentPlayer.IsValid())
|
|
{
|
|
IMediaTracks& Tracks = CurrentPlayer->GetTracks();
|
|
|
|
if (Tracks.GetNumTracks(TrackType) > TrackIndex)
|
|
{
|
|
TrackSelection.UserSelection[(int32)TrackType] = TrackIndex;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
int32 FMediaPlayerFacade::GetSelectedTrack(EMediaTrackType TrackType) const
|
|
{
|
|
return TrackSelection.UserSelection[(int32)TrackType];
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::UpdateTrackSelectionWithPlayer()
|
|
{
|
|
check(Player.IsValid());
|
|
|
|
bool bChanges = false;
|
|
|
|
IMediaTracks& Tracks = Player->GetTracks();
|
|
for (int32 Idx = 0; Idx < (int32)EMediaTrackType::Num; ++Idx)
|
|
{
|
|
// Player and user selection are different?
|
|
if (TrackSelection.PlayerSelection[Idx] != TrackSelection.UserSelection[Idx])
|
|
{
|
|
// Yes...
|
|
int32 UserSelection = TrackSelection.UserSelection[Idx];
|
|
|
|
// Filter selection against the configured sinks...
|
|
if (UserSelection != -1)
|
|
{
|
|
if ((Idx == (int)EMediaTrackType::Audio && !PrimaryAudioSink.IsValid()) ||
|
|
(Idx == (int)EMediaTrackType::Video && VideoSampleSinks.IsEmpty()) ||
|
|
(Idx == (int)EMediaTrackType::Caption && CaptionSampleSinks.IsEmpty()) ||
|
|
(Idx == (int)EMediaTrackType::Subtitle && SubtitleSampleSinks.IsEmpty()) ||
|
|
(Idx == (int)EMediaTrackType::Metadata && MetadataSampleSinks.IsEmpty()))
|
|
{
|
|
UserSelection = -1;
|
|
}
|
|
}
|
|
|
|
// After filtering the user's selection, do we still have to change things?
|
|
if (TrackSelection.PlayerSelection[Idx] != UserSelection)
|
|
{
|
|
// Yes!
|
|
if (Tracks.SelectTrack((EMediaTrackType)Idx, UserSelection))
|
|
{
|
|
// Recall what is now selected with the player...
|
|
TrackSelection.PlayerSelection[Idx] = UserSelection;
|
|
|
|
bChanges = true;
|
|
}
|
|
else
|
|
{
|
|
// Track selection failed. Patch the user selection to be what we know of the player's, so we do not reattempt this over and over...
|
|
TrackSelection.UserSelection[Idx] = TrackSelection.PlayerSelection[Idx];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bChanges)
|
|
{
|
|
if (!Player->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::IsTrackSwitchSeamless))
|
|
{
|
|
Flush();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
float FMediaPlayerFacade::GetUnpausedRate() const
|
|
{
|
|
return (CurrentRate == 0.0f) ? LastRate : CurrentRate;
|
|
}
|
|
|
|
|
|
/* IMediaClockSink interface
|
|
*****************************************************************************/
|
|
|
|
void FMediaPlayerFacade::TickInput(FTimespan DeltaTime, FTimespan Timecode)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_MediaUtils_FacadeTickInput);
|
|
|
|
if (Player.IsValid())
|
|
{
|
|
UpdateTrackSelectionWithPlayer();
|
|
MonitorAudioEnablement();
|
|
|
|
Player->TickInput(DeltaTime, Timecode);
|
|
|
|
bool bIsBroadcastAllowed = bAreEventsSafeForAnyThread || IsInGameThread();
|
|
if (Player->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::UsePlaybackTimingV2))
|
|
{
|
|
//
|
|
// New timing control (handled before any engine world, object etc. updates; so "all frame" (almost) see the state produced here)
|
|
//
|
|
|
|
// process deferred events
|
|
// NOTE: if there is no player anymore we execute the remaining queued events in TickFetch (backwards compatibility - should move here once V1 support removed)
|
|
EMediaEvent Event;
|
|
if (bIsBroadcastAllowed)
|
|
{
|
|
while (QueuedEventBroadcasts.Dequeue(Event))
|
|
{
|
|
MediaEvent.Broadcast(Event);
|
|
}
|
|
}
|
|
while (QueuedEvents.Dequeue(Event))
|
|
{
|
|
ProcessEvent(Event, bIsBroadcastAllowed);
|
|
}
|
|
|
|
// Handling events may have killed the player. Did it?
|
|
if (!Player.IsValid())
|
|
{
|
|
// If so: nothing more to do!
|
|
return;
|
|
}
|
|
|
|
if (bIsSinkFlushPending)
|
|
{
|
|
bIsSinkFlushPending = false;
|
|
Flush();
|
|
}
|
|
|
|
//
|
|
// Setup timing for sample processing
|
|
//
|
|
PreSampleProcessingTimeHandling();
|
|
|
|
TRange<FMediaTimeStamp> TimeRange;
|
|
if (!GetCurrentPlaybackTimeRange(TimeRange, CurrentRate, DeltaTime, false))
|
|
{
|
|
return;
|
|
}
|
|
|
|
SET_FLOAT_STAT(STAT_MediaUtils_FacadeTime, TimeRange.GetLowerBoundValue().Time.GetTotalSeconds());
|
|
|
|
//
|
|
// Process samples in range
|
|
//
|
|
IMediaSamples& Samples = Player->GetSamples();
|
|
|
|
double BlockingStart = FPlatformTime::Seconds();
|
|
while (1)
|
|
{
|
|
ProcessCaptionSamples(Samples, TimeRange);
|
|
ProcessSubtitleSamples(Samples, TimeRange);
|
|
|
|
if (ProcessVideoSamples(Samples, TimeRange))
|
|
{
|
|
// We either got a new sample or a current one is still the best choice...
|
|
break;
|
|
}
|
|
|
|
// The current one is outdated and no new one was delivered. Should we block for one?
|
|
if (!BlockOnFetch())
|
|
{
|
|
// No... continue...
|
|
break;
|
|
}
|
|
|
|
// Issue tick call with dummy timing as some players advance some state in the tick, which we wait for
|
|
Player->TickInput(FTimespan::Zero(), FTimespan::MinValue());
|
|
|
|
// Monitor / update seek status
|
|
UpdateSeekStatus();
|
|
|
|
// Process deferred events & check for events that break the block
|
|
bool bEventCancelsBlock = false;
|
|
while (QueuedEvents.Dequeue(Event))
|
|
{
|
|
if (Event == EMediaEvent::MediaClosed || Event == EMediaEvent::MediaOpenFailed)
|
|
{
|
|
bEventCancelsBlock = true;
|
|
}
|
|
ProcessEvent(Event, bIsBroadcastAllowed);
|
|
}
|
|
|
|
// We might have lost the player during event handling or an event breaks the block...
|
|
if (!Player.IsValid() || bEventCancelsBlock)
|
|
{
|
|
// Disable blocking feature for now (a new open would reset this)
|
|
UE_LOG(LogMediaUtils, Warning, TEXT("Blocking media playback closed or failed. Disabling it for this playback session."));
|
|
BlockOnRangeDisabled = true;
|
|
break;
|
|
}
|
|
|
|
// Timeout?
|
|
if ((FPlatformTime::Seconds() - BlockingStart) > MEDIAUTILS_MAX_BLOCKONFETCH_SECONDS)
|
|
{
|
|
FString Url;
|
|
#if !UE_BUILD_SHIPPING
|
|
Url = Player->GetUrl();
|
|
#endif // !UE_BUILD_SHIPPING
|
|
UE_LOG(LogMediaUtils, Error, TEXT("Blocking media playback timed out. Disabling it for this playback session. URL:%s"),
|
|
*Url);
|
|
BlockOnRangeDisabled = true;
|
|
break;
|
|
}
|
|
|
|
FPlatformProcess::Sleep(0.0f);
|
|
}
|
|
|
|
SET_DWORD_STAT(STAT_MediaUtils_FacadeNumVideoSamples, Samples.NumVideoSamples());
|
|
|
|
//
|
|
// Advance timing etc.
|
|
//
|
|
PostSampleProcessingTimeHandling(DeltaTime);
|
|
|
|
if (bHaveActiveAudio)
|
|
{
|
|
// Keep currently last processed audio sample timestamp available for all frame (to provide consistent info)
|
|
FScopeLock Lock(&LastTimeValuesCS);
|
|
CurrentFrameAudioTimeStamp = LastAudioSampleProcessedTime.TimeStamp;
|
|
}
|
|
}
|
|
|
|
// Check if primary audio sink needs a change and make sure invalid sinks are purged at all times
|
|
PrimaryAudioSink = AudioSampleSinks.GetPrimaryAudioSink();
|
|
}
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::TickFetch(FTimespan DeltaTime, FTimespan Timecode)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_MediaUtils_FacadeTickFetch);
|
|
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
// Send out deferred broadcasts.
|
|
EMediaEvent Event;
|
|
bool bIsBroadcastAllowed = bAreEventsSafeForAnyThread || IsInGameThread();
|
|
if (bIsBroadcastAllowed)
|
|
{
|
|
while (QueuedEventBroadcasts.Dequeue(Event))
|
|
{
|
|
MediaEvent.Broadcast(Event);
|
|
}
|
|
}
|
|
|
|
// process deferred events
|
|
while (QueuedEvents.Dequeue(Event))
|
|
{
|
|
ProcessEvent(Event, bIsBroadcastAllowed);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!CurrentPlayer->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::UsePlaybackTimingV2))
|
|
{
|
|
//
|
|
// Old timing control
|
|
//
|
|
|
|
// let the player generate samples & process events
|
|
CurrentPlayer->TickFetch(DeltaTime, Timecode);
|
|
|
|
{
|
|
// process deferred events
|
|
EMediaEvent Event;
|
|
while (QueuedEvents.Dequeue(Event))
|
|
{
|
|
ProcessEvent(Event, true);
|
|
}
|
|
}
|
|
|
|
TRange<FTimespan> TimeRange;
|
|
|
|
const FTimespan CurrentTime = GetTime();
|
|
|
|
SET_FLOAT_STAT(STAT_MediaUtils_FacadeTime, CurrentTime.GetTotalSeconds());
|
|
|
|
// get current play rate
|
|
float Rate = GetUnpausedRate();
|
|
|
|
if (Rate > 0.0f)
|
|
{
|
|
TimeRange = TRange<FTimespan>::AtMost(CurrentTime);
|
|
}
|
|
else if (Rate < 0.0f)
|
|
{
|
|
TimeRange = TRange<FTimespan>::AtLeast(CurrentTime);
|
|
}
|
|
else
|
|
{
|
|
TimeRange = TRange<FTimespan>(CurrentTime);
|
|
}
|
|
|
|
// process samples in range
|
|
IMediaSamples& Samples = CurrentPlayer->GetSamples();
|
|
|
|
bool Blocked = false;
|
|
FDateTime BlockedTime;
|
|
|
|
while (true)
|
|
{
|
|
ProcessCaptionSamplesV1(Samples, TimeRange);
|
|
ProcessSubtitleSamplesV1(Samples, TimeRange);
|
|
ProcessVideoSamplesV1(Samples, TimeRange);
|
|
|
|
if (!BlockOnFetch())
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (Blocked)
|
|
{
|
|
if ((FDateTime::UtcNow() - BlockedTime) >= FTimespan::FromSeconds(MEDIAUTILS_MAX_BLOCKONFETCH_SECONDS))
|
|
{
|
|
UE_LOG(LogMediaUtils, Verbose, TEXT("PlayerFacade %p: Aborted block on fetch %s after %i seconds"),
|
|
this,
|
|
*BlockOnRange.GetRange().GetLowerBoundValue().Time.ToString(TEXT("%h:%m:%s.%t")),
|
|
MEDIAUTILS_MAX_BLOCKONFETCH_SECONDS
|
|
);
|
|
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade %p: Blocking on fetch %s"), this, *BlockOnRange.GetRange().GetLowerBoundValue().Time.ToString(TEXT("%h:%m:%s.%t")));
|
|
|
|
Blocked = true;
|
|
BlockedTime = FDateTime::UtcNow();
|
|
}
|
|
|
|
FPlatformProcess::Sleep(0.0f);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::TickOutput(FTimespan DeltaTime, FTimespan /*Timecode*/)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_MediaUtils_FacadeTickOutput);
|
|
|
|
if (!Player.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
Cache->Tick(DeltaTime, CurrentRate, GetTime());
|
|
}
|
|
|
|
|
|
/* IMediaTickable interface
|
|
*****************************************************************************/
|
|
|
|
void FMediaPlayerFacade::TickTickable()
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_MediaUtils_FacadeTickTickable);
|
|
|
|
FScopeLock Lock(&CriticalSection);
|
|
|
|
if (!Player.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
float Rate = GetUnpausedRate();
|
|
if (Rate == 0.0f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
{
|
|
FScopeLock Lock1(&LastTimeValuesCS);
|
|
Player->SetLastAudioRenderedSampleTime(LastAudioRenderedSampleTime.TimeStamp.Time);
|
|
}
|
|
|
|
Player->TickAudio();
|
|
|
|
// determine range of valid samples
|
|
|
|
// process samples in range
|
|
IMediaSamples& Samples = Player->GetSamples();
|
|
|
|
if (Player->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::UsePlaybackTimingV2))
|
|
{
|
|
const FMediaTimeStamp Time = GetTimeStamp();
|
|
|
|
auto TimeRange = TRange<FMediaTimeStamp>::Inclusive(FMediaTimeStamp(FTimespan::MinValue(), MIN_int32), Time + MediaPlayerFacade::MetadataPreroll);
|
|
|
|
ProcessAudioSamples(Samples, TRange<FMediaTimeStamp>());
|
|
ProcessMetadataSamples(Samples, TimeRange);
|
|
}
|
|
else
|
|
{
|
|
TRange<FTimespan> AudioTimeRange;
|
|
TRange<FTimespan> MetadataTimeRange;
|
|
|
|
const FTimespan Time = GetTime();
|
|
|
|
if (Rate >= 0.0f)
|
|
{
|
|
AudioTimeRange = TRange<FTimespan>::Inclusive(FTimespan::MinValue(), Time + MediaPlayerFacade::AudioPreroll);
|
|
MetadataTimeRange = TRange<FTimespan>::Inclusive(FTimespan::MinValue(), Time + MediaPlayerFacade::MetadataPreroll);
|
|
}
|
|
else
|
|
{
|
|
AudioTimeRange = TRange<FTimespan>::Inclusive(Time - MediaPlayerFacade::AudioPreroll, FTimespan::MaxValue());
|
|
MetadataTimeRange = TRange<FTimespan>::Inclusive(Time - MediaPlayerFacade::MetadataPreroll, FTimespan::MaxValue());
|
|
}
|
|
|
|
ProcessAudioSamplesV1(Samples, AudioTimeRange);
|
|
ProcessMetadataSamplesV1(Samples, MetadataTimeRange);
|
|
}
|
|
|
|
SET_DWORD_STAT(STAT_MediaUtils_FacadeNumAudioSamples, Samples.NumAudio());
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::UpdateSeekStatus(const FMediaTimeStamp* pCheckTimeStamp)
|
|
{
|
|
check(Player.IsValid());
|
|
|
|
FScopeLock Lock(&CriticalSection);
|
|
|
|
if (HaveVideoPlayback())
|
|
{
|
|
if (SeekTargetTime.IsValid())
|
|
{
|
|
// Either peek for the newest available sample or take a given timestamp to check against
|
|
FMediaTimeStamp VideoTimeStamp;
|
|
if (pCheckTimeStamp)
|
|
{
|
|
VideoTimeStamp = *pCheckTimeStamp;
|
|
}
|
|
else
|
|
{
|
|
Player->GetSamples().PeekVideoSampleTime(VideoTimeStamp);
|
|
}
|
|
|
|
if (VideoTimeStamp.IsValid())
|
|
{
|
|
bool bRunningNonAudioClock = bHaveActiveAudio && !BlockOnRange.IsSet();
|
|
|
|
if (GetUnpausedRate() >= 0.0f)
|
|
{
|
|
// See if we already are looking at a sample from the target sequence index...
|
|
// (we are not checking for the precise location as some players might not able to deliver it)
|
|
if (FMediaTimeStamp::GetPrimaryIndex(VideoTimeStamp.SequenceIndex) < FMediaTimeStamp::GetPrimaryIndex(SeekTargetTime.SequenceIndex))
|
|
{
|
|
// No. Make sure we drop the sample & possible more up to the seek target (we use the fetch code to avoid any races with a old-sample-purge logic operating async)
|
|
Player->GetSamples().DiscardVideoSamples(TRange<FMediaTimeStamp>(VideoTimeStamp, SeekTargetTime), false);
|
|
}
|
|
else
|
|
{
|
|
// We have reached the sequence of the seek target, reset everything to normal operation...
|
|
// (we do not care if we reached the precise location beyond the index as we do not know if we even can)
|
|
if (bRunningNonAudioClock)
|
|
{
|
|
NextEstVideoTimeAtFrameStart = FMediaTimeStampSample(VideoTimeStamp, FPlatformTime::Seconds());
|
|
}
|
|
|
|
FScopeLock LockLT(&LastTimeValuesCS);
|
|
|
|
// Update the display version of then "current frame time" right now (to avoid any glitches as it can take a little while for the frame to actually change)
|
|
CurrentFrameVideoDisplayTimeStamp = SeekTargetTime;
|
|
|
|
// Seeking done
|
|
SeekTargetTime.Invalidate();
|
|
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// See if we already are looking at a sample from the target sequence index...
|
|
// (we are not checking for the precise location as some players might not able to deliver it)
|
|
if (FMediaTimeStamp::GetPrimaryIndex(VideoTimeStamp.SequenceIndex) > FMediaTimeStamp::GetPrimaryIndex(SeekTargetTime.SequenceIndex))
|
|
{
|
|
// No. Make sure we drop the sample & possible more up to the seek target (we use the fetch code to avoid any races with a old-sample-purge logic operating async)
|
|
Player->GetSamples().DiscardVideoSamples(TRange<FMediaTimeStamp>(VideoTimeStamp, SeekTargetTime), true);
|
|
}
|
|
else
|
|
{
|
|
// We have reached the sequence of the seek target, reset everything to normal operation...
|
|
// (we do not care if we reached the precise location beyond the index as we do not know if we even can)
|
|
if (bRunningNonAudioClock)
|
|
{
|
|
NextEstVideoTimeAtFrameStart = FMediaTimeStampSample(VideoTimeStamp, FPlatformTime::Seconds());
|
|
}
|
|
|
|
FScopeLock LockLT(&LastTimeValuesCS);
|
|
SeekTargetTime.Invalidate();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (bHaveActiveAudio)
|
|
{
|
|
FScopeLock LockLT(&LastTimeValuesCS);
|
|
if (CurrentFrameAudioTimeStamp >= SeekTargetTime)
|
|
{
|
|
SeekTargetTime.Invalidate();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Neither audio nor video are presently active. We just assume we reached the seek target and continue...
|
|
// (we currently have no other source of a current sample timestamp)
|
|
SeekTargetTime.Invalidate();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::MonitorAudioEnablement()
|
|
{
|
|
// Update flag reflecting presence of audio in the current stream
|
|
// (doing it just once per gameloop is enough)
|
|
bool bHadActiveAudio = bHaveActiveAudio;
|
|
bHaveActiveAudio = HaveAudioPlayback();
|
|
if (bHadActiveAudio && !bHaveActiveAudio)
|
|
{
|
|
// Reset state for dt-based playback so we grab a new PTS value immediately
|
|
NextEstVideoTimeAtFrameStart.Invalidate();
|
|
}
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::PreSampleProcessingTimeHandling()
|
|
{
|
|
check(Player.IsValid());
|
|
|
|
FScopeLock Lock(&CriticalSection);
|
|
|
|
UpdateSeekStatus();
|
|
|
|
// No seeking?
|
|
if (!SeekTargetTime.IsValid())
|
|
{
|
|
// No seek pending & not paused. Can we / Do we need to prime a non-audio clock?
|
|
if (!bHaveActiveAudio && !BlockOnRange.IsSet() && !NextEstVideoTimeAtFrameStart.IsValid())
|
|
{
|
|
FMediaTimeStamp VideoTimeStamp;
|
|
if (Player->GetSamples().PeekVideoSampleTime(VideoTimeStamp))
|
|
{
|
|
NextEstVideoTimeAtFrameStart = FMediaTimeStampSample(VideoTimeStamp, FPlatformTime::Seconds());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::PostSampleProcessingTimeHandling(FTimespan DeltaTime)
|
|
{
|
|
check(Player.IsValid());
|
|
|
|
float Rate = CurrentRate;
|
|
|
|
// No Audio clock?
|
|
if (!bHaveActiveAudio)
|
|
{
|
|
// No external clock? (blocking)
|
|
if (!BlockOnRange.IsSet())
|
|
{
|
|
// Move video frame start estimate forward
|
|
// (the initial NextEstVideoTimeAtFrameStart will never be valid if no video is present)
|
|
if (NextEstVideoTimeAtFrameStart.IsValid())
|
|
{
|
|
if (Player->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::UseRealtimeWithVideoOnly))
|
|
{
|
|
double NewBaseTime = FPlatformTime::Seconds();
|
|
NextEstVideoTimeAtFrameStart.TimeStamp.Time += FMath::TruncToInt64((NewBaseTime - NextEstVideoTimeAtFrameStart.SampledAtTime) * Rate);
|
|
NextEstVideoTimeAtFrameStart.SampledAtTime = NewBaseTime;
|
|
}
|
|
else
|
|
{
|
|
NextEstVideoTimeAtFrameStart.TimeStamp.Time += DeltaTime * Rate;
|
|
}
|
|
|
|
// note: infinite duration (e.g. live playback - or players not yet supporting sequence indices on loops, when looping is enabled)
|
|
// -> no need for special handling as FTimespan::MaxValue() is expected to be returned to signify this, which is quite "infinite" in practical terms
|
|
FTimespan Duration = Player->GetControls().GetDuration();
|
|
|
|
if (Player->GetControls().IsLooping())
|
|
{
|
|
if (Rate >= 0.0f)
|
|
{
|
|
while (NextEstVideoTimeAtFrameStart.TimeStamp.Time >= Duration)
|
|
{
|
|
NextEstVideoTimeAtFrameStart.TimeStamp.Time -= Duration;
|
|
NextEstVideoTimeAtFrameStart.TimeStamp.SequenceIndex = FMediaTimeStamp::AdjustSecondaryIndex(NextEstVideoTimeAtFrameStart.TimeStamp.SequenceIndex, 1);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
while (NextEstVideoTimeAtFrameStart.TimeStamp.Time < FTimespan::Zero())
|
|
{
|
|
NextEstVideoTimeAtFrameStart.TimeStamp.Time += Duration;
|
|
NextEstVideoTimeAtFrameStart.TimeStamp.SequenceIndex = FMediaTimeStamp::AdjustSecondaryIndex(NextEstVideoTimeAtFrameStart.TimeStamp.SequenceIndex, -1);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (Rate >= 0.0f)
|
|
{
|
|
if (NextEstVideoTimeAtFrameStart.TimeStamp.Time >= Duration)
|
|
{
|
|
NextEstVideoTimeAtFrameStart.TimeStamp.Time = Duration - FTimespan::FromSeconds(0.0001);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (NextEstVideoTimeAtFrameStart.TimeStamp.Time < FTimespan::Zero())
|
|
{
|
|
NextEstVideoTimeAtFrameStart.TimeStamp.Time = FTimespan::Zero();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::GetCurrentPlaybackTimeRange(TRange<FMediaTimeStamp>& TimeRange, float Rate, FTimespan DeltaTime, bool bPurgeSampleRelated) const
|
|
{
|
|
/*
|
|
* Note: while a seek operation is still in progress (no sample from target location has been processed) this will
|
|
* return on an empty time range.
|
|
*/
|
|
check(Player->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::UsePlaybackTimingV2));
|
|
|
|
TSharedPtr<FMediaAudioSampleSink, ESPMode::ThreadSafe> AudioSink = PrimaryAudioSink.Pin();
|
|
|
|
if (bHaveActiveAudio && AudioSink.IsValid())
|
|
{
|
|
//
|
|
// Audio is available...
|
|
//
|
|
|
|
FMediaTimeStampSample AudioTime = AudioSink->GetAudioTime();
|
|
if (!AudioTime.IsValid())
|
|
{
|
|
if (!bPurgeSampleRelated)
|
|
{
|
|
// If paused and not seeking, make sure we get one sample nonetheless...
|
|
if (Rate == 0.0f && !SeekTargetTime.IsValid())
|
|
{
|
|
// Do this once after open / seek...
|
|
if (LastVideoSampleProcessedTimeRange.IsEmpty())
|
|
{
|
|
// Use the video sample timestamp for simplicity (although we otherwise sync with audio timestamps)
|
|
FMediaTimeStamp TimeStamp;
|
|
if (Player->GetSamples().PeekVideoSampleTime(TimeStamp))
|
|
{
|
|
TimeRange = TRange<FMediaTimeStamp>(TimeStamp, TimeStamp + DeltaTime);
|
|
return !TimeRange.IsEmpty();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// No timing info available, no time range available, no samples to process
|
|
return false;
|
|
}
|
|
|
|
FMediaTimeStamp EstAudioTimeAtFrameStart;
|
|
|
|
double Now = FPlatformTime::Seconds();
|
|
|
|
if (!bPurgeSampleRelated)
|
|
{
|
|
// Normal estimation relative to current frame start...
|
|
// (on gamethread operation)
|
|
|
|
check(IsInGameThread() || IsInSlateThread());
|
|
|
|
double AgeOfFrameStart = Now - MediaModule->GetFrameStartTime();
|
|
double AgeOfAudioTime = Now - AudioTime.SampledAtTime;
|
|
|
|
if (AgeOfFrameStart >= 0.0 && AgeOfFrameStart <= kMaxTimeSinceFrameStart &&
|
|
AgeOfAudioTime >= 0.0 && AgeOfAudioTime <= kMaxTimeSinceAudioTimeSampling)
|
|
{
|
|
// All realtime timestamps seem in sane ranges - we most likely did not have a lengthy interruption (suspended / debugging step)
|
|
EstAudioTimeAtFrameStart = AudioTime.TimeStamp + FTimespan::FromSeconds((MediaModule->GetFrameStartTime() - AudioTime.SampledAtTime) * Rate);
|
|
}
|
|
else
|
|
{
|
|
// Realtime timestamps seem wonky. Proceed without them (worse estimation quality)
|
|
EstAudioTimeAtFrameStart = AudioTime.TimeStamp;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Do not use frame start reference -> we compute relative to "now"
|
|
// (for use off gamethread)
|
|
EstAudioTimeAtFrameStart = AudioTime.TimeStamp + FTimespan::FromSeconds((Now - AudioTime.SampledAtTime) * Rate);
|
|
}
|
|
|
|
// Are we paused?
|
|
if (Rate == 0.0f)
|
|
{
|
|
// Yes. We need to fetch a frame for the current display frame - once. Asking over and over until we get one...
|
|
if (LastVideoSampleProcessedTimeRange.IsEmpty())
|
|
{
|
|
// We simply fake the rate to the last non-zero or 1.0 to fetch a frame fitting the time frame representing the whole current frame
|
|
Rate = (LastRate == 0.0f) ? 1.0f : LastRate;
|
|
}
|
|
}
|
|
|
|
TimeRange = TRange<FMediaTimeStamp>(EstAudioTimeAtFrameStart, EstAudioTimeAtFrameStart + DeltaTime * FGenericPlatformMath::Abs(Rate));
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// No Audio (no data and/or no sink)
|
|
//
|
|
if (!BlockOnRange.IsSet())
|
|
{
|
|
//
|
|
// Internal clock (DT based)
|
|
//
|
|
|
|
// Do we now have a current timestamp estimation?
|
|
if (!NextEstVideoTimeAtFrameStart.IsValid())
|
|
{
|
|
// No timing info available, no time range available, no samples to process
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
// Yes. Setup current time range & advance time estimation...
|
|
|
|
// Are we paused?
|
|
if (Rate == 0.0f)
|
|
{
|
|
// Yes. We need to fetch a frame for the current display frame - once. Asking over and over until we get one...
|
|
if (LastVideoSampleProcessedTimeRange.IsEmpty())
|
|
{
|
|
// We simply fake the rate to the last non-zero or 1.0 to fetch a frame fitting the time frame representing the whole current frame
|
|
Rate = (LastRate == 0.0f) ? 1.0f : LastRate;
|
|
}
|
|
}
|
|
|
|
TimeRange = TRange<FMediaTimeStamp>(NextEstVideoTimeAtFrameStart.TimeStamp, NextEstVideoTimeAtFrameStart.TimeStamp + DeltaTime * FGenericPlatformMath::Abs(Rate));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// External clock delivers time-range
|
|
// (for now we just use the blocking time range as this clock type is solely used in that case)
|
|
//
|
|
TimeRange = GetAdjustedBlockOnRange();
|
|
}
|
|
}
|
|
|
|
if (TimeRange.IsEmpty())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// If we are looping we check to prepare proper ranges should we wrap around either end of the media...
|
|
// (we do not clamp in the non-looping case as the rest of the code should deal with that fine)
|
|
if (Player->GetControls().IsLooping())
|
|
{
|
|
// Can the player already hand out a duration? (we might get here very early on)
|
|
const FTimespan Duration = Player->GetControls().GetDuration();
|
|
if (Duration != FTimespan::Zero())
|
|
{
|
|
FTimespan WrappedStart = WrappedModulo(TimeRange.GetLowerBoundValue().Time, Duration);
|
|
FTimespan WrappedEnd = WrappedModulo(TimeRange.GetUpperBoundValue().Time, Duration);
|
|
if (WrappedStart > WrappedEnd)
|
|
{
|
|
if (WrappedStart != TimeRange.GetLowerBoundValue().Time)
|
|
{
|
|
TimeRange.SetLowerBoundValue(FMediaTimeStamp(WrappedStart, FMediaTimeStamp::AdjustSecondaryIndex(TimeRange.GetLowerBoundValue().SequenceIndex, -1)));
|
|
}
|
|
if (WrappedEnd != TimeRange.GetUpperBoundValue().Time)
|
|
{
|
|
TimeRange.SetUpperBoundValue(FMediaTimeStamp(WrappedEnd, FMediaTimeStamp::AdjustSecondaryIndex(TimeRange.GetUpperBoundValue().SequenceIndex, 1)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return !TimeRange.IsEmpty();
|
|
}
|
|
|
|
|
|
TRange<FMediaTimeStamp> FMediaPlayerFacade::GetAdjustedBlockOnRange() const
|
|
{
|
|
TRange<FMediaTimeStamp> TimeRange = BlockOnRange.GetRange();
|
|
|
|
// The blocking range returned will base any secondary index values on zero. We need to patch in the secondary index value as last returned by the player plugin
|
|
if (!LastVideoSampleProcessedTimeRange.IsEmpty())
|
|
{
|
|
int32 BaseLoopIndex = FMediaTimeStamp::GetSecondaryIndex(LastVideoSampleProcessedTimeRange.GetLowerBoundValue().SequenceIndex);
|
|
|
|
// We still might have a time that is before the current one, in which case we need to push the loop index up (or down)...
|
|
if (GetUnpausedRate() >= 0.0f)
|
|
{
|
|
if (LastVideoSampleProcessedTimeRange.GetLowerBoundValue() > FMediaTimeStamp(TimeRange.GetLowerBoundValue().Time, FMediaTimeStamp::AdjustSecondaryIndex(TimeRange.GetLowerBoundValue().SequenceIndex, BaseLoopIndex)))
|
|
{
|
|
++BaseLoopIndex;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (LastVideoSampleProcessedTimeRange.GetLowerBoundValue() < FMediaTimeStamp(TimeRange.GetLowerBoundValue().Time, FMediaTimeStamp::AdjustSecondaryIndex(TimeRange.GetLowerBoundValue().SequenceIndex, BaseLoopIndex)))
|
|
{
|
|
--BaseLoopIndex;
|
|
}
|
|
}
|
|
|
|
TimeRange.SetLowerBoundValue(FMediaTimeStamp(TimeRange.GetLowerBoundValue().Time, FMediaTimeStamp::AdjustSecondaryIndex(TimeRange.GetLowerBoundValue().SequenceIndex, BaseLoopIndex)));
|
|
TimeRange.SetUpperBoundValue(FMediaTimeStamp(TimeRange.GetUpperBoundValue().Time, FMediaTimeStamp::AdjustSecondaryIndex(TimeRange.GetUpperBoundValue().SequenceIndex, BaseLoopIndex)));
|
|
}
|
|
|
|
return TimeRange;
|
|
}
|
|
|
|
|
|
/* FMediaPlayerFacade implementation
|
|
*****************************************************************************/
|
|
|
|
void FMediaPlayerFacade::ProcessAudioSamples(IMediaSamples& Samples, const TRange<FMediaTimeStamp>& TimeRange)
|
|
{
|
|
TSharedPtr<IMediaAudioSample, ESPMode::ThreadSafe> Sample;
|
|
|
|
check(Player->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::UsePlaybackTimingV2));
|
|
|
|
// For V2 we basically expect to get no timerange at all: totally open
|
|
// (we just have it around to be compatible / use older code that expects it)
|
|
check(TimeRange.GetLowerBound().IsOpen() && TimeRange.GetUpperBound().IsOpen());
|
|
|
|
// Seek in progress?
|
|
if (SeekTargetTime.IsValid())
|
|
{
|
|
// Yes. Fetch (and discard) all samples up to the seek target time...
|
|
// (we only throw out samples from prior sequence indices to make sure we do not swallow any audio from overlapping samples)
|
|
auto DiscardRange = TRange<FMediaTimeStamp>(FMediaTimeStamp(0), FMediaTimeStamp((CurrentRate >= 0.0f) ? FTimespan::Zero() : FTimespan::MaxValue(), SeekTargetTime.SequenceIndex));
|
|
Samples.DiscardAudioSamples(DiscardRange, GetUnpausedRate() < 0.0f);
|
|
}
|
|
|
|
//
|
|
// "Modern" 1-Audio-Sink-Only case (aka: we only feed the primary sink)
|
|
//
|
|
if (TSharedPtr< FMediaAudioSampleSink, ESPMode::ThreadSafe> PinnedPrimaryAudioSink = PrimaryAudioSink.Pin())
|
|
{
|
|
while (PinnedPrimaryAudioSink->CanAcceptSamples(1))
|
|
{
|
|
if (!Samples.FetchAudio(TimeRange, Sample))
|
|
break;
|
|
|
|
if (!Sample.IsValid())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
{
|
|
FScopeLock Lock(&LastTimeValuesCS);
|
|
LastAudioSampleProcessedTime.TimeStamp = FMediaTimeStamp(Sample->GetTime());
|
|
LastAudioSampleProcessedTime.SampledAtTime = FPlatformTime::Seconds();
|
|
}
|
|
|
|
PinnedPrimaryAudioSink->Enqueue(Sample.ToSharedRef());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Do we have video playback?
|
|
if (HaveVideoPlayback())
|
|
{
|
|
TRange<FMediaTimeStamp> TempRange;
|
|
// We got video and audio, but no audio sink - throw away anything up to video playback time...
|
|
// (rough estimate, as this is off-gamethread; but better than throwing things out with no throttling at all)
|
|
{
|
|
bool bReverse = (CurrentRate < 0.0f);
|
|
FScopeLock Lock(&LastTimeValuesCS);
|
|
if (!bReverse)
|
|
{
|
|
TempRange.SetUpperBound(CurrentFrameVideoTimeStamp);
|
|
}
|
|
else
|
|
{
|
|
TempRange.SetLowerBound(CurrentFrameVideoTimeStamp);
|
|
}
|
|
|
|
}
|
|
while (Samples.FetchAudio(TempRange, Sample))
|
|
;
|
|
}
|
|
else
|
|
{
|
|
// No Video and no primary audio sink: we throw all away (sub-optimal as it will keep audio decoding busy; but this should be an edge case)
|
|
while (Samples.FetchAudio(TimeRange, Sample))
|
|
;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::IsVideoSampleStillGood(const TRange<FMediaTimeStamp>& LastSampleTimeRange, const TRange<FMediaTimeStamp>& TimeRange, bool bReverse) const
|
|
{
|
|
// If we have no valid time range or a seek is in progress we assume the current frame can be considered "done" in any case
|
|
if (!TimeRange.IsEmpty() && !SeekTargetTime.IsValid())
|
|
{
|
|
// This is not the case: check more detailed!
|
|
TRange<FMediaTimeStamp> LastSampleTimeRange0(FMediaTimeStamp(LastSampleTimeRange.GetLowerBoundValue().Time, 0), FMediaTimeStamp(LastSampleTimeRange.GetUpperBoundValue().Time, 0));
|
|
TRange<FMediaTimeStamp> TimeRange0(FMediaTimeStamp(TimeRange.GetLowerBoundValue().Time, 0), FMediaTimeStamp(TimeRange.GetUpperBoundValue().Time, 0));
|
|
|
|
// Is the sample time range at all still valid?
|
|
if (LastSampleTimeRange0.Overlaps(TimeRange0))
|
|
{
|
|
// Yes. Assuming we could get more samples (of the same type) from the player, would the next one "better"?
|
|
// (this does not say which "next one" is actually current (e.g. if the rendering FPS is real bad), just that "another one" would be better)
|
|
|
|
// Compute the "theoretical" next sample range...
|
|
TRange<FMediaTimeStamp> NextSampleTimeRange = !bReverse ? TRange<FMediaTimeStamp>(LastSampleTimeRange0.GetUpperBoundValue(), LastSampleTimeRange0.GetUpperBoundValue() + LastSampleTimeRange0.Size<FMediaTimeStamp>().Time)
|
|
: TRange<FMediaTimeStamp>(LastSampleTimeRange0.GetLowerBoundValue() - LastSampleTimeRange0.Size<FMediaTimeStamp>().Time, LastSampleTimeRange0.GetLowerBoundValue());
|
|
|
|
FTimespan Duration = Player->GetControls().GetDuration();
|
|
|
|
if (!Player->GetControls().IsLooping())
|
|
{
|
|
// If we are not looping we need to clamp against the media's duration
|
|
// (we assume it starts at zero here!)
|
|
NextSampleTimeRange = TRange<FMediaTimeStamp>::Intersection(NextSampleTimeRange, TRange<FMediaTimeStamp>(FMediaTimeStamp(0, NextSampleTimeRange.GetLowerBoundValue().SequenceIndex), FMediaTimeStamp(Duration, NextSampleTimeRange.GetLowerBoundValue().SequenceIndex)));
|
|
}
|
|
else
|
|
{
|
|
if (NextSampleTimeRange.GetLowerBoundValue().Time >= Duration)
|
|
{
|
|
check(!bReverse);
|
|
NextSampleTimeRange = TRange<FMediaTimeStamp>(FMediaTimeStamp(NextSampleTimeRange.GetLowerBoundValue().Time - Duration, FMediaTimeStamp::AdjustPrimaryIndex(NextSampleTimeRange.GetLowerBoundValue().SequenceIndex, 1)),
|
|
FMediaTimeStamp(NextSampleTimeRange.GetUpperBoundValue().Time - Duration, FMediaTimeStamp::AdjustPrimaryIndex(NextSampleTimeRange.GetUpperBoundValue().SequenceIndex, 1)));
|
|
}
|
|
else if (NextSampleTimeRange.GetLowerBoundValue().Time < FTimespan::Zero())
|
|
{
|
|
check(bReverse);
|
|
NextSampleTimeRange = TRange<FMediaTimeStamp>(FMediaTimeStamp(NextSampleTimeRange.GetLowerBoundValue().Time + Duration, FMediaTimeStamp::AdjustPrimaryIndex(NextSampleTimeRange.GetLowerBoundValue().SequenceIndex, -1)),
|
|
FMediaTimeStamp(NextSampleTimeRange.GetUpperBoundValue().Time + Duration, FMediaTimeStamp::AdjustPrimaryIndex(NextSampleTimeRange.GetUpperBoundValue().SequenceIndex, -1)));
|
|
}
|
|
}
|
|
|
|
// Compute which one is larger inside the current range...
|
|
int64 LastSampleCoverage = TRange<FMediaTimeStamp>::Intersection(TimeRange0, LastSampleTimeRange0).Size<FMediaTimeStamp>().Time.GetTicks();
|
|
int64 NextSampleCoverage = TRange<FMediaTimeStamp>::Intersection(TimeRange0, NextSampleTimeRange).Size<FMediaTimeStamp>().Time.GetTicks();
|
|
|
|
if (LastSampleCoverage > NextSampleCoverage)
|
|
{
|
|
// Last one we returned is still good. No new one needed...
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::ProcessVideoSamples(IMediaSamples& Samples, const TRange<FMediaTimeStamp>& TimeRange)
|
|
{
|
|
// Let the player do some processing if needed.
|
|
if (Player.IsValid())
|
|
{
|
|
// note: avoid using this - it will be deprecated
|
|
Player->ProcessVideoSamples();
|
|
}
|
|
|
|
// This is not to be used with V1 timing
|
|
check(Player->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::UsePlaybackTimingV2));
|
|
// We expect a fully closed range or we assume: nothing to do...
|
|
check(TimeRange.GetLowerBound().IsClosed() && TimeRange.GetUpperBound().IsClosed());
|
|
|
|
TSharedPtr<IMediaTextureSample, ESPMode::ThreadSafe> Sample;
|
|
|
|
if (!Player->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::AlwaysPullNewestVideoFrame))
|
|
{
|
|
//
|
|
// Normal playback with timing control provided by MediaFramework
|
|
//
|
|
const bool bReverse = (GetUnpausedRate() < 0.0f);
|
|
|
|
if (IsVideoSampleStillGood(LastVideoSampleProcessedTimeRange, TimeRange, bReverse))
|
|
{
|
|
// We got all the samples we need. Processing was successful...
|
|
return true;
|
|
}
|
|
|
|
switch (Samples.FetchBestVideoSampleForTimeRange(TimeRange, Sample, bReverse))
|
|
{
|
|
case IMediaSamples::EFetchBestSampleResult::Ok:
|
|
break;
|
|
|
|
case IMediaSamples::EFetchBestSampleResult::NoSample:
|
|
{
|
|
break;
|
|
}
|
|
|
|
case IMediaSamples::EFetchBestSampleResult::NotSupported:
|
|
{
|
|
//
|
|
// Fallback for players supporting V2 timing, but do not supply FetchBestVideoSampleForTimeRange() due to some
|
|
// custom implementation of IMediaSamples (here to ease adoption of the new timing code - eventually should go away)
|
|
//
|
|
|
|
// Find newest sample that satisfies the time range
|
|
// (the FetchXYZ() code does not work well with a lower range limit at all - we ask for a "up to" type range instead
|
|
// and limit the other side of the range in code here to not change the older logic & possibly cause trouble in old code)
|
|
TRange<FMediaTimeStamp> TempRange = bReverse ? TRange<FMediaTimeStamp>::AtLeast(TimeRange.GetUpperBoundValue()) : TRange<FMediaTimeStamp>::AtMost(TimeRange.GetUpperBoundValue());
|
|
while (Samples.FetchVideo(TempRange, Sample))
|
|
;
|
|
if (Sample.IsValid() &&
|
|
((!bReverse && ((Sample->GetTime() + Sample->GetDuration()) > TimeRange.GetLowerBoundValue())) ||
|
|
(bReverse && ((Sample->GetTime() - Sample->GetDuration()) < TimeRange.GetLowerBoundValue()))))
|
|
{
|
|
// Sample is good - nothing more to do here
|
|
}
|
|
else
|
|
{
|
|
Sample.Reset();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// Use newest video frame available at all times (no Mediaframework timing control)
|
|
//
|
|
TRange<FMediaTimeStamp> TempRange; // fully open range
|
|
while (Samples.FetchVideo(TempRange, Sample))
|
|
;
|
|
}
|
|
|
|
// Any sample?
|
|
if (Sample.IsValid())
|
|
{
|
|
// Yes. If we are in blocking playback mode we need to make sure that the sample is really in the range we asked for and block on...
|
|
// (same players might return an older sample as stop-gap measure if nothing can be found in the current range)
|
|
|
|
TRange<FMediaTimeStamp> SampleTimeRange(Sample->GetTime(), Sample->GetTime() + Sample->GetDuration());
|
|
|
|
// Enqueue the sample to render
|
|
// (we use a queue to stay compatible with existing structure and older sinks - new sinks will read this single entry right away on the gamethread
|
|
// and pass it along to rendering outside the queue)
|
|
bool bOk = VideoSampleSinks.Enqueue(Sample.ToSharedRef());
|
|
check(bOk);
|
|
|
|
{
|
|
FScopeLock Lock(&LastTimeValuesCS);
|
|
CurrentFrameVideoDisplayTimeStamp = CurrentFrameVideoTimeStamp = SampleTimeRange.GetLowerBoundValue();
|
|
LastVideoSampleProcessedTimeRange = SampleTimeRange;
|
|
}
|
|
|
|
UpdateSeekStatus(&CurrentFrameVideoTimeStamp);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::ProcessCaptionSamples(IMediaSamples& Samples, const TRange<FMediaTimeStamp>& TimeRange)
|
|
{
|
|
check(Player->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::UsePlaybackTimingV2));
|
|
|
|
TSharedPtr<IMediaOverlaySample, ESPMode::ThreadSafe> Sample;
|
|
|
|
// Seek in progress?
|
|
if (SeekTargetTime.IsValid())
|
|
{
|
|
// Yes. Fetch (and discard) all samples up to the seek target time...
|
|
// (we only throw out samples from prior sequence indices to make sure we do not swallow any audio from overlapping samples)
|
|
auto DiscardRange = TRange<FMediaTimeStamp>(FMediaTimeStamp(0), FMediaTimeStamp((CurrentRate >= 0.0f) ? FTimespan::Zero() : FTimespan::MaxValue(), SeekTargetTime.SequenceIndex));
|
|
Samples.DiscardCaptionSamples(DiscardRange, GetUnpausedRate() < 0.0f);
|
|
}
|
|
else
|
|
{
|
|
while (Samples.FetchCaption(TimeRange, Sample))
|
|
{
|
|
if (Sample.IsValid() && !CaptionSampleSinks.Enqueue(Sample.ToSharedRef()))
|
|
{
|
|
#if MEDIAPLAYERFACADE_TRACE_SINKOVERFLOWS
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade %p: Caption sample sink overflow"), this);
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::ProcessSubtitleSamples(IMediaSamples& Samples, const TRange<FMediaTimeStamp>& TimeRange)
|
|
{
|
|
check(Player->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::UsePlaybackTimingV2));
|
|
|
|
TSharedPtr<IMediaOverlaySample, ESPMode::ThreadSafe> Sample;
|
|
|
|
// Seek in progress?
|
|
if (SeekTargetTime.IsValid())
|
|
{
|
|
// Yes. Fetch (and discard) all samples up to the seek target time...
|
|
// (we only throw out samples from prior sequence indices to make sure we do not swallow any audio from overlapping samples)
|
|
auto DiscardRange = TRange<FMediaTimeStamp>(FMediaTimeStamp(0), FMediaTimeStamp((CurrentRate >= 0.0f) ? FTimespan::Zero() : FTimespan::MaxValue(), SeekTargetTime.SequenceIndex));
|
|
Samples.DiscardSubtitleSamples(DiscardRange, GetUnpausedRate() < 0.0f);
|
|
}
|
|
else
|
|
{
|
|
while (Samples.FetchSubtitle(TimeRange, Sample))
|
|
{
|
|
//UE_LOG(LogMediaUtils, Display, TEXT("Subtitle @%.3f: %s"), Sample->GetTime().Time.GetTotalSeconds(), *Sample->GetText().ToString());
|
|
if (Sample.IsValid() && !SubtitleSampleSinks.Enqueue(Sample.ToSharedRef()))
|
|
{
|
|
#if MEDIAPLAYERFACADE_TRACE_SINKOVERFLOWS
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade %p: Subtitle sample sink overflow"), this);
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::ProcessMetadataSamples(IMediaSamples& Samples, const TRange<FMediaTimeStamp>& TimeRange)
|
|
{
|
|
check(Player->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::UsePlaybackTimingV2));
|
|
|
|
TSharedPtr<IMediaBinarySample, ESPMode::ThreadSafe> Sample;
|
|
|
|
// Seek in progress?
|
|
if (SeekTargetTime.IsValid())
|
|
{
|
|
// Yes. Fetch (and discard) all samples up to the seek target time...
|
|
// (we only throw out samples from prior sequence indices to make sure we do not swallow any audio from overlapping samples)
|
|
auto DiscardRange = TRange<FMediaTimeStamp>(FMediaTimeStamp(0), FMediaTimeStamp((CurrentRate >= 0.0f) ? FTimespan::Zero() : FTimespan::MaxValue(), SeekTargetTime.SequenceIndex));
|
|
Samples.DiscardMetadataSamples(DiscardRange, GetUnpausedRate() < 0.0f);
|
|
}
|
|
else
|
|
{
|
|
while (Samples.FetchMetadata(TimeRange, Sample))
|
|
{
|
|
if (Sample.IsValid() && !MetadataSampleSinks.Enqueue(Sample.ToSharedRef()))
|
|
{
|
|
#if MEDIAPLAYERFACADE_TRACE_SINKOVERFLOWS
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade %p: Metadata sample sink overflow"), this);
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::ProcessAudioSamplesV1(IMediaSamples& Samples, TRange<FTimespan> TimeRange)
|
|
{
|
|
TSharedPtr<IMediaAudioSample, ESPMode::ThreadSafe> Sample;
|
|
|
|
while (Samples.FetchAudio(TimeRange, Sample))
|
|
{
|
|
if (!Sample.IsValid())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!AudioSampleSinks.Enqueue(Sample.ToSharedRef()))
|
|
{
|
|
#if MEDIAPLAYERFACADE_TRACE_SINKOVERFLOWS
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade %p: Audio sample sink overflow"), this);
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
FScopeLock Lock(&LastTimeValuesCS);
|
|
LastAudioSampleProcessedTime.TimeStamp = Sample->GetTime();
|
|
LastAudioSampleProcessedTime.SampledAtTime = FPlatformTime::Seconds();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::ProcessVideoSamplesV1(IMediaSamples& Samples, TRange<FTimespan> TimeRange)
|
|
{
|
|
// Let the player do some processing if needed.
|
|
if (Player.IsValid())
|
|
{
|
|
Player->ProcessVideoSamples();
|
|
}
|
|
|
|
// This is not to be used with V2 timing
|
|
check(!Player->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::UsePlaybackTimingV2));
|
|
|
|
TSharedPtr<IMediaTextureSample, ESPMode::ThreadSafe> Sample;
|
|
|
|
while (Samples.FetchVideo(TimeRange, Sample))
|
|
{
|
|
if (!Sample.IsValid())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
{
|
|
FScopeLock Lock(&LastTimeValuesCS);
|
|
CurrentFrameVideoDisplayTimeStamp = CurrentFrameVideoTimeStamp = Sample->GetTime();
|
|
}
|
|
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade %p: Fetched video sample %s"), this, *Sample->GetTime().Time.ToString(TEXT("%h:%m:%s.%t")));
|
|
|
|
if (VideoSampleSinks.Enqueue(Sample.ToSharedRef()))
|
|
{
|
|
if (CurrentRate >= 0.0f)
|
|
{
|
|
NextVideoSampleTime = Sample->GetTime().Time + Sample->GetDuration();
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade %p: Next video sample time %s"), this, *NextVideoSampleTime.ToString(TEXT("%h:%m:%s.%t")));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
#if MEDIAPLAYERFACADE_TRACE_SINKOVERFLOWS
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade %p: Video sample sink overflow"), this);
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
void FMediaPlayerFacade::ProcessCaptionSamplesV1(IMediaSamples& Samples, TRange<FTimespan> TimeRange)
|
|
{
|
|
TSharedPtr<IMediaOverlaySample, ESPMode::ThreadSafe> Sample;
|
|
|
|
while (Samples.FetchCaption(TimeRange, Sample))
|
|
{
|
|
if (Sample.IsValid() && !CaptionSampleSinks.Enqueue(Sample.ToSharedRef()))
|
|
{
|
|
#if MEDIAPLAYERFACADE_TRACE_SINKOVERFLOWS
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade %p: Caption sample sink overflow"), this);
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::ProcessSubtitleSamplesV1(IMediaSamples& Samples, TRange<FTimespan> TimeRange)
|
|
{
|
|
TSharedPtr<IMediaOverlaySample, ESPMode::ThreadSafe> Sample;
|
|
|
|
while (Samples.FetchSubtitle(TimeRange, Sample))
|
|
{
|
|
if (Sample.IsValid() && !SubtitleSampleSinks.Enqueue(Sample.ToSharedRef()))
|
|
{
|
|
FString Caption = Sample->GetText().ToString();
|
|
UE_LOG(LogMediaUtils, Log, TEXT("New caption @%.3f: %s"), Sample->GetTime().Time.GetTotalSeconds(), *Caption);
|
|
|
|
#if MEDIAPLAYERFACADE_TRACE_SINKOVERFLOWS
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade %p: Subtitle sample sink overflow"), this);
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::ProcessMetadataSamplesV1(IMediaSamples& Samples, TRange<FTimespan> TimeRange)
|
|
{
|
|
TSharedPtr<IMediaBinarySample, ESPMode::ThreadSafe> Sample;
|
|
|
|
while (Samples.FetchMetadata(TimeRange, Sample))
|
|
{
|
|
if (Sample.IsValid() && !MetadataSampleSinks.Enqueue(Sample.ToSharedRef()))
|
|
{
|
|
#if MEDIAPLAYERFACADE_TRACE_SINKOVERFLOWS
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade %p: Metadata sample sink overflow"), this);
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* IMediaEventSink interface
|
|
*****************************************************************************/
|
|
|
|
void FMediaPlayerFacade::ReceiveMediaEvent(EMediaEvent Event)
|
|
{
|
|
if (Event >= EMediaEvent::Internal_Start)
|
|
{
|
|
switch (Event)
|
|
{
|
|
case EMediaEvent::Internal_PurgeVideoSamplesHint:
|
|
{
|
|
//
|
|
// Player asks to attempt to purge older samples in the video output queue it maintains
|
|
// (ask goes via facade as the player does not have accurate timing info)
|
|
//
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer = Player;
|
|
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// We only support this for V2 timing players
|
|
check(CurrentPlayer->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::UsePlaybackTimingV2));
|
|
|
|
// Only do this if we do not block on time ranges
|
|
if (BlockOnRange.IsSet())
|
|
{
|
|
// We do not purge as we do not need max perf, but max reliability to actually get certain frames
|
|
return;
|
|
}
|
|
|
|
float Rate = CurrentRate;
|
|
if (Rate == 0.0f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get current playback time
|
|
// (Note: the delta time is entirely synthetic - we do not pass zero to avoid an empty range, but we do not look far into the future either
|
|
// -> after all: we are mainly focused on purging samples up to the current time
|
|
// Remarks:
|
|
// - this version does not take any estimations from any frame start into account as this is entirely async to the main thread
|
|
// - video streams with no audio content will be played using the UE DeltaTime -> so if that stops, the progress of the video stops!
|
|
// -> hence we will not see (other then one initial purge) any purging of samples here!
|
|
// )
|
|
TRange<FMediaTimeStamp> TimeRange;
|
|
if (!GetCurrentPlaybackTimeRange(TimeRange, Rate, FTimespan::FromMilliseconds(kOutdatedSamplePurgeRange), true))
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool bReverse = (Rate < 0.0f);
|
|
const float RateFactor = (Rate != 0.0f) ? (1.0f / Rate) : 1.0f;
|
|
|
|
// Don't purge frames if the queue is small (to avoid purging if players deliver frames late persistently)
|
|
uint32 NumPurged = 0;
|
|
if (CurrentPlayer->GetSamples().NumVideoSamples() >= kMinFramesInVideoQueueToPurge)
|
|
{
|
|
NumPurged = CurrentPlayer->GetSamples().PurgeOutdatedVideoSamples(TimeRange.GetLowerBoundValue(), bReverse, FTimespan::FromSeconds(kOutdatedVideoSamplesTolerance * RateFactor));
|
|
}
|
|
SET_DWORD_STAT(STAT_MediaUtils_FacadeNumPurgedVideoSamples, NumPurged);
|
|
INC_DWORD_STAT_BY(STAT_MediaUtils_FacadeTotalPurgedVideoSamples, NumPurged);
|
|
|
|
// Take the opportunity to also purge any samples related to video samples directly (and evaluated on the game thread)
|
|
|
|
// Captions...
|
|
NumPurged = 0;
|
|
if (CurrentPlayer->GetSamples().NumVideoSamples() >= kMinFramesInCaptionQueueToPurge)
|
|
{
|
|
NumPurged = CurrentPlayer->GetSamples().PurgeOutdatedCaptionSamples(TimeRange.GetLowerBoundValue(), bReverse, FTimespan::FromSeconds(kOutdatedVideoSamplesTolerance * RateFactor));
|
|
}
|
|
SET_DWORD_STAT(STAT_MediaUtils_FacadeNumPurgedSubtitleSamples, NumPurged);
|
|
INC_DWORD_STAT_BY(STAT_MediaUtils_FacadeTotalPurgedSubtitleSamples, NumPurged);
|
|
|
|
// Subtitles...
|
|
NumPurged = 0;
|
|
if (CurrentPlayer->GetSamples().NumVideoSamples() >= kMinFramesInSubtitleQueueToPurge)
|
|
{
|
|
NumPurged = CurrentPlayer->GetSamples().PurgeOutdatedSubtitleSamples(TimeRange.GetLowerBoundValue(), bReverse, FTimespan::FromSeconds(kOutdatedVideoSamplesTolerance * RateFactor));
|
|
}
|
|
SET_DWORD_STAT(STAT_MediaUtils_FacadeNumPurgedCaptionSamples, NumPurged);
|
|
INC_DWORD_STAT_BY(STAT_MediaUtils_FacadeTotalPurgedCaptionSamples, NumPurged);
|
|
|
|
break;
|
|
}
|
|
|
|
case EMediaEvent::Internal_ResetForDiscontinuity:
|
|
{
|
|
// Disabled for now to prevent a flush on the next handling iteration that might discard the samples a blocking range is waiting for.
|
|
#if 0
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade %p: Reset for discontinuity"), this);
|
|
bIsSinkFlushPending = true;
|
|
#endif
|
|
break;
|
|
}
|
|
case EMediaEvent::Internal_RenderClockStart:
|
|
{
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade %p: Render clock shall start"), this);
|
|
break;
|
|
}
|
|
case EMediaEvent::Internal_RenderClockStop:
|
|
{
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade %p: Render clock shall stop"), this);
|
|
break;
|
|
}
|
|
|
|
case EMediaEvent::Internal_VideoSamplesAvailable:
|
|
{
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade %p: Video samples ARE available"), this);
|
|
VideoSampleAvailability = 1;
|
|
break;
|
|
}
|
|
case EMediaEvent::Internal_VideoSamplesUnavailable:
|
|
{
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade %p: Video samples are NOT available"), this);
|
|
VideoSampleAvailability = 0;
|
|
break;
|
|
}
|
|
case EMediaEvent::Internal_AudioSamplesAvailable:
|
|
{
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade %p: Audio samples ARE available"), this);
|
|
AudioSampleAvailability = 1;
|
|
break;
|
|
}
|
|
case EMediaEvent::Internal_AudioSamplesUnavailable:
|
|
{
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade %p: Audio samples are NOT available"), this);
|
|
AudioSampleAvailability = 0;
|
|
break;
|
|
}
|
|
|
|
default:
|
|
{
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade %p: Received media event %s"), this, *MediaUtils::EventToString(Event));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade %p: Received media event %s"), this, *MediaUtils::EventToString(Event));
|
|
QueuedEvents.Enqueue(Event);
|
|
}
|
|
}
|