3669 lines
114 KiB
C++
3669 lines
114 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 = 1.0; // 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)
|
|
|
|
/* Cvars
|
|
*****************************************************************************/
|
|
|
|
namespace UE::MediaUtils::Private
|
|
{
|
|
|
|
#if !UE_BUILD_SHIPPING
|
|
|
|
TAutoConsoleVariable<bool> CVarTestForcePlayerCreateFailed(
|
|
TEXT("m.Test.ForcePlayerCreateFailed"),
|
|
false,
|
|
TEXT("Whether force media player creation to fail."),
|
|
ECVF_ReadOnly
|
|
);
|
|
|
|
#endif // !UE_BUILD_SHIPPING
|
|
|
|
float GBlockOnFetchTimeout = 10.0f;
|
|
static FAutoConsoleVariableRef CVarMediaUtilsBlockOnFetchTimeout(
|
|
TEXT("MediaUtils.BlockOnFetchTimeout"),
|
|
GBlockOnFetchTimeout,
|
|
TEXT("Maximum time that TickInput/Fetch will block waiting for samples (in seconds).\n")
|
|
);
|
|
|
|
}
|
|
|
|
/* 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));
|
|
}
|
|
|
|
static bool IsDurationValidAndFinite(FTimespan Duration)
|
|
{
|
|
return (Duration != FTimespan::Zero() && Duration.GetTicks() != TNumericLimits<int64>::Max());
|
|
}
|
|
|
|
/* 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)
|
|
, 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);
|
|
}
|
|
|
|
|
|
bool FMediaPlayerFacade::SupportsPlaybackTimeRange() const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
return CurrentPlayer.IsValid() ? CurrentPlayer->GetControls().CanControl(EMediaControl::PlaybackRange) : false;
|
|
}
|
|
|
|
|
|
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();
|
|
}
|
|
|
|
Flush();
|
|
ReInit();
|
|
BlockOnRange.Reset();
|
|
bDidRecentPlayerHaveError = false;
|
|
}
|
|
|
|
|
|
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);
|
|
}
|
|
|
|
TOptional<FTimecode> FMediaPlayerFacade::GetVideoTimecode() const
|
|
{
|
|
FScopeLock Lock(&LastTimeValuesCS);
|
|
return MostRecentlyDeliveredVideoFrameTimecode;
|
|
}
|
|
|
|
TRange<FMediaTimeStamp> FMediaPlayerFacade::GetLastProcessedVideoSampleTimeRange() const
|
|
{
|
|
FScopeLock Lock(&LastTimeValuesCS);
|
|
return LastVideoSampleProcessedTimeRange;
|
|
}
|
|
|
|
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)
|
|
, OptionsObject(InOptions ? InOptions->ToUObject() : nullptr)
|
|
, 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 OptionsObject.IsStale() ? nullptr : 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);
|
|
}
|
|
|
|
const TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe>& GetReusedPlayer() const
|
|
{
|
|
return ReusedPlayer;
|
|
}
|
|
|
|
private:
|
|
FString Url;
|
|
const IMediaOptions* Options;
|
|
FWeakObjectPtr OptionsObject;
|
|
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->GetUrl(), OR->GetOptions(), OR->GetPlayerOptions(), OR->GetPlayerFactory(), OR->GetReusedPlayer(), OR->WillCreateNewPlayer(), 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;
|
|
}
|
|
|
|
bool bIsRequestInvalid = (Player == nullptr);
|
|
|
|
#if !UE_BUILD_SHIPPING
|
|
bIsRequestInvalid = bIsRequestInvalid || UE::MediaUtils::Private::CVarTestForcePlayerCreateFailed.GetValueOnAnyThread();
|
|
#endif
|
|
|
|
if (bIsRequestInvalid)
|
|
{
|
|
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();
|
|
NextSeekTime.Reset();
|
|
NextSequenceIndex.Reset();
|
|
if (Player->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::UsePlaybackTimingV2))
|
|
{
|
|
NextSequenceIndex = 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)
|
|
{
|
|
NextSeekTime.Reset();
|
|
|
|
auto CurrentPlayer = Player;
|
|
|
|
if (!CurrentPlayer.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FTimespan Duration = CurrentPlayer->GetControls().GetDuration();
|
|
|
|
FTimespan Time;
|
|
if (IsDurationValidAndFinite(Duration))
|
|
{
|
|
const TRange<FTimespan> ActiveRange = GetActivePlaybackRange();
|
|
|
|
if (CurrentPlayer->GetControls().IsLooping())
|
|
{
|
|
const FTimespan ActiveRangeDuration = ActiveRange.GetUpperBoundValue() - ActiveRange.GetLowerBoundValue();
|
|
Time = WrappedModulo(InTime - ActiveRange.GetLowerBoundValue(), ActiveRangeDuration) + ActiveRange.GetLowerBoundValue();
|
|
}
|
|
else
|
|
{
|
|
Time = FTimespan(FMath::Clamp(InTime.GetTicks(), ActiveRange.GetLowerBoundValue().GetTicks(), ActiveRange.GetUpperBoundValue().GetTicks()));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Time = InTime;
|
|
}
|
|
|
|
FMediaSeekParams SeekParams;
|
|
// v2 timing players are *required* to use the new sequence index we set up.
|
|
TOptional<int32> SequenceIndexNow = NextSequenceIndex;
|
|
if (CurrentPlayer->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::UsePlaybackTimingV2))
|
|
{
|
|
SeekParams.NewSequenceIndex = NextSequenceIndex = NextSequenceIndex.Get(0) + 1;
|
|
}
|
|
// Issue the seek.
|
|
if (!CurrentPlayer->GetControls().Seek(Time, SeekParams))
|
|
{
|
|
// If that failed restore the sequence index.
|
|
NextSequenceIndex = SequenceIndexNow;
|
|
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)
|
|
PrepareSampleQueueForSequenceIndex();
|
|
Flush(CurrentPlayer->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::PlayerUsesInternalFlushOnSeek), true);
|
|
}
|
|
else
|
|
{
|
|
// No. Flush as requested...
|
|
if (CurrentPlayer->FlushOnSeekStarted())
|
|
{
|
|
Flush(CurrentPlayer->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::PlayerUsesInternalFlushOnSeek), false);
|
|
}
|
|
}
|
|
|
|
SeekTargetTime = FMediaTimeStamp(Time, NextSequenceIndex.Get(0), 0);
|
|
return true;
|
|
}
|
|
|
|
bool FMediaPlayerFacade::IsSeeking() const
|
|
{
|
|
// Code inspection notes:
|
|
// - Usually protected by LastTimeValuesCS, but sometimes CriticalSection (or both interlocked).
|
|
// - GetCurrentPlaybackTimeRange reads it outside of a scope lock.
|
|
FScopeLock Lock(&LastTimeValuesCS);
|
|
return SeekTargetTime.IsValid();
|
|
}
|
|
|
|
FMediaTimeStamp FMediaPlayerFacade::GetSeekTarget() const
|
|
{
|
|
FScopeLock Lock(&LastTimeValuesCS);
|
|
return SeekTargetTime;
|
|
}
|
|
|
|
void FMediaPlayerFacade::SetNextSeek(const FTimespan& InTime)
|
|
{
|
|
NextSeekTime = InTime;
|
|
}
|
|
|
|
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 = TRange<FTimespan>::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()
|
|
{
|
|
LastProcessedTimeRange = TRange<FTimespan>::Empty();
|
|
OnBlockSequenceIndex = 0;
|
|
OnBlockLoopIndexOffset = 0;
|
|
RangeIsDirty = true;
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::FBlockOnRange::OnSeek(int32 PrimaryIndex)
|
|
{
|
|
LastProcessedTimeRange = TRange<FTimespan>::Empty();
|
|
OnBlockSequenceIndex = PrimaryIndex;
|
|
OnBlockLoopIndexOffset = 0;
|
|
RangeIsDirty = true;
|
|
}
|
|
|
|
|
|
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))
|
|
{
|
|
LastProcessedTimeRange = 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;
|
|
}
|
|
|
|
const int64 StartTicks = CurrentTimeRange.GetLowerBoundValue().GetTicks();
|
|
const int64 EndExclusiveTicks = CurrentTimeRange.GetUpperBoundValue().GetTicks();
|
|
const int64 DurationTicks = CurrentPlayer->GetControls().GetDuration().GetTicks();
|
|
check(StartTicks >= 0 && EndExclusiveTicks >= StartTicks);
|
|
|
|
/*
|
|
When looping we need to synthesize the expected start and end loop indices of the range we are returning.
|
|
This is because on playback start/seeking the media player implicitly starts with a loop index of 0,
|
|
while we could be in any of the looping repetitions in the sequencer track of this movie (if the track
|
|
has been pulled out "to the right" to have it repeat the clip n times).
|
|
Because of that we need to "lock" an initial loop offset that represents this initial difference and
|
|
needs to be adjusted with any looping of the sequencer.
|
|
Please note that this does not include the case where the media track ends before the end of the
|
|
sequencer. In that case the media player is closed and re-opened on the media track boundaries.
|
|
*/
|
|
if (!CurrentPlayer->GetControls().IsLooping())
|
|
{
|
|
FTimespan Start(CurrentTimeRange.GetLowerBoundValue());
|
|
FTimespan End(CurrentTimeRange.GetUpperBoundValue());
|
|
const int32 LoopIdx = LastProcessedTimeRange.IsEmpty() ? 0 : LastProcessedTimeRange.GetLowerBoundValue().GetTicks() / DurationTicks;
|
|
BlockOnRange = TRange<FMediaTimeStamp>(FMediaTimeStamp(Start, OnBlockSequenceIndex, OnBlockLoopIndexOffset + LoopIdx), FMediaTimeStamp(End, OnBlockSequenceIndex, OnBlockLoopIndexOffset + LoopIdx));
|
|
}
|
|
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))
|
|
*/
|
|
if (!IsDurationValidAndFinite(CurrentPlayer->GetControls().GetDuration()))
|
|
{
|
|
// Catch if this is called to early and reset blocking...
|
|
BlockOnRange = TRange<FMediaTimeStamp>::Empty();
|
|
CurrentPlayer->GetControls().SetBlockingPlaybackHint(false);
|
|
return BlockOnRange;
|
|
}
|
|
|
|
FMediaTimeStamp t0, t1;
|
|
t0.SetTime(FTimespan(StartTicks % DurationTicks)).SetSequenceIndex(OnBlockSequenceIndex).SetLoopIndex(StartTicks / DurationTicks);
|
|
t1.SetTime(FTimespan(EndExclusiveTicks% DurationTicks)).SetSequenceIndex(OnBlockSequenceIndex).SetLoopIndex(EndExclusiveTicks / DurationTicks);
|
|
const bool bReverse = (Facade->GetUnpausedRate() < 0.0f);
|
|
// Did we process a time range before?
|
|
if (LastProcessedTimeRange.IsEmpty())
|
|
{
|
|
/*
|
|
No, playback has just started fresh or through a seek.
|
|
The media player will start with a loop index of 0, but the blocking range could be anywhere within
|
|
a movie repetition (ie when the movie has been pulled out in the sequencer track to repeat a number of times).
|
|
We set that repetion count as the base for the loop index.
|
|
*/
|
|
check(OnBlockLoopIndexOffset == 0);
|
|
OnBlockLoopIndexOffset = !bReverse ? -t0.GetLoopIndex() : -t1.GetLoopIndex();
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
We already processed a time range. We now need to check if the current one has wrapped around in the
|
|
current playback direction.
|
|
|
|
Theoretically, with either very, very short movies or an excessively huge delta time this could have
|
|
wrapped around more than once. We cannot detect this and hope this will not occur.
|
|
*/
|
|
if (!bReverse)
|
|
{
|
|
if (LastProcessedTimeRange.GetLowerBoundValue() > CurrentTimeRange.GetLowerBoundValue())
|
|
{
|
|
// Figure the loop index of the last range's start time.
|
|
const int32 LastRangeLoopIdx = LastProcessedTimeRange.GetLowerBoundValue().GetTicks() / DurationTicks;
|
|
OnBlockLoopIndexOffset += LastRangeLoopIdx + 1;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (LastProcessedTimeRange.GetLowerBoundValue() < CurrentTimeRange.GetLowerBoundValue())
|
|
{
|
|
// Figure the loop index of the last range's start time.
|
|
const int32 ThisRangeLoopIdx = t0.GetLoopIndex();
|
|
OnBlockLoopIndexOffset -= ThisRangeLoopIdx + 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Assemble final blocking range
|
|
t0.AdjustLoopIndex(OnBlockLoopIndexOffset);
|
|
t1.AdjustLoopIndex(OnBlockLoopIndexOffset);
|
|
BlockOnRange = TRange<FMediaTimeStamp>(t0, t1);
|
|
check(!BlockOnRange.IsEmpty());
|
|
}
|
|
|
|
|
|
// Note: Due to varying DTs the new range will NOT be a simple monotone progression in playback direction, but might overlap or even be a subset of the previous one
|
|
// We do not put any safeguards in place here, but rather use the "is last sample still valid" logic to reject illogical / impossible range requests.
|
|
// All that aside: we DO expect ranges start (lower bound if forward, upper if reverse playback) to be moving in a monotone manner according to the set playback direction.
|
|
CurrentPlayer->GetControls().SetBlockingPlaybackHint(!BlockOnRange.IsEmpty());
|
|
LastProcessedTimeRange = 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)
|
|
{
|
|
// direction change
|
|
Flush(Player->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::UsePlaybackTimingV2), false);
|
|
}
|
|
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)
|
|
{
|
|
return Player.IsValid() ? Player->SetNativeVolume(Volume) : false;
|
|
}
|
|
|
|
|
|
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);
|
|
}
|
|
|
|
TRange<FTimespan> FMediaPlayerFacade::GetPlaybackTimeRange(EMediaTimeRangeType InRangeToGet) const
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
return CurrentPlayer.IsValid() ? CurrentPlayer->GetControls().GetPlaybackTimeRange(InRangeToGet) : TRange<FTimespan>();
|
|
}
|
|
|
|
bool FMediaPlayerFacade::SetPlaybackTimeRange(const TRange<FTimespan>& InTimeRange)
|
|
{
|
|
TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CurrentPlayer(Player);
|
|
return CurrentPlayer.IsValid() ? CurrentPlayer->GetControls().SetPlaybackTimeRange(InTimeRange) : false;
|
|
}
|
|
|
|
|
|
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. live playback) -- for now we assume a player will flagged blocked playback as invalid in that case!)
|
|
if (BR.GetUpperBoundValue().GetTime() < FTimespan::Zero() || Player->GetControls().GetDuration() <= BR.GetLowerBoundValue().GetTime())
|
|
{
|
|
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: Flushing sinks"));
|
|
|
|
FScopeLock Lock(&CriticalSection);
|
|
|
|
auto RawMediaPlayer = MediaPlayer.Get();
|
|
AudioSampleSinks.Flush(RawMediaPlayer);
|
|
CaptionSampleSinks.Flush(RawMediaPlayer);
|
|
MetadataSampleSinks.Flush(RawMediaPlayer);
|
|
SubtitleSampleSinks.Flush(RawMediaPlayer);
|
|
VideoSampleSinks.Flush(RawMediaPlayer);
|
|
MostRecentlyDeliveredVideoFrameTimecode.Reset();
|
|
|
|
if (Player.IsValid() && !bExcludePlayer)
|
|
{
|
|
Player->GetSamples().FlushSamples();
|
|
}
|
|
|
|
LastAudioRenderedSampleTime.Invalidate();
|
|
if (bOnSeek)
|
|
{
|
|
BlockOnRange.OnSeek(NextSequenceIndex.Get(0));
|
|
}
|
|
else
|
|
{
|
|
BlockOnRange.OnFlush();
|
|
}
|
|
|
|
if (Player.IsValid() && Player->GetPlayerFeatureFlag(IMediaPlayer::EFeatureFlag::UsePlaybackTimingV2))
|
|
{
|
|
// Logically we have no old sample anymore if we did seek
|
|
// (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)
|
|
if (bOnSeek)
|
|
{
|
|
LastVideoSampleProcessedTimeRange = TRange<FMediaTimeStamp>::Empty();
|
|
}
|
|
else
|
|
{
|
|
if (!bExcludePlayer && !LastVideoSampleProcessedTimeRange.IsEmpty())
|
|
{
|
|
// Players will reset their sequence index related values, but keep the playback position. Adjust our record accordingly...
|
|
int32 LoopIdxS = LastVideoSampleProcessedTimeRange.GetLowerBoundValue().GetLoopIndex();
|
|
int32 LoopIdxE = LastVideoSampleProcessedTimeRange.GetUpperBoundValue().GetLoopIndex();
|
|
LastVideoSampleProcessedTimeRange.SetLowerBoundValue(FMediaTimeStamp(LastVideoSampleProcessedTimeRange.GetLowerBoundValue().Time, 0, 0));
|
|
LastVideoSampleProcessedTimeRange.SetUpperBoundValue(FMediaTimeStamp(LastVideoSampleProcessedTimeRange.GetUpperBoundValue().Time, 0, LoopIdxE - LoopIdxS));
|
|
}
|
|
}
|
|
|
|
// Invalidate next video time to fetch (non-audio case)
|
|
NextEstVideoTimeAtFrameStart.Invalidate();
|
|
// ...and seek target
|
|
SeekTargetTime.Invalidate();
|
|
}
|
|
|
|
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
|
|
//
|
|
const TArray<IMediaPlayerFactory*>& PlayerFactories = MediaModule->GetPlayerFactories();
|
|
if (PlayerFactories.Num() == 0)
|
|
{
|
|
UE_LOG(LogMediaUtils, Error, TEXT("Cannot play %s: no media player plug-ins are installed and enabled in this project"), *Url);
|
|
return nullptr;
|
|
}
|
|
struct FCandidate
|
|
{
|
|
FName Name;
|
|
IMediaPlayerFactory* Factory = nullptr;
|
|
int32 ConfidenceScore = 0;
|
|
};
|
|
TArray<FCandidate> Candidates;
|
|
const FString RunningPlatformName(FPlatformProperties::IniPlatformName());
|
|
for(IMediaPlayerFactory* Factory : PlayerFactories)
|
|
{
|
|
if (Factory->SupportsPlatform(RunningPlatformName))
|
|
{
|
|
int32 ConfidenceScore = Factory->GetPlayabilityConfidenceScore(Url, Options, nullptr, nullptr);
|
|
if (ConfidenceScore > 0)
|
|
{
|
|
FCandidate& Candidate = Candidates.Emplace_GetRef();
|
|
Candidate.Name = Factory->GetPlayerName();
|
|
Candidate.Factory = Factory;
|
|
Candidate.ConfidenceScore = ConfidenceScore;
|
|
}
|
|
}
|
|
}
|
|
Candidates.Sort([](const FCandidate& c1, const FCandidate& c2)
|
|
{
|
|
// If both factories are equally confident, sort alphabetically by name.
|
|
if (c1.ConfidenceScore == c2.ConfidenceScore)
|
|
{
|
|
return c1.Name.ToString() < c2.Name.ToString();
|
|
}
|
|
// Sort by descending confidence score.
|
|
return c1.ConfidenceScore > c2.ConfidenceScore;
|
|
});
|
|
if (Candidates.Num())
|
|
{
|
|
return Candidates[0].Factory;
|
|
}
|
|
//
|
|
// No suitable player found!
|
|
//
|
|
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);
|
|
}
|
|
}
|
|
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: Media Info: n/a"));
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogMediaUtils, Verbose, TEXT("PlayerFacade: Media Info:\n%s"), *MediaInfo);
|
|
}
|
|
}
|
|
else if (Event == EMediaEvent::TracksChanged)
|
|
{
|
|
SelectDefaultTracks();
|
|
if (Player.IsValid())
|
|
{
|
|
// Apply track selection immediately so the selection can be queried.
|
|
UpdateTrackSelectionWithPlayer();
|
|
if (!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())
|
|
{
|
|
if (ActivePlayerOptions.GetValue().TrackSelection == EMediaPlayerOptionTrackSelectMode::UseTrackOptionIndices)
|
|
{
|
|
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())
|
|
{
|
|
if (ActivePlayerOptions.GetValue().TrackSelection == EMediaPlayerOptionTrackSelectMode::UseTrackOptionIndices)
|
|
{
|
|
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;
|
|
}
|
|
|
|
//
|
|
// 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) > static_cast<double>(UE::MediaUtils::Private::GBlockOnFetchTimeout))
|
|
{
|
|
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();
|
|
|
|
const double BlockOnFetchTimeout = static_cast<double>(UE::MediaUtils::Private::GBlockOnFetchTimeout);
|
|
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(BlockOnFetchTimeout))
|
|
{
|
|
UE_LOG(LogMediaUtils, Verbose, TEXT("PlayerFacade: Aborted block on fetch %s after %i seconds"),
|
|
*BlockOnRange.GetRange().GetLowerBoundValue().Time.ToString(TEXT("%h:%m:%s.%t")),
|
|
static_cast<int32>(BlockOnFetchTimeout)
|
|
);
|
|
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade: Blocking on fetch %s"), *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());
|
|
|
|
ExecuteNextSeek();
|
|
}
|
|
|
|
|
|
/* 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))
|
|
{
|
|
ProcessAudioSamples(Samples, TRange<FMediaTimeStamp>());
|
|
|
|
const FMediaTimeStamp Time = GetTimeStamp();
|
|
auto TimeRange = TRange<FMediaTimeStamp>::Inclusive(FMediaTimeStamp(FTimespan::MinValue(), 0, 0), Time + MediaPlayerFacade::MetadataPreroll);
|
|
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.NumAudioSamples());
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::PrepareSampleQueueForSequenceIndex()
|
|
{
|
|
FScopeLock Lock(&CriticalSection);
|
|
if (!Player.IsValid() || !NextSequenceIndex.IsSet())
|
|
{
|
|
return;
|
|
}
|
|
int32 MinSeqIdx = NextSequenceIndex.GetValue();
|
|
IMediaSamples& Samples = Player->GetSamples();
|
|
Samples.SetMinExpectedNextSequenceIndex(NextSequenceIndex);
|
|
}
|
|
|
|
|
|
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() && VideoTimeStamp.GetSequenceIndex() >= NextSequenceIndex.Get(0))
|
|
{
|
|
bool bRunningNonAudioClock = bHaveActiveAudio && !BlockOnRange.IsSet();
|
|
if (bRunningNonAudioClock)
|
|
{
|
|
NextEstVideoTimeAtFrameStart = FMediaTimeStampSample(VideoTimeStamp, FPlatformTime::Seconds());
|
|
}
|
|
FScopeLock LockLT(&LastTimeValuesCS);
|
|
CurrentFrameVideoDisplayTimeStamp = SeekTargetTime;
|
|
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::ExecuteNextSeek()
|
|
{
|
|
if (NextSeekTime.IsSet() && !IsSeeking())
|
|
{
|
|
if (!Seek(NextSeekTime.GetValue()))
|
|
{
|
|
// todo: signal error for failed seek.
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
PrepareSampleQueueForSequenceIndex();
|
|
|
|
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())
|
|
{
|
|
// Nothing at all?
|
|
if (!NextEstVideoTimeAtFrameStart.IsValid())
|
|
{
|
|
// Try getting a new sample time to start things up...
|
|
FMediaTimeStamp VideoTimeStamp;
|
|
if (Player->GetSamples().PeekVideoSampleTime(VideoTimeStamp))
|
|
{
|
|
NextEstVideoTimeAtFrameStart = FMediaTimeStampSample(VideoTimeStamp, FPlatformTime::Seconds());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// We have a time. But if we are actively playing forward...
|
|
if (CurrentRate > 0.0f)
|
|
{
|
|
// ...and got some sample waiting for us...
|
|
FMediaTimeStamp VideoTimeStamp;
|
|
if (Player->GetSamples().PeekVideoSampleTime(VideoTimeStamp))
|
|
{
|
|
// ...we need to see if the player's next sample might be so far in the future that we need to re-calibrate our timing
|
|
// (this could happen if the stream has a "gap" in PTS values - e.g. after pausing a live feed from a camera)
|
|
// (^^^ we do not do this on reverse playback as it is unlikely for such streams and might be thinned, hence show gaps under normal conditions)
|
|
if (VideoTimeStamp.GetIndexValue() == NextEstVideoTimeAtFrameStart.TimeStamp.GetIndexValue())
|
|
{
|
|
FTimespan Delta = VideoTimeStamp.Time - NextEstVideoTimeAtFrameStart.TimeStamp.Time;
|
|
if (GetUnpausedRate() < 0.0f)
|
|
{
|
|
Delta = -Delta;
|
|
}
|
|
|
|
// Our threshold for re-calibration is twice the length of the last sample we got
|
|
// (or 100ms if we have nothing)
|
|
FTimespan DeltaLimit;
|
|
if (!LastVideoSampleProcessedTimeRange.IsEmpty())
|
|
{
|
|
DeltaLimit = LastVideoSampleProcessedTimeRange.Size<FMediaTimeStamp>().Time * 2;
|
|
}
|
|
else
|
|
{
|
|
DeltaLimit = FTimespan::FromSeconds(0.100);
|
|
}
|
|
|
|
if (Delta >= DeltaLimit)
|
|
{
|
|
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();
|
|
const TRange<FTimespan> ActiveRange = GetActivePlaybackRange();
|
|
const FTimespan ActiveRangeStart = ActiveRange.GetLowerBoundValue();
|
|
const FTimespan ActiveRangeEnd = ActiveRange.GetUpperBoundValue();
|
|
|
|
if (Player->GetControls().IsLooping())
|
|
{
|
|
if (IsDurationValidAndFinite(Duration))
|
|
{
|
|
const FTimespan ActiveRangeDuration = ActiveRange.GetUpperBoundValue() - ActiveRange.GetLowerBoundValue();
|
|
if (Rate >= 0.0f)
|
|
{
|
|
while(NextEstVideoTimeAtFrameStart.TimeStamp.Time >= ActiveRangeEnd)
|
|
{
|
|
NextEstVideoTimeAtFrameStart.TimeStamp.Time -= ActiveRangeDuration;
|
|
NextEstVideoTimeAtFrameStart.TimeStamp.AdjustLoopIndex(1);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
while(NextEstVideoTimeAtFrameStart.TimeStamp.Time < ActiveRangeStart)
|
|
{
|
|
NextEstVideoTimeAtFrameStart.TimeStamp.Time += ActiveRangeDuration;
|
|
NextEstVideoTimeAtFrameStart.TimeStamp.AdjustLoopIndex(-1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (Rate >= 0.0f)
|
|
{
|
|
if (IsDurationValidAndFinite(Duration))
|
|
{
|
|
if (NextEstVideoTimeAtFrameStart.TimeStamp.Time >= ActiveRangeEnd)
|
|
{
|
|
NextEstVideoTimeAtFrameStart.TimeStamp.Time = ActiveRangeEnd - FTimespan(1);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (NextEstVideoTimeAtFrameStart.TimeStamp.Time < ActiveRangeStart)
|
|
{
|
|
NextEstVideoTimeAtFrameStart.TimeStamp.Time = ActiveRangeStart;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
TRange<FTimespan> FMediaPlayerFacade::GetActivePlaybackRange() const
|
|
{
|
|
TRange<FTimespan> Rng(FTimespan::Zero(), FTimespan::Zero());
|
|
if (Player)
|
|
{
|
|
if (SupportsPlaybackTimeRange())
|
|
{
|
|
Rng = GetPlaybackTimeRange(EMediaTimeRangeType::Current);
|
|
}
|
|
else
|
|
{
|
|
FTimespan Duration = Player->GetControls().GetDuration();
|
|
if (Duration <= FTimespan::Zero())
|
|
{
|
|
Duration = FTimespan::MaxValue();
|
|
}
|
|
Rng.SetUpperBound(Duration);
|
|
}
|
|
}
|
|
return Rng;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
const FTimespan Duration = Player->GetControls().GetDuration();
|
|
TRange<FTimespan> ActiveRange = GetActivePlaybackRange();
|
|
|
|
// We need a valid duration for the next steps (we may not have one e.g. for live material)
|
|
if (IsDurationValidAndFinite(Duration))
|
|
{
|
|
const FTimespan ActiveRangeDuration = ActiveRange.GetUpperBoundValue() - ActiveRange.GetLowerBoundValue();
|
|
// 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())
|
|
{
|
|
FTimespan WrappedStart = WrappedModulo(TimeRange.GetLowerBoundValue().Time - ActiveRange.GetLowerBoundValue(), ActiveRangeDuration) + ActiveRange.GetLowerBoundValue();
|
|
FTimespan WrappedEnd = WrappedModulo(TimeRange.GetUpperBoundValue().Time - ActiveRange.GetLowerBoundValue(), ActiveRangeDuration) + ActiveRange.GetLowerBoundValue();
|
|
if (WrappedStart > WrappedEnd)
|
|
{
|
|
if (WrappedStart != TimeRange.GetLowerBoundValue().Time)
|
|
{
|
|
TimeRange.SetLowerBoundValue(FMediaTimeStamp(WrappedStart, TimeRange.GetLowerBoundValue().GetSequenceIndex(), TimeRange.GetLowerBoundValue().GetLoopIndex() -1));
|
|
}
|
|
if (WrappedEnd != TimeRange.GetUpperBoundValue().Time)
|
|
{
|
|
TimeRange.SetUpperBoundValue(FMediaTimeStamp(WrappedEnd, TimeRange.GetUpperBoundValue().GetSequenceIndex(), TimeRange.GetUpperBoundValue().GetLoopIndex() + 1));
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TimeRange.SetLowerBoundValue(FMediaTimeStamp(FMath::Clamp(TimeRange.GetLowerBoundValue().Time, ActiveRange.GetLowerBoundValue(), ActiveRange.GetUpperBoundValue()), TimeRange.GetLowerBoundValue().GetSequenceIndex(), TimeRange.GetLowerBoundValue().GetLoopIndex()));
|
|
TimeRange.SetUpperBoundValue(FMediaTimeStamp(FMath::Clamp(TimeRange.GetUpperBoundValue().Time, ActiveRange.GetLowerBoundValue(), ActiveRange.GetUpperBoundValue()), TimeRange.GetUpperBoundValue().GetSequenceIndex(), TimeRange.GetUpperBoundValue().GetLoopIndex()));
|
|
}
|
|
}
|
|
|
|
return !TimeRange.IsEmpty();
|
|
}
|
|
|
|
|
|
TRange<FMediaTimeStamp> FMediaPlayerFacade::GetAdjustedBlockOnRange() const
|
|
{
|
|
TRange<FMediaTimeStamp> TimeRange = BlockOnRange.GetRange();
|
|
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());
|
|
|
|
//
|
|
// "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;
|
|
}
|
|
else 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() && !LastSampleTimeRange.IsEmpty())
|
|
{
|
|
// This is not the case: check more detailed!
|
|
|
|
// This better be true at all times
|
|
check(LastSampleTimeRange.GetLowerBoundValue().GetIndexValue() == LastSampleTimeRange.GetUpperBoundValue().GetIndexValue());
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
// Remap all values so we can assume all of them to be in a single "sequence index range" so the math doesn't get too unruly below.
|
|
// For that the sequence index must not have changed as we otherwise cannot assume any valid frame.
|
|
if (TimeRange.GetLowerBoundValue().GetSequenceIndex() != TimeRange.GetUpperBoundValue().GetSequenceIndex())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FTimespan Duration = Player->GetControls().GetDuration();
|
|
|
|
TRange<FMediaTimeStamp> TimeRange0;
|
|
const int32 LoopIndex0 = TimeRange.GetLowerBoundValue().GetLoopIndex();
|
|
const int32 LoopIndex1 = TimeRange.GetUpperBoundValue().GetLoopIndex();
|
|
int32 RefLoopIndex = LoopIndex0;
|
|
// If we encounter a loop we need to check if we can "unroll" it...
|
|
if (LoopIndex0 != LoopIndex1)
|
|
{
|
|
// We only should get here with a looping player that knows its duration
|
|
check(Player->GetControls().IsLooping());
|
|
check(IsDurationValidAndFinite(Duration));
|
|
|
|
// Compute how many loops and change the range into one "unrolled" one as indicated by the playback direction...
|
|
int32 LoopIdxDiff = LoopIndex1 - LoopIndex0;
|
|
// Note: this will be positive even with reverse playback as the orientation of the range will no change
|
|
check(LoopIdxDiff > 0);
|
|
|
|
double DurationD = Duration.GetTotalSeconds();
|
|
|
|
if (!bReverse)
|
|
{
|
|
TimeRange0 = TRange<FMediaTimeStamp>(FMediaTimeStamp(TimeRange.GetLowerBoundValue().Time, 0), FMediaTimeStamp(TimeRange.GetUpperBoundValue().Time + FTimespan::FromSeconds(LoopIdxDiff * DurationD), 0));
|
|
}
|
|
else
|
|
{
|
|
TimeRange0 = TRange<FMediaTimeStamp>(FMediaTimeStamp(TimeRange.GetLowerBoundValue().Time - FTimespan::FromSeconds(LoopIdxDiff * DurationD), 0), FMediaTimeStamp(TimeRange.GetUpperBoundValue().Time, 0));
|
|
RefLoopIndex = LoopIndex1;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Simple case, just bring everything down to "zero sequence index" for ease of processing below...
|
|
TimeRange0 = TRange<FMediaTimeStamp>(FMediaTimeStamp(TimeRange.GetLowerBoundValue().Time, 0, 0), FMediaTimeStamp(TimeRange.GetUpperBoundValue().Time, 0, 0));
|
|
|
|
// Is looping off?
|
|
if (!Player->GetControls().IsLooping())
|
|
{
|
|
// Yes. We clamp the range to the duration of the video to avoid looking at non-existent "next" frames... (unless we have no duration)
|
|
if (IsDurationValidAndFinite(Duration))
|
|
{
|
|
const TRange<FTimespan> ActiveRange = GetActivePlaybackRange();
|
|
TimeRange0 = TRange<FMediaTimeStamp>::Intersection(TimeRange0, TRange<FMediaTimeStamp>(FMediaTimeStamp(ActiveRange.GetLowerBoundValue(), 0), FMediaTimeStamp(ActiveRange.GetUpperBoundValue(), 0)));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Map the last sample's time range to the same "sequence index" range as the time range
|
|
// (note: for Live streams that do not have any set duration all this will not change the timerange - just as needed)
|
|
const int32 LastSampleLoopDiff = LastSampleTimeRange.GetLowerBoundValue().GetLoopIndex() - RefLoopIndex;
|
|
FTimespan TimeOffset = IsDurationValidAndFinite(Duration) ? Duration * LastSampleLoopDiff : FTimespan::Zero();
|
|
TRange<FMediaTimeStamp> LastSampleTimeRange0(FMediaTimeStamp(LastSampleTimeRange.GetLowerBoundValue().Time + TimeOffset, 0, 0), FMediaTimeStamp(LastSampleTimeRange.GetUpperBoundValue().Time + TimeOffset, 0, 0));
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
// Now we can begin the checks with all time ranges mapped back to "zero sequence index"
|
|
|
|
// Is the sample time range ahead of the given time range?
|
|
// (did the range move in an unexpected way?)
|
|
if (!bReverse ? TimeRange0.GetUpperBoundValue() <= LastSampleTimeRange0.GetLowerBoundValue()
|
|
: TimeRange0.GetLowerBoundValue() >= LastSampleTimeRange0.GetUpperBoundValue())
|
|
{
|
|
// We simply let the last sample stay around...
|
|
return true;
|
|
}
|
|
|
|
// 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 be "better"?
|
|
// (we assume samples of equal length)
|
|
|
|
// 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());
|
|
|
|
// Note: Loops (or the end of the time line in non-looping setups)
|
|
//
|
|
// - We could check for them and generate proper changes to the sequence index
|
|
// - Doing this would leave us with quite complex setups to compute the coverage
|
|
// - We opt for a cleaner, simpler approach: as we are NOT interested in proper PTS values here, we can safely work with an "infinite" time line when computing any overlaps, coverage and such
|
|
// (note: we DO need to restrict the range to the actual media duration if not looping - the code above does this)
|
|
//
|
|
// --> we simply keep what we compute above!
|
|
//
|
|
|
|
// 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();
|
|
|
|
// A new one is only desirable if it's BETTER than the current one
|
|
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)
|
|
{
|
|
if (!Player.IsValid())
|
|
{
|
|
// Nothing to do, but in a sense: "successful"...
|
|
return true;
|
|
}
|
|
|
|
// 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, BlockOnRange.IsSet()))
|
|
{
|
|
case IMediaSamples::EFetchBestSampleResult::Ok:
|
|
{
|
|
break;
|
|
}
|
|
|
|
case IMediaSamples::EFetchBestSampleResult::NoSample:
|
|
{
|
|
break;
|
|
}
|
|
|
|
case IMediaSamples::EFetchBestSampleResult::PurgedToEmpty:
|
|
{
|
|
// When there is no audio to sync to then we are extrapolating the next expected video timestamp
|
|
// from the last plus the elapsed deltatime, which may overshoot the next decoder output.
|
|
// In this case we resynchronize the timestamp to the next available video frame.
|
|
if (!HaveAudioPlayback())
|
|
{
|
|
NextEstVideoTimeAtFrameStart.Invalidate();
|
|
}
|
|
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, deliver it and update state...
|
|
|
|
FMediaTimeStamp SampleTime = Sample->GetTime();
|
|
TRange<FMediaTimeStamp> SampleTimeRange(SampleTime, SampleTime + 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;
|
|
MostRecentlyDeliveredVideoFrameTimecode = Sample->GetTimecode();
|
|
}
|
|
|
|
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.GetSequenceIndex(), SeekTargetTime.GetLoopIndex()));
|
|
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: Caption sample sink overflow"));
|
|
#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.GetSequenceIndex(), SeekTargetTime.GetLoopIndex()));
|
|
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: Subtitle sample sink overflow"));
|
|
#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.GetSequenceIndex(), SeekTargetTime.GetLoopIndex()));
|
|
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: Metadata sample sink overflow"));
|
|
#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: Audio sample sink overflow"));
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
FScopeLock Lock(&LastTimeValuesCS);
|
|
LastAudioSampleProcessedTime.TimeStamp = Sample->GetTime();
|
|
LastAudioSampleProcessedTime.SampledAtTime = FPlatformTime::Seconds();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FMediaPlayerFacade::ProcessVideoSamplesV1(IMediaSamples& Samples, TRange<FTimespan> TimeRange)
|
|
{
|
|
// 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: Fetched video sample %s"), *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: Next video sample time %s"), *NextVideoSampleTime.ToString(TEXT("%h:%m:%s.%t")));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
#if MEDIAPLAYERFACADE_TRACE_SINKOVERFLOWS
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade: Video sample sink overflow"));
|
|
#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: Caption sample sink overflow"));
|
|
#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: Subtitle sample sink overflow"));
|
|
#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: Metadata sample sink overflow"));
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* IMediaEventSink interface
|
|
*****************************************************************************/
|
|
|
|
void FMediaPlayerFacade::ReceiveMediaEvent(EMediaEvent Event)
|
|
{
|
|
if (Event >= EMediaEvent::Internal_Start)
|
|
{
|
|
switch (Event)
|
|
{
|
|
case EMediaEvent::Internal_PurgeVideoSamplesHint:
|
|
{
|
|
/*
|
|
Sent by some media players to ask to purge older samples from the video output queue.
|
|
This is done to ensure that, even if the game thread is stalled and the facade is not
|
|
being ticked regularly where it would perform this task by passing frames from the
|
|
queue to the sink, frames that have passed the point where they should have been
|
|
sent to the sink will not clog the queue.
|
|
The player cannot perform this task on its own because it does not know the current
|
|
precise playback position.
|
|
|
|
Here we need to handle only everything not audio because audio is pulled by the
|
|
audio thread and not the gamethread, so it can never stall.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
const 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().NumCaptionSamples() >= kMinFramesInCaptionQueueToPurge)
|
|
{
|
|
NumPurged = CurrentPlayer->GetSamples().PurgeOutdatedCaptionSamples(TimeRange.GetLowerBoundValue(), bReverse, FTimespan::FromSeconds(kOutdatedSubtitleSamplesTolerance * RateFactor));
|
|
}
|
|
SET_DWORD_STAT(STAT_MediaUtils_FacadeNumPurgedSubtitleSamples, NumPurged);
|
|
INC_DWORD_STAT_BY(STAT_MediaUtils_FacadeTotalPurgedSubtitleSamples, NumPurged);
|
|
|
|
// Subtitles...
|
|
NumPurged = 0;
|
|
if (CurrentPlayer->GetSamples().NumSubtitleSamples() >= kMinFramesInSubtitleQueueToPurge)
|
|
{
|
|
NumPurged = CurrentPlayer->GetSamples().PurgeOutdatedSubtitleSamples(TimeRange.GetLowerBoundValue(), bReverse, FTimespan::FromSeconds(kOutdatedSubtitleSamplesTolerance * RateFactor));
|
|
}
|
|
SET_DWORD_STAT(STAT_MediaUtils_FacadeNumPurgedCaptionSamples, NumPurged);
|
|
INC_DWORD_STAT_BY(STAT_MediaUtils_FacadeTotalPurgedCaptionSamples, NumPurged);
|
|
break;
|
|
}
|
|
|
|
case EMediaEvent::Internal_VideoSamplesAvailable:
|
|
{
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade: Video samples ARE available"));
|
|
VideoSampleAvailability = 1;
|
|
break;
|
|
}
|
|
case EMediaEvent::Internal_VideoSamplesUnavailable:
|
|
{
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade: Video samples are NOT available"));
|
|
VideoSampleAvailability = 0;
|
|
break;
|
|
}
|
|
case EMediaEvent::Internal_AudioSamplesAvailable:
|
|
{
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade: Audio samples ARE available"));
|
|
AudioSampleAvailability = 1;
|
|
break;
|
|
}
|
|
case EMediaEvent::Internal_AudioSamplesUnavailable:
|
|
{
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade: Audio samples are NOT available"));
|
|
AudioSampleAvailability = 0;
|
|
break;
|
|
}
|
|
|
|
default:
|
|
{
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade: Received media event %s"), *MediaUtils::EventToString(Event));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogMediaUtils, VeryVerbose, TEXT("PlayerFacade: Received media event %s"), *MediaUtils::EventToString(Event));
|
|
QueuedEvents.Enqueue(Event);
|
|
}
|
|
}
|
|
|
|
void FMediaPlayerFacade::ReInit()
|
|
{
|
|
// We leave the registered sinks and delegates alone
|
|
{
|
|
FScopeLock lock(&CriticalSection);
|
|
BlockOnRange.Reset();
|
|
BlockOnRangeDisabled = false;
|
|
CurrentUrl.Empty();
|
|
LastRate = 0.0f;
|
|
CurrentRate = 0.0f;
|
|
bHaveActiveAudio = false;
|
|
VideoSampleAvailability = -1;
|
|
AudioSampleAvailability = -1;
|
|
NextVideoSampleTime = FTimespan::Zero();
|
|
}
|
|
|
|
{
|
|
FScopeLock lock(&LastTimeValuesCS);
|
|
LastAudioRenderedSampleTime.Invalidate();
|
|
LastAudioSampleProcessedTime.Invalidate();
|
|
LastVideoSampleProcessedTimeRange = TRange<FMediaTimeStamp>::Empty();
|
|
CurrentFrameAudioTimeStamp.Invalidate();
|
|
CurrentFrameVideoTimeStamp.Invalidate();
|
|
CurrentFrameVideoDisplayTimeStamp.Invalidate();
|
|
NextEstVideoTimeAtFrameStart.Invalidate();
|
|
MostRecentlyDeliveredVideoFrameTimecode.Reset();
|
|
SeekTargetTime.Invalidate();
|
|
NextSeekTime.Reset();
|
|
NextSequenceIndex.Reset();
|
|
}
|
|
}
|
|
|