Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 86baa476f4 | |||
| 8732ce99e3 |
@@ -19,7 +19,7 @@ public static final XAPKFile[] xAPKS = {
|
||||
new XAPKFile(
|
||||
true, // true signifies a main file
|
||||
"1", // the version of the APK that the file was uploaded against
|
||||
295265920L // the length of the file in bytes
|
||||
259568643L // the length of the file in bytes
|
||||
)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
[/Script/Engine.DeviceProfileManager]
|
||||
|
||||
[Android_Mali_G77_Vulkan DeviceProfile]
|
||||
DeviceType=Android
|
||||
BaseProfileName=Android_Mali_G77
|
||||
+CVars=r.Android.DisableVulkanSupport=0
|
||||
+CVars=t.maxfps=35
|
||||
+CVars=r.AllowOcclusionQueries=0
|
||||
+CVars=r.OcclusionFeedback.Enable=0
|
||||
+CVars=r.HZBOcclusion=0
|
||||
+CVars=r.ScreenPercentage=80
|
||||
+CVars=sg.ViewDistanceQuality=2
|
||||
+CVars=sg.AntiAliasingQuality=1
|
||||
+CVars=sg.ShadowQuality=1
|
||||
+CVars=sg.PostProcessQuality=1
|
||||
+CVars=sg.TextureQuality=2
|
||||
+CVars=sg.EffectsQuality=4
|
||||
+CVars=sg.FoliageQuality=1
|
||||
+CVars=sg.ShadingQuality=2
|
||||
+CVars=r.BloomQuality=2
|
||||
+CVars=r.Mobile.AmbientOcclusion=0
|
||||
+CVars=r.Mobile.ScreenSpaceReflections=0
|
||||
+CVars=r.Streaming.PoolSize=3000
|
||||
+CVars=r.MaxAnisotropy=4
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,8 @@
|
||||
|
||||
|
||||
[/Script/Engine.Engine]
|
||||
+ActiveClassRedirects=(OldClassName="/Script/AndroidRadioPlayer.AndroidRadioPlayerBPLibrary",NewClassName="/Script/FunnyGirl.FunnyGirlUtilityBlueprintLibrary")
|
||||
|
||||
[/Script/EngineSettings.GameMapsSettings]
|
||||
EditorStartupMap=/Game/Game/Maps/M_GirlHouse.M_GirlHouse
|
||||
LocalMapOptions=
|
||||
@@ -265,7 +268,7 @@ UIScaleCurve=(EditorCurveData=(Keys=((Time=480.000000,Value=0.440000),(Time=576.
|
||||
bExplicitCanvasChildZOrder=True
|
||||
|
||||
[/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings]
|
||||
bEnablePlugin=False
|
||||
bEnablePlugin=True
|
||||
bAllowNetworkConnection=True
|
||||
SecurityToken=E63F664EDF4A840AACB2E4A0C6D55FC6
|
||||
bIncludeInShipping=False
|
||||
|
||||
@@ -109,7 +109,5 @@ bSkipMovies=False
|
||||
+IniSectionDenylist=StorageServers
|
||||
+IniSectionDenylist=/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings
|
||||
+MapsToCook=(FilePath="/Game/Game/Maps/M_GirlHouse")
|
||||
+DirectoriesToAlwaysCook=(Path="/NNEDenoiser")
|
||||
bRetainStagedDirectory=False
|
||||
CustomStageCopyHandler=
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"FileVersion": 3,
|
||||
"Version": 1,
|
||||
"VersionName": "1.0.0",
|
||||
"FriendlyName": "Android Radio Player",
|
||||
"Description": "Blueprint radio playback for Android using AndroidX Media3 ExoPlayer.",
|
||||
"Category": "Media Players",
|
||||
"CreatedBy": "FunnyGirl",
|
||||
"CanContainContent": false,
|
||||
"IsBetaVersion": true,
|
||||
"EnabledByDefault": true,
|
||||
"Modules": [
|
||||
{
|
||||
"Name": "AndroidRadioPlayer",
|
||||
"Type": "Runtime",
|
||||
"LoadingPhase": "Default"
|
||||
}
|
||||
]
|
||||
}
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<init>
|
||||
<log text="AndroidRadioPlayer init"/>
|
||||
</init>
|
||||
|
||||
<androidManifestUpdates>
|
||||
<addPermission android:name="android.permission.INTERNET"/>
|
||||
<addAttribute tag="application" name="android:usesCleartextTraffic" value="true"/>
|
||||
</androidManifestUpdates>
|
||||
|
||||
<buildGradleAdditions>
|
||||
<insert>
|
||||
dependencies {
|
||||
implementation('androidx.media3:media3-exoplayer:1.3.1')
|
||||
implementation('androidx.media3:media3-exoplayer-hls:1.3.1')
|
||||
}
|
||||
</insert>
|
||||
</buildGradleAdditions>
|
||||
|
||||
<prebuildCopies>
|
||||
<log text="Copying AndroidRadioPlayer Java source"/>
|
||||
<copyFile src="$S(PluginDir)/Java/AndroidRadioPlayer.java" dst="$S(BuildDir)/src/com/epicgames/unreal/AndroidRadioPlayer.java"/>
|
||||
</prebuildCopies>
|
||||
|
||||
<proguardAdditions>
|
||||
<insert>
|
||||
-keep class com.epicgames.unreal.AndroidRadioPlayer {
|
||||
public *;
|
||||
}
|
||||
</insert>
|
||||
</proguardAdditions>
|
||||
|
||||
<gameActivityOnPauseAdditions>
|
||||
<insert>
|
||||
AndroidRadioPlayer.handleActivityPause();
|
||||
</insert>
|
||||
</gameActivityOnPauseAdditions>
|
||||
|
||||
<gameActivityOnResumeAdditions>
|
||||
<insert>
|
||||
AndroidRadioPlayer.handleActivityResume();
|
||||
</insert>
|
||||
</gameActivityOnResumeAdditions>
|
||||
|
||||
<gameActivityOnDestroyAdditions>
|
||||
<insert>
|
||||
AndroidRadioPlayer.releaseRadio();
|
||||
</insert>
|
||||
</gameActivityOnDestroyAdditions>
|
||||
</root>
|
||||
-274
@@ -1,274 +0,0 @@
|
||||
package com.epicgames.unreal;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.media3.common.AudioAttributes;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.MimeTypes;
|
||||
import androidx.media3.common.PlaybackException;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.exoplayer.DefaultLoadControl;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
|
||||
public final class AndroidRadioPlayer {
|
||||
private static final String TAG = "AndroidRadioPlayer";
|
||||
|
||||
private static final int STATE_IDLE = 0;
|
||||
private static final int STATE_BUFFERING = 1;
|
||||
private static final int STATE_READY = 2;
|
||||
private static final int STATE_ENDED = 3;
|
||||
private static final int STATE_ERROR = 4;
|
||||
|
||||
private static ExoPlayer player;
|
||||
private static volatile int playbackState = STATE_IDLE;
|
||||
private static volatile String lastError = "";
|
||||
private static volatile boolean isPlaying = false;
|
||||
private static volatile boolean shouldResumeAfterActivityPause = false;
|
||||
private static volatile long cachedDurationMillis = 0;
|
||||
private static volatile long cachedCurrentPositionMillis = 0;
|
||||
private static float pendingVolume = 1.0f;
|
||||
private static final Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
private AndroidRadioPlayer() {
|
||||
}
|
||||
|
||||
public static boolean playRadio(final Activity activity, final String url, final float volume) {
|
||||
if (activity == null) {
|
||||
lastError = "Activity is null";
|
||||
playbackState = STATE_ERROR;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (url == null || url.trim().isEmpty()) {
|
||||
lastError = "Radio URL is empty";
|
||||
playbackState = STATE_ERROR;
|
||||
return false;
|
||||
}
|
||||
|
||||
pendingVolume = clampVolume(volume);
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
ensurePlayer(activity);
|
||||
lastError = "";
|
||||
playbackState = STATE_BUFFERING;
|
||||
player.stop();
|
||||
player.clearMediaItems();
|
||||
isPlaying = false;
|
||||
player.setVolume(pendingVolume);
|
||||
player.setMediaItem(buildMediaItem(url.trim()));
|
||||
player.prepare();
|
||||
player.play();
|
||||
} catch (Throwable error) {
|
||||
lastError = error.toString();
|
||||
playbackState = STATE_ERROR;
|
||||
Log.e(TAG, "Failed to play radio", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void stopRadio() {
|
||||
runOnPlayerThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
releasePlayer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void releaseRadio() {
|
||||
runOnPlayerThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
releasePlayer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void pauseRadio() {
|
||||
runOnPlayerThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (player != null) {
|
||||
player.pause();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void resumeRadio() {
|
||||
runOnPlayerThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (player != null) {
|
||||
player.play();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void setRadioVolume(final float volume) {
|
||||
pendingVolume = clampVolume(volume);
|
||||
runOnPlayerThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (player != null) {
|
||||
player.setVolume(pendingVolume);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void handleActivityPause() {
|
||||
runOnPlayerThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
shouldResumeAfterActivityPause = player != null && player.isPlaying();
|
||||
if (player != null && shouldResumeAfterActivityPause) {
|
||||
player.pause();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void handleActivityResume() {
|
||||
runOnPlayerThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (player != null && shouldResumeAfterActivityPause) {
|
||||
shouldResumeAfterActivityPause = false;
|
||||
player.play();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static boolean isRadioPlaying() {
|
||||
return isPlaying;
|
||||
}
|
||||
|
||||
public static int getPlaybackState() {
|
||||
return playbackState;
|
||||
}
|
||||
|
||||
public static String getLastError() {
|
||||
return lastError == null ? "" : lastError;
|
||||
}
|
||||
|
||||
public static long getDurationMillis() {
|
||||
return cachedDurationMillis;
|
||||
}
|
||||
|
||||
public static long getCurrentPositionMillis() {
|
||||
return cachedCurrentPositionMillis;
|
||||
}
|
||||
|
||||
private static void ensurePlayer(Activity activity) {
|
||||
if (player != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
DefaultLoadControl loadControl = new DefaultLoadControl.Builder()
|
||||
.setBufferDurationsMs(5000, 15000, 1000, 2000)
|
||||
.build();
|
||||
|
||||
player = new ExoPlayer.Builder(activity.getApplicationContext())
|
||||
.setLoadControl(loadControl)
|
||||
.setHandleAudioBecomingNoisy(true)
|
||||
.build();
|
||||
|
||||
AudioAttributes audioAttributes = new AudioAttributes.Builder()
|
||||
.setUsage(C.USAGE_MEDIA)
|
||||
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
|
||||
.build();
|
||||
player.setAudioAttributes(audioAttributes, true);
|
||||
player.setVolume(pendingVolume);
|
||||
player.addListener(new Player.Listener() {
|
||||
@Override
|
||||
public void onPlaybackStateChanged(int state) {
|
||||
if (state == Player.STATE_BUFFERING) {
|
||||
playbackState = STATE_BUFFERING;
|
||||
} else if (state == Player.STATE_READY) {
|
||||
playbackState = STATE_READY;
|
||||
long duration = player.getDuration();
|
||||
cachedDurationMillis = duration == C.TIME_UNSET ? 0 : duration;
|
||||
} else if (state == Player.STATE_ENDED) {
|
||||
playbackState = STATE_ENDED;
|
||||
} else {
|
||||
playbackState = STATE_IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIsPlayingChanged(boolean playing) {
|
||||
isPlaying = playing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(PlaybackException error) {
|
||||
lastError = error == null ? "Unknown ExoPlayer error" : error.toString();
|
||||
playbackState = STATE_ERROR;
|
||||
isPlaying = false;
|
||||
Log.e(TAG, "Playback error", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void releasePlayer() {
|
||||
if (player != null) {
|
||||
try {
|
||||
player.stop();
|
||||
player.clearMediaItems();
|
||||
player.release();
|
||||
} catch (Throwable error) {
|
||||
Log.w(TAG, "Failed to release radio player cleanly", error);
|
||||
}
|
||||
player = null;
|
||||
}
|
||||
shouldResumeAfterActivityPause = false;
|
||||
isPlaying = false;
|
||||
cachedDurationMillis = 0;
|
||||
cachedCurrentPositionMillis = 0;
|
||||
playbackState = STATE_IDLE;
|
||||
}
|
||||
|
||||
private static MediaItem buildMediaItem(String url) {
|
||||
MediaItem.Builder builder = new MediaItem.Builder().setUri(Uri.parse(url));
|
||||
String lowerUrl = url.toLowerCase();
|
||||
|
||||
if (lowerUrl.contains(".m3u8") || lowerUrl.contains("m3u8")) {
|
||||
builder.setMimeType(MimeTypes.APPLICATION_M3U8);
|
||||
} else if (lowerUrl.contains("aacp") || lowerUrl.contains(".aac") || lowerUrl.contains("audio/aac")) {
|
||||
builder.setMimeType(MimeTypes.AUDIO_AAC);
|
||||
} else if (lowerUrl.contains(".mp3") || lowerUrl.contains("mp3")) {
|
||||
builder.setMimeType(MimeTypes.AUDIO_MPEG);
|
||||
} else if (lowerUrl.contains(".ogg") || lowerUrl.contains("opus")) {
|
||||
builder.setMimeType(MimeTypes.AUDIO_OGG);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static void runOnPlayerThread(Runnable runnable) {
|
||||
mainHandler.post(runnable);
|
||||
}
|
||||
|
||||
private static float clampVolume(float volume) {
|
||||
if (volume < 0.0f) {
|
||||
return 0.0f;
|
||||
}
|
||||
if (volume > 1.0f) {
|
||||
return 1.0f;
|
||||
}
|
||||
return volume;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using System.IO;
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class AndroidRadioPlayer : ModuleRules
|
||||
{
|
||||
public AndroidRadioPlayer(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Core",
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"Media",
|
||||
"MediaAssets"
|
||||
});
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Projects"
|
||||
});
|
||||
|
||||
if (Target.Platform == UnrealTargetPlatform.Android)
|
||||
{
|
||||
PrivateDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"ApplicationCore",
|
||||
"Launch"
|
||||
});
|
||||
|
||||
string PluginPath = Utils.MakePathRelativeTo(ModuleDirectory, Target.RelativeEnginePath);
|
||||
AdditionalPropertiesForReceipt.Add("AndroidPlugin", Path.Combine(PluginPath, "Android", "AndroidRadioPlayer_UPL.xml"));
|
||||
}
|
||||
}
|
||||
}
|
||||
-207
@@ -1,207 +0,0 @@
|
||||
#include "AndroidRadioJavaBridge.h"
|
||||
|
||||
#if PLATFORM_ANDROID
|
||||
#include "Android/AndroidApplication.h"
|
||||
#include "Android/AndroidJavaEnv.h"
|
||||
#include "Android/AndroidJNI.h"
|
||||
#endif
|
||||
|
||||
namespace AndroidRadioJavaBridge
|
||||
{
|
||||
#if PLATFORM_ANDROID
|
||||
jclass GetJavaClass(JNIEnv* Env)
|
||||
{
|
||||
static jclass RadioPlayerClass = FAndroidApplication::FindJavaClassGlobalRef("com/epicgames/unreal/AndroidRadioPlayer");
|
||||
if (RadioPlayerClass == nullptr)
|
||||
{
|
||||
FAndroidApplication::CheckJavaException();
|
||||
UE_LOG(LogTemp, Error, TEXT("AndroidRadioPlayer Java class was not found."));
|
||||
}
|
||||
return RadioPlayerClass;
|
||||
}
|
||||
|
||||
jmethodID GetStaticMethod(JNIEnv* Env, const ANSICHAR* MethodName, const ANSICHAR* Signature)
|
||||
{
|
||||
jclass RadioPlayerClass = GetJavaClass(Env);
|
||||
if (RadioPlayerClass == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
jmethodID Method = FJavaWrapper::FindStaticMethod(Env, RadioPlayerClass, MethodName, Signature, false);
|
||||
if (Method == nullptr)
|
||||
{
|
||||
FAndroidApplication::CheckJavaException();
|
||||
UE_LOG(LogTemp, Error, TEXT("AndroidRadioPlayer Java method was not found: %s"), ANSI_TO_TCHAR(MethodName));
|
||||
}
|
||||
return Method;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool Play(const FString& Url, float Volume)
|
||||
{
|
||||
#if PLATFORM_ANDROID
|
||||
if (Url.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
|
||||
if (Env == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
jclass RadioPlayerClass = GetJavaClass(Env);
|
||||
jmethodID Method = GetStaticMethod(Env, "playRadio", "(Landroid/app/Activity;Ljava/lang/String;F)Z");
|
||||
if (RadioPlayerClass == nullptr || Method == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto JavaUrl = FJavaHelper::ToJavaString(Env, Url);
|
||||
const jfloat ClampedVolume = FMath::Clamp(Volume, 0.0f, 1.0f);
|
||||
const jboolean Result = Env->CallStaticBooleanMethod(RadioPlayerClass, Method, FJavaWrapper::GameActivityThis, *JavaUrl, ClampedVolume);
|
||||
FAndroidApplication::CheckJavaException();
|
||||
return Result == JNI_TRUE;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void Stop()
|
||||
{
|
||||
#if PLATFORM_ANDROID
|
||||
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
|
||||
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
|
||||
jmethodID Method = Env ? GetStaticMethod(Env, "stopRadio", "()V") : nullptr;
|
||||
if (RadioPlayerClass != nullptr && Method != nullptr)
|
||||
{
|
||||
Env->CallStaticVoidMethod(RadioPlayerClass, Method);
|
||||
FAndroidApplication::CheckJavaException();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void Pause()
|
||||
{
|
||||
#if PLATFORM_ANDROID
|
||||
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
|
||||
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
|
||||
jmethodID Method = Env ? GetStaticMethod(Env, "pauseRadio", "()V") : nullptr;
|
||||
if (RadioPlayerClass != nullptr && Method != nullptr)
|
||||
{
|
||||
Env->CallStaticVoidMethod(RadioPlayerClass, Method);
|
||||
FAndroidApplication::CheckJavaException();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void Resume()
|
||||
{
|
||||
#if PLATFORM_ANDROID
|
||||
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
|
||||
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
|
||||
jmethodID Method = Env ? GetStaticMethod(Env, "resumeRadio", "()V") : nullptr;
|
||||
if (RadioPlayerClass != nullptr && Method != nullptr)
|
||||
{
|
||||
Env->CallStaticVoidMethod(RadioPlayerClass, Method);
|
||||
FAndroidApplication::CheckJavaException();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void SetVolume(float Volume)
|
||||
{
|
||||
#if PLATFORM_ANDROID
|
||||
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
|
||||
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
|
||||
jmethodID Method = Env ? GetStaticMethod(Env, "setRadioVolume", "(F)V") : nullptr;
|
||||
if (RadioPlayerClass != nullptr && Method != nullptr)
|
||||
{
|
||||
Env->CallStaticVoidMethod(RadioPlayerClass, Method, FMath::Clamp(Volume, 0.0f, 1.0f));
|
||||
FAndroidApplication::CheckJavaException();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool IsPlaying()
|
||||
{
|
||||
#if PLATFORM_ANDROID
|
||||
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
|
||||
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
|
||||
jmethodID Method = Env ? GetStaticMethod(Env, "isRadioPlaying", "()Z") : nullptr;
|
||||
if (RadioPlayerClass != nullptr && Method != nullptr)
|
||||
{
|
||||
const jboolean Result = Env->CallStaticBooleanMethod(RadioPlayerClass, Method);
|
||||
FAndroidApplication::CheckJavaException();
|
||||
return Result == JNI_TRUE;
|
||||
}
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
int32 GetPlaybackState()
|
||||
{
|
||||
#if PLATFORM_ANDROID
|
||||
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
|
||||
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
|
||||
jmethodID Method = Env ? GetStaticMethod(Env, "getPlaybackState", "()I") : nullptr;
|
||||
if (RadioPlayerClass != nullptr && Method != nullptr)
|
||||
{
|
||||
const jint State = Env->CallStaticIntMethod(RadioPlayerClass, Method);
|
||||
FAndroidApplication::CheckJavaException();
|
||||
return static_cast<int32>(State);
|
||||
}
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
|
||||
FString GetLastError()
|
||||
{
|
||||
#if PLATFORM_ANDROID
|
||||
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
|
||||
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
|
||||
jmethodID Method = Env ? GetStaticMethod(Env, "getLastError", "()Ljava/lang/String;") : nullptr;
|
||||
if (RadioPlayerClass != nullptr && Method != nullptr)
|
||||
{
|
||||
jstring Error = static_cast<jstring>(Env->CallStaticObjectMethod(RadioPlayerClass, Method));
|
||||
FAndroidApplication::CheckJavaException();
|
||||
return FJavaHelper::FStringFromLocalRef(Env, Error);
|
||||
}
|
||||
#endif
|
||||
return FString();
|
||||
}
|
||||
|
||||
int64 GetDurationMillis()
|
||||
{
|
||||
#if PLATFORM_ANDROID
|
||||
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
|
||||
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
|
||||
jmethodID Method = Env ? GetStaticMethod(Env, "getDurationMillis", "()J") : nullptr;
|
||||
if (RadioPlayerClass != nullptr && Method != nullptr)
|
||||
{
|
||||
const jlong Duration = Env->CallStaticLongMethod(RadioPlayerClass, Method);
|
||||
FAndroidApplication::CheckJavaException();
|
||||
return static_cast<int64>(Duration);
|
||||
}
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
int64 GetCurrentPositionMillis()
|
||||
{
|
||||
#if PLATFORM_ANDROID
|
||||
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
|
||||
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
|
||||
jmethodID Method = Env ? GetStaticMethod(Env, "getCurrentPositionMillis", "()J") : nullptr;
|
||||
if (RadioPlayerClass != nullptr && Method != nullptr)
|
||||
{
|
||||
const jlong Position = Env->CallStaticLongMethod(RadioPlayerClass, Method);
|
||||
FAndroidApplication::CheckJavaException();
|
||||
return static_cast<int64>(Position);
|
||||
}
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
namespace AndroidRadioJavaBridge
|
||||
{
|
||||
bool Play(const FString& Url, float Volume);
|
||||
void Stop();
|
||||
void Pause();
|
||||
void Resume();
|
||||
void SetVolume(float Volume);
|
||||
bool IsPlaying();
|
||||
int32 GetPlaybackState();
|
||||
FString GetLastError();
|
||||
int64 GetDurationMillis();
|
||||
int64 GetCurrentPositionMillis();
|
||||
}
|
||||
-365
@@ -1,365 +0,0 @@
|
||||
#include "AndroidRadioMediaPlayer.h"
|
||||
|
||||
#include "AndroidRadioJavaBridge.h"
|
||||
#include "IMediaEventSink.h"
|
||||
#include "Misc/Guid.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int32 JavaStateIdle = 0;
|
||||
constexpr int32 JavaStateBuffering = 1;
|
||||
constexpr int32 JavaStateReady = 2;
|
||||
constexpr int32 JavaStateEnded = 3;
|
||||
constexpr int32 JavaStateError = 4;
|
||||
}
|
||||
|
||||
FAndroidRadioMediaPlayer::FAndroidRadioMediaPlayer(IMediaEventSink& InEventSink)
|
||||
: EventSink(InEventSink)
|
||||
, CurrentState(EMediaState::Closed)
|
||||
, CurrentStatus(EMediaStatus::None)
|
||||
, CurrentRate(0.0f)
|
||||
, CurrentVolume(1.0f)
|
||||
, bLooping(false)
|
||||
, bHasSentOpenEvent(false)
|
||||
, bHasSentBufferingEvent(false)
|
||||
, bHasSentEndEvent(false)
|
||||
{
|
||||
}
|
||||
|
||||
FAndroidRadioMediaPlayer::~FAndroidRadioMediaPlayer()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
void FAndroidRadioMediaPlayer::Close()
|
||||
{
|
||||
if (CurrentState != EMediaState::Closed)
|
||||
{
|
||||
AndroidRadioJavaBridge::Stop();
|
||||
CurrentUrl.Empty();
|
||||
CurrentState = EMediaState::Closed;
|
||||
CurrentStatus = EMediaStatus::None;
|
||||
CurrentRate = 0.0f;
|
||||
bHasSentOpenEvent = false;
|
||||
bHasSentBufferingEvent = false;
|
||||
bHasSentEndEvent = false;
|
||||
EventSink.ReceiveMediaEvent(EMediaEvent::MediaClosed);
|
||||
}
|
||||
}
|
||||
|
||||
IMediaCache& FAndroidRadioMediaPlayer::GetCache()
|
||||
{
|
||||
return *this;
|
||||
}
|
||||
|
||||
IMediaControls& FAndroidRadioMediaPlayer::GetControls()
|
||||
{
|
||||
return *this;
|
||||
}
|
||||
|
||||
FString FAndroidRadioMediaPlayer::GetInfo() const
|
||||
{
|
||||
return FString::Printf(TEXT("Android Radio Player\nUrl: %s\nState: %d\nLast error: %s"), *CurrentUrl, static_cast<int32>(CurrentState), *AndroidRadioJavaBridge::GetLastError());
|
||||
}
|
||||
|
||||
FGuid FAndroidRadioMediaPlayer::GetPlayerPluginGUID() const
|
||||
{
|
||||
static FGuid PlayerPluginGUID(0x2547a7af, 0xe9504c2f, 0xa090e1b7, 0x26c7d716);
|
||||
return PlayerPluginGUID;
|
||||
}
|
||||
|
||||
IMediaSamples& FAndroidRadioMediaPlayer::GetSamples()
|
||||
{
|
||||
return *this;
|
||||
}
|
||||
|
||||
FString FAndroidRadioMediaPlayer::GetStats() const
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
IMediaTracks& FAndroidRadioMediaPlayer::GetTracks()
|
||||
{
|
||||
return *this;
|
||||
}
|
||||
|
||||
FString FAndroidRadioMediaPlayer::GetUrl() const
|
||||
{
|
||||
return CurrentUrl;
|
||||
}
|
||||
|
||||
IMediaView& FAndroidRadioMediaPlayer::GetView()
|
||||
{
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool FAndroidRadioMediaPlayer::Open(const FString& Url, const IMediaOptions* Options)
|
||||
{
|
||||
Close();
|
||||
|
||||
if (!Url.StartsWith(TEXT("http://")) && !Url.StartsWith(TEXT("https://")))
|
||||
{
|
||||
CurrentState = EMediaState::Error;
|
||||
return false;
|
||||
}
|
||||
|
||||
CurrentUrl = Url;
|
||||
CurrentState = EMediaState::Preparing;
|
||||
CurrentStatus = EMediaStatus::Connecting | EMediaStatus::Buffering;
|
||||
CurrentRate = 1.0f;
|
||||
bHasSentOpenEvent = false;
|
||||
bHasSentBufferingEvent = false;
|
||||
bHasSentEndEvent = false;
|
||||
EventSink.ReceiveMediaEvent(EMediaEvent::MediaConnecting);
|
||||
|
||||
if (!AndroidRadioJavaBridge::Play(Url, CurrentVolume))
|
||||
{
|
||||
CurrentState = EMediaState::Error;
|
||||
CurrentStatus = EMediaStatus::None;
|
||||
CurrentRate = 0.0f;
|
||||
EventSink.ReceiveMediaEvent(EMediaEvent::MediaOpenFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FAndroidRadioMediaPlayer::Open(const TSharedRef<FArchive, ESPMode::ThreadSafe>& Archive, const FString& OriginalUrl, const IMediaOptions* Options)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void FAndroidRadioMediaPlayer::TickInput(FTimespan DeltaTime, FTimespan Timecode)
|
||||
{
|
||||
UpdateStateFromJava();
|
||||
}
|
||||
|
||||
bool FAndroidRadioMediaPlayer::SetNativeVolume(float Volume)
|
||||
{
|
||||
CurrentVolume = FMath::Clamp(Volume, 0.0f, 1.0f);
|
||||
AndroidRadioJavaBridge::SetVolume(CurrentVolume);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FAndroidRadioMediaPlayer::CanControl(EMediaControl Control) const
|
||||
{
|
||||
return Control == EMediaControl::Pause || Control == EMediaControl::Resume;
|
||||
}
|
||||
|
||||
FTimespan FAndroidRadioMediaPlayer::GetDuration() const
|
||||
{
|
||||
const int64 DurationMillis = AndroidRadioJavaBridge::GetDurationMillis();
|
||||
return DurationMillis > 0 ? FTimespan::FromMilliseconds(DurationMillis) : FTimespan::Zero();
|
||||
}
|
||||
|
||||
float FAndroidRadioMediaPlayer::GetRate() const
|
||||
{
|
||||
return CurrentRate;
|
||||
}
|
||||
|
||||
EMediaState FAndroidRadioMediaPlayer::GetState() const
|
||||
{
|
||||
return CurrentState;
|
||||
}
|
||||
|
||||
EMediaStatus FAndroidRadioMediaPlayer::GetStatus() const
|
||||
{
|
||||
return CurrentStatus;
|
||||
}
|
||||
|
||||
TRangeSet<float> FAndroidRadioMediaPlayer::GetSupportedRates(EMediaRateThinning Thinning) const
|
||||
{
|
||||
TRangeSet<float> Result;
|
||||
Result.Add(TRange<float>(0.0f));
|
||||
Result.Add(TRange<float>(1.0f));
|
||||
return Result;
|
||||
}
|
||||
|
||||
FTimespan FAndroidRadioMediaPlayer::GetTime() const
|
||||
{
|
||||
return FTimespan::FromMilliseconds(AndroidRadioJavaBridge::GetCurrentPositionMillis());
|
||||
}
|
||||
|
||||
bool FAndroidRadioMediaPlayer::IsLooping() const
|
||||
{
|
||||
return bLooping;
|
||||
}
|
||||
|
||||
bool FAndroidRadioMediaPlayer::Seek(const FTimespan& Time)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FAndroidRadioMediaPlayer::SetLooping(bool Looping)
|
||||
{
|
||||
bLooping = Looping;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FAndroidRadioMediaPlayer::SetRate(float Rate)
|
||||
{
|
||||
if (CurrentState == EMediaState::Closed || CurrentState == EMediaState::Error)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (FMath::IsNearlyZero(Rate))
|
||||
{
|
||||
AndroidRadioJavaBridge::Pause();
|
||||
CurrentRate = 0.0f;
|
||||
CurrentState = EMediaState::Paused;
|
||||
EventSink.ReceiveMediaEvent(EMediaEvent::PlaybackSuspended);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (FMath::IsNearlyEqual(Rate, 1.0f))
|
||||
{
|
||||
AndroidRadioJavaBridge::Resume();
|
||||
CurrentRate = 1.0f;
|
||||
CurrentState = EMediaState::Playing;
|
||||
EventSink.ReceiveMediaEvent(EMediaEvent::PlaybackResumed);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FAndroidRadioMediaPlayer::GetAudioTrackFormat(int32 TrackIndex, int32 FormatIndex, FMediaAudioTrackFormat& OutFormat) const
|
||||
{
|
||||
if (TrackIndex == 0 && FormatIndex == 0)
|
||||
{
|
||||
OutFormat.BitsPerSample = 16;
|
||||
OutFormat.NumChannels = 2;
|
||||
OutFormat.SampleRate = 44100;
|
||||
OutFormat.TypeName = TEXT("External");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int32 FAndroidRadioMediaPlayer::GetNumTracks(EMediaTrackType TrackType) const
|
||||
{
|
||||
return TrackType == EMediaTrackType::Audio ? 1 : 0;
|
||||
}
|
||||
|
||||
int32 FAndroidRadioMediaPlayer::GetNumTrackFormats(EMediaTrackType TrackType, int32 TrackIndex) const
|
||||
{
|
||||
return TrackType == EMediaTrackType::Audio && TrackIndex == 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
int32 FAndroidRadioMediaPlayer::GetSelectedTrack(EMediaTrackType TrackType) const
|
||||
{
|
||||
return TrackType == EMediaTrackType::Audio ? 0 : INDEX_NONE;
|
||||
}
|
||||
|
||||
FText FAndroidRadioMediaPlayer::GetTrackDisplayName(EMediaTrackType TrackType, int32 TrackIndex) const
|
||||
{
|
||||
return TrackType == EMediaTrackType::Audio && TrackIndex == 0 ? NSLOCTEXT("AndroidRadioMediaPlayer", "AudioTrackName", "Radio") : FText::GetEmpty();
|
||||
}
|
||||
|
||||
int32 FAndroidRadioMediaPlayer::GetTrackFormat(EMediaTrackType TrackType, int32 TrackIndex) const
|
||||
{
|
||||
return TrackType == EMediaTrackType::Audio && TrackIndex == 0 ? 0 : INDEX_NONE;
|
||||
}
|
||||
|
||||
FString FAndroidRadioMediaPlayer::GetTrackLanguage(EMediaTrackType TrackType, int32 TrackIndex) const
|
||||
{
|
||||
return TEXT("und");
|
||||
}
|
||||
|
||||
FString FAndroidRadioMediaPlayer::GetTrackName(EMediaTrackType TrackType, int32 TrackIndex) const
|
||||
{
|
||||
return TrackType == EMediaTrackType::Audio && TrackIndex == 0 ? TEXT("Radio") : FString();
|
||||
}
|
||||
|
||||
bool FAndroidRadioMediaPlayer::GetVideoTrackFormat(int32 TrackIndex, int32 FormatIndex, FMediaVideoTrackFormat& OutFormat) const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FAndroidRadioMediaPlayer::SelectTrack(EMediaTrackType TrackType, int32 TrackIndex)
|
||||
{
|
||||
return TrackType == EMediaTrackType::Audio && TrackIndex == 0;
|
||||
}
|
||||
|
||||
bool FAndroidRadioMediaPlayer::SetTrackFormat(EMediaTrackType TrackType, int32 TrackIndex, int32 FormatIndex)
|
||||
{
|
||||
return TrackType == EMediaTrackType::Audio && TrackIndex == 0 && FormatIndex == 0;
|
||||
}
|
||||
|
||||
bool FAndroidRadioMediaPlayer::PeekVideoSampleTime(FMediaTimeStamp& TimeStamp)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void FAndroidRadioMediaPlayer::UpdateStateFromJava()
|
||||
{
|
||||
if (CurrentState == EMediaState::Closed || CurrentUrl.IsEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const int32 JavaState = AndroidRadioJavaBridge::GetPlaybackState();
|
||||
const bool bIsPlaying = AndroidRadioJavaBridge::IsPlaying();
|
||||
|
||||
if (JavaState == JavaStateError)
|
||||
{
|
||||
if (CurrentState != EMediaState::Error)
|
||||
{
|
||||
CurrentState = EMediaState::Error;
|
||||
CurrentStatus = EMediaStatus::None;
|
||||
CurrentRate = 0.0f;
|
||||
EventSink.ReceiveMediaEvent(bHasSentOpenEvent ? EMediaEvent::PlaybackSuspended : EMediaEvent::MediaOpenFailed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (JavaState == JavaStateBuffering)
|
||||
{
|
||||
CurrentState = EMediaState::Preparing;
|
||||
CurrentStatus = EMediaStatus::Buffering;
|
||||
if (!bHasSentBufferingEvent)
|
||||
{
|
||||
bHasSentBufferingEvent = true;
|
||||
EventSink.ReceiveMediaEvent(EMediaEvent::MediaBuffering);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (JavaState == JavaStateReady)
|
||||
{
|
||||
if (!bHasSentOpenEvent)
|
||||
{
|
||||
bHasSentOpenEvent = true;
|
||||
EventSink.ReceiveMediaEvent(EMediaEvent::MediaOpened);
|
||||
EventSink.ReceiveMediaEvent(EMediaEvent::TracksChanged);
|
||||
EventSink.ReceiveMediaEvent(EMediaEvent::MediaBufferingComplete);
|
||||
}
|
||||
|
||||
CurrentStatus = EMediaStatus::None;
|
||||
CurrentState = bIsPlaying ? EMediaState::Playing : EMediaState::Paused;
|
||||
CurrentRate = bIsPlaying ? 1.0f : 0.0f;
|
||||
bHasSentBufferingEvent = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (JavaState == JavaStateEnded)
|
||||
{
|
||||
CurrentState = EMediaState::Stopped;
|
||||
CurrentStatus = EMediaStatus::None;
|
||||
CurrentRate = 0.0f;
|
||||
if (!bHasSentEndEvent)
|
||||
{
|
||||
bHasSentEndEvent = true;
|
||||
EventSink.ReceiveMediaEvent(EMediaEvent::PlaybackEndReached);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (JavaState == JavaStateIdle)
|
||||
{
|
||||
CurrentState = CurrentUrl.IsEmpty() ? EMediaState::Closed : EMediaState::Stopped;
|
||||
CurrentStatus = EMediaStatus::None;
|
||||
CurrentRate = 0.0f;
|
||||
}
|
||||
}
|
||||
-78
@@ -1,78 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "IMediaCache.h"
|
||||
#include "IMediaControls.h"
|
||||
#include "IMediaPlayer.h"
|
||||
#include "IMediaSamples.h"
|
||||
#include "IMediaTracks.h"
|
||||
#include "IMediaView.h"
|
||||
|
||||
class IMediaEventSink;
|
||||
|
||||
class FAndroidRadioMediaPlayer
|
||||
: public IMediaPlayer
|
||||
, protected IMediaCache
|
||||
, protected IMediaControls
|
||||
, protected IMediaSamples
|
||||
, protected IMediaTracks
|
||||
, protected IMediaView
|
||||
{
|
||||
public:
|
||||
explicit FAndroidRadioMediaPlayer(IMediaEventSink& InEventSink);
|
||||
virtual ~FAndroidRadioMediaPlayer() override;
|
||||
|
||||
virtual void Close() override;
|
||||
virtual IMediaCache& GetCache() override;
|
||||
virtual IMediaControls& GetControls() override;
|
||||
virtual FString GetInfo() const override;
|
||||
virtual FGuid GetPlayerPluginGUID() const override;
|
||||
virtual IMediaSamples& GetSamples() override;
|
||||
virtual FString GetStats() const override;
|
||||
virtual IMediaTracks& GetTracks() override;
|
||||
virtual FString GetUrl() const override;
|
||||
virtual IMediaView& GetView() override;
|
||||
virtual bool Open(const FString& Url, const IMediaOptions* Options) override;
|
||||
virtual bool Open(const TSharedRef<FArchive, ESPMode::ThreadSafe>& Archive, const FString& OriginalUrl, const IMediaOptions* Options) override;
|
||||
virtual void TickInput(FTimespan DeltaTime, FTimespan Timecode) override;
|
||||
virtual bool SetNativeVolume(float Volume) override;
|
||||
|
||||
virtual bool CanControl(EMediaControl Control) const override;
|
||||
virtual FTimespan GetDuration() const override;
|
||||
virtual float GetRate() const override;
|
||||
virtual EMediaState GetState() const override;
|
||||
virtual EMediaStatus GetStatus() const override;
|
||||
virtual TRangeSet<float> GetSupportedRates(EMediaRateThinning Thinning) const override;
|
||||
virtual FTimespan GetTime() const override;
|
||||
virtual bool IsLooping() const override;
|
||||
virtual bool Seek(const FTimespan& Time) override;
|
||||
virtual bool SetLooping(bool Looping) override;
|
||||
virtual bool SetRate(float Rate) override;
|
||||
|
||||
virtual bool GetAudioTrackFormat(int32 TrackIndex, int32 FormatIndex, FMediaAudioTrackFormat& OutFormat) const override;
|
||||
virtual int32 GetNumTracks(EMediaTrackType TrackType) const override;
|
||||
virtual int32 GetNumTrackFormats(EMediaTrackType TrackType, int32 TrackIndex) const override;
|
||||
virtual int32 GetSelectedTrack(EMediaTrackType TrackType) const override;
|
||||
virtual FText GetTrackDisplayName(EMediaTrackType TrackType, int32 TrackIndex) const override;
|
||||
virtual int32 GetTrackFormat(EMediaTrackType TrackType, int32 TrackIndex) const override;
|
||||
virtual FString GetTrackLanguage(EMediaTrackType TrackType, int32 TrackIndex) const override;
|
||||
virtual FString GetTrackName(EMediaTrackType TrackType, int32 TrackIndex) const override;
|
||||
virtual bool GetVideoTrackFormat(int32 TrackIndex, int32 FormatIndex, FMediaVideoTrackFormat& OutFormat) const override;
|
||||
virtual bool SelectTrack(EMediaTrackType TrackType, int32 TrackIndex) override;
|
||||
virtual bool SetTrackFormat(EMediaTrackType TrackType, int32 TrackIndex, int32 FormatIndex) override;
|
||||
|
||||
virtual bool PeekVideoSampleTime(FMediaTimeStamp& TimeStamp) override;
|
||||
|
||||
private:
|
||||
void UpdateStateFromJava();
|
||||
|
||||
IMediaEventSink& EventSink;
|
||||
FString CurrentUrl;
|
||||
EMediaState CurrentState;
|
||||
EMediaStatus CurrentStatus;
|
||||
float CurrentRate;
|
||||
float CurrentVolume;
|
||||
bool bLooping;
|
||||
bool bHasSentOpenEvent;
|
||||
bool bHasSentBufferingEvent;
|
||||
bool bHasSentEndEvent;
|
||||
};
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
#include "AndroidRadioPlayerBPLibrary.h"
|
||||
|
||||
#include "AndroidRadioJavaBridge.h"
|
||||
|
||||
bool UAndroidRadioPlayerBPLibrary::OpenStreamMediaSourceUrl(UMediaPlayer* MediaPlayer, UStreamMediaSource* StreamMediaSource, const FString& Url)
|
||||
{
|
||||
if (MediaPlayer == nullptr || StreamMediaSource == nullptr || !Url.Contains(TEXT("://")))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
StreamMediaSource->StreamUrl = Url;
|
||||
MediaPlayer->Close();
|
||||
return MediaPlayer->OpenSource(StreamMediaSource);
|
||||
}
|
||||
|
||||
bool UAndroidRadioPlayerBPLibrary::PlayRadio(const FString& Url, float Volume)
|
||||
{
|
||||
return AndroidRadioJavaBridge::Play(Url, Volume);
|
||||
}
|
||||
|
||||
void UAndroidRadioPlayerBPLibrary::StopRadio()
|
||||
{
|
||||
AndroidRadioJavaBridge::Stop();
|
||||
}
|
||||
|
||||
void UAndroidRadioPlayerBPLibrary::PauseRadio()
|
||||
{
|
||||
AndroidRadioJavaBridge::Pause();
|
||||
}
|
||||
|
||||
void UAndroidRadioPlayerBPLibrary::ResumeRadio()
|
||||
{
|
||||
AndroidRadioJavaBridge::Resume();
|
||||
}
|
||||
|
||||
void UAndroidRadioPlayerBPLibrary::SetRadioVolume(float Volume)
|
||||
{
|
||||
AndroidRadioJavaBridge::SetVolume(Volume);
|
||||
}
|
||||
|
||||
bool UAndroidRadioPlayerBPLibrary::IsRadioPlaying()
|
||||
{
|
||||
return AndroidRadioJavaBridge::IsPlaying();
|
||||
}
|
||||
|
||||
EAndroidRadioPlaybackState UAndroidRadioPlayerBPLibrary::GetRadioPlaybackState()
|
||||
{
|
||||
switch (AndroidRadioJavaBridge::GetPlaybackState())
|
||||
{
|
||||
case 0:
|
||||
return EAndroidRadioPlaybackState::Idle;
|
||||
case 1:
|
||||
return EAndroidRadioPlaybackState::Buffering;
|
||||
case 2:
|
||||
return EAndroidRadioPlaybackState::Ready;
|
||||
case 3:
|
||||
return EAndroidRadioPlaybackState::Ended;
|
||||
case 4:
|
||||
return EAndroidRadioPlaybackState::Error;
|
||||
default:
|
||||
return EAndroidRadioPlaybackState::Unsupported;
|
||||
}
|
||||
}
|
||||
|
||||
FString UAndroidRadioPlayerBPLibrary::GetRadioLastError()
|
||||
{
|
||||
return AndroidRadioJavaBridge::GetLastError();
|
||||
}
|
||||
|
||||
bool UAndroidRadioPlayerBPLibrary::IsAndroidRadioPlayerSupported()
|
||||
{
|
||||
return PLATFORM_ANDROID;
|
||||
}
|
||||
-115
@@ -1,115 +0,0 @@
|
||||
#include "AndroidRadioMediaPlayer.h"
|
||||
|
||||
#include "IMediaModule.h"
|
||||
#include "IMediaPlayerFactory.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FAndroidRadioPlayerModule"
|
||||
|
||||
class FAndroidRadioPlayerModule
|
||||
: public IModuleInterface
|
||||
, public IMediaPlayerFactory
|
||||
{
|
||||
public:
|
||||
virtual void StartupModule() override
|
||||
{
|
||||
SupportedPlatforms.Add(TEXT("Android"));
|
||||
SupportedUriSchemes.Add(TEXT("http"));
|
||||
SupportedUriSchemes.Add(TEXT("https"));
|
||||
|
||||
if (IMediaModule* MediaModule = FModuleManager::LoadModulePtr<IMediaModule>("Media"))
|
||||
{
|
||||
MediaModule->RegisterPlayerFactory(*this);
|
||||
}
|
||||
}
|
||||
|
||||
virtual void ShutdownModule() override
|
||||
{
|
||||
if (IMediaModule* MediaModule = FModuleManager::GetModulePtr<IMediaModule>("Media"))
|
||||
{
|
||||
MediaModule->UnregisterPlayerFactory(*this);
|
||||
}
|
||||
}
|
||||
|
||||
virtual bool CanPlayUrl(const FString& Url, const IMediaOptions* Options, TArray<FText>* OutWarnings, TArray<FText>* OutErrors) const override
|
||||
{
|
||||
return GetPlayabilityConfidenceScore(Url, Options, OutWarnings, OutErrors) > 0;
|
||||
}
|
||||
|
||||
virtual int32 GetPlayabilityConfidenceScore(const FString& Url, const IMediaOptions* Options, TArray<FText>* OutWarnings, TArray<FText>* OutErrors) const override
|
||||
{
|
||||
FString Scheme;
|
||||
FString Location;
|
||||
|
||||
if (!Url.Split(TEXT("://"), &Scheme, &Location, ESearchCase::CaseSensitive))
|
||||
{
|
||||
if (OutErrors != nullptr)
|
||||
{
|
||||
OutErrors->Add(LOCTEXT("NoSchemeFound", "No URI scheme found"));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!SupportedUriSchemes.Contains(Scheme))
|
||||
{
|
||||
if (OutErrors != nullptr)
|
||||
{
|
||||
OutErrors->Add(FText::Format(LOCTEXT("SchemeNotSupported", "The URI scheme '{0}' is not supported"), FText::FromString(Scheme)));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const FString Extension = FPaths::GetExtension(Location, false).ToLower();
|
||||
if (Extension.IsEmpty() || Extension == TEXT("mp3") || Extension == TEXT("aac") || Extension == TEXT("m3u8") || Extension == TEXT("pls") || Extension == TEXT("ogg") || Extension == TEXT("opus"))
|
||||
{
|
||||
return 90;
|
||||
}
|
||||
|
||||
if (OutWarnings != nullptr)
|
||||
{
|
||||
OutWarnings->Add(FText::Format(LOCTEXT("ExtensionProbablyNotRadio", "The file extension '{0}' is not a known radio stream extension"), FText::FromString(Extension)));
|
||||
}
|
||||
return 20;
|
||||
}
|
||||
|
||||
virtual TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CreatePlayer(IMediaEventSink& EventSink) override
|
||||
{
|
||||
return MakeShared<FAndroidRadioMediaPlayer, ESPMode::ThreadSafe>(EventSink);
|
||||
}
|
||||
|
||||
virtual FText GetDisplayName() const override
|
||||
{
|
||||
return LOCTEXT("MediaPlayerDisplayName", "Android Radio Player");
|
||||
}
|
||||
|
||||
virtual FName GetPlayerName() const override
|
||||
{
|
||||
static FName PlayerName(TEXT("AndroidRadioPlayer"));
|
||||
return PlayerName;
|
||||
}
|
||||
|
||||
virtual FGuid GetPlayerPluginGUID() const override
|
||||
{
|
||||
static FGuid PlayerPluginGUID(0x2547a7af, 0xe9504c2f, 0xa090e1b7, 0x26c7d716);
|
||||
return PlayerPluginGUID;
|
||||
}
|
||||
|
||||
virtual const TArray<FString>& GetSupportedPlatforms() const override
|
||||
{
|
||||
return SupportedPlatforms;
|
||||
}
|
||||
|
||||
virtual bool SupportsFeature(EMediaFeature Feature) const override
|
||||
{
|
||||
return Feature == EMediaFeature::AudioTracks;
|
||||
}
|
||||
|
||||
private:
|
||||
TArray<FString> SupportedPlatforms;
|
||||
TArray<FString> SupportedUriSchemes;
|
||||
};
|
||||
|
||||
IMPLEMENT_MODULE(FAndroidRadioPlayerModule, AndroidRadioPlayer)
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "MediaPlayer.h"
|
||||
#include "StreamMediaSource.h"
|
||||
#include "AndroidRadioPlayerBPLibrary.generated.h"
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EAndroidRadioPlaybackState : uint8
|
||||
{
|
||||
Idle UMETA(DisplayName = "Idle"),
|
||||
Buffering UMETA(DisplayName = "Buffering"),
|
||||
Ready UMETA(DisplayName = "Ready"),
|
||||
Ended UMETA(DisplayName = "Ended"),
|
||||
Error UMETA(DisplayName = "Error"),
|
||||
Unsupported UMETA(DisplayName = "Unsupported")
|
||||
};
|
||||
|
||||
UCLASS()
|
||||
class ANDROIDRADIOPLAYER_API UAndroidRadioPlayerBPLibrary : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintCallable, Category = "Android Radio Player|Media")
|
||||
static bool OpenStreamMediaSourceUrl(UMediaPlayer* MediaPlayer, UStreamMediaSource* StreamMediaSource, const FString& Url);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Android Radio Player")
|
||||
static bool PlayRadio(const FString& Url, float Volume = 1.0f);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Android Radio Player")
|
||||
static void StopRadio();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Android Radio Player")
|
||||
static void PauseRadio();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Android Radio Player")
|
||||
static void ResumeRadio();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Android Radio Player")
|
||||
static void SetRadioVolume(float Volume);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Android Radio Player")
|
||||
static bool IsRadioPlaying();
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Android Radio Player")
|
||||
static EAndroidRadioPlaybackState GetRadioPlaybackState();
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Android Radio Player")
|
||||
static FString GetRadioLastError();
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Android Radio Player")
|
||||
static bool IsAndroidRadioPlayerSupported();
|
||||
};
|
||||
@@ -8,7 +8,7 @@ public class FunnyGirl : ModuleRules
|
||||
{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "HTTP", "Json", "JsonUtilities", "AssetRegistry", "UMG" });
|
||||
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "HTTP", "Json", "JsonUtilities", "AssetRegistry", "MediaAssets", "UMG" });
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
|
||||
|
||||
|
||||
@@ -169,6 +169,87 @@ bool UPersonaBrainComponent::GetLastSavedLLMMessage(FString& OutMessage) const
|
||||
return false;
|
||||
}
|
||||
|
||||
TArray<FPersonaChatMessage> UPersonaBrainComponent::GetSavedChatMessages() const
|
||||
{
|
||||
TArray<FPersonaChatMessage> ChatMessages;
|
||||
|
||||
const FString FilePath = GetChatFilePath();
|
||||
FString ChatJson;
|
||||
if (!FFileHelper::LoadFileToString(ChatJson, *FilePath))
|
||||
{
|
||||
return ChatMessages;
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonObject> RootObject;
|
||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ChatJson);
|
||||
if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
|
||||
{
|
||||
return ChatMessages;
|
||||
}
|
||||
|
||||
const TArray<TSharedPtr<FJsonValue>>* SavedMessages = nullptr;
|
||||
if (!RootObject->TryGetArrayField(TEXT("messages"), SavedMessages) || !SavedMessages)
|
||||
{
|
||||
return ChatMessages;
|
||||
}
|
||||
|
||||
const FString GirlSpeaker = Identity && !Identity->Name.IsEmpty() && !Identity->Name.ToString().TrimStartAndEnd().IsEmpty()
|
||||
? Identity->Name.ToString().TrimStartAndEnd()
|
||||
: FString(TEXT("Girl"));
|
||||
|
||||
for (const TSharedPtr<FJsonValue>& MessageValue : *SavedMessages)
|
||||
{
|
||||
const TSharedPtr<FJsonObject>* MessageObject = nullptr;
|
||||
if (!MessageValue.IsValid() || !MessageValue->TryGetObject(MessageObject) || !MessageObject || !MessageObject->IsValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FString Role;
|
||||
FString Content;
|
||||
if (!(*MessageObject)->TryGetStringField(TEXT("role"), Role) || !(*MessageObject)->TryGetStringField(TEXT("content"), Content))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((Role != TEXT("user") && Role != TEXT("assistant")) || Content.TrimStartAndEnd().IsEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FPersonaChatMessage ChatMessage;
|
||||
(*MessageObject)->TryGetStringField(TEXT("timestamp"), ChatMessage.Timestamp);
|
||||
ChatMessage.Role = Role;
|
||||
ChatMessage.Speaker = Role == TEXT("assistant") ? GirlSpeaker : FString(TEXT("Player"));
|
||||
ChatMessage.Content = Content;
|
||||
|
||||
const TSharedPtr<FJsonValue> InteractActionValue = (*MessageObject)->TryGetField(TEXT("interact_action"));
|
||||
if (InteractActionValue.IsValid() && InteractActionValue->Type != EJson::Null)
|
||||
{
|
||||
if (InteractActionValue->Type == EJson::Object)
|
||||
{
|
||||
const TSharedPtr<FJsonObject>* InteractActionObject = nullptr;
|
||||
if (InteractActionValue->TryGetObject(InteractActionObject) && InteractActionObject && InteractActionObject->IsValid())
|
||||
{
|
||||
FString InteractActionJson;
|
||||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&InteractActionJson);
|
||||
FJsonSerializer::Serialize(InteractActionObject->ToSharedRef(), Writer);
|
||||
ChatMessage.InteractActionJson = InteractActionJson;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
InteractActionValue->TryGetString(ChatMessage.InteractActionJson);
|
||||
}
|
||||
}
|
||||
|
||||
(*MessageObject)->TryGetStringField(TEXT("emotion"), ChatMessage.Emotion);
|
||||
ChatMessages.Add(ChatMessage);
|
||||
}
|
||||
|
||||
return ChatMessages;
|
||||
}
|
||||
|
||||
void UPersonaBrainComponent::BroadcastBrainError(const FString& ErrorMessage) const
|
||||
{
|
||||
OnLLMError.Broadcast(ErrorMessage);
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
#include "Morph/MorphControllerComponent.h"
|
||||
#include "Morph/MorphPresetDataAsset.h"
|
||||
#include "Animation/AnimInstance.h"
|
||||
#include "Animation/AnimMontage.h"
|
||||
#include "Components/SkeletalMeshComponent.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "Engine/World.h"
|
||||
@@ -37,6 +35,12 @@ void UMorphControllerComponent::BeginPlay()
|
||||
Super::BeginPlay();
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
ClearMorphsImmediately();
|
||||
Super::EndPlay(EndPlayReason);
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
|
||||
{
|
||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||
@@ -212,7 +216,6 @@ bool UMorphControllerComponent::AddPreset(UMorphPresetDataAsset* Preset, float W
|
||||
if (Slot.Preset == Preset)
|
||||
{
|
||||
Slot.TargetWeight = FMath::Clamp(Weight, 0.0f, 1.0f);
|
||||
Slot.CurrentWeight = Slot.TargetWeight;
|
||||
Slot.PlaybackTime = 0.0f;
|
||||
Slot.LoopDelayRemaining = 0.0f;
|
||||
Slot.TimelineDuration = TimelineDuration;
|
||||
@@ -224,7 +227,7 @@ bool UMorphControllerComponent::AddPreset(UMorphPresetDataAsset* Preset, float W
|
||||
FActiveMorphPresetSlot NewSlot;
|
||||
NewSlot.Preset = Preset;
|
||||
NewSlot.TargetWeight = FMath::Clamp(Weight, 0.0f, 1.0f);
|
||||
NewSlot.CurrentWeight = NewSlot.TargetWeight;
|
||||
NewSlot.CurrentWeight = 0.0f;
|
||||
NewSlot.PlaybackTime = 0.0f;
|
||||
NewSlot.LoopDelayRemaining = 0.0f;
|
||||
NewSlot.TimelineDuration = TimelineDuration;
|
||||
@@ -239,78 +242,32 @@ bool UMorphControllerComponent::PlayPreset(UMorphPresetDataAsset* Preset, float
|
||||
{
|
||||
if (bLogWarnings)
|
||||
{
|
||||
UE_LOG(LogMorphController, Warning, TEXT("PlayPreset ignored on %s: preset is null or has no morph clips or animation montage."), *GetNameSafe(GetOwner()));
|
||||
UE_LOG(LogMorphController, Warning, TEXT("PlayPreset ignored on %s: preset is null or has no morph clips."), *GetNameSafe(GetOwner()));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool bPlayedMorphs = Preset->IsValidPreset() ? AddPreset(Preset, Weight) : false;
|
||||
const bool bPlayedAnimation = PlayPresetAnimation(Preset);
|
||||
return bPlayedMorphs || bPlayedAnimation;
|
||||
}
|
||||
|
||||
bool UMorphControllerComponent::PlayPresetAnimation(UMorphPresetDataAsset* Preset)
|
||||
{
|
||||
if (!Preset || !Preset->AnimationMontage)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
USkeletalMeshComponent* Mesh = ResolveMeshComponent();
|
||||
if (!Mesh)
|
||||
{
|
||||
if (bLogWarnings)
|
||||
{
|
||||
UE_LOG(LogMorphController, Warning, TEXT("Preset animation ignored on %s: no SkeletalMeshComponent was found."), *GetNameSafe(GetOwner()));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
UAnimInstance* AnimInstance = Mesh->GetAnimInstance();
|
||||
if (!AnimInstance)
|
||||
{
|
||||
if (bLogWarnings)
|
||||
{
|
||||
UE_LOG(LogMorphController, Warning, TEXT("Preset animation ignored on %s: mesh %s has no AnimInstance."), *GetNameSafe(GetOwner()), *GetNameSafe(Mesh));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
UAnimMontage* Montage = Preset->AnimationMontage;
|
||||
if (Preset->bRestartAnimationOnReplay && AnimInstance->Montage_IsPlaying(Montage))
|
||||
{
|
||||
AnimInstance->Montage_Stop(0.0f, Montage);
|
||||
}
|
||||
|
||||
const float MontageLength = AnimInstance->Montage_Play(Montage, FMath::Max(Preset->AnimationPlayRate, 0.01f));
|
||||
if (MontageLength <= 0.0f)
|
||||
{
|
||||
if (bLogWarnings)
|
||||
{
|
||||
UE_LOG(LogMorphController, Warning, TEXT("Preset animation failed on %s: montage %s could not play. Check the Anim Blueprint slot setup."), *GetNameSafe(GetOwner()), *GetNameSafe(Montage));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Preset->AnimationStartSection.IsNone())
|
||||
{
|
||||
AnimInstance->Montage_JumpToSection(Preset->AnimationStartSection, Montage);
|
||||
}
|
||||
|
||||
return true;
|
||||
return AddPreset(Preset, Weight);
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::RemovePreset(UMorphPresetDataAsset* Preset)
|
||||
{
|
||||
if (!Preset) return;
|
||||
|
||||
bool bRemovedAny = false;
|
||||
for (int32 i = ActivePresets.Num() - 1; i >= 0; --i)
|
||||
{
|
||||
if (ActivePresets[i].Preset == Preset)
|
||||
{
|
||||
ActivePresets.RemoveAt(i);
|
||||
bRemovedAny = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (bRemovedAny)
|
||||
{
|
||||
UpdateMorphTargets(0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::SetPresetWeight(UMorphPresetDataAsset* Preset, float Weight)
|
||||
@@ -336,6 +293,7 @@ void UMorphControllerComponent::SetActivePresets(const TArray<FActiveMorphPreset
|
||||
if (Slot.Preset && Slot.Preset->IsValidPreset())
|
||||
{
|
||||
FActiveMorphPresetSlot NewSlot = Slot;
|
||||
NewSlot.TargetWeight = FMath::Clamp(NewSlot.TargetWeight, 0.0f, 1.0f);
|
||||
NewSlot.CurrentWeight = 0.0f;
|
||||
NewSlot.PlaybackTime = 0.0f;
|
||||
NewSlot.LoopDelayRemaining = 0.0f;
|
||||
@@ -346,11 +304,14 @@ void UMorphControllerComponent::SetActivePresets(const TArray<FActiveMorphPreset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UpdateMorphTargets(0.0f);
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::ClearAllPresets()
|
||||
{
|
||||
ActivePresets.Empty();
|
||||
UpdateMorphTargets(0.0f);
|
||||
}
|
||||
|
||||
bool UMorphControllerComponent::IsPresetActive(UMorphPresetDataAsset* Preset) const
|
||||
@@ -382,17 +343,27 @@ TArray<FActiveMorphPresetSlot> UMorphControllerComponent::GetActivePresets() con
|
||||
|
||||
void UMorphControllerComponent::SetMorphTarget(FName MorphTargetName, float Value)
|
||||
{
|
||||
if (MorphTargetName.IsNone())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DirectMorphOverrides.Add(MorphTargetName, ClampMorphTargetValue(Value));
|
||||
UpdateMorphTargets(0.0f);
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::RemoveMorphTargetOverride(FName MorphTargetName)
|
||||
{
|
||||
DirectMorphOverrides.Remove(MorphTargetName);
|
||||
if (DirectMorphOverrides.Remove(MorphTargetName) > 0)
|
||||
{
|
||||
UpdateMorphTargets(0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::ClearDirectMorphOverrides()
|
||||
{
|
||||
DirectMorphOverrides.Empty();
|
||||
UpdateMorphTargets(0.0f);
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::SetLipSyncMorphValues(const TMap<FName, float>& MorphValues)
|
||||
@@ -405,11 +376,13 @@ void UMorphControllerComponent::SetLipSyncMorphValues(const TMap<FName, float>&
|
||||
LipSyncMorphValues.Add(Pair.Key, ClampMorphTargetValue(Pair.Value));
|
||||
}
|
||||
}
|
||||
UpdateMorphTargets(0.0f);
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::ClearLipSyncMorphValues()
|
||||
{
|
||||
LipSyncMorphValues.Empty();
|
||||
UpdateMorphTargets(0.0f);
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::UpdateMorphTargetsNow(float DeltaTime)
|
||||
@@ -501,6 +474,19 @@ void UMorphControllerComponent::UpdateMorphTargets(float DeltaTime)
|
||||
|
||||
// 2. Gather final morph values. Overlapping clips use the strongest contribution.
|
||||
TMap<FName, float> FinalValues;
|
||||
float ActivePresetWeightSum = 0.0f;
|
||||
for (const FActiveMorphPresetSlot& Slot : ActivePresets)
|
||||
{
|
||||
if (Slot.Preset && Slot.CurrentWeight >= 0.001f && Slot.LoopDelayRemaining <= 0.0f)
|
||||
{
|
||||
ActivePresetWeightSum += Slot.CurrentWeight;
|
||||
}
|
||||
}
|
||||
|
||||
const float PresetLayerScale = bNormalizePresetLayerWeights && MaxCombinedPresetWeight > 0.0f && ActivePresetWeightSum > MaxCombinedPresetWeight
|
||||
? MaxCombinedPresetWeight / ActivePresetWeightSum
|
||||
: 1.0f;
|
||||
|
||||
for (const FActiveMorphPresetSlot& Slot : ActivePresets)
|
||||
{
|
||||
if (!Slot.Preset || Slot.CurrentWeight < 0.001f || Slot.LoopDelayRemaining > 0.0f)
|
||||
@@ -508,7 +494,7 @@ void UMorphControllerComponent::UpdateMorphTargets(float DeltaTime)
|
||||
continue;
|
||||
}
|
||||
|
||||
MorphPresetRuntime::AccumulatePresetMorphValuesAtTime(*Slot.Preset, Slot.PlaybackTime, Slot.CurrentWeight, FinalValues);
|
||||
MorphPresetRuntime::AccumulatePresetMorphValuesAtTime(*Slot.Preset, Slot.PlaybackTime, Slot.CurrentWeight * PresetLayerScale, FinalValues);
|
||||
}
|
||||
|
||||
// 3. Apply direct overrides with the same max-based priority rule.
|
||||
|
||||
@@ -9,11 +9,13 @@
|
||||
#include "Engine/SkeletalMesh.h"
|
||||
#include "Engine/Texture2D.h"
|
||||
#include "HAL/FileManager.h"
|
||||
#include "MediaPlayer.h"
|
||||
#include "Misc/Base64.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
#include "StreamMediaSource.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -358,3 +360,15 @@ bool UFunnyGirlUtilityBlueprintLibrary::DeleteLightString(const FString& Field,
|
||||
SaveObject->RemoveField(EncodedField);
|
||||
return SaveLightSaveObject(SaveObject.ToSharedRef(), OutError);
|
||||
}
|
||||
|
||||
bool UFunnyGirlUtilityBlueprintLibrary::OpenStreamMediaSourceUrl(UMediaPlayer* MediaPlayer, UStreamMediaSource* StreamMediaSource, const FString& Url)
|
||||
{
|
||||
if (!MediaPlayer || !StreamMediaSource || !Url.Contains(TEXT("://")))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
StreamMediaSource->StreamUrl = Url;
|
||||
MediaPlayer->Close();
|
||||
return MediaPlayer->OpenSource(StreamMediaSource);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,30 @@ struct FUNNYGIRL_API FLLMReplyResponse
|
||||
FString Emotion;
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FUNNYGIRL_API FPersonaChatMessage
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|History")
|
||||
FString Timestamp;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|History")
|
||||
FString Role;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|History")
|
||||
FString Speaker;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|History")
|
||||
FString Content;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|History|Interaction")
|
||||
FString InteractActionJson;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|History|Emotion")
|
||||
FString Emotion;
|
||||
};
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FLLMResponseDelegate, const FLLMReplyResponse&, Response);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FLLMReplyStreamChunkDelegate, const FString&, ReplyChunk);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FLLMErrorDelegate, const FString&, ErrorMessage);
|
||||
@@ -85,6 +109,9 @@ public:
|
||||
UFUNCTION(BlueprintPure, Category = "LLM|History")
|
||||
bool GetLastSavedLLMMessage(FString& OutMessage) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "LLM|History")
|
||||
TArray<FPersonaChatMessage> GetSavedChatMessages() const;
|
||||
|
||||
virtual void CancelRequest() override;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -74,6 +74,14 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller")
|
||||
bool bLogWarnings = true;
|
||||
|
||||
/** Keep layered presets from overdriving the face when several expressions are active at once. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller|Layering")
|
||||
bool bNormalizePresetLayerWeights = true;
|
||||
|
||||
/** Maximum total expression weight shared by active presets when normalization is enabled. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller|Layering", meta = (ClampMin = "0", ClampMax = "5"))
|
||||
float MaxCombinedPresetWeight = 1.0f;
|
||||
|
||||
/** Emotion presets available for AI/director prompting. Use these as the character's facial expression vocabulary. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller|Emotions")
|
||||
TArray<TObjectPtr<UMorphPresetDataAsset>> EmotionPresets;
|
||||
@@ -178,6 +186,7 @@ public:
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
||||
|
||||
private:
|
||||
UPROPERTY()
|
||||
@@ -197,7 +206,6 @@ private:
|
||||
|
||||
USkeletalMeshComponent* ResolveMeshComponent() const;
|
||||
bool PresetHasAnyPositiveCurveValue(const UMorphPresetDataAsset& Preset) const;
|
||||
bool PlayPresetAnimation(UMorphPresetDataAsset* Preset);
|
||||
void UpdateMorphTargets(float DeltaTime);
|
||||
void BlendMorphLayer(const TMap<FName, float>& LayerValues, float LayerWeight, EMorphLayerBlendMode BlendMode, TMap<FName, float>& InOutFinalValues) const;
|
||||
void ApplyFinalMorphValues(const TMap<FName, float>& FinalValues);
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
#include "Engine/SkeletalMesh.h"
|
||||
#include "MorphPresetDataAsset.generated.h"
|
||||
|
||||
class UAnimMontage;
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FUNNYGIRL_API FMorphTimelineClip
|
||||
{
|
||||
@@ -77,26 +75,6 @@ public:
|
||||
ClampMin = "0"))
|
||||
float BlendSpeedOverride = 0.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Animation", meta = (
|
||||
DisplayName = "Animation Montage",
|
||||
ToolTip = "Optional animation montage played when this preset starts. The montage must use a slot that is present in the character Anim Blueprint."))
|
||||
TObjectPtr<UAnimMontage> AnimationMontage = nullptr;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Animation", meta = (
|
||||
DisplayName = "Animation Play Rate",
|
||||
ClampMin = "0.01"))
|
||||
float AnimationPlayRate = 1.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Animation", meta = (
|
||||
DisplayName = "Start Section",
|
||||
ToolTip = "Optional montage section to jump to after playback starts."))
|
||||
FName AnimationStartSection;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Animation", meta = (
|
||||
DisplayName = "Restart Animation On Replay",
|
||||
ToolTip = "If enabled, an already playing montage is stopped and restarted when this preset is triggered again."))
|
||||
bool bRestartAnimationOnReplay = true;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
|
||||
int32 GetMorphCount() const;
|
||||
|
||||
@@ -104,10 +82,7 @@ public:
|
||||
float GetTimelineLength() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
|
||||
bool HasAnimation() const { return AnimationMontage != nullptr; }
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
|
||||
bool HasPlayableContent() const { return IsValidPreset() || HasAnimation(); }
|
||||
bool HasPlayableContent() const { return IsValidPreset(); }
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
|
||||
bool IsValidPreset() const { return GetMorphCount() > 0; }
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
class USkeletalMesh;
|
||||
class UButton;
|
||||
class UMediaPlayer;
|
||||
class UStreamMediaSource;
|
||||
class UTexture2D;
|
||||
|
||||
UENUM(BlueprintType)
|
||||
@@ -53,4 +55,7 @@ public:
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Save")
|
||||
static bool DeleteLightString(const FString& Field, FString& OutError);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Media")
|
||||
static bool OpenStreamMediaSourceUrl(UMediaPlayer* MediaPlayer, UStreamMediaSource* StreamMediaSource, const FString& Url);
|
||||
};
|
||||
|
||||
@@ -64,10 +64,6 @@ namespace
|
||||
TargetPreset.bLoop = SourcePreset.bLoop;
|
||||
TargetPreset.LoopDelay = SourcePreset.LoopDelay;
|
||||
TargetPreset.BlendSpeedOverride = SourcePreset.BlendSpeedOverride;
|
||||
TargetPreset.AnimationMontage = SourcePreset.AnimationMontage;
|
||||
TargetPreset.AnimationPlayRate = SourcePreset.AnimationPlayRate;
|
||||
TargetPreset.AnimationStartSection = SourcePreset.AnimationStartSection;
|
||||
TargetPreset.bRestartAnimationOnReplay = SourcePreset.bRestartAnimationOnReplay;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,7 @@
|
||||
|
||||
#include "Animation/AnimData/IAnimationDataController.h"
|
||||
#include "AnimPreviewInstance.h"
|
||||
#include "Animation/AnimMontage.h"
|
||||
#include "Animation/AnimSequence.h"
|
||||
#include "Animation/AnimSequenceBase.h"
|
||||
#include "Animation/AnimationAsset.h"
|
||||
#include "Animation/DebugSkelMeshComponent.h"
|
||||
#include "Animation/Skeleton.h"
|
||||
#include "Components/SkeletalMeshComponent.h"
|
||||
@@ -115,15 +112,7 @@ float FMorphPresetEditorController::GetTimelineLength() const
|
||||
{
|
||||
if (const UMorphPresetDataAsset* PresetAsset = Preset.Get())
|
||||
{
|
||||
float TimelineLength = PresetAsset->GetTimelineLength();
|
||||
if (PresetAsset->AnimationMontage)
|
||||
{
|
||||
const float PlayRate = FMath::Max(PresetAsset->AnimationPlayRate, 0.01f);
|
||||
const UAnimSequenceBase* PreviewSequence = PresetAsset->AnimationMontage->GetFirstAnimReference();
|
||||
const float AnimationLength = PreviewSequence ? PreviewSequence->GetPlayLength() : PresetAsset->AnimationMontage->GetPlayLength();
|
||||
TimelineLength = FMath::Max(TimelineLength, AnimationLength / PlayRate);
|
||||
}
|
||||
return FMath::Max(TimelineLength, 0.1f);
|
||||
return FMath::Max(PresetAsset->GetTimelineLength(), 0.1f);
|
||||
}
|
||||
|
||||
return 1.0f;
|
||||
@@ -145,7 +134,7 @@ void FMorphPresetEditorController::SetCurrentTime(float InTime)
|
||||
if (PreviewMeshComponent->PreviewInstance)
|
||||
{
|
||||
PreviewMeshComponent->PreviewInstance->SetPlaying(false);
|
||||
PreviewMeshComponent->PreviewInstance->SetPosition(GetPreviewAnimationPosition(), false);
|
||||
PreviewMeshComponent->PreviewInstance->SetPosition(CurrentTime, false);
|
||||
RefreshPreviewAnimationPose();
|
||||
}
|
||||
}
|
||||
@@ -225,20 +214,21 @@ void FMorphPresetEditorController::NotifyPresetEdited(bool bBroadcastPresetChang
|
||||
PresetAsset->MarkPackageDirty();
|
||||
}
|
||||
|
||||
UAnimationAsset* DesiredPreviewAsset = ResolvePreviewAnimationAsset();
|
||||
UAnimationAsset* CurrentPreviewAsset = nullptr;
|
||||
if (TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene())
|
||||
if (ActivePreviewMesh.Get() != GetPreviewMesh())
|
||||
{
|
||||
CurrentPreviewAsset = PreviewScene->GetPreviewAnimationAsset();
|
||||
RebuildPreviewScene();
|
||||
if (bBroadcastPresetChanged)
|
||||
{
|
||||
PresetChanged.Broadcast();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (DesiredPreviewAsset != CurrentPreviewAsset ||
|
||||
(DesiredPreviewAsset && PreviewAnimation.IsValid()) ||
|
||||
(!DesiredPreviewAsset && !PreviewAnimation.IsValid()))
|
||||
if (!PreviewAnimation.IsValid())
|
||||
{
|
||||
RebuildPreviewAnimation();
|
||||
}
|
||||
else if (PreviewAnimation.IsValid())
|
||||
else
|
||||
{
|
||||
SetAnimSequencePlayLength(*PreviewAnimation, GetTimelineLength(), false);
|
||||
}
|
||||
@@ -436,6 +426,7 @@ void FMorphPresetEditorController::RebuildPreviewScene()
|
||||
PreviewAnimation.Reset();
|
||||
|
||||
USkeletalMesh* PreviewMesh = GetPreviewMesh();
|
||||
ActivePreviewMesh = PreviewMesh;
|
||||
if (PreviewMesh == nullptr)
|
||||
{
|
||||
PreviewSceneChanged.Broadcast();
|
||||
@@ -464,56 +455,29 @@ void FMorphPresetEditorController::RebuildPreviewAnimation()
|
||||
PreviewAnimation.Reset();
|
||||
PreviewStatusText = FText::GetEmpty();
|
||||
|
||||
UAnimationAsset* PreviewAsset = ResolvePreviewAnimationAsset();
|
||||
if (PreviewAsset && !IsPreviewAnimationCompatible(PreviewAsset))
|
||||
PreviewAnimation = TStrongObjectPtr<UAnimSequence>(AnimSequence);
|
||||
SetAnimSequencePlayLength(*AnimSequence, GetTimelineLength(), false);
|
||||
if (USkeletalMesh* PreviewMesh = GetPreviewMesh())
|
||||
{
|
||||
const USkeletalMesh* PreviewMesh = GetPreviewMesh();
|
||||
const FString PreviewMeshSkeletonName = PreviewMesh && PreviewMesh->GetSkeleton()
|
||||
? PreviewMesh->GetSkeleton()->GetPathName()
|
||||
: TEXT("None");
|
||||
const FString AnimationSkeletonName = PreviewAsset->GetSkeleton()
|
||||
? PreviewAsset->GetSkeleton()->GetPathName()
|
||||
: TEXT("None");
|
||||
|
||||
PreviewStatusText = FText::FromString(FString::Printf(
|
||||
TEXT("Animation preview is disabled: montage sequence '%s' uses skeleton '%s', but Preview Mesh uses '%s'. Retarget the animation to the Preview Mesh skeleton."),
|
||||
*GetNameSafe(PreviewAsset),
|
||||
*AnimationSkeletonName,
|
||||
*PreviewMeshSkeletonName));
|
||||
UE_LOG(LogMorphPresetEditorPreview, Warning, TEXT("%s"), *PreviewStatusText.ToString());
|
||||
PreviewAsset = nullptr;
|
||||
}
|
||||
|
||||
if (!PreviewAsset)
|
||||
{
|
||||
PreviewAnimation = TStrongObjectPtr<UAnimSequence>(AnimSequence);
|
||||
PreviewAsset = AnimSequence;
|
||||
|
||||
SetAnimSequencePlayLength(*AnimSequence, GetTimelineLength(), false);
|
||||
if (USkeletalMesh* PreviewMesh = GetPreviewMesh())
|
||||
if (USkeleton* Skeleton = PreviewMesh->GetSkeleton())
|
||||
{
|
||||
if (USkeleton* Skeleton = PreviewMesh->GetSkeleton())
|
||||
{
|
||||
AnimSequence->SetSkeleton(Skeleton);
|
||||
}
|
||||
AnimSequence->SetSkeleton(Skeleton);
|
||||
}
|
||||
}
|
||||
|
||||
PreviewScene->SetPreviewAnimationAsset(PreviewAsset, true);
|
||||
PreviewScene->SetPreviewAnimationAsset(AnimSequence, true);
|
||||
if (UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent())
|
||||
{
|
||||
PreviewMeshComponent->SetAnimationMode(EAnimationMode::AnimationSingleNode);
|
||||
if (PreviewMeshComponent->PreviewInstance)
|
||||
{
|
||||
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
|
||||
const float PlayRate = PresetAsset ? FMath::Max(PresetAsset->AnimationPlayRate, 0.01f) : 1.0f;
|
||||
PreviewMeshComponent->PreviewInstance->SetAnimationAsset(PreviewAsset, false, PlayRate);
|
||||
PreviewMeshComponent->PreviewInstance->SetAnimationAsset(AnimSequence, false, 1.0f);
|
||||
PreviewMeshComponent->PreviewInstance->EnableControllers(true);
|
||||
PreviewMeshComponent->PreviewInstance->SetSkeletalControlAlpha(1.0f);
|
||||
PreviewMeshComponent->PreviewInstance->SetPlaying(false);
|
||||
PreviewMeshComponent->PreviewInstance->SetLooping(false);
|
||||
|
||||
PreviewMeshComponent->PreviewInstance->SetPosition(GetPreviewAnimationPosition(), false);
|
||||
PreviewMeshComponent->PreviewInstance->SetPosition(CurrentTime, false);
|
||||
RefreshPreviewAnimationPose();
|
||||
}
|
||||
}
|
||||
@@ -539,52 +503,6 @@ void FMorphPresetEditorController::RefreshPreviewAnimationPose()
|
||||
PreviewScene->InvalidateViews();
|
||||
}
|
||||
|
||||
UAnimationAsset* FMorphPresetEditorController::ResolvePreviewAnimationAsset() const
|
||||
{
|
||||
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
|
||||
if (PresetAsset && PresetAsset->AnimationMontage)
|
||||
{
|
||||
if (UAnimSequenceBase* FirstAnimReference = PresetAsset->AnimationMontage->GetFirstAnimReference())
|
||||
{
|
||||
return FirstAnimReference;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
float FMorphPresetEditorController::GetPreviewAnimationPosition() const
|
||||
{
|
||||
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
|
||||
UAnimationAsset* PreviewAsset = ResolvePreviewAnimationAsset();
|
||||
if (!PresetAsset || !PreviewAsset)
|
||||
{
|
||||
return CurrentTime;
|
||||
}
|
||||
|
||||
const float PlayRate = FMath::Max(PresetAsset->AnimationPlayRate, 0.01f);
|
||||
float PreviewPosition = CurrentTime * PlayRate;
|
||||
if (const UAnimSequenceBase* PreviewSequence = Cast<UAnimSequenceBase>(PreviewAsset))
|
||||
{
|
||||
return FMath::Clamp(PreviewPosition, 0.0f, PreviewSequence->GetPlayLength());
|
||||
}
|
||||
|
||||
return PreviewPosition;
|
||||
}
|
||||
|
||||
bool FMorphPresetEditorController::IsPreviewAnimationCompatible(UAnimationAsset* PreviewAsset) const
|
||||
{
|
||||
if (!PreviewAsset)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
const USkeletalMesh* PreviewMesh = GetPreviewMesh();
|
||||
const USkeleton* PreviewSkeleton = PreviewMesh ? PreviewMesh->GetSkeleton() : nullptr;
|
||||
const USkeleton* AnimationSkeleton = PreviewAsset->GetSkeleton();
|
||||
return PreviewSkeleton && AnimationSkeleton && PreviewSkeleton->IsCompatibleForEditor(AnimationSkeleton);
|
||||
}
|
||||
|
||||
void FMorphPresetEditorController::ApplyPresetToPreview()
|
||||
{
|
||||
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
#include "PropertyCustomizationHelpers.h"
|
||||
#include "SEditorViewport.h"
|
||||
#include "Sound/SoundBase.h"
|
||||
#include "ToolMenus.h"
|
||||
#include "Toolkits/AssetEditorToolkit.h"
|
||||
#include "ViewportToolbar/UnrealEdViewportToolbar.h"
|
||||
#include "Widgets/Input/SButton.h"
|
||||
#include "Widgets/Input/SEditableTextBox.h"
|
||||
#include "Widgets/Input/SSlider.h"
|
||||
@@ -118,11 +120,13 @@ namespace
|
||||
public:
|
||||
SLATE_BEGIN_ARGS(SMorphPresetPreviewViewport) {}
|
||||
SLATE_ARGUMENT(TSharedPtr<FMorphPresetEditorController>, Controller)
|
||||
SLATE_ARGUMENT(TWeakPtr<FAssetEditorToolkit>, AssetEditorToolkit)
|
||||
SLATE_END_ARGS()
|
||||
|
||||
void Construct(const FArguments& InArgs)
|
||||
{
|
||||
Controller = InArgs._Controller;
|
||||
AssetEditorToolkit = InArgs._AssetEditorToolkit;
|
||||
SEditorViewport::Construct(SEditorViewport::FArguments());
|
||||
}
|
||||
|
||||
@@ -146,11 +150,45 @@ namespace
|
||||
|
||||
virtual TSharedPtr<SWidget> BuildViewportToolbar() override
|
||||
{
|
||||
return SNullWidget::NullWidget;
|
||||
const FName ViewportToolbarName = TEXT("MorphPresetEditor.ViewportToolbar");
|
||||
if (!UToolMenus::Get()->IsMenuRegistered(ViewportToolbarName))
|
||||
{
|
||||
UToolMenu* const ViewportToolbarMenu = UToolMenus::Get()->RegisterMenu(
|
||||
ViewportToolbarName,
|
||||
NAME_None,
|
||||
EMultiBoxType::SlimHorizontalToolBar);
|
||||
|
||||
ViewportToolbarMenu->StyleName = TEXT("ViewportToolbar");
|
||||
|
||||
FToolMenuSection& LeftSection = ViewportToolbarMenu->AddSection(TEXT("Left"));
|
||||
LeftSection.AddEntry(UE::UnrealEd::CreateTransformsSubmenu());
|
||||
LeftSection.AddEntry(UE::UnrealEd::CreateSnappingSubmenu());
|
||||
|
||||
FToolMenuSection& RightSection = ViewportToolbarMenu->AddSection(TEXT("Right"));
|
||||
RightSection.Alignment = EToolMenuSectionAlign::Last;
|
||||
RightSection.AddEntry(UE::UnrealEd::CreateCameraSubmenu(UE::UnrealEd::FViewportCameraMenuOptions().ShowAll()));
|
||||
RightSection.AddEntry(UE::UnrealEd::CreateViewModesSubmenu());
|
||||
RightSection.AddEntry(UE::UnrealEd::CreatePerformanceAndScalabilitySubmenu());
|
||||
}
|
||||
|
||||
FToolMenuContext ViewportToolbarContext;
|
||||
if (TSharedPtr<IPersonaPreviewScene> PreviewScene = Controller.IsValid() ? Controller->GetPreviewScene() : nullptr)
|
||||
{
|
||||
ViewportToolbarContext.AppendCommandList(PreviewScene->GetCommandList());
|
||||
}
|
||||
ViewportToolbarContext.AppendCommandList(GetCommandList());
|
||||
|
||||
UUnrealEdViewportToolbarContext* const ContextObject =
|
||||
UE::UnrealEd::CreateViewportToolbarDefaultContext(SharedThis(this));
|
||||
ContextObject->AssetEditorToolkit = AssetEditorToolkit;
|
||||
ViewportToolbarContext.AddObject(ContextObject);
|
||||
|
||||
return UToolMenus::Get()->GenerateWidget(ViewportToolbarName, ViewportToolbarContext);
|
||||
}
|
||||
|
||||
private:
|
||||
TSharedPtr<FMorphPresetEditorController> Controller;
|
||||
TWeakPtr<FAssetEditorToolkit> AssetEditorToolkit;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -330,6 +368,7 @@ void SMorphPresetViewportPanel::RebuildContent()
|
||||
[
|
||||
SNew(SMorphPresetPreviewViewport)
|
||||
.Controller(Controller)
|
||||
.AssetEditorToolkit(AssetEditorToolkit)
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ class FUICommandList;
|
||||
class AActor;
|
||||
class IPersonaPreviewScene;
|
||||
class IPersonaToolkit;
|
||||
class UAnimationAsset;
|
||||
class UAnimSequence;
|
||||
class ULipSyncComponent;
|
||||
class ULipSyncMappingDataAsset;
|
||||
@@ -68,9 +67,6 @@ private:
|
||||
void RebuildPreviewAnimation();
|
||||
void ApplyPresetToPreview();
|
||||
void RefreshPreviewAnimationPose();
|
||||
UAnimationAsset* ResolvePreviewAnimationAsset() const;
|
||||
float GetPreviewAnimationPosition() const;
|
||||
bool IsPreviewAnimationCompatible(UAnimationAsset* PreviewAsset) const;
|
||||
void RebuildLipSyncPreviewComponents();
|
||||
void DestroyLipSyncPreviewComponents();
|
||||
void TickLipSyncPreviewComponents(float DeltaTime);
|
||||
@@ -82,6 +78,7 @@ private:
|
||||
TSharedPtr<FUICommandList> CommandList;
|
||||
TStrongObjectPtr<UAnimSequence> PreviewAnimation;
|
||||
TStrongObjectPtr<ULipSyncMappingDataAsset> DefaultLipSyncPreviewMapping;
|
||||
TWeakObjectPtr<USkeletalMesh> ActivePreviewMesh;
|
||||
TWeakObjectPtr<ULipSyncMappingDataAsset> LipSyncPreviewMapping;
|
||||
TWeakObjectPtr<USoundBase> LipSyncPreviewSound;
|
||||
TWeakObjectPtr<AActor> LipSyncPreviewActor;
|
||||
|
||||
Reference in New Issue
Block a user