Add Games File
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"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
@@ -0,0 +1,51 @@
|
||||
<?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
@@ -0,0 +1,274 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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
@@ -0,0 +1,207 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#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
@@ -0,0 +1,365 @@
|
||||
#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
@@ -0,0 +1,78 @@
|
||||
#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
@@ -0,0 +1,74 @@
|
||||
#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
@@ -0,0 +1,115 @@
|
||||
#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
@@ -0,0 +1,54 @@
|
||||
#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();
|
||||
};
|
||||
Reference in New Issue
Block a user