Refactoring

This commit is contained in:
Ivan Anikin
2026-05-20 15:33:00 +07:00
parent 8732ce99e3
commit 86baa476f4
41 changed files with 287 additions and 1513 deletions
@@ -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"
}
]
}
@@ -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>
@@ -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"));
}
}
}
@@ -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();
}
@@ -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;
}
}
@@ -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;
};
@@ -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;
}
@@ -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
@@ -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();
};