Compare commits

...

14 Commits

22 changed files with 358 additions and 87 deletions
+96
View File
@@ -795,3 +795,99 @@ class URLSearchParams {
return searchString;
}
}
var __REGEX_SPACE_CHARACTERS = /<%= spaceCharacters %>/g;
var __btoa_TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
function btoa(input) {
input = String(input);
if (/[^\0-\xFF]/.test(input)) {
// Note: no need to special-case astral symbols here, as surrogates are
// matched, and the input is supposed to only contain ASCII anyway.
error(
'The string to be encoded contains characters outside of the ' +
'Latin1 range.'
);
}
var padding = input.length % 3;
var output = '';
var position = -1;
var a;
var b;
var c;
var buffer;
// Make sure any padding is handled outside of the loop.
var length = input.length - padding;
while (++position < length) {
// Read three bytes, i.e. 24 bits.
a = input.charCodeAt(position) << 16;
b = input.charCodeAt(++position) << 8;
c = input.charCodeAt(++position);
buffer = a + b + c;
// Turn the 24 bits into four chunks of 6 bits each, and append the
// matching character for each of them to the output.
output += (
__btoa_TABLE.charAt(buffer >> 18 & 0x3F) +
__btoa_TABLE.charAt(buffer >> 12 & 0x3F) +
__btoa_TABLE.charAt(buffer >> 6 & 0x3F) +
__btoa_TABLE.charAt(buffer & 0x3F)
);
}
if (padding == 2) {
a = input.charCodeAt(position) << 8;
b = input.charCodeAt(++position);
buffer = a + b;
output += (
__btoa_TABLE.charAt(buffer >> 10) +
__btoa_TABLE.charAt((buffer >> 4) & 0x3F) +
__btoa_TABLE.charAt((buffer << 2) & 0x3F) +
'='
);
} else if (padding == 1) {
buffer = input.charCodeAt(position);
output += (
__btoa_TABLE.charAt(buffer >> 2) +
__btoa_TABLE.charAt((buffer << 4) & 0x3F) +
'=='
);
}
return output;
};
function atob(input) {
input = String(input)
.replace(__REGEX_SPACE_CHARACTERS, '');
var length = input.length;
if (length % 4 == 0) {
input = input.replace(/==?$/, '');
length = input.length;
}
if (
length % 4 == 1 ||
// http://whatwg.org/C#alphanumeric-ascii-characters
/[^+a-zA-Z0-9/]/.test(input)
) {
error(
'Invalid character: the string to be decoded is not correctly encoded.'
);
}
var bitCounter = 0;
var bitStorage;
var buffer;
var output = '';
var position = -1;
while (++position < length) {
buffer = __btoa_TABLE.indexOf(input.charAt(position));
bitStorage = bitCounter % 4 ? bitStorage * 64 + buffer : buffer;
// Unless this is the first of a group of 4 characters…
if (bitCounter++ % 4) {
// …convert the first 8 bits to a single ASCII character.
output += String.fromCharCode(
0xFF & bitStorage >> (-2 * bitCounter & 6)
);
}
}
return output;
};
@@ -77,10 +77,14 @@ class AdvancedOrientationListener(private val activity: Activity, private val li
lastStableOrientation = newOrientation
lifecycleScope.launch(Dispatchers.Main) {
delay(stabilityThresholdTime)
if (newOrientation == lastStableOrientation) {
lastOrientation = newOrientation
onOrientationChanged.emit(newOrientation)
try {
delay(stabilityThresholdTime)
if (newOrientation == lastStableOrientation) {
lastOrientation = newOrientation
onOrientationChanged.emit(newOrientation)
}
} catch (e: Throwable) {
Logger.i(TAG, "Failed to trigger onOrientationChanged", e)
}
}
}
@@ -111,4 +115,8 @@ class AdvancedOrientationListener(private val activity: Activity, private val li
fun stopListening() {
sensorManager.unregisterListener(sensorListener)
}
companion object {
private val TAG = "AdvancedOrientationListener"
}
}
@@ -478,7 +478,6 @@ class Settings : FragmentedStorageFileJson() {
var preferWebmAudio: Boolean = false;
@FormField(R.string.allow_under_cutout, FieldForm.TOGGLE, R.string.allow_under_cutout_description, 16)
@FormFieldWarning(R.string.changing_this_field_requires_restart)
var allowVideoToGoUnderCutout: Boolean = true;
}
@@ -488,7 +487,7 @@ class Settings : FragmentedStorageFileJson() {
class CommentSettings {
@FormField(R.string.default_comment_section, FieldForm.DROPDOWN, -1, 0)
@DropdownFieldOptionsId(R.array.comment_sections)
var defaultCommentSection: Int = 0;
var defaultCommentSection: Int = 1;
@FormField(R.string.bad_reputation_comments_fading, FieldForm.TOGGLE, R.string.bad_reputation_comments_fading_description, 0)
var badReputationCommentsFading: Boolean = true;
@@ -5,6 +5,7 @@ import android.content.pm.ActivityInfo
import android.hardware.SensorManager
import android.view.OrientationEventListener
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.logging.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@@ -34,10 +35,14 @@ class SimpleOrientationListener(
lastStableOrientation = newOrientation
lifecycleScope.launch(Dispatchers.Main) {
delay(stabilityThresholdTime)
if (newOrientation == lastStableOrientation) {
lastOrientation = newOrientation
onOrientationChanged.emit(newOrientation)
try {
delay(stabilityThresholdTime)
if (newOrientation == lastStableOrientation) {
lastOrientation = newOrientation
onOrientationChanged.emit(newOrientation)
}
} catch (e: Throwable) {
Logger.i(TAG, "Failed to trigger onOrientationChanged", e)
}
}
}
@@ -52,4 +57,8 @@ class SimpleOrientationListener(
fun stopListening() {
orientationListener.disable()
}
companion object {
private val TAG = "SimpleOrientationListener"
}
}
@@ -147,8 +147,6 @@ fun InputStream.copyToOutputStream(inputStreamLength: Long, outputStream: Output
@Suppress("DEPRECATION")
fun Activity.setNavigationBarColorAndIcons() {
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.black);
if (Settings.instance.playback.allowVideoToGoUnderCutout)
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.insetsController?.setSystemBarsAppearance(0, WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS);
@@ -7,12 +7,14 @@ import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.net.Uri
import android.net.wifi.WifiManager
import android.os.Bundle
import android.os.StrictMode
import android.os.StrictMode.VmPolicy
import android.util.Log
import android.util.TypedValue
import android.view.View
import android.view.WindowManager
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.activity.result.ActivityResult
@@ -252,7 +254,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setNavigationBarColorAndIcons();
if (Settings.instance.playback.allowVideoToGoUnderCutout)
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
runBlocking {
StatePlatform.instance.updateAvailableClients(this@MainActivity);
@@ -514,7 +517,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
}
/*
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
@@ -0,0 +1,78 @@
package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.post.TextType
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullableList
import kotlin.streams.toList
open class JSArticle : JSContent, IPluginSourced {
final override val contentType: ContentType get() = ContentType.POST;
val summary: String;
val thumbnails: Thumbnails?;
val segments: List<IJSArticleSegment>;
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
val contextName = "PlatformPost";
summary = _content.getOrThrow(config, "summary", contextName);
if(_content.has("thumbnails"))
thumbnails = Thumbnails.fromV8(config, _content.getOrThrow(config, "thumbnails", contextName));
else
thumbnails = null;
segments = (obj.getOrThrowNullableList<V8ValueObject>(config, "segments", contextName)
?.map { fromV8Segment(config, it) }
?.filterNotNull() ?: listOf());
}
companion object {
fun fromV8Segment(config: SourcePluginConfig, obj: V8ValueObject): IJSArticleSegment? {
if(!obj.has("type"))
throw IllegalArgumentException("Object missing type field");
return when(obj.getOrThrow<SegmentType>(config, "type", "JSArticle.Segment")) {
SegmentType.TEXT -> JSTextSegment(config, obj);
SegmentType.IMAGES -> JSImagesSegment(config, obj);
else -> null;
}
}
}
}
enum class SegmentType(i: Int) {
UNKNOWN(0),
TEXT(1),
IMAGES(2)
}
interface IJSArticleSegment {
val type: SegmentType;
}
class JSTextSegment: IJSArticleSegment {
override val type = SegmentType.TEXT;
val textType: TextType;
val content: String;
constructor(config: SourcePluginConfig, obj: V8ValueObject) {
val contextName = "JSTextSegment";
textType = TextType.fromInt((obj.getOrDefault<Int>(config, "textType", contextName, null) ?: 0));
content = obj.getOrDefault(config, "content", contextName, "") ?: "";
}
}
class JSImagesSegment: IJSArticleSegment {
override val type = SegmentType.IMAGES;
val images: List<String>;
constructor(config: SourcePluginConfig, obj: V8ValueObject) {
val contextName = "JSTextSegment";
images = obj.getOrThrowNullableList<String>(config, "images", contextName) ?: listOf();
}
}
@@ -68,6 +68,10 @@ class PackageDOMParser : V8Package {
return result;
}
@V8Property
fun parentElement(): DOMNode? {
return parentNode();
}
@V8Property
fun attributes(): Map<String, String> = _element.attributes().associate { Pair(it.key, it.value) }
@V8Property
fun innerHTML(): String = _element.html();
@@ -76,6 +80,8 @@ class PackageDOMParser : V8Package {
@V8Property
fun textContent(): String = _element.text();
@V8Property
fun tagName(): String = _element.tagName().uppercase();
@V8Property
fun text(): String = _element.text().ifEmpty { data() };
@V8Property
fun data(): String = _element.data();
@@ -99,6 +99,8 @@ class PackageHttp: V8Package {
if(body is V8ValueString)
return client.POST(url, body.value, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
else if(body is String)
return client.POST(url, body, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
else if(body is V8ValueTypedArray)
return client.POST(url, body.toBytes(), headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
else if(body is ByteArray)
@@ -2,6 +2,7 @@ package com.futo.platformplayer.fragment.mainactivity.main
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.util.Log
@@ -10,6 +11,7 @@ import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
import android.view.WindowInsetsController
import android.view.WindowManager
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
@@ -426,22 +428,42 @@ class VideoDetailFragment : MainFragment {
onMaximized.clear();
}
private fun hideSystemUI() {
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, false)
activity?.window?.insetsController?.let { controller ->
controller.hide(WindowInsets.Type.statusBars())
controller.hide(WindowInsets.Type.systemBars())
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, false)
activity?.window?.insetsController?.let { controller ->
controller.hide(WindowInsets.Type.statusBars())
controller.hide(WindowInsets.Type.systemBars())
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
} else {
@Suppress("DEPRECATION")
activity?.window?.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
@Suppress("DEPRECATION")
activity?.window?.decorView?.systemUiVisibility = (
View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
)
}
}
private fun showSystemUI() {
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, true)
activity?.window?.insetsController?.let { controller ->
controller.show(WindowInsets.Type.statusBars())
controller.show(WindowInsets.Type.systemBars())
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_DEFAULT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, true)
activity?.window?.insetsController?.let { controller ->
controller.show(WindowInsets.Type.statusBars())
controller.show(WindowInsets.Type.systemBars())
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_DEFAULT
}
} else {
@Suppress("DEPRECATION")
activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
@Suppress("DEPRECATION")
activity?.window?.decorView?.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE
}
}
@@ -18,9 +18,9 @@ class MDNSListener {
}
private val _lockObject = ReentrantLock()
private var _receiver4: DatagramSocket? = null
private var _receiver6: DatagramSocket? = null
private val _senders = mutableListOf<DatagramSocket>()
private var _receiver4: MulticastSocket? = null
private var _receiver6: MulticastSocket? = null
private val _senders = mutableListOf<MulticastSocket>()
private val _nicMonitor = NICMonitor()
private val _serviceRecordAggregator = ServiceRecordAggregator()
private var _started = false
@@ -53,13 +53,13 @@ class MDNSListener {
Logger.i(TAG, "Starting")
_lockObject.withLock {
val receiver4 = DatagramSocket(null).apply {
val receiver4 = MulticastSocket(null).apply {
reuseAddress = true
bind(InetSocketAddress(InetAddress.getByName("0.0.0.0"), MulticastPort))
}
_receiver4 = receiver4
val receiver6 = DatagramSocket(null).apply {
val receiver6 = MulticastSocket(null).apply {
reuseAddress = true
bind(InetSocketAddress(InetAddress.getByName("::"), MulticastPort))
}
@@ -166,6 +166,11 @@ class MDNSListener {
try {
when (address) {
is Inet4Address -> {
_receiver4?.let { receiver4 ->
//receiver4.setOption(StandardSocketOptions.IP_MULTICAST_IF, NetworkInterface.getByInetAddress(address))
receiver4.joinGroup(InetSocketAddress(MulticastAddressIPv4, MulticastPort), NetworkInterface.getByInetAddress(address))
}
val sender = MulticastSocket(null).apply {
reuseAddress = true
bind(InetSocketAddress(address, MulticastPort))
@@ -175,6 +180,11 @@ class MDNSListener {
}
is Inet6Address -> {
_receiver6?.let { receiver6 ->
//receiver6.setOption(StandardSocketOptions.IP_MULTICAST_IF, NetworkInterface.getByInetAddress(address))
receiver6.joinGroup(InetSocketAddress(MulticastAddressIPv6, MulticastPort), NetworkInterface.getByInetAddress(address))
}
val sender = MulticastSocket(null).apply {
reuseAddress = true
bind(InetSocketAddress(address, MulticastPort))
@@ -222,7 +232,7 @@ class MDNSListener {
private fun receiveLoop(client: DatagramSocket) {
Logger.i(TAG, "Started receive loop")
val buffer = ByteArray(1024)
val buffer = ByteArray(8972)
val packet = DatagramPacket(buffer, buffer.size)
while (_started) {
try {
@@ -10,7 +10,6 @@ import com.futo.platformplayer.constructs.Event1
class MediaControlReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val act = intent?.getStringExtra(EXTRA_MEDIA_ACTION);
Logger.i(TAG, "Received MediaControl Event $act");
@@ -55,9 +55,15 @@ class MediaPlaybackService : Service() {
private var _notificationChannel: NotificationChannel? = null;
private var _mediaSession: MediaSessionCompat? = null;
private var _hasFocus: Boolean = false;
private var _isTransientLoss: Boolean = false;
private var _focusRequest: AudioFocusRequest? = null;
private var _audioFocusLossTime_ms: Long? = null
private var _playbackState = PlaybackStateCompat.STATE_NONE;
private var _lastAudioFocusAttempt_ms: Long? = null
private val isPlaying get() = _playbackState != PlaybackStateCompat.STATE_PAUSED &&
_playbackState != PlaybackStateCompat.STATE_STOPPED &&
_playbackState != PlaybackStateCompat.STATE_NONE &&
_playbackState != PlaybackStateCompat.STATE_ERROR
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Logger.v(TAG, "onStartCommand");
@@ -159,12 +165,7 @@ class MediaPlaybackService : Service() {
Logger.v(TAG, "closeMediaSession");
stopForeground(STOP_FOREGROUND_REMOVE);
val focusRequest = _focusRequest;
if (focusRequest != null) {
_audioManager?.abandonAudioFocusRequest(focusRequest);
_focusRequest = null;
}
_hasFocus = false;
abandonAudioFocus()
val notifManager = _notificationManager;
Logger.i(TAG, "Cancelling playback notification (notifManager: ${notifManager != null})");
@@ -335,29 +336,73 @@ class MediaPlaybackService : Service() {
.setState(state, pos, 1f, SystemClock.elapsedRealtime())
.build());
if(_focusRequest == null)
setAudioFocus();
_playbackState = state;
try {
setAudioFocus()
} catch (e: Throwable) {
Logger.e(TAG, "Failed to set audio focus", e)
}
}
//TODO: (TBD) This code probably more fitting inside FutoVideoPlayer, as this service is generally only used for global events
private fun setAudioFocus() {
Log.i(TAG, "Requested audio focus.");
if (!isPlaying) {
return
}
val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAcceptsDelayedFocusGain(true)
.setOnAudioFocusChangeListener(_audioFocusChangeListener)
.build()
if (_hasFocus || _isTransientLoss) {
return;
}
_focusRequest = focusRequest;
val result = _audioManager?.requestAudioFocus(focusRequest)
val now = System.currentTimeMillis()
val lastAudioFocusAttempt_ms = _lastAudioFocusAttempt_ms
if (lastAudioFocusAttempt_ms == null || now - lastAudioFocusAttempt_ms > 1000) {
_lastAudioFocusAttempt_ms = now
} else {
Log.v(TAG, "Skipped trying to get audio focus because gaining audio focus was recently attempted.");
return
}
if (_focusRequest == null) {
val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAcceptsDelayedFocusGain(true)
.setOnAudioFocusChangeListener(_audioFocusChangeListener)
.build()
_focusRequest = focusRequest;
Log.i(TAG, "Created audio focus request.");
}
Log.i(TAG, "Requesting audio focus.");
val result = _audioManager?.requestAudioFocus(_focusRequest!!)
Log.i(TAG, "Audio focus request result $result");
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
//TODO: Handle when not possible to get audio focus
_hasFocus = true;
_hasFocus = true
_isTransientLoss = false
Log.i(TAG, "Audio focus received");
} else if (result == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
_hasFocus = false
_isTransientLoss = false
Log.i(TAG, "Audio focus delayed, waiting for focus")
} else {
_hasFocus = false
_isTransientLoss = false
Log.i(TAG, "Audio focus not granted, retrying later")
}
Log.i(TAG, "Audio focus requested.");
}
private fun abandonAudioFocus() {
val focusRequest = _focusRequest;
if (focusRequest != null) {
Logger.i(TAG, "Audio focus abandoned")
_audioManager?.abandonAudioFocusRequest(focusRequest);
_focusRequest = null;
}
_hasFocus = false;
_isTransientLoss = false;
}
private val _audioFocusChangeListener =
@@ -365,9 +410,8 @@ class MediaPlaybackService : Service() {
try {
when (focusChange) {
AudioManager.AUDIOFOCUS_GAIN -> {
//Do not start playing on gaining audo focus
//MediaControlReceiver.onPlayReceived.emit();
_hasFocus = true;
_isTransientLoss = false;
Log.i(TAG, "Audio focus gained (restartPlaybackAfterLoss = ${Settings.instance.playback.restartPlaybackAfterLoss}, _audioFocusLossTime_ms = $_audioFocusLossTime_ms)");
if (Settings.instance.playback.restartPlaybackAfterLoss == 1) {
@@ -385,40 +429,28 @@ class MediaPlaybackService : Service() {
}
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
MediaControlReceiver.onPauseReceived.emit();
if (_playbackState != PlaybackStateCompat.STATE_PAUSED &&
_playbackState != PlaybackStateCompat.STATE_STOPPED &&
_playbackState != PlaybackStateCompat.STATE_NONE &&
_playbackState != PlaybackStateCompat.STATE_ERROR) {
_audioFocusLossTime_ms = System.currentTimeMillis()
}
Log.i(TAG, "Audio focus transient loss");
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
Log.i(TAG, "Audio focus transient loss, can duck");
}
AudioManager.AUDIOFOCUS_LOSS -> {
if (_playbackState != PlaybackStateCompat.STATE_PAUSED &&
_playbackState != PlaybackStateCompat.STATE_STOPPED &&
_playbackState != PlaybackStateCompat.STATE_NONE &&
_playbackState != PlaybackStateCompat.STATE_ERROR) {
if (isPlaying) {
_audioFocusLossTime_ms = System.currentTimeMillis()
}
_hasFocus = false;
_isTransientLoss = true;
MediaControlReceiver.onPauseReceived.emit();
Log.i(TAG, "Audio focus lost");
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val runningAppProcesses = activityManager.runningAppProcesses
for (processInfo in runningAppProcesses) {
// Check the importance of the running app process
if (processInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
// This app is in the foreground, which might have caused the loss of audio focus
Log.i("AudioFocus", "App ${processInfo.processName} might have caused the loss of audio focus")
}
Log.i(TAG, "Audio focus transient loss (_audioFocusLossTime_ms = ${_audioFocusLossTime_ms})");
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
Log.i(TAG, "Audio focus transient loss, can duck");
_hasFocus = true;
_isTransientLoss = true;
}
AudioManager.AUDIOFOCUS_LOSS -> {
if (isPlaying) {
_audioFocusLossTime_ms = System.currentTimeMillis()
}
MediaControlReceiver.onPauseReceived.emit();
abandonAudioFocus();
Log.i(TAG, "Audio focus lost");
}
}
} catch(ex: Throwable) {
@@ -13,6 +13,10 @@ annotation class FormField(val title: Int, val type: String, val subtitle: Int =
@Retention(AnnotationRetention.RUNTIME)
annotation class FormFieldWarning(val messageRes: Int)
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class FormFieldHint(val messageRes: Int)
interface IField {
var descriptor: FormField?;
val obj : Any?;
@@ -293,6 +293,12 @@ class FieldForm : LinearLayout {
}, UIDialogs.ActionStyle.PRIMARY));
}
}
val hint = propertyMap[field.field]?.findAnnotation<FormFieldHint>();
if(hint != null){
field.onChanged.subscribe { f, value, oldValue ->
UIDialogs.appToast(context.getString(hint.messageRes), false);
}
}
}
}
+1 -1
View File
@@ -378,7 +378,7 @@
<string name="prefer_webm_audio">Prefer Webm Audio Codecs</string>
<string name="prefer_webm_audio_description">If player should prefer Webm codecs (opus) over mp4 codecs (AAC), may result in worse compatibility.</string>
<string name="allow_under_cutout">Allow video under cutout</string>
<string name="allow_under_cutout_description">Allow video to go underneath the screen cutout in full-screen.</string>
<string name="allow_under_cutout_description">Allow video to go underneath the screen cutout in full-screen.\nMay require restart</string>
<string name="allow_full_screen_portrait">Allow fullscreen portrait</string>
<string name="background_switch_audio">Switch to Audio in Background</string>
<string name="background_switch_audio_description">Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter</string>