Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay

This commit is contained in:
Kelvin 2024-01-09 22:25:49 +01:00
commit 9306024d17
11 changed files with 251 additions and 90 deletions

View file

@ -1,5 +1,6 @@
package com.futo.platformplayer package com.futo.platformplayer
import android.util.Log
import com.google.common.base.CharMatcher import com.google.common.base.CharMatcher
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
@ -215,17 +216,20 @@ private fun ByteArray.toInetAddress(): InetAddress {
} }
fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? { fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
val timeout = 5000 val timeout = 2000
if (addresses.isEmpty()) { if (addresses.isEmpty()) {
return null; return null;
} }
if (addresses.size == 1) { if (addresses.size == 1) {
val socket = Socket()
try { try {
return Socket().apply { this.connect(InetSocketAddress(addresses[0], port), timeout) }; return socket.apply { this.connect(InetSocketAddress(addresses[0], port), timeout) }
} catch (e: Throwable) { } catch (e: Throwable) {
//Ignored. Log.i("getConnectedSocket", "Failed to connect to: ${addresses[0]}", e)
socket.close()
} }
return null; return null;
@ -264,7 +268,7 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
} }
} }
} catch (e: Throwable) { } catch (e: Throwable) {
//Ignore Log.i("getConnectedSocket", "Failed to connect to: $address", e)
} }
}; };

View file

@ -809,7 +809,27 @@ class Settings : FragmentedStorageFileJson() {
var polycentricEnabled: Boolean = true; var polycentricEnabled: Boolean = true;
} }
@FormField(R.string.info, FieldForm.GROUP, -1, 19) @FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
var gestureControls = GestureControls();
@Serializable
class GestureControls {
@FormField(R.string.volume_slider, FieldForm.TOGGLE, R.string.volume_slider_descr, 1)
var volumeSlider: Boolean = true;
@FormField(R.string.brightness_slider, FieldForm.TOGGLE, R.string.brightness_slider_descr, 2)
var brightnessSlider: Boolean = true;
@FormField(R.string.toggle_full_screen, FieldForm.TOGGLE, R.string.toggle_full_screen_descr, 3)
var toggleFullscreen: Boolean = true;
@FormField(R.string.system_brightness, FieldForm.TOGGLE, R.string.system_brightness_descr, 4)
var useSystemBrightness: Boolean = true;
@FormField(R.string.system_volume, FieldForm.TOGGLE, R.string.system_volume_descr, 5)
var useSystemVolume: Boolean = true;
}
@FormField(R.string.info, FieldForm.GROUP, -1, 20)
var info = Info(); var info = Info();
@Serializable @Serializable
class Info { class Info {

View file

@ -328,6 +328,8 @@ class ChromecastCastingDevice : CastingDevice {
val factory = sslContext.socketFactory; val factory = sslContext.socketFactory;
val address = InetSocketAddress(usedRemoteAddress, port)
//Connection loop //Connection loop
while (_scopeIO?.isActive == true) { while (_scopeIO?.isActive == true) {
Logger.i(TAG, "Connecting to Chromecast."); Logger.i(TAG, "Connecting to Chromecast.");
@ -341,7 +343,7 @@ class ChromecastCastingDevice : CastingDevice {
connectedSocket = null connectedSocket = null
} else { } else {
Logger.i(TAG, "Using new socket.") Logger.i(TAG, "Using new socket.")
val s = Socket().apply { this.connect(InetSocketAddress(usedRemoteAddress, port), 5000) } val s = Socket().apply { this.connect(address, 2000) }
_socket = factory.createSocket(s, s.inetAddress.hostAddress, s.port, true) as SSLSocket _socket = factory.createSocket(s, s.inetAddress.hostAddress, s.port, true) as SSLSocket
} }
@ -444,10 +446,11 @@ class ChromecastCastingDevice : CastingDevice {
while (_scopeIO?.isActive == true) { while (_scopeIO?.isActive == true) {
try { try {
sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.tp.heartbeat", pingObject.toString()); sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.tp.heartbeat", pingObject.toString());
Thread.sleep(5000);
} catch (e: Throwable) { } catch (e: Throwable) {
Log.w(TAG, "Failed to send ping."); Log.w(TAG, "Failed to send ping.");
} }
Thread.sleep(5000);
} }
Logger.i(TAG, "Stopped ping loop."); Logger.i(TAG, "Stopped ping loop.");

View file

@ -15,6 +15,7 @@ import com.futo.platformplayer.casting.models.FCastSetSpeedMessage
import com.futo.platformplayer.casting.models.FCastSetVolumeMessage import com.futo.platformplayer.casting.models.FCastSetVolumeMessage
import com.futo.platformplayer.casting.models.FCastVersionMessage import com.futo.platformplayer.casting.models.FCastVersionMessage
import com.futo.platformplayer.casting.models.FCastVolumeUpdateMessage import com.futo.platformplayer.casting.models.FCastVolumeUpdateMessage
import com.futo.platformplayer.ensureNotMainThread
import com.futo.platformplayer.getConnectedSocket import com.futo.platformplayer.getConnectedSocket
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.CastingDeviceInfo import com.futo.platformplayer.models.CastingDeviceInfo
@ -27,9 +28,9 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.math.BigInteger import java.math.BigInteger
import java.net.InetAddress import java.net.InetAddress
import java.net.InetSocketAddress import java.net.InetSocketAddress
@ -82,12 +83,15 @@ class FCastCastingDevice : CastingDevice {
var port: Int = 0; var port: Int = 0;
private var _socket: Socket? = null; private var _socket: Socket? = null;
private var _outputStream: DataOutputStream? = null; private var _outputStream: OutputStream? = null;
private var _inputStream: DataInputStream? = null; private var _inputStream: InputStream? = null;
private var _scopeIO: CoroutineScope? = null; private var _scopeIO: CoroutineScope? = null;
private var _started: Boolean = false; private var _started: Boolean = false;
private var _version: Long = 1; private var _version: Long = 1;
private var _thread: Thread? = null private var _thread: Thread? = null
private var _pingThread: Thread? = null
private var _lastPongTime = -1L
private var _outputStreamLock = Object()
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() { constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
this.name = name; this.name = name;
@ -207,7 +211,13 @@ class FCastCastingDevice : CastingDevice {
private fun invokeInIOScopeIfRequired(action: () -> Unit): Boolean { private fun invokeInIOScopeIfRequired(action: () -> Unit): Boolean {
if(Looper.getMainLooper().thread == Thread.currentThread()) { if(Looper.getMainLooper().thread == Thread.currentThread()) {
_scopeIO?.launch { action(); } _scopeIO?.launch {
try {
action();
} catch (e: Throwable) {
Logger.e(TAG, "Failed to invoke in IO scope.", e)
}
}
return true; return true;
} }
@ -241,7 +251,8 @@ class FCastCastingDevice : CastingDevice {
val adrs = addresses ?: return; val adrs = addresses ?: return;
val thread = _thread val thread = _thread
if (thread == null || !thread.isAlive) { val pingThread = _pingThread
if (_started && (thread == null || !thread.isAlive || pingThread == null || !pingThread.isAlive)) {
Log.i(TAG, "(Re)starting thread because the thread has died") Log.i(TAG, "(Re)starting thread because the thread has died")
_scopeIO?.let { _scopeIO?.let {
@ -258,7 +269,7 @@ class FCastCastingDevice : CastingDevice {
var connectedSocket: Socket? = null var connectedSocket: Socket? = null
while (_scopeIO?.isActive == true) { while (_scopeIO?.isActive == true) {
try { try {
Log.i(TAG, "getConnectedSocket.") Log.i(TAG, "getConnectedSocket (adrs = [ ${adrs.joinToString(", ")} ], port = ${port}).")
val resultSocket = getConnectedSocket(adrs.toList(), port); val resultSocket = getConnectedSocket(adrs.toList(), port);
@ -279,6 +290,8 @@ class FCastCastingDevice : CastingDevice {
} }
} }
val address = InetSocketAddress(usedRemoteAddress, port)
//Connection loop //Connection loop
while (_scopeIO?.isActive == true) { while (_scopeIO?.isActive == true) {
Logger.i(TAG, "Connecting to FastCast."); Logger.i(TAG, "Connecting to FastCast.");
@ -286,20 +299,24 @@ class FCastCastingDevice : CastingDevice {
try { try {
_socket?.close() _socket?.close()
_inputStream?.close()
_outputStream?.close()
if (connectedSocket != null) { if (connectedSocket != null) {
Logger.i(TAG, "Using connected socket."); Logger.i(TAG, "Using connected socket.");
_socket = connectedSocket _socket = connectedSocket
connectedSocket = null connectedSocket = null
} else { } else {
Logger.i(TAG, "Using new socket."); Logger.i(TAG, "Using new socket.");
_socket = Socket().apply { this.connect(InetSocketAddress(usedRemoteAddress, port), 5000) }; _socket = Socket().apply { this.connect(address, 2000) };
} }
Logger.i(TAG, "Successfully connected to FastCast at $usedRemoteAddress:$port"); Logger.i(TAG, "Successfully connected to FastCast at $usedRemoteAddress:$port");
_outputStream = DataOutputStream(_socket?.outputStream); _outputStream = _socket?.outputStream;
_inputStream = DataInputStream(_socket?.inputStream); _inputStream = _socket?.inputStream;
} catch (e: IOException) { } catch (e: IOException) {
_socket?.close(); _socket?.close()
_inputStream?.close()
_outputStream?.close()
Logger.i(TAG, "Failed to connect to FastCast.", e); Logger.i(TAG, "Failed to connect to FastCast.", e);
connectionState = CastConnectionState.CONNECTING; connectionState = CastConnectionState.CONNECTING;
@ -309,28 +326,38 @@ class FCastCastingDevice : CastingDevice {
localAddress = _socket?.localAddress; localAddress = _socket?.localAddress;
connectionState = CastConnectionState.CONNECTED; connectionState = CastConnectionState.CONNECTED;
_lastPongTime = -1L
val buffer = ByteArray(4096); val buffer = ByteArray(4096);
Logger.i(TAG, "Started receiving."); Logger.i(TAG, "Started receiving.");
var exceptionOccurred = false; while (_scopeIO?.isActive == true) {
while (_scopeIO?.isActive == true && !exceptionOccurred) {
try { try {
val inputStream = _inputStream ?: break; val inputStream = _inputStream ?: break;
Log.d(TAG, "Receiving next packet..."); Log.d(TAG, "Receiving next packet...");
val b1 = inputStream.readUnsignedByte();
val b2 = inputStream.readUnsignedByte(); var headerBytesRead = 0
val b3 = inputStream.readUnsignedByte(); while (headerBytesRead < 4) {
val b4 = inputStream.readUnsignedByte(); val read = inputStream.read(buffer, headerBytesRead, 4 - headerBytesRead)
val size = ((b4.toLong() shl 24) or (b3.toLong() shl 16) or (b2.toLong() shl 8) or b1.toLong()).toInt(); if (read == -1)
throw Exception("Stream closed")
headerBytesRead += read
}
val size = ((buffer[3].toLong() shl 24) or (buffer[2].toLong() shl 16) or (buffer[1].toLong() shl 8) or buffer[0].toLong()).toInt();
if (size > buffer.size) { if (size > buffer.size) {
Logger.w(TAG, "Skipping packet that is too large $size bytes.") Logger.w(TAG, "Packets larger than $size bytes are not supported.")
inputStream.skip(size.toLong()); break
continue;
} }
Log.d(TAG, "Received header indicating $size bytes. Waiting for message."); Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
inputStream.read(buffer, 0, size); var bytesRead = 0
while (bytesRead < size) {
val read = inputStream.read(buffer, bytesRead, size - bytesRead)
if (read == -1)
throw Exception("Stream closed")
bytesRead += read
}
val messageBytes = buffer.sliceArray(IntRange(0, size)); val messageBytes = buffer.sliceArray(IntRange(0, size));
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}."); Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
@ -344,19 +371,22 @@ class FCastCastingDevice : CastingDevice {
try { try {
handleMessage(Opcode.find(opcode), json); handleMessage(Opcode.find(opcode), json);
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.w(TAG, "Failed to handle message.", e); Logger.w(TAG, "Failed to handle message.", e)
break
} }
} catch (e: java.net.SocketException) { } catch (e: java.net.SocketException) {
Logger.e(TAG, "Socket exception while receiving.", e); Logger.e(TAG, "Socket exception while receiving.", e);
exceptionOccurred = true; break
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Exception while receiving.", e); Logger.e(TAG, "Exception while receiving.", e);
exceptionOccurred = true; break
} }
} }
try { try {
_socket?.close(); _socket?.close()
_inputStream?.close()
_outputStream?.close()
Logger.i(TAG, "Socket disconnected."); Logger.i(TAG, "Socket disconnected.");
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to close socket.", e) Logger.e(TAG, "Failed to close socket.", e)
@ -368,7 +398,41 @@ class FCastCastingDevice : CastingDevice {
Logger.i(TAG, "Stopped connection loop."); Logger.i(TAG, "Stopped connection loop.");
connectionState = CastConnectionState.DISCONNECTED; connectionState = CastConnectionState.DISCONNECTED;
}.apply { start() }; }.apply { start() }
_pingThread = Thread {
Logger.i(TAG, "Started ping loop.")
while (_scopeIO?.isActive == true) {
try {
send(Opcode.Ping)
} catch (e: Throwable) {
Log.w(TAG, "Failed to send ping.")
try {
_socket?.close()
_inputStream?.close()
_outputStream?.close()
} catch (e: Throwable) {
Log.w(TAG, "Failed to close socket.", e)
}
}
/*if (_lastPongTime != -1L && System.currentTimeMillis() - _lastPongTime > 6000) {
Logger.w(TAG, "Closing socket due to last pong time being larger than 6 seconds.")
try {
_socket?.close()
} catch (e: Throwable) {
Log.w(TAG, "Failed to close socket.", e)
}
}*/
Thread.sleep(2000)
}
Logger.i(TAG, "Stopped ping loop.");
}.apply { start() }
} else { } else {
Log.i(TAG, "Thread was still alive, not restarted") Log.i(TAG, "Thread was still alive, not restarted")
} }
@ -421,11 +485,15 @@ class FCastCastingDevice : CastingDevice {
Logger.i(TAG, "Remote version received: $version") Logger.i(TAG, "Remote version received: $version")
} }
Opcode.Ping -> send(Opcode.Pong) Opcode.Ping -> send(Opcode.Pong)
Opcode.Pong -> _lastPongTime = System.currentTimeMillis()
else -> { } else -> { }
} }
} }
private fun send(opcode: Opcode, message: String? = null) { private fun send(opcode: Opcode, message: String? = null) {
ensureNotMainThread()
synchronized (_outputStreamLock) {
try { try {
val data: ByteArray = message?.encodeToByteArray() ?: ByteArray(0) val data: ByteArray = message?.encodeToByteArray() ?: ByteArray(0)
val size = 1 + data.size val size = 1 + data.size
@ -456,6 +524,7 @@ class FCastCastingDevice : CastingDevice {
throw e throw e
} }
} }
}
private inline fun <reified T> send(opcode: Opcode, message: T) { private inline fun <reified T> send(opcode: Opcode, message: T) {
try { try {
@ -473,6 +542,7 @@ class FCastCastingDevice : CastingDevice {
_started = false; _started = false;
//TODO: Kill and/or join thread? //TODO: Kill and/or join thread?
_thread = null; _thread = null;
_pingThread = null;
val socket = _socket; val socket = _socket;
val scopeIO = _scopeIO; val scopeIO = _scopeIO;
@ -482,6 +552,8 @@ class FCastCastingDevice : CastingDevice {
scopeIO.launch { scopeIO.launch {
socket.close(); socket.close();
_inputStream?.close()
_outputStream?.close()
connectionState = CastConnectionState.DISCONNECTED; connectionState = CastConnectionState.DISCONNECTED;
scopeIO.cancel(); scopeIO.cancel();
Logger.i(TAG, "Cancelled scopeIO with open socket.") Logger.i(TAG, "Cancelled scopeIO with open socket.")

View file

@ -143,7 +143,9 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
StateCasting.instance.onActiveDeviceDurationChanged.remove(this); StateCasting.instance.onActiveDeviceDurationChanged.remove(this);
StateCasting.instance.onActiveDeviceDurationChanged.subscribe { StateCasting.instance.onActiveDeviceDurationChanged.subscribe {
_sliderPosition.valueTo = it.toFloat().coerceAtLeast(1.0f); val dur = it.toFloat().coerceAtLeast(1.0f)
_sliderPosition.value = _sliderPosition.value.coerceAtLeast(0.0f).coerceAtMost(dur);
_sliderPosition.valueTo = dur
}; };
_device = StateCasting.instance.activeDevice; _device = StateCasting.instance.activeDevice;
@ -185,8 +187,10 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
_sliderPosition.valueFrom = 0.0f; _sliderPosition.valueFrom = 0.0f;
_sliderVolume.valueFrom = 0.0f; _sliderVolume.valueFrom = 0.0f;
_sliderVolume.value = d.volume.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo); _sliderVolume.value = d.volume.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo);
_sliderPosition.valueTo = d.duration.toFloat().coerceAtLeast(1.0f);
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo); val dur = d.duration.toFloat().coerceAtLeast(1.0f)
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(dur)
_sliderPosition.valueTo = dur
if (d.canSetVolume) { if (d.canSetVolume) {
_layoutVolumeAdjustable.visibility = View.VISIBLE; _layoutVolumeAdjustable.visibility = View.VISIBLE;

View file

@ -145,7 +145,6 @@ import com.futo.polycentric.core.Opinion
import com.futo.polycentric.core.toURLInfoSystemLinkUrl import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -1372,7 +1371,9 @@ class VideoDetailView : ConstraintLayout {
val toResume = _videoResumePositionMilliseconds; val toResume = _videoResumePositionMilliseconds;
_videoResumePositionMilliseconds = 0; _videoResumePositionMilliseconds = 0;
loadCurrentVideo(toResume); loadCurrentVideo(toResume);
if (!Settings.instance.gestureControls.useSystemVolume) {
_player.setGestureSoundFactor(1.0f); _player.setGestureSoundFactor(1.0f);
}
updateQueueState(); updateQueueState();

View file

@ -13,17 +13,17 @@ import android.view.inputmethod.InputMethodManager
import android.widget.EditText import android.widget.EditText
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.TextView import android.widget.TextView
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.stores.SearchHistoryStorage
import com.futo.platformplayer.Settings import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.fragment.mainactivity.main.* import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragment
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragmentData
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.SearchType import com.futo.platformplayer.models.SearchType
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.SearchHistoryStorage
class SearchTopBarFragment : TopFragment() { class SearchTopBarFragment : TopFragment() {
private val TAG = "SearchTopBarFragment" private val TAG = "SearchTopBarFragment"
@ -54,11 +54,12 @@ class SearchTopBarFragment : TopFragment() {
private val _searchDoneListener = object : TextView.OnEditorActionListener { private val _searchDoneListener = object : TextView.OnEditorActionListener {
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
if (actionId != EditorInfo.IME_ACTION_DONE) val isEnterPress = event?.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN
if (actionId != EditorInfo.IME_ACTION_DONE && !isEnterPress)
return false return false
onDone(); onDone()
return true; return true
} }
}; };

View file

@ -3,8 +3,10 @@ package com.futo.platformplayer.views.behavior
import android.animation.Animator import android.animation.Animator
import android.animation.AnimatorSet import android.animation.AnimatorSet
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.app.Activity
import android.content.Context import android.content.Context
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.media.AudioManager
import android.util.AttributeSet import android.util.AttributeSet
import android.view.GestureDetector import android.view.GestureDetector
import android.view.LayoutInflater import android.view.LayoutInflater
@ -19,6 +21,7 @@ import androidx.core.animation.doOnEnd
import androidx.core.animation.doOnStart import androidx.core.animation.doOnStart
import androidx.core.view.GestureDetectorCompat import androidx.core.view.GestureDetectorCompat
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
@ -59,6 +62,7 @@ class GestureControlView : LinearLayout {
private val _progressSound: CircularProgressBar; private val _progressSound: CircularProgressBar;
private var _animatorSound: ObjectAnimator? = null; private var _animatorSound: ObjectAnimator? = null;
private var _brightnessFactor = 1.0f; private var _brightnessFactor = 1.0f;
private var _originalBrightnessFactor = 1.0f;
private var _adjustingBrightness: Boolean = false; private var _adjustingBrightness: Boolean = false;
private val _layoutControlsBrightness: FrameLayout; private val _layoutControlsBrightness: FrameLayout;
private val _progressBrightness: CircularProgressBar; private val _progressBrightness: CircularProgressBar;
@ -126,11 +130,11 @@ class GestureControlView : LinearLayout {
val rx = (p0.x + p1.x) / (2 * width); val rx = (p0.x + p1.x) / (2 * width);
val ry = (p0.y + p1.y) / (2 * height); val ry = (p0.y + p1.y) / (2 * height);
if (ry > 0.1 && ry < 0.9) { if (ry > 0.1 && ry < 0.9) {
if (_isFullScreen && rx < 0.2) { if (Settings.instance.gestureControls.brightnessSlider && _isFullScreen && rx < 0.2) {
startAdjustingBrightness(); startAdjustingBrightness();
} else if (_isFullScreen && rx > 0.8) { } else if (Settings.instance.gestureControls.volumeSlider && _isFullScreen && rx > 0.8) {
startAdjustingSound(); startAdjustingSound();
} else if (fullScreenGestureEnabled && rx in 0.3..0.7) { } else if (Settings.instance.gestureControls.toggleFullscreen && fullScreenGestureEnabled && rx in 0.3..0.7) {
if (_isFullScreen) { if (_isFullScreen) {
startAdjustingFullscreenDown(); startAdjustingFullscreenDown();
} else { } else {
@ -559,10 +563,34 @@ class GestureControlView : LinearLayout {
fun setFullscreen(isFullScreen: Boolean) { fun setFullscreen(isFullScreen: Boolean) {
if (isFullScreen) { if (isFullScreen) {
val c = context
if (c is Activity && Settings.instance.gestureControls.useSystemBrightness) {
_brightnessFactor = c.window.attributes.screenBrightness
if (_brightnessFactor == -1.0f) {
_brightnessFactor = android.provider.Settings.System.getInt(
context.contentResolver,
android.provider.Settings.System.SCREEN_BRIGHTNESS
) / 255.0f;
}
_originalBrightnessFactor = _brightnessFactor
}
if (Settings.instance.gestureControls.useSystemVolume) {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
_soundFactor = currentVolume.toFloat() / maxVolume.toFloat()
}
onBrightnessAdjusted.emit(_brightnessFactor); onBrightnessAdjusted.emit(_brightnessFactor);
onSoundAdjusted.emit(_soundFactor); onSoundAdjusted.emit(_soundFactor);
} else {
val c = context
if (c is Activity && Settings.instance.gestureControls.useSystemBrightness) {
onBrightnessAdjusted.emit(_originalBrightnessFactor);
} else { } else {
onBrightnessAdjusted.emit(1.0f); onBrightnessAdjusted.emit(1.0f);
}
//onSoundAdjusted.emit(1.0f); //onSoundAdjusted.emit(1.0f);
stopAdjustingBrightness(); stopAdjustingBrightness();
stopAdjustingSound(); stopAdjustingSound();

View file

@ -24,8 +24,8 @@ import com.futo.platformplayer.casting.AirPlayCastingDevice
import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event2 import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.formatDuration
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.toHumanTime
import com.futo.platformplayer.views.behavior.GestureControlView import com.futo.platformplayer.views.behavior.GestureControlView
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -252,8 +252,8 @@ class CastView : ConstraintLayout {
.load(video.thumbnails.getHQThumbnail()) .load(video.thumbnails.getHQThumbnail())
.placeholder(R.drawable.placeholder_video_thumbnail) .placeholder(R.drawable.placeholder_video_thumbnail)
.into(_thumbnail); .into(_thumbnail);
_textPosition.text = position.toHumanTime(false); _textPosition.text = (position * 1000).formatDuration();
_textDuration.text = video.duration.toHumanTime(false); _textDuration.text = (video.duration * 1000).formatDuration();
_timeBar.setPosition(position); _timeBar.setPosition(position);
_timeBar.setDuration(video.duration); _timeBar.setDuration(video.duration);
} }
@ -261,7 +261,7 @@ class CastView : ConstraintLayout {
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
fun setTime(ms: Long) { fun setTime(ms: Long) {
updateCurrentChapter(ms); updateCurrentChapter(ms);
_textPosition.text = ms.toHumanTime(true); _textPosition.text = ms.formatDuration();
_timeBar.setPosition(ms / 1000); _timeBar.setPosition(ms / 1000);
StatePlayer.instance.updateMediaSessionPlaybackState(getPlaybackStateCompat(), ms); StatePlayer.instance.updateMediaSessionPlaybackState(getPlaybackStateCompat(), ms);
} }

View file

@ -1,15 +1,18 @@
package com.futo.platformplayer.views.video package com.futo.platformplayer.views.video
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.media.AudioManager
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log import android.util.Log
import android.util.TypedValue import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.WindowManager
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.TextView import android.widget.TextView
@ -235,15 +238,30 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
gestureControl.setupTouchArea(_layoutControls, background); gestureControl.setupTouchArea(_layoutControls, background);
gestureControl.onSeek.subscribe { seekFromCurrent(it); }; gestureControl.onSeek.subscribe { seekFromCurrent(it); };
gestureControl.onSoundAdjusted.subscribe { setVolume(it) }; gestureControl.onSoundAdjusted.subscribe {
if (Settings.instance.gestureControls.useSystemVolume) {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, (it * maxVolume).toInt(), 0)
} else {
setVolume(it)
}
};
gestureControl.onToggleFullscreen.subscribe { setFullScreen(!isFullScreen) }; gestureControl.onToggleFullscreen.subscribe { setFullScreen(!isFullScreen) };
gestureControl.onBrightnessAdjusted.subscribe { gestureControl.onBrightnessAdjusted.subscribe {
if (context is Activity && Settings.instance.gestureControls.useSystemBrightness) {
val window = context.window
val layout: WindowManager.LayoutParams = window.attributes
layout.screenBrightness = it
window.attributes = layout
} else {
if (it == 1.0f) { if (it == 1.0f) {
_overlay_brightness.visibility = View.GONE; _overlay_brightness.visibility = View.GONE;
} else { } else {
_overlay_brightness.visibility = View.VISIBLE; _overlay_brightness.visibility = View.VISIBLE;
_overlay_brightness.setBackgroundColor(Color.valueOf(0.0f, 0.0f, 0.0f, (1.0f - it)).toArgb()); _overlay_brightness.setBackgroundColor(Color.valueOf(0.0f, 0.0f, 0.0f, (1.0f - it)).toArgb());
} }
}
}; };
if(!isInEditMode) { if(!isInEditMode) {
@ -531,7 +549,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; _videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
_videoControls_fullscreen.show(); _videoControls_fullscreen.show();
videoControls.hide(); videoControls.hideImmediately();
} }
else { else {
val lp = background.layoutParams as ConstraintLayout.LayoutParams; val lp = background.layoutParams as ConstraintLayout.LayoutParams;
@ -543,7 +561,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM; _videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
videoControls.show(); videoControls.show();
_videoControls_fullscreen.hide(); _videoControls_fullscreen.hideImmediately();
} }
fitOrFill(fullScreen); fitOrFill(fullScreen);
@ -730,7 +748,6 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
} }
} }
fun setGestureSoundFactor(soundFactor: Float) { fun setGestureSoundFactor(soundFactor: Float) {
gestureControl.setSoundFactor(soundFactor); gestureControl.setSoundFactor(soundFactor);
} }

View file

@ -344,6 +344,17 @@
<string name="get_answers_to_common_questions">Get answers to common questions</string> <string name="get_answers_to_common_questions">Get answers to common questions</string>
<string name="give_feedback_on_the_application">Give feedback on the application</string> <string name="give_feedback_on_the_application">Give feedback on the application</string>
<string name="info">Info</string> <string name="info">Info</string>
<string name="gesture_controls">Gesture controls</string>
<string name="volume_slider">Volume slider</string>
<string name="volume_slider_descr">Enable slide gesture to change volume</string>
<string name="brightness_slider">Brightness slider</string>
<string name="brightness_slider_descr">Enable slide gesture to change brightness</string>
<string name="toggle_full_screen">Toggle full screen</string>
<string name="toggle_full_screen_descr">Enable swipe gesture to toggle fullscreen</string>
<string name="system_brightness">System brightness</string>
<string name="system_brightness_descr">Gesture controls adjust system brightness</string>
<string name="system_volume">System volume</string>
<string name="system_volume_descr">Gesture controls adjust system volume</string>
<string name="live_chat_webview">Live Chat Webview</string> <string name="live_chat_webview">Live Chat Webview</string>
<string name="full_screen_portrait">Fullscreen portrait</string> <string name="full_screen_portrait">Fullscreen portrait</string>
<string name="allow_full_screen_portrait">Allow fullscreen portrait</string> <string name="allow_full_screen_portrait">Allow fullscreen portrait</string>