mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-29 11:03:01 +02:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 249e77a5d3 | |||
| 3cf4a52a69 | |||
| eb8b02756b | |||
| 0510d34ed3 | |||
| 1c8d12e72a | |||
| 0a36a6b674 | |||
| b887c9d50f | |||
| ee4e108e4f | |||
| 5e14a0fed4 | |||
| 6045205ea9 | |||
| f2d763cdec | |||
| e5e348205a |
@@ -795,3 +795,99 @@ class URLSearchParams {
|
|||||||
return searchString;
|
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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -478,7 +478,6 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
var preferWebmAudio: Boolean = false;
|
var preferWebmAudio: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.allow_under_cutout, FieldForm.TOGGLE, R.string.allow_under_cutout_description, 16)
|
@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;
|
var allowVideoToGoUnderCutout: Boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
|||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.net.wifi.WifiManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
import android.os.StrictMode.VmPolicy
|
import android.os.StrictMode.VmPolicy
|
||||||
@@ -253,7 +254,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
setContentView(R.layout.activity_main);
|
setContentView(R.layout.activity_main);
|
||||||
setNavigationBarColorAndIcons();
|
setNavigationBarColorAndIcons();
|
||||||
|
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
StatePlatform.instance.updateAvailableClients(this@MainActivity);
|
StatePlatform.instance.updateAvailableClients(this@MainActivity);
|
||||||
}
|
}
|
||||||
@@ -514,7 +514,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
@V8Property
|
@V8Property
|
||||||
|
fun parentElement(): DOMNode? {
|
||||||
|
return parentNode();
|
||||||
|
}
|
||||||
|
@V8Property
|
||||||
fun attributes(): Map<String, String> = _element.attributes().associate { Pair(it.key, it.value) }
|
fun attributes(): Map<String, String> = _element.attributes().associate { Pair(it.key, it.value) }
|
||||||
@V8Property
|
@V8Property
|
||||||
fun innerHTML(): String = _element.html();
|
fun innerHTML(): String = _element.html();
|
||||||
@@ -76,6 +80,8 @@ class PackageDOMParser : V8Package {
|
|||||||
@V8Property
|
@V8Property
|
||||||
fun textContent(): String = _element.text();
|
fun textContent(): String = _element.text();
|
||||||
@V8Property
|
@V8Property
|
||||||
|
fun tagName(): String = _element.tagName().uppercase();
|
||||||
|
@V8Property
|
||||||
fun text(): String = _element.text().ifEmpty { data() };
|
fun text(): String = _element.text().ifEmpty { data() };
|
||||||
@V8Property
|
@V8Property
|
||||||
fun data(): String = _element.data();
|
fun data(): String = _element.data();
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ class PackageHttp: V8Package {
|
|||||||
|
|
||||||
if(body is V8ValueString)
|
if(body is V8ValueString)
|
||||||
return client.POST(url, body.value, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
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)
|
else if(body is V8ValueTypedArray)
|
||||||
return client.POST(url, body.toBytes(), headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
return client.POST(url, body.toBytes(), headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||||
else if(body is ByteArray)
|
else if(body is ByteArray)
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ class MDNSListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val _lockObject = ReentrantLock()
|
private val _lockObject = ReentrantLock()
|
||||||
private var _receiver4: DatagramSocket? = null
|
private var _receiver4: MulticastSocket? = null
|
||||||
private var _receiver6: DatagramSocket? = null
|
private var _receiver6: MulticastSocket? = null
|
||||||
private val _senders = mutableListOf<DatagramSocket>()
|
private val _senders = mutableListOf<MulticastSocket>()
|
||||||
private val _nicMonitor = NICMonitor()
|
private val _nicMonitor = NICMonitor()
|
||||||
private val _serviceRecordAggregator = ServiceRecordAggregator()
|
private val _serviceRecordAggregator = ServiceRecordAggregator()
|
||||||
private var _started = false
|
private var _started = false
|
||||||
@@ -53,13 +53,13 @@ class MDNSListener {
|
|||||||
|
|
||||||
Logger.i(TAG, "Starting")
|
Logger.i(TAG, "Starting")
|
||||||
_lockObject.withLock {
|
_lockObject.withLock {
|
||||||
val receiver4 = DatagramSocket(null).apply {
|
val receiver4 = MulticastSocket(null).apply {
|
||||||
reuseAddress = true
|
reuseAddress = true
|
||||||
bind(InetSocketAddress(InetAddress.getByName("0.0.0.0"), MulticastPort))
|
bind(InetSocketAddress(InetAddress.getByName("0.0.0.0"), MulticastPort))
|
||||||
}
|
}
|
||||||
_receiver4 = receiver4
|
_receiver4 = receiver4
|
||||||
|
|
||||||
val receiver6 = DatagramSocket(null).apply {
|
val receiver6 = MulticastSocket(null).apply {
|
||||||
reuseAddress = true
|
reuseAddress = true
|
||||||
bind(InetSocketAddress(InetAddress.getByName("::"), MulticastPort))
|
bind(InetSocketAddress(InetAddress.getByName("::"), MulticastPort))
|
||||||
}
|
}
|
||||||
@@ -166,6 +166,11 @@ class MDNSListener {
|
|||||||
try {
|
try {
|
||||||
when (address) {
|
when (address) {
|
||||||
is Inet4Address -> {
|
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 {
|
val sender = MulticastSocket(null).apply {
|
||||||
reuseAddress = true
|
reuseAddress = true
|
||||||
bind(InetSocketAddress(address, MulticastPort))
|
bind(InetSocketAddress(address, MulticastPort))
|
||||||
@@ -175,6 +180,11 @@ class MDNSListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
is Inet6Address -> {
|
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 {
|
val sender = MulticastSocket(null).apply {
|
||||||
reuseAddress = true
|
reuseAddress = true
|
||||||
bind(InetSocketAddress(address, MulticastPort))
|
bind(InetSocketAddress(address, MulticastPort))
|
||||||
@@ -222,7 +232,7 @@ class MDNSListener {
|
|||||||
private fun receiveLoop(client: DatagramSocket) {
|
private fun receiveLoop(client: DatagramSocket) {
|
||||||
Logger.i(TAG, "Started receive loop")
|
Logger.i(TAG, "Started receive loop")
|
||||||
|
|
||||||
val buffer = ByteArray(1024)
|
val buffer = ByteArray(8972)
|
||||||
val packet = DatagramPacket(buffer, buffer.size)
|
val packet = DatagramPacket(buffer, buffer.size)
|
||||||
while (_started) {
|
while (_started) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import com.futo.platformplayer.constructs.Event1
|
|||||||
|
|
||||||
|
|
||||||
class MediaControlReceiver : BroadcastReceiver() {
|
class MediaControlReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
val act = intent?.getStringExtra(EXTRA_MEDIA_ACTION);
|
val act = intent?.getStringExtra(EXTRA_MEDIA_ACTION);
|
||||||
Logger.i(TAG, "Received MediaControl Event $act");
|
Logger.i(TAG, "Received MediaControl Event $act");
|
||||||
|
|||||||
@@ -55,9 +55,15 @@ class MediaPlaybackService : Service() {
|
|||||||
private var _notificationChannel: NotificationChannel? = null;
|
private var _notificationChannel: NotificationChannel? = null;
|
||||||
private var _mediaSession: MediaSessionCompat? = null;
|
private var _mediaSession: MediaSessionCompat? = null;
|
||||||
private var _hasFocus: Boolean = false;
|
private var _hasFocus: Boolean = false;
|
||||||
|
private var _isTransientLoss: Boolean = false;
|
||||||
private var _focusRequest: AudioFocusRequest? = null;
|
private var _focusRequest: AudioFocusRequest? = null;
|
||||||
private var _audioFocusLossTime_ms: Long? = null
|
private var _audioFocusLossTime_ms: Long? = null
|
||||||
private var _playbackState = PlaybackStateCompat.STATE_NONE;
|
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 {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
Logger.v(TAG, "onStartCommand");
|
Logger.v(TAG, "onStartCommand");
|
||||||
@@ -159,12 +165,7 @@ class MediaPlaybackService : Service() {
|
|||||||
Logger.v(TAG, "closeMediaSession");
|
Logger.v(TAG, "closeMediaSession");
|
||||||
stopForeground(STOP_FOREGROUND_REMOVE);
|
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||||
|
|
||||||
val focusRequest = _focusRequest;
|
abandonAudioFocus()
|
||||||
if (focusRequest != null) {
|
|
||||||
_audioManager?.abandonAudioFocusRequest(focusRequest);
|
|
||||||
_focusRequest = null;
|
|
||||||
}
|
|
||||||
_hasFocus = false;
|
|
||||||
|
|
||||||
val notifManager = _notificationManager;
|
val notifManager = _notificationManager;
|
||||||
Logger.i(TAG, "Cancelling playback notification (notifManager: ${notifManager != null})");
|
Logger.i(TAG, "Cancelling playback notification (notifManager: ${notifManager != null})");
|
||||||
@@ -335,29 +336,72 @@ class MediaPlaybackService : Service() {
|
|||||||
.setState(state, pos, 1f, SystemClock.elapsedRealtime())
|
.setState(state, pos, 1f, SystemClock.elapsedRealtime())
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
if(_focusRequest == null)
|
|
||||||
setAudioFocus();
|
|
||||||
|
|
||||||
_playbackState = state;
|
_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
|
//TODO: (TBD) This code probably more fitting inside FutoVideoPlayer, as this service is generally only used for global events
|
||||||
private fun setAudioFocus() {
|
private fun setAudioFocus() {
|
||||||
Log.i(TAG, "Requested audio focus.");
|
if (!isPlaying) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
|
if (_hasFocus || _isTransientLoss) {
|
||||||
.setAcceptsDelayedFocusGain(true)
|
return;
|
||||||
.setOnAudioFocusChangeListener(_audioFocusChangeListener)
|
}
|
||||||
.build()
|
|
||||||
|
|
||||||
_focusRequest = focusRequest;
|
val now = System.currentTimeMillis()
|
||||||
val result = _audioManager?.requestAudioFocus(focusRequest)
|
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");
|
Log.i(TAG, "Audio focus request result $result");
|
||||||
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
||||||
//TODO: Handle when not possible to get audio focus
|
_hasFocus = true
|
||||||
_hasFocus = true;
|
|
||||||
Log.i(TAG, "Audio focus received");
|
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 =
|
private val _audioFocusChangeListener =
|
||||||
@@ -365,9 +409,8 @@ class MediaPlaybackService : Service() {
|
|||||||
try {
|
try {
|
||||||
when (focusChange) {
|
when (focusChange) {
|
||||||
AudioManager.AUDIOFOCUS_GAIN -> {
|
AudioManager.AUDIOFOCUS_GAIN -> {
|
||||||
//Do not start playing on gaining audo focus
|
|
||||||
//MediaControlReceiver.onPlayReceived.emit();
|
|
||||||
_hasFocus = true;
|
_hasFocus = true;
|
||||||
|
_isTransientLoss = false;
|
||||||
Log.i(TAG, "Audio focus gained (restartPlaybackAfterLoss = ${Settings.instance.playback.restartPlaybackAfterLoss}, _audioFocusLossTime_ms = $_audioFocusLossTime_ms)");
|
Log.i(TAG, "Audio focus gained (restartPlaybackAfterLoss = ${Settings.instance.playback.restartPlaybackAfterLoss}, _audioFocusLossTime_ms = $_audioFocusLossTime_ms)");
|
||||||
|
|
||||||
if (Settings.instance.playback.restartPlaybackAfterLoss == 1) {
|
if (Settings.instance.playback.restartPlaybackAfterLoss == 1) {
|
||||||
@@ -385,40 +428,28 @@ class MediaPlaybackService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
||||||
MediaControlReceiver.onPauseReceived.emit();
|
if (isPlaying) {
|
||||||
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) {
|
|
||||||
_audioFocusLossTime_ms = System.currentTimeMillis()
|
_audioFocusLossTime_ms = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
_hasFocus = false;
|
_hasFocus = false;
|
||||||
|
_isTransientLoss = true;
|
||||||
MediaControlReceiver.onPauseReceived.emit();
|
MediaControlReceiver.onPauseReceived.emit();
|
||||||
Log.i(TAG, "Audio focus lost");
|
Log.i(TAG, "Audio focus transient loss (_audioFocusLossTime_ms = ${_audioFocusLossTime_ms})");
|
||||||
|
}
|
||||||
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
||||||
val runningAppProcesses = activityManager.runningAppProcesses
|
Log.i(TAG, "Audio focus transient loss, can duck");
|
||||||
for (processInfo in runningAppProcesses) {
|
_hasFocus = true;
|
||||||
// Check the importance of the running app process
|
_isTransientLoss = true;
|
||||||
if (processInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
|
}
|
||||||
// This app is in the foreground, which might have caused the loss of audio focus
|
AudioManager.AUDIOFOCUS_LOSS -> {
|
||||||
Log.i("AudioFocus", "App ${processInfo.processName} might have caused the loss of audio focus")
|
if (isPlaying) {
|
||||||
}
|
_audioFocusLossTime_ms = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MediaControlReceiver.onPauseReceived.emit();
|
||||||
|
abandonAudioFocus();
|
||||||
|
Log.i(TAG, "Audio focus lost");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch(ex: Throwable) {
|
} catch(ex: Throwable) {
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ annotation class FormField(val title: Int, val type: String, val subtitle: Int =
|
|||||||
@Retention(AnnotationRetention.RUNTIME)
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
annotation class FormFieldWarning(val messageRes: Int)
|
annotation class FormFieldWarning(val messageRes: Int)
|
||||||
|
|
||||||
|
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
annotation class FormFieldHint(val messageRes: Int)
|
||||||
|
|
||||||
interface IField {
|
interface IField {
|
||||||
var descriptor: FormField?;
|
var descriptor: FormField?;
|
||||||
val obj : Any?;
|
val obj : Any?;
|
||||||
|
|||||||
@@ -293,6 +293,12 @@ class FieldForm : LinearLayout {
|
|||||||
}, UIDialogs.ActionStyle.PRIMARY));
|
}, 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -378,7 +378,7 @@
|
|||||||
<string name="prefer_webm_audio">Prefer Webm Audio Codecs</string>
|
<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="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">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="allow_full_screen_portrait">Allow fullscreen portrait</string>
|
||||||
<string name="background_switch_audio">Switch to Audio in Background</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>
|
<string name="background_switch_audio_description">Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter</string>
|
||||||
|
|||||||
Submodule app/src/stable/assets/sources/bilibili updated: 850acb49a8...31490e10f9
Submodule app/src/stable/assets/sources/rumble updated: bedbc4a989...cbfe372bcc
Submodule app/src/stable/assets/sources/youtube updated: 6f3352a276...a4766ec223
Submodule app/src/unstable/assets/sources/bilibili updated: 850acb49a8...31490e10f9
Submodule app/src/unstable/assets/sources/rumble updated: bedbc4a989...cbfe372bcc
Submodule app/src/unstable/assets/sources/youtube updated: 6f3352a276...a4766ec223
Reference in New Issue
Block a user