// Copyright Epic Games Tools LLC // Licenced under the Unreal Engine EULA #include "BinkMediaPlayer.h" #include "BinkMediaPlayerPrivate.h" #include "BinkMovieStreamer.h" #include "Engine/GameViewportClient.h" #include "Engine/Texture.h" #include "Misc/Paths.h" #include "RenderingThread.h" #include "HDRHelper.h" #include "HAL/PlatformFileManager.h" #include "Internationalization/Culture.h" #include "SubtitleManager.h" #include "UObject/UObjectIterator.h" #include "binkplugin_ue4.h" #include "egttypes.h" #include "RectLightTexture.h" //#include "Media/Public/IMediaEventSink.h" //For EMediaEvent static void Command_ShowBinks() { for (TObjectIterator It; It; ++It) { if (!It->HasAnyFlags(RF_ClassDefaultObject)) { const TCHAR* StateStr = TEXT("Unloaded"); if (It->IsInitialized()) { if (It->IsPaused()) { StateStr = TEXT("Paused"); } else { StateStr = TEXT("Playing"); } } const TCHAR* DrawStyleStr = TEXT("RenderToTexture"); if (It->BinkDrawStyle != BMASM_Bink_DS_RenderToTexture) { DrawStyleStr = TEXT("Overlay"); } const double DurationSeconds = It->GetDuration().GetTotalSeconds(); const double PlaybackCompletion = (DurationSeconds > 0.0) ? It->GetTime().GetTotalSeconds() / DurationSeconds : 0.0; UE_LOG(LogBinkMoviePlayer, Display, TEXT("[%s] URL: %s, DrawStyle: %s, State: %s, PlaybackPercentage: %d%%"), *It->GetName(), *It->GetUrl(), DrawStyleStr, StateStr, static_cast(PlaybackCompletion * 100.f)); } } } static FAutoConsoleCommand ConsoleCmdShowBinks( TEXT("bink.List"), TEXT("Display all Bink Media Player objects and their current state"), FConsoleCommandDelegate::CreateStatic(&Command_ShowBinks)); UBinkMediaPlayer::UBinkMediaPlayer( const FObjectInitializer& ObjectInitializer ) : Super(ObjectInitializer) , Looping(true) , StartImmediately(true) , DelayedOpen(true) , BinkDestinationUpperLeft(0,0) , BinkDestinationLowerRight(1,1) , BinkBufferMode(BMASM_Bink_Stream) , BinkSoundTrack(BMASM_Bink_Sound_None) , BinkSoundTrackStart(0) , BinkDrawStyle() , BinkLayerDepth() , CurrentBinkBufferMode(BMASM_Bink_MAX) , CurrentBinkSoundTrack(BMASM_Bink_Sound_MAX) , CurrentBinkSoundTrackStart(-1) , CurrentUrl() , CurrentDrawStyle() , CurrentLayerDepth() , bnk() , paused() , reached_end() { } bool UBinkMediaPlayer::CanPause() const { return IsPlaying(); } bool UBinkMediaPlayer::CanPlay() const { return IsReady(); } bool UBinkMediaPlayer::IsStopped() const { return !IsReady(); } const FString& UBinkMediaPlayer::GetUrl() const { return CurrentUrl; } bool UBinkMediaPlayer::Pause() { return SetRate(0.0f); } bool UBinkMediaPlayer::Play() { return SetRate(1.0f); } bool UBinkMediaPlayer::Rewind() { return Seek(FTimespan::Zero()); } bool UBinkMediaPlayer::SupportsRate( float Rate, bool Unthinned ) const { return Rate == 1; } bool UBinkMediaPlayer::SupportsScrubbing() const { return true; } bool UBinkMediaPlayer::SupportsSeeking() const { return true; } FString UBinkMediaPlayer::GetDesc() { return TEXT("UBinkMediaPlayer"); } FTimespan UBinkMediaPlayer::GetDuration() const { double ms = 0; if(bnk) { BINKPLUGININFO bpinfo = {}; BinkPluginInfo(bnk, &bpinfo); ms = ((double)bpinfo.Frames) * ((double)bpinfo.FrameRateDiv) * 1000.0 / ((double)bpinfo.FrameRate); } return FTimespan::FromMilliseconds(ms); } float UBinkMediaPlayer::GetRate() const { if(bnk) { BINKPLUGININFO bpinfo = {}; BinkPluginInfo(bnk, &bpinfo); return bpinfo.PlaybackState == 0 && !paused ? 1 : 0; } return 0; } FTimespan UBinkMediaPlayer::GetTime() const { double ms = 0; if(bnk) { BINKPLUGININFO bpinfo = {}; BinkPluginInfo(bnk, &bpinfo); ms = ((double)bpinfo.FrameNum) * ((double)bpinfo.FrameRateDiv) *1000.0 / ((double)bpinfo.FrameRate); } return FTimespan::FromMilliseconds(ms); } bool UBinkMediaPlayer::IsLooping() const { if(bnk) { BINKPLUGININFO bpinfo = {}; BinkPluginInfo(bnk, &bpinfo); if (bpinfo.PlaybackState < 3 && !bpinfo.LoopsRemaining) { return true; } } return false; } bool UBinkMediaPlayer::IsPaused() const { if(bnk) { BINKPLUGININFO bpinfo = {}; BinkPluginInfo(bnk, &bpinfo); return bpinfo.PlaybackState == 1 || paused; // TODO: When the video is paused, the PlaybackState is 0 (should be 1). } return false; } bool UBinkMediaPlayer::IsPlaying() const { if(bnk) { BINKPLUGININFO bpinfo = {}; BinkPluginInfo(bnk, &bpinfo); return bpinfo.PlaybackState == 0 && !paused; } return false; } bool UBinkMediaPlayer::IsGotoing() const { if (bnk) { BINKPLUGININFO bpinfo = {}; BinkPluginInfo(bnk, &bpinfo); return bpinfo.PlaybackState == 2; } return false; } bool UBinkMediaPlayer::OpenUrl( const FString& NewUrl ) { if (NewUrl.IsEmpty()) { return false; } URL = NewUrl; InitializePlayer(); return CurrentUrl == NewUrl; } void UBinkMediaPlayer::CloseUrl( ) { URL = ""; InitializePlayer(); } bool UBinkMediaPlayer::SetLooping( bool InLooping ) { if(bnk) { BinkPluginLoop(bnk, InLooping ? 0 : 1); } return false; } bool UBinkMediaPlayer::SetRate( float Rate ) { if(bnk) { BINKPLUGININFO bpinfo = {}; BinkPluginInfo(bnk, &bpinfo); if((bpinfo.PlaybackState == 1 || paused) && Rate == 1) // If paused and set the rate to 1.0 { BinkPluginPause(bnk, 0); paused = false; reached_end = false; //MediaEvent.Broadcast(EMediaEvent::PlaybackResumed); return true; } else if(bpinfo.PlaybackState == 3 && Rate == 1) { BinkPluginGoto(bnk, 1, -1); BinkPluginPause(bnk, 0); paused = false; reached_end = false; //MediaEvent.Broadcast(EMediaEvent::PlaybackResumed); return true; } else if (bpinfo.PlaybackState == 0 && Rate == 0) { BinkPluginPause(bnk, -1); paused = true; //MediaEvent.Broadcast(EMediaEvent::PlaybackSuspended); OnPlaybackSuspended.Broadcast(); return true; } } return false; } void UBinkMediaPlayer::SetVolume( float Volume ) { if(bnk) { // Clamp 0 .. 1 Volume = Volume < 0 ? 0 : Volume > 1 ? 1 : Volume; BinkPluginVolume(bnk, Volume); } } bool UBinkMediaPlayer::Seek( const FTimespan& InTime ) { if (!bnk) { return false; } BINKPLUGININFO bpinfo = {}; BinkPluginInfo(bnk, &bpinfo); U32 desiredFrame = (U32)(InTime.GetTotalMilliseconds() * ((double)bpinfo.FrameRate) / (1000.0*((double)bpinfo.FrameRateDiv))); if (bpinfo.FrameNum != (desiredFrame + 1)) { BinkPluginGoto(bnk, desiredFrame + 1, -1); reached_end = false; } return true; } void UBinkMediaPlayer::BeginDestroy() { Super::BeginDestroy(); Close(); } void UBinkMediaPlayer::PostLoad() { Super::PostLoad(); if (!HasAnyFlags(RF_ClassDefaultObject) && !GIsBuildMachine && (!DelayedOpen || StartImmediately)) { InitializePlayer(); } } #if BINKPLUGIN_UE4_EDITOR void UBinkMediaPlayer::PostEditChangeProperty( FPropertyChangedEvent& PropertyChangedEvent ) { Super::PostEditChangeProperty(PropertyChangedEvent); InitializePlayer(); } #endif bool UBinkMediaPlayer::Open(const FString& Url) { if (!BinkInitialize()) { UE_LOG(LogBinkMoviePlayer, Error, TEXT("UBinkMediaPlayer::Open: failed to initialize bink!")); return false; } if (Url.IsEmpty()) { UE_LOG(LogBinkMoviePlayer, Error, TEXT("UBinkMediaPlayer::Open: Failed! Url is empty.")); return false; } if (IsPlaying()) { UE_LOG(LogBinkMoviePlayer, Error, TEXT("UBinkMediaPlayer::Open: Failed! Already playing.")); return false; } if(bnk) { ENQUEUE_RENDER_COMMAND(BinkMediaPlayer_Open_CloseBink)([bnk=bnk](FRHICommandListImmediate& RHICmdList) { BinkPluginClose(bnk); }); bnk = NULL; } BinkPluginLimitSpeakers(GetNumSpeakers()); // Use the platform file layer to open the media file. We // need to access Bink specific information to allow // for playing media that is embedded in the APK, OBBs, // and/or PAKs. IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); #if PLATFORM_ANDROID // Construct a canonical path for the movie. FString MoviePath = Url; FPaths::NormalizeFilename(MoviePath); // Don't bother trying to play it if we can't find it. if (!IAndroidPlatformFile::GetPlatformPhysical().FileExists(*MoviePath)) { UE_LOG(LogBinkMoviePlayer, Error, TEXT("UBinkMediaPlayer::Open: Failed! File doesn't exist. URL :%s File :%s"), *Url, *MoviePath); return false; } // Get information about the movie. int64 FileOffset = IAndroidPlatformFile::GetPlatformPhysical().FileStartOffset(*MoviePath); FString FileRootPath = IAndroidPlatformFile::GetPlatformPhysical().FileRootPath(*MoviePath); // Play the movie as a file or asset. if (IAndroidPlatformFile::GetPlatformPhysical().IsAsset(*MoviePath)) { if (JNIEnv* env = FAndroidApplication::GetJavaEnv()) { extern struct android_app* GNativeAndroidApp; jclass clazz = env->GetObjectClass(GNativeAndroidApp->activity->clazz); jmethodID methodID = env->GetMethodID(clazz, "getPackageCodePath", "()Ljava/lang/String;"); jobject result = env->CallObjectMethod(GNativeAndroidApp->activity->clazz, methodID); jboolean isCopy; const char *apkPath = env->GetStringUTFChars((jstring)result, &isCopy); bnk = BinkPluginOpen(apkPath, BinkSoundTrack, BinkSoundTrackStart, BinkBufferMode, FileOffset); env->ReleaseStringUTFChars((jstring)result, apkPath); } } else { bnk = BinkPluginOpen(TCHAR_TO_ANSI(*FileRootPath), BinkSoundTrack, BinkSoundTrackStart, BinkBufferMode, FileOffset); } #else if (FPlatformFileManager::Get().GetPlatformFile().FileExists(*Url)) { #if PLATFORM_WINDOWS bnk = BinkPluginOpenUTF16((unsigned short *)*PlatformFile.ConvertToAbsolutePathForExternalAppForRead(*Url), BinkSoundTrack, BinkSoundTrackStart, BinkBufferMode, 0); #else bnk = BinkPluginOpen(TCHAR_TO_ANSI(*PlatformFile.ConvertToAbsolutePathForExternalAppForRead(*Url)), BinkSoundTrack, BinkSoundTrackStart, BinkBufferMode, 0); #endif } #endif if(!bnk) { //MediaEvent.Broadcast(EMediaEvent::MediaOpenFailed); UE_LOG(LogBinkMoviePlayer, Error, TEXT("UBinkMediaPlayer::Open: Failed! BinkPluginOpen failed. Invalid bnk.")); return false; } // Try to open subtitles... { // Determine what out current culture is, and grab the most appropriate set of subtitles for it FInternationalization& Internationalization = FInternationalization::Get(); const TArray PrioritizedLanguageNames = Internationalization.GetPrioritizedCultureNames(Internationalization.GetCurrentLanguage()->GetName()); CurrentHasSubtitles = 0; for (const FString& LanguageName : PrioritizedLanguageNames) { int32 Pos = INDEX_NONE; if (Url.FindLastChar(TEXT('.'), Pos)) { const int32 PathEndPos = Url.FindLastCharByPredicate([](TCHAR C) { return C == TEXT('/') || C == TEXT('\\'); }); if (PathEndPos != INDEX_NONE && PathEndPos > Pos) { // The dot found was part of the path rather than the name Pos = INDEX_NONE; } } FString SubtitleUrl = (Pos == INDEX_NONE ? Url : Url.Left(Pos)) + "_" + LanguageName + ".srt"; if (FPlatformFileManager::Get().GetPlatformFile().FileExists(*SubtitleUrl)) { #if PLATFORM_WINDOWS if (BinkPluginLoadSubtitlesUTF16(bnk, (unsigned short*)*PlatformFile.ConvertToAbsolutePathForExternalAppForRead(*SubtitleUrl))) #else if (BinkPluginLoadSubtitles(bnk, TCHAR_TO_ANSI(*PlatformFile.ConvertToAbsolutePathForExternalAppForRead(*SubtitleUrl)))) #endif { CurrentHasSubtitles = 1; break; } } } } paused = false; reached_end = false; HandleMediaPlayerMediaOpened(Url); return true; } void UBinkMediaPlayer::Close() { if(bnk) { if (CurrentHasSubtitles) { CurrentHasSubtitles = 0; // Clear the movie subtitle for this object TArray StopSubtitles = { TEXT("") }; FSubtitleManager::GetSubtitleManager()->SetMovieSubtitle(this, StopSubtitles); } ENQUEUE_RENDER_COMMAND(BinkMediaPlayer_Close_CloseBink)([bnk=bnk](FRHICommandListImmediate& RHICmdList) { BinkPluginClose(bnk); }); bnk = NULL; } CurrentUrl = FString(); HandleMediaPlayerMediaClosed(); } void UBinkMediaPlayer::InitializePlayer() { if (URL != CurrentUrl || BinkBufferMode != CurrentBinkBufferMode || BinkSoundTrack != CurrentBinkSoundTrack || BinkSoundTrackStart != CurrentBinkSoundTrackStart || BinkDrawStyle != CurrentDrawStyle || BinkLayerDepth != CurrentLayerDepth ) { Close(); if (URL.IsEmpty()) { return; } // open the new media file bool OpenedSuccessfully = false; IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); FString FullUrl = FPaths::ConvertRelativePathToFull(FPaths::IsRelative(URL) ? BINKCONTENTPATH / URL : URL); OpenedSuccessfully = Open(FullUrl); if (!OpenedSuccessfully) { FString cookpath = BinkUE4CookOnTheFlyPath(FPaths::ConvertRelativePathToFull(BINKCONTENTPATH), *URL); OpenedSuccessfully = Open(cookpath); } // finish initialization if (OpenedSuccessfully) { CurrentUrl = URL; CurrentBinkBufferMode = BinkBufferMode; CurrentBinkSoundTrack = BinkSoundTrack; CurrentBinkSoundTrackStart = BinkSoundTrackStart; CurrentDrawStyle = BinkDrawStyle; CurrentLayerDepth = BinkLayerDepth; } } SetLooping(Looping); SetRate(StartImmediately ? 1 : 0); } void UBinkMediaPlayer::HandleMediaPlayerMediaClosed() { MediaChangedEvent.Broadcast(); OnMediaClosed.Broadcast(); //MediaEvent.Broadcast(EMediaEvent::MediaClosed); } void UBinkMediaPlayer::HandleMediaPlayerMediaOpened( FString OpenedUrl ) { MediaChangedEvent.Broadcast(); OnMediaOpened.Broadcast(OpenedUrl); //MediaEvent.Broadcast(EMediaEvent::MediaOpened); } void UBinkMediaPlayer::Tick(float DeltaTime) { // Check for if we should issue the reached end event if (bnk && !reached_end && !IsPlaying() && !IsGotoing() && !IsPaused() && !IsLooping()) { OnMediaReachedEnd.Broadcast(); reached_end = true; //MediaEvent.Broadcast(EMediaEvent::PlaybackEndReached); } if (bnk && GEngine && GEngine->GameViewport) { if (!IsPlaying() && !IsPaused()) { return; } FVector2D screenSize; GEngine->GameViewport->GetViewportSize(screenSize); if (CurrentHasSubtitles == 1) { TArray SubtitlesText; unsigned i = 0; while(const char *sub = BinkPluginCurrentSubtitle(bnk, &i)) { SubtitlesText.Add((UTF8CHAR*)sub); } FSubtitleManager::GetSubtitleManager()->SetMovieSubtitle(this, SubtitlesText); } if (BinkDrawStyle != 0) { BINKPLUGININFO bpinfo = {}; BinkPluginInfo(bnk, &bpinfo); int binkw = bpinfo.Width; int binkh = bpinfo.Height; float ulx = BinkDestinationUpperLeft.X; float uly = BinkDestinationUpperLeft.Y; float lrx = BinkDestinationLowerRight.X; float lry = BinkDestinationLowerRight.Y; // figure out the x,y screencoords for all of the overlay types if (BinkDrawStyle == 1/*BNK_DS_OverlayFillScreenWithAspectRatio*/) { lrx = binkw / screenSize.X; lry = binkh / screenSize.Y; if (lrx > lry) { lry /= lrx; lrx = 1; } else { lrx /= lry; lry = 1; } ulx = (1.0f - lrx) / 2.0f; uly = (1.0f - lry) / 2.0f; lrx += ulx; lry += uly; } else if (BinkDrawStyle == 2/*BNK_DS_OverlayOriginalMovieSize*/) { ulx = (screenSize.X - binkw) / (2.0f * screenSize.X); uly = (screenSize.Y - binkh) / (2.0f * screenSize.Y); lrx = binkw / screenSize.X + ulx; lry = binkh / screenSize.Y + uly; } #if PLATFORM_ANDROID uly = 1 - uly; lry = 1 - lry; #endif ENQUEUE_RENDER_COMMAND(BinkScheduleOverlay)([bnk=bnk,ulx,uly,lrx,lry](FRHICommandListImmediate& RHICmdList) { static const auto CVarHDROutputEnabled = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.HDR.EnableHDROutput")); static const auto CVarDisplayOutputDevice = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.HDR.Display.OutputDevice")); if (GRHISupportsHDROutput && CVarHDROutputEnabled->GetValueOnRenderThread() != 0) { EDisplayOutputFormat outDev = static_cast(CVarDisplayOutputDevice->GetValueOnRenderThread()); float DisplayMaxLuminance = HDRGetDisplayMaximumLuminance(); switch (outDev) { // LDR case EDisplayOutputFormat::SDR_sRGB: case EDisplayOutputFormat::SDR_Rec709: case EDisplayOutputFormat::SDR_ExplicitGammaMapping: BinkPluginSetHdrSettings(bnk, 1, 1.0f, 80); break; // 1k nits case EDisplayOutputFormat::HDR_ACES_1000nit_ST2084: BinkPluginSetHdrSettings(bnk, 2, 1.0f, DisplayMaxLuminance); break; case EDisplayOutputFormat::HDR_ACES_1000nit_ScRGB: BinkPluginSetHdrSettings(bnk, 1, 1.0f, DisplayMaxLuminance); break; // 2k nits case EDisplayOutputFormat::HDR_ACES_2000nit_ST2084: BinkPluginSetHdrSettings(bnk, 2, 1.0f, DisplayMaxLuminance); break; case EDisplayOutputFormat::HDR_ACES_2000nit_ScRGB: BinkPluginSetHdrSettings(bnk, 1, 1.0f, DisplayMaxLuminance); break; // no tonemap default: BinkPluginSetHdrSettings(bnk, 0, 1.0f, 1000); break; } BinkPluginSetRenderTargetFormat(bnk, 1); } else { BinkPluginSetHdrSettings(bnk, 1, 1.0f, 80); BinkPluginSetRenderTargetFormat(bnk, 0); } BinkPluginSetDrawFlags(bnk, 0); BinkPluginScheduleOverlay(bnk, ulx, uly, lrx, lry, 0); }); } } } void UBinkMediaPlayer::UpdateTexture(const UTexture *tex, FRHICommandListImmediate &RHICmdList, FTextureRHIRef ref, void *nativePtr, int width, int height, bool isEditor, bool tonemap, int output_nits, float alpha, bool srgb_decode, bool is_hdr) { check(IsInRenderingThread()); if (bnk && (BinkDrawStyle == 0 || isEditor)) { BinkPluginSetHdrSettings(bnk, tonemap, 1.0f, output_nits); BinkPluginSetAlphaSettings(bnk, alpha); BinkPluginSetDrawFlags(bnk, srgb_decode ? BinkDrawDecodeSRGB : 0); BinkPluginSetRenderTargetFormat(bnk, is_hdr ? 1 : 0); BinkPluginScheduleToTexture(bnk, BinkDestinationUpperLeft.X, BinkDestinationUpperLeft.Y, BinkDestinationLowerRight.X, BinkDestinationLowerRight.Y, 0, ref.GetReference(), width, height); BinkActiveTextureRefs.Push(ref); // Update rect-lights with this texture if (tex) { // Lock/Enqueue rect atlas refresh if that texture is used by a rect. light RectLightAtlas::FAtlasTextureInvalidationScope InvalidationScope(tex); } } } void UBinkMediaPlayer::Draw(UTexture *texture, bool tonemap, int out_nits, float alpha, bool srgb_decode, bool hdr) { if (!bnk || !texture->GetResource()) { return; } FTextureRHIRef ref = texture->GetResource()->TextureRHI->GetTexture2D(); if ((!IsPlaying() && !IsPaused()) || !ref) { return; } int width = texture->GetSurfaceWidth(); int height = texture->GetSurfaceHeight(); void *native = ref->GetNativeResource(); if (!native) { texture->UpdateResource(); return; } struct parms_t { UBinkMediaPlayer *player; UTexture* tex; FTextureRHIRef ref; void *native; int width, height; bool tonemap; bool srgb_decode; bool hdr; int out_nits; float alpha; } parms = { this, texture, ref, native, width, height, tonemap, srgb_decode, hdr, out_nits, alpha }; ENQUEUE_RENDER_COMMAND(BinkMediaPlayer_Draw)([parms](FRHICommandListImmediate& RHICmdList) { parms.player->UpdateTexture(parms.tex, RHICmdList, parms.ref, parms.native, parms.width, parms.height, false, parms.tonemap, parms.out_nits, parms.alpha, parms.srgb_decode, parms.hdr); }); } FIntPoint UBinkMediaPlayer::GetDimensions() const { if (bnk) { BINKPLUGININFO bpinfo = {}; BinkPluginInfo(bnk, &bpinfo); return FIntPoint(bpinfo.Width, bpinfo.Height); } return FIntPoint(0, 0); } float UBinkMediaPlayer::GetFrameRate() const { if (bnk) { BINKPLUGININFO bpinfo = {}; BinkPluginInfo(bnk, &bpinfo); return (float)(((double)bpinfo.FrameRate) / ((double)bpinfo.FrameRateDiv)); } return 0; }