Files
TerracottaWarriors/Plugins/AndroidVulkanVideo_5.3/Source/AndroidVulkanVideo/Private/AndroidVulkanMediaPlayer.cpp

717 lines
22 KiB
C++
Raw Normal View History

2025-07-14 22:24:27 +08:00
// ------------------------------------------------
// Copyright Joe Marshall 2024- All Rights Reserved
// ------------------------------------------------
//
// The main IMediaPlayer implementation. This is a
// shim which provides Unreal interfaces, and then
// defers everything vulkan related to the main
// AndroidVulkanVideoImpl class.
// ------------------------------------------------
#include "AndroidVulkanMediaPlayer.h"
#include "AndroidVulkanTextureSample.h"
#include "IVulkanImpl.h"
#include "VideoMediaSampleHolder.h"
#include "UnrealArchiveFileSource.h"
#include "UnrealAudioOut.h"
#include "HAL/FileManager.h"
#include "HAL/PlatformFilemanager.h"
#include "IPlatformFilePak.h"
#include "Misc/ConfigCacheIni.h" // for GConfig access
#include "UnrealLogging.h"
#define LOCTEXT_NAMESPACE "FAndroidVulkanVideoModule"
#include <dlfcn.h>
static void *implDLL = NULL;
// logging object passed to impl
static UnrealLogger logger;
typedef IVulkanImpl *(*CreateImplType)();
typedef void (*DestroyImplType)(IVulkanImpl *);
static CreateImplType createImpl = NULL;
static DestroyImplType destroyImpl = NULL;
FAndroidVulkanMediaPlayer::FAndroidVulkanMediaPlayer(IMediaEventSink &InEventSink)
: SampleQueue(MakeShared<FVideoMediaSampleHolder, ESPMode::ThreadSafe>()),
EventSink(InEventSink)
{
if (implDLL == NULL)
{
implDLL = dlopen("libVkLayer_OverrideLib.so", RTLD_NOW | RTLD_LOCAL);
createImpl = (CreateImplType)(dlsym(implDLL, "createImpl"));
destroyImpl = (DestroyImplType)(dlsym(implDLL, "destroyImpl"));
}
impl = createImpl();
impl->setDataCallback(this);
impl->setLogger(&logger);
impl->setLooping(Looping);
// get log format bitmask from defaultEngine.ini
int32 LogVisibility;
if (GConfig->GetInt(TEXT("DirectVideo"), TEXT("LogBitmask"), LogVisibility, GEngineIni))
{
logger.SetLogVisibilityBitmask((int64)LogVisibility);
}
else
{
logger.SetLogVisibilityBitmask(ILogger::LogTypes::ALL_LOGS_BITMASK);
}
// get output format from defaultEngine.ini
FString OutFormat;
EMediaTextureSampleFormat SampleFormat = EMediaTextureSampleFormat::CharBGR10A2;
if (GConfig->GetString(TEXT("DirectVideo"), TEXT("OutputFormat"), OutFormat, GEngineIni))
{
if (OutFormat.Equals(TEXT("CharBGR10A2"), ESearchCase::IgnoreCase))
{
SampleFormat = EMediaTextureSampleFormat::CharBGR10A2;
}
else if (OutFormat.Equals(TEXT("CharBGRA"), ESearchCase::IgnoreCase))
{
SampleFormat = EMediaTextureSampleFormat::CharBGRA;
}
else if (OutFormat.Equals(TEXT("CharRGBA"), ESearchCase::IgnoreCase))
{
SampleFormat = EMediaTextureSampleFormat::CharRGBA;
}
else if (OutFormat.Equals(TEXT("RGBA16"), ESearchCase::IgnoreCase))
{
SampleFormat = EMediaTextureSampleFormat::RGBA16;
}
else if (OutFormat.Equals(TEXT("FloatRGB"), ESearchCase::IgnoreCase))
{
SampleFormat = EMediaTextureSampleFormat::FloatRGB;
}
else if (OutFormat.Equals(TEXT("FloatRGBA"), ESearchCase::IgnoreCase))
{
SampleFormat = EMediaTextureSampleFormat::FloatRGBA;
}
else
{
UE_LOGFMT(LogDirectVideo, Error, "Bad sample format in defaultEngine.ini");
}
}
AndroidVulkanTextureSample::SetVideoFormat(SampleFormat);
AudioOut = new UnrealAudioOut(NULL);
impl->setAudioOut(AudioOut);
PlayState = EMediaState::Closed;
CurInfo.Empty();
DelegateEnterBackground = FCoreDelegates::ApplicationWillEnterBackgroundDelegate.AddRaw(
this, &FAndroidVulkanMediaPlayer::OnEnterBackground);
DelegateEnterForeground = FCoreDelegates::ApplicationHasEnteredForegroundDelegate.AddRaw(
this, &FAndroidVulkanMediaPlayer::OnEnterForeground);
SeekIndex = 0;
SentBlankFrame = false;
Seeking = false;
}
FAndroidVulkanMediaPlayer::~FAndroidVulkanMediaPlayer()
{
Close();
SentBlankFrame = true;
if (DelegateEnterBackground.IsValid())
{
FCoreDelegates::ApplicationWillEnterBackgroundDelegate.Remove(DelegateEnterBackground);
}
if (DelegateEnterForeground.IsValid())
{
FCoreDelegates::ApplicationHasEnteredForegroundDelegate.Remove(DelegateEnterForeground);
}
destroyImpl(impl);
impl = NULL;
delete AudioOut;
AudioOut = NULL;
}
// IMediaPlayer
// --------------------------------
void FAndroidVulkanMediaPlayer::Close()
{
SentBlankFrame = false;
if (PlayState != EMediaState::Closed)
{
UE_LOGFMT(LogDirectVideo, VeryVerbose, "Media player close called");
SampleQueue->FlushSamples();
VideoSamplePool.ReleaseEverything();
SeekIndex = 0;
Seeking = false;
EventSink.ReceiveMediaEvent(EMediaEvent::TracksChanged);
EventSink.ReceiveMediaEvent(EMediaEvent::MediaClosed);
PlayState = EMediaState::Closed;
// close the impl last because it will
// kill all the texture images that we release in the pool above
impl->close();
}
}
FString FAndroidVulkanMediaPlayer::GetInfo() const
{
return CurInfo;
}
FGuid FAndroidVulkanMediaPlayer::GetPlayerPluginGUID() const
{
static FGuid OurGUID(0x9bf2d7c6, 0xb2b84d26, 0xb6ae5a3a, 0xc9883569);
return OurGUID;
}
FString FAndroidVulkanMediaPlayer::GetStats() const
{
return TEXT("Not implemented");
}
FString FAndroidVulkanMediaPlayer::GetUrl() const
{
return VideoURL;
}
bool FAndroidVulkanMediaPlayer::Open(const FString &Url, const IMediaOptions *Options)
{
Close();
UE_LOGFMT(LogDirectVideo, VeryVerbose, "Try open Url {0}", Url);
SentBlankFrame = false;
bool started = false;
FString fullPath = Url;
if (fullPath.StartsWith("file://"))
{
fullPath = fullPath.RightChop(7);
}
if (fullPath.Contains("://"))
{
// a (non-file url)
started = impl->startVideoURL(TCHAR_TO_UTF8(*fullPath), false);
}
else
{
// a file path - check if it is local or not
if (fullPath.StartsWith("./"))
{
fullPath = FPaths::ProjectContentDir() + fullPath.RightChop(2);
}
FPaths::NormalizeFilename(fullPath);
UE_LOGFMT(LogDirectVideo, VeryVerbose, "Start video {0}", fullPath);
IAndroidPlatformFile &PlatformFile = IAndroidPlatformFile::GetPlatformPhysical();
if (PlatformFile.FileExists(*fullPath))
{
int64 FileOffset = PlatformFile.FileStartOffset(*fullPath);
int64 FileSize = PlatformFile.FileSize(*fullPath);
FString FileRootPath = PlatformFile.FileRootPath(*fullPath);
UE_LOGFMT(LogDirectVideo, VeryVerbose, "File exists: {0} {1} {2} {3}", FileRootPath,
FileOffset, FileSize, fullPath);
started =
impl->startVideoFile(TCHAR_TO_UTF8(*FileRootPath), FileOffset, FileSize, false);
}
else
{
// check if the file exists in an archive / encrypted etc.
FPakPlatformFile *PakPlatformFile =
(FPakPlatformFile *)(FPlatformFileManager::Get().FindPlatformFile(
FPakPlatformFile::GetTypeName()));
TRefCountPtr<FPakFile> PakFile;
FPakEntry FileEntry;
if (PakPlatformFile != nullptr &&
PakPlatformFile->FindFileInPakFiles(*fullPath, &PakFile, &FileEntry))
{
// we have the file in a pak file
// use a custom media data source to read it
// n.b. if pak file is uncompressed and the file isn't encrypted we could skip this
// step but this should work whatever
TSharedRef<FArchive, ESPMode::ThreadSafe> Archive =
MakeShareable(IFileManager::Get().CreateFileReader(*fullPath));
UnrealArchiveFileSource *fs = new UnrealArchiveFileSource(Archive);
started = impl->startVideoCustomSource(fs, false);
}
}
}
if (started)
{
PlayState = EMediaState::Stopped;
EventSink.ReceiveMediaEvent(EMediaEvent::TracksChanged);
EventSink.ReceiveMediaEvent(EMediaEvent::MediaOpened);
UE_LOGFMT(LogDirectVideo, Verbose, "Opening {0}", *Url);
SeekIndex = 0;
Seeking = false;
}
else
{
// couldn't open
UE_LOGFMT(LogDirectVideo, Error, "Can't find video file {0}", *Url);
}
return started;
}
bool FAndroidVulkanMediaPlayer::Open(const TSharedRef<FArchive, ESPMode::ThreadSafe> &Archive,
const FString &OriginalUrl, const IMediaOptions *Options)
{
UE_LOGFMT(LogDirectVideo, Error, "Opening archive {0} not supported", *OriginalUrl);
return false;
}
void FAndroidVulkanMediaPlayer::SetGuid(const FGuid &Guid)
{
PlayerGUID = Guid;
}
void FAndroidVulkanMediaPlayer::TickFetch(FTimespan DeltaTime, FTimespan Timecode)
{
UE_LOGFMT(LogDirectVideo, VeryVerbose, "TickFetch");
}
void FAndroidVulkanMediaPlayer::TickInput(FTimespan DeltaTime, FTimespan Timecode)
{
UE_LOGFMT(LogDirectVideo, VeryVerbose, "TickInput start");
/* int64_t presTimeNs = impl->getTimeNS();
SampleQueue->SetLastTimeStamp(
FMediaTimeStamp(DeltaTime,
#if UE_VERSION_OLDER_THAN(5, 3, 0)
static_cast<int64>(SeekIndex) << 32));
#else
FMediaTimeStamp::MakeSequenceIndex(SeekIndex, 0)));
#endif
*/
HasVideoThisFrame = false;
VideoSamplePool.Tick();
if (!SentBlankFrame && impl->numVideoTracks() == 0)
{
UE_LOGFMT(LogDirectVideo, VeryVerbose, "blank frame");
SentBlankFrame = true;
auto textureSample = VideoSamplePool.AcquireShared();
textureSample->InitNoVideo();
SampleQueue->AddVideo(textureSample);
}
UE_LOGFMT(LogDirectVideo, VeryVerbose, "TickInput dt:{0} tc {1} num {2}", DeltaTime.GetTicks(),
Timecode.GetTicks(), SampleQueue->NumVideoSamples());
}
bool FAndroidVulkanMediaPlayer::GetPlayerFeatureFlag(EFeatureFlag flag) const
{
switch (flag)
{
case EFeatureFlag::PlayerUsesInternalFlushOnSeek:
return true;
case EFeatureFlag::AlwaysPullNewestVideoFrame:
return true;
case EFeatureFlag::UsePlaybackTimingV2:
return true;
case EFeatureFlag::IsTrackSwitchSeamless:
return true;
case EFeatureFlag::UseRealtimeWithVideoOnly:
UE_LOGFMT(LogDirectVideo, VeryVerbose, "Feature flag query");
return true;
default:
return false;
}
}
// IMediaControl
// --------------------------------
bool FAndroidVulkanMediaPlayer::CanControl(EMediaControl Control) const
{
switch (Control)
{
case EMediaControl::Pause:
return PlayState == EMediaState::Playing;
case EMediaControl::Resume:
return PlayState == EMediaState::Stopped;
case EMediaControl::Seek:
return PlayState != EMediaState::Closed;
}
return false;
}
FTimespan FAndroidVulkanMediaPlayer::GetDuration() const
{
int64_t timeNS = impl->getDurationNS();
if (timeNS >= 0 && PlayState != EMediaState::Closed)
{
UE_LOGFMT(LogDirectVideo, VeryVerbose, "Get Duration {0}", int64(timeNS));
return FTimespan(timeNS / 100LL);
}
else
{
UE_LOGFMT(LogDirectVideo, VeryVerbose, "Get Duration empty");
return FTimespan::MaxValue();
}
}
float FAndroidVulkanMediaPlayer::GetRate() const
{
if (PlayState == EMediaState::Playing)
{
return impl->getRate();
}
else
{
return 0.0;
}
}
EMediaState FAndroidVulkanMediaPlayer::GetState() const
{
return PlayState;
}
EMediaStatus FAndroidVulkanMediaPlayer::GetStatus() const
{
// not supported yet
return EMediaStatus::None;
}
TRangeSet<float> FAndroidVulkanMediaPlayer::GetSupportedRates(EMediaRateThinning Thinning) const
{
TRangeSet<float> Retval;
if (impl->numAudioTracks() > 0)
{
Retval.Add(TRange<float>(0.0f));
Retval.Add(TRange<float>(1.0f));
}
else
{
Retval.Add(TRange<float>(TRange<float>::BoundsType::Inclusive(0.0f),
TRange<float>::BoundsType::Inclusive(10.0f)));
}
return Retval;
}
FTimespan FAndroidVulkanMediaPlayer::GetTime() const
{
int64_t timeNS = impl->getTimeNS();
UE_LOGFMT(LogDirectVideo, VeryVerbose, "Get Time {0}", int64(timeNS));
if (timeNS >= 0)
{
return FTimespan(timeNS / 100LL);
}
return FTimespan::Zero();
}
bool FAndroidVulkanMediaPlayer::IsLooping() const
{
return Looping;
}
bool FAndroidVulkanMediaPlayer::Seek(const FTimespan &Time)
{
UE_LOGFMT(LogDirectVideo, Verbose, "Seek index:{0}", SeekIndex);
Seeking = true;
int64_t ticks = Time.GetTicks();
int64_t nanoseconds = ticks * 100LL;
impl->seek(nanoseconds);
return true;
}
bool FAndroidVulkanMediaPlayer::SetLooping(bool Loop)
{
Looping = Loop;
impl->setLooping(Looping);
return true;
}
bool FAndroidVulkanMediaPlayer::SetRate(float Rate)
{
bool retval = false;
int iInitialState = (int)PlayState;
switch (PlayState)
{
case EMediaState::Playing:
if (Rate == 0.0)
{
impl->setPlaying(false);
PlayState = EMediaState::Stopped;
EventSink.ReceiveMediaEvent(EMediaEvent::PlaybackSuspended);
retval = true;
}
else
{
return impl->setRate(Rate);
retval = true;
}
break;
case EMediaState::Stopped:
if (Rate == 0.0)
{
retval = true;
}
else if (Rate > 0.0)
{
impl->setPlaying(true);
PlayState = EMediaState::Playing;
retval = impl->setRate(Rate);
if (retval)
{
EventSink.ReceiveMediaEvent(EMediaEvent::PlaybackResumed);
}
}
break;
};
int iFinalState = (int)PlayState;
UE_LOGFMT(LogDirectVideo, Verbose, "Set rate {0} {1} state:{2}->{3}", Rate, retval,
iInitialState, iFinalState);
// return false;
return retval;
}
bool FAndroidVulkanMediaPlayer::SetNativeVolume(float Volume)
{
return AudioOut->setVolume(Volume);
}
// IMediaTracks
// --------------------------------
bool FAndroidVulkanMediaPlayer::GetAudioTrackFormat(int32 TrackIndex, int32 FormatIndex,
FMediaAudioTrackFormat &OutFormat) const
{
if (FormatIndex != 0 || PlayState == EMediaState::Closed)
{
return false;
}
int32 bitsPerSample;
int32 channels;
int32 rate;
if (!impl->getAudioTrackFormat(TrackIndex, &bitsPerSample, &channels, &rate))
{
return false;
}
OutFormat.BitsPerSample = bitsPerSample;
OutFormat.NumChannels = channels;
OutFormat.SampleRate = rate;
OutFormat.TypeName = TEXT("Native");
UE_LOGFMT(LogDirectVideo, VeryVerbose, "Get audio trackformat {0} {1} {2} {3}", TrackIndex,
bitsPerSample, channels, rate);
return true;
}
int32 FAndroidVulkanMediaPlayer::GetNumTracks(EMediaTrackType TrackType) const
{
UE_LOGFMT(LogDirectVideo, VeryVerbose, "Get num tracks type: {0}", int(TrackType));
// TODO: support audio / video only
switch (TrackType)
{
case EMediaTrackType::Audio:
return impl->numAudioTracks();
case EMediaTrackType::Video:
return impl->numVideoTracks();
default:
return 0;
}
}
int32 FAndroidVulkanMediaPlayer::GetNumTrackFormats(EMediaTrackType TrackType,
int32 TrackIndex) const
{
if (TrackIndex >= GetNumTracks(TrackType))
{
return 0;
}
switch (TrackType)
{
case EMediaTrackType::Audio:
return 1;
case EMediaTrackType::Video:
return 1;
default:
return 0;
}
}
int32 FAndroidVulkanMediaPlayer::GetSelectedTrack(EMediaTrackType TrackType) const
{
if (TrackType == EMediaTrackType::Audio)
{
UE_LOGFMT(LogDirectVideo, VeryVerbose, "Returning no selected audio track");
return INDEX_NONE;
}
UE_LOGFMT(LogDirectVideo, VeryVerbose, "Returning selected track type {0}", int(TrackType));
return 0;
}
FText FAndroidVulkanMediaPlayer::GetTrackDisplayName(EMediaTrackType TrackType,
int32 TrackIndex) const
{
// TODO: pass through display names
return FText::Format(LOCTEXT("TrackName", "Track {0} type {1}"), (int)TrackType, TrackIndex);
}
int32 FAndroidVulkanMediaPlayer::GetTrackFormat(EMediaTrackType TrackType, int32 TrackIndex) const
{
return 0;
}
FString FAndroidVulkanMediaPlayer::GetTrackLanguage(EMediaTrackType TrackType,
int32 TrackIndex) const
{
// TODO - pass language through
return TEXT("");
}
FString FAndroidVulkanMediaPlayer::GetTrackName(EMediaTrackType TrackType, int32 TrackIndex) const
{
if (TrackIndex == 0 && PlayState != EMediaState::Closed)
{
return TEXT("TRACK");
}
else
{
return TEXT("");
}
}
bool FAndroidVulkanMediaPlayer::GetVideoTrackFormat(int32 TrackIndex, int32 FormatIndex,
FMediaVideoTrackFormat &OutFormat) const
{
if (FormatIndex != 0 || PlayState == EMediaState::Closed)
{
return false;
}
int w = 0, h = 0;
float frameRate = 0;
if (!impl->getVideoTrackFormat(TrackIndex, &w, &h, &frameRate))
{
return false;
}
OutFormat.Dim.X = w;
OutFormat.Dim.Y = h;
OutFormat.FrameRate = frameRate;
OutFormat.FrameRates = TRange<float>(frameRate);
OutFormat.TypeName = TEXT("Vulkan Video Frame");
UE_LOGFMT(LogDirectVideo, VeryVerbose, "Got out video format");
return true;
}
bool FAndroidVulkanMediaPlayer::SelectTrack(EMediaTrackType TrackType, int32 TrackIndex)
{
if (TrackType == EMediaTrackType::Audio)
{
UE_LOGFMT(LogDirectVideo, VeryVerbose, "Not selecting audio track");
return false;
}
UE_LOGFMT(LogDirectVideo, VeryVerbose, "Selecting track type {0} = {0}", int(TrackType),
TrackIndex);
return true;
// return TrackIndex == 0 && PlayState != EMediaState::Closed;
}
bool FAndroidVulkanMediaPlayer::SetTrackFormat(EMediaTrackType TrackType, int32 TrackIndex,
int32 FormatIndex)
{
// todo: support multiple formats / track
return TrackIndex == 0 && FormatIndex == 0 && PlayState != EMediaState::Closed;
}
IMediaSamples &FAndroidVulkanMediaPlayer::GetSamples()
{
return *SampleQueue.Get();
}
void FAndroidVulkanMediaPlayer::onVideoFrame(void *frameHwBuffer, int w, int h, int64_t presTimeNs)
{
if (PlayState == EMediaState::Closed)
{
UE_LOGFMT(LogDirectVideo, VeryVerbose, "Releasing frame as we are closed");
impl->releaseFrame(frameHwBuffer);
return;
}
if (HasVideoThisFrame)
{
impl->releaseFrame(frameHwBuffer);
return;
}
HasVideoThisFrame = true;
auto textureSample = VideoSamplePool.AcquireShared();
textureSample->Init(impl, frameHwBuffer, w, h,
FMediaTimeStamp(FTimespan(presTimeNs / 100LL),
FMediaTimeStamp::MakeSequenceIndex(SeekIndex, 0)));
SampleQueue->AddVideo(textureSample);
UE_LOGFMT(LogDirectVideo, VeryVerbose, "Add video sample");
// UE_LOGFMT(LogDirectVideo, VeryVerbose, "Video samples {0}",
// SampleQueue->NumVideoSamples());
// NB: this needs to happen after the sample is in the queue, so that
// the queue has something in when media player facade samples it for playback
// time
if (Seeking)
{
SeekIndex += 1;
UE_LOGFMT(LogDirectVideo, Verbose, "Seek completed {0}", SeekIndex);
Seeking = false;
EventSink.ReceiveMediaEvent(EMediaEvent::SeekCompleted);
}
}
void FAndroidVulkanMediaPlayer::OnEnterBackground()
{
// going into backgroud - if playing, pause implementation player
if (PlayState == EMediaState::Playing)
{
impl->setPlaying(false);
EventSink.ReceiveMediaEvent(EMediaEvent::PlaybackSuspended);
UE_LOGFMT(LogDirectVideo, Verbose, "Enter suspend");
}
}
void FAndroidVulkanMediaPlayer::OnEnterForeground()
{
// back into foreground - if playing, start
if (PlayState == EMediaState::Playing)
{
impl->setPlaying(true);
EventSink.ReceiveMediaEvent(EMediaEvent::PlaybackResumed);
UE_LOGFMT(LogDirectVideo, Verbose, "Enter foreground");
}
}
void FAndroidVulkanMediaPlayer::ProcessVideoSamples()
{
UE_LOGFMT(LogDirectVideo, VeryVerbose, "Process video samples");
}
void *FAndroidVulkanMediaPlayer::getVkDeviceProcAddr(const char *name)
{
void *result = static_cast<void *>(
(static_cast<struct IVulkanDynamicRHI *>(GDynamicRHI))->RHIGetVkDeviceProcAddr(name));
return result;
}
VkDevice FAndroidVulkanMediaPlayer::getVkDevice()
{
IVulkanDynamicRHI *rhi = static_cast<struct IVulkanDynamicRHI *>(GDynamicRHI);
if (rhi == NULL)
{
return VK_NULL_HANDLE;
}
return rhi->RHIGetVkDevice();
}
const VkAllocationCallbacks *FAndroidVulkanMediaPlayer::getVkAllocationCallbacks()
{
IVulkanDynamicRHI *rhi = static_cast<struct IVulkanDynamicRHI *>(GDynamicRHI);
if (rhi == NULL)
{
return NULL;
}
return rhi->RHIGetVkAllocationCallbacks();
}
VkPhysicalDevice FAndroidVulkanMediaPlayer::getNativePhysicalDevice()
{
return static_cast<VkPhysicalDevice>(GDynamicRHI->RHIGetNativePhysicalDevice());
}
void FAndroidVulkanMediaPlayer::onPlaybackEnd(bool looping)
{
EventSink.ReceiveMediaEvent(EMediaEvent::PlaybackEndReached);
if (!looping)
{
PlayState = EMediaState::Stopped;
EventSink.ReceiveMediaEvent(EMediaEvent::PlaybackSuspended);
}
}
#undef LOCTEXT_NAMESPACE