diff --git a/app/src/main/java/com/futo/platformplayer/Extensions_Network.kt b/app/src/main/java/com/futo/platformplayer/Extensions_Network.kt
index 0ec735da383943fec2d049a94bfcd42f13f1e79c..0c83a06dcb55c9e3188f456fd75257c2fc5482cf 100644
--- a/app/src/main/java/com/futo/platformplayer/Extensions_Network.kt
+++ b/app/src/main/java/com/futo/platformplayer/Extensions_Network.kt
@@ -1,5 +1,6 @@
 package com.futo.platformplayer
 
+import android.util.Log
 import com.google.common.base.CharMatcher
 import java.io.ByteArrayOutputStream
 import java.io.IOException
@@ -215,17 +216,20 @@ private fun ByteArray.toInetAddress(): InetAddress {
 }
 
 fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
-    val timeout = 5000
+    val timeout = 2000
 
     if (addresses.isEmpty()) {
         return null;
     }
 
     if (addresses.size == 1) {
+        val socket = Socket()
+
         try {
-            return Socket().apply { this.connect(InetSocketAddress(addresses[0], port), timeout) };
+            return socket.apply { this.connect(InetSocketAddress(addresses[0], port), timeout) }
         } catch (e: Throwable) {
-            //Ignored.
+            Log.i("getConnectedSocket", "Failed to connect to: ${addresses[0]}", e)
+            socket.close()
         }
 
         return null;
@@ -264,7 +268,7 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
                     }
                 }
             } catch (e: Throwable) {
-                //Ignore
+                Log.i("getConnectedSocket", "Failed to connect to: $address", e)
             }
         };
 
diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt
index 168cddfb674863820dd1085bb77b499fd5d9ccd7..30e912315e359b93ad24f80e5038c393cfe2fff4 100644
--- a/app/src/main/java/com/futo/platformplayer/Settings.kt
+++ b/app/src/main/java/com/futo/platformplayer/Settings.kt
@@ -809,7 +809,27 @@ class Settings : FragmentedStorageFileJson() {
         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();
     @Serializable
     class Info {
diff --git a/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt b/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt
index a16cd83d180f90e3fbec4e9254ae13a8b8463401..a0a9695c1282cff91ccb01ea6725cb61ff3aef56 100644
--- a/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt
+++ b/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt
@@ -328,6 +328,8 @@ class ChromecastCastingDevice : CastingDevice {
 
                 val factory = sslContext.socketFactory;
 
+                val address = InetSocketAddress(usedRemoteAddress, port)
+
                 //Connection loop
                 while (_scopeIO?.isActive == true) {
                     Logger.i(TAG, "Connecting to Chromecast.");
@@ -341,7 +343,7 @@ class ChromecastCastingDevice : CastingDevice {
                             connectedSocket = null
                         } else {
                             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
                         }
 
@@ -444,10 +446,11 @@ class ChromecastCastingDevice : CastingDevice {
                 while (_scopeIO?.isActive == true) {
                     try {
                         sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.tp.heartbeat", pingObject.toString());
-                        Thread.sleep(5000);
                     } catch (e: Throwable) {
                         Log.w(TAG, "Failed to send ping.");
                     }
+
+                    Thread.sleep(5000);
                 }
 
                 Logger.i(TAG, "Stopped ping loop.");
diff --git a/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt b/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt
index ceb2ee969a2badd56a146ed101bfdfe287d7fd9b..9e12f78c26f11a29d2563263d0abebc475210256 100644
--- a/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt
+++ b/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt
@@ -15,6 +15,7 @@ import com.futo.platformplayer.casting.models.FCastSetSpeedMessage
 import com.futo.platformplayer.casting.models.FCastSetVolumeMessage
 import com.futo.platformplayer.casting.models.FCastVersionMessage
 import com.futo.platformplayer.casting.models.FCastVolumeUpdateMessage
+import com.futo.platformplayer.ensureNotMainThread
 import com.futo.platformplayer.getConnectedSocket
 import com.futo.platformplayer.logging.Logger
 import com.futo.platformplayer.models.CastingDeviceInfo
@@ -27,9 +28,9 @@ import kotlinx.coroutines.isActive
 import kotlinx.coroutines.launch
 import kotlinx.serialization.encodeToString
 import kotlinx.serialization.json.Json
-import java.io.DataInputStream
-import java.io.DataOutputStream
 import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
 import java.math.BigInteger
 import java.net.InetAddress
 import java.net.InetSocketAddress
@@ -82,12 +83,15 @@ class FCastCastingDevice : CastingDevice {
     var port: Int = 0;
 
     private var _socket: Socket? = null;
-    private var _outputStream: DataOutputStream? = null;
-    private var _inputStream: DataInputStream? = null;
+    private var _outputStream: OutputStream? = null;
+    private var _inputStream: InputStream? = null;
     private var _scopeIO: CoroutineScope? = null;
     private var _started: Boolean = false;
     private var _version: Long = 1;
     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() {
         this.name = name;
@@ -207,7 +211,13 @@ class FCastCastingDevice : CastingDevice {
 
     private fun invokeInIOScopeIfRequired(action: () -> Unit): Boolean {
         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;
         }
 
@@ -241,7 +251,8 @@ class FCastCastingDevice : CastingDevice {
         val adrs = addresses ?: return;
 
         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")
 
             _scopeIO?.let {
@@ -258,7 +269,7 @@ class FCastCastingDevice : CastingDevice {
                 var connectedSocket: Socket? = null
                 while (_scopeIO?.isActive == true) {
                     try {
-                        Log.i(TAG, "getConnectedSocket.")
+                        Log.i(TAG, "getConnectedSocket (adrs = [ ${adrs.joinToString(", ")} ], port = ${port}).")
 
                         val resultSocket = getConnectedSocket(adrs.toList(), port);
 
@@ -279,6 +290,8 @@ class FCastCastingDevice : CastingDevice {
                     }
                 }
 
+                val address = InetSocketAddress(usedRemoteAddress, port)
+
                 //Connection loop
                 while (_scopeIO?.isActive == true) {
                     Logger.i(TAG, "Connecting to FastCast.");
@@ -286,20 +299,24 @@ class FCastCastingDevice : CastingDevice {
 
                     try {
                         _socket?.close()
+                        _inputStream?.close()
+                        _outputStream?.close()
                         if (connectedSocket != null) {
                             Logger.i(TAG, "Using connected socket.");
                             _socket = connectedSocket
                             connectedSocket = null
                         } else {
                             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");
 
-                        _outputStream = DataOutputStream(_socket?.outputStream);
-                        _inputStream = DataInputStream(_socket?.inputStream);
+                        _outputStream = _socket?.outputStream;
+                        _inputStream = _socket?.inputStream;
                     } catch (e: IOException) {
-                        _socket?.close();
+                        _socket?.close()
+                        _inputStream?.close()
+                        _outputStream?.close()
                         Logger.i(TAG, "Failed to connect to FastCast.", e);
 
                         connectionState = CastConnectionState.CONNECTING;
@@ -309,28 +326,38 @@ class FCastCastingDevice : CastingDevice {
 
                     localAddress = _socket?.localAddress;
                     connectionState = CastConnectionState.CONNECTED;
+                    _lastPongTime = -1L
 
                     val buffer = ByteArray(4096);
 
                     Logger.i(TAG, "Started receiving.");
-                    var exceptionOccurred = false;
-                    while (_scopeIO?.isActive == true && !exceptionOccurred) {
+                    while (_scopeIO?.isActive == true) {
                         try {
                             val inputStream = _inputStream ?: break;
                             Log.d(TAG, "Receiving next packet...");
-                            val b1 = inputStream.readUnsignedByte();
-                            val b2 = inputStream.readUnsignedByte();
-                            val b3 = inputStream.readUnsignedByte();
-                            val b4 = inputStream.readUnsignedByte();
-                            val size = ((b4.toLong() shl 24) or (b3.toLong() shl 16) or (b2.toLong() shl 8) or b1.toLong()).toInt();
+
+                            var headerBytesRead = 0
+                            while (headerBytesRead < 4) {
+                                val read = inputStream.read(buffer, headerBytesRead, 4 - headerBytesRead)
+                                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) {
-                                Logger.w(TAG, "Skipping packet that is too large $size bytes.")
-                                inputStream.skip(size.toLong());
-                                continue;
+                                Logger.w(TAG, "Packets larger than $size bytes are not supported.")
+                                break
                             }
 
                             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));
                             Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
@@ -344,19 +371,22 @@ class FCastCastingDevice : CastingDevice {
                             try {
                                 handleMessage(Opcode.find(opcode), json);
                             } 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) {
                             Logger.e(TAG, "Socket exception while receiving.", e);
-                            exceptionOccurred = true;
+                            break
                         } catch (e: Throwable) {
                             Logger.e(TAG, "Exception while receiving.", e);
-                            exceptionOccurred = true;
+                            break
                         }
                     }
 
                     try {
-                        _socket?.close();
+                        _socket?.close()
+                        _inputStream?.close()
+                        _outputStream?.close()
                         Logger.i(TAG, "Socket disconnected.");
                     } catch (e: Throwable) {
                         Logger.e(TAG, "Failed to close socket.", e)
@@ -368,7 +398,41 @@ class FCastCastingDevice : CastingDevice {
 
                 Logger.i(TAG, "Stopped connection loop.");
                 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 {
             Log.i(TAG, "Thread was still alive, not restarted")
         }
@@ -421,39 +485,44 @@ class FCastCastingDevice : CastingDevice {
                 Logger.i(TAG, "Remote version received: $version")
             }
             Opcode.Ping -> send(Opcode.Pong)
+            Opcode.Pong -> _lastPongTime = System.currentTimeMillis()
             else -> { }
         }
     }
 
     private fun send(opcode: Opcode, message: String? = null) {
-        try {
-            val data: ByteArray = message?.encodeToByteArray() ?: ByteArray(0)
-            val size = 1 + data.size
-            val outputStream = _outputStream
-            if (outputStream == null) {
-                Log.w(TAG, "Failed to send $size bytes, output stream is null.")
-                return
-            }
+        ensureNotMainThread()
+
+        synchronized (_outputStreamLock) {
+            try {
+                val data: ByteArray = message?.encodeToByteArray() ?: ByteArray(0)
+                val size = 1 + data.size
+                val outputStream = _outputStream
+                if (outputStream == null) {
+                    Log.w(TAG, "Failed to send $size bytes, output stream is null.")
+                    return
+                }
 
-            val serializedSizeLE = ByteArray(4)
-            serializedSizeLE[0] = (size and 0xff).toByte()
-            serializedSizeLE[1] = (size shr 8 and 0xff).toByte()
-            serializedSizeLE[2] = (size shr 16 and 0xff).toByte()
-            serializedSizeLE[3] = (size shr 24 and 0xff).toByte()
-            outputStream.write(serializedSizeLE)
+                val serializedSizeLE = ByteArray(4)
+                serializedSizeLE[0] = (size and 0xff).toByte()
+                serializedSizeLE[1] = (size shr 8 and 0xff).toByte()
+                serializedSizeLE[2] = (size shr 16 and 0xff).toByte()
+                serializedSizeLE[3] = (size shr 24 and 0xff).toByte()
+                outputStream.write(serializedSizeLE)
 
-            val opcodeBytes = ByteArray(1)
-            opcodeBytes[0] = opcode.value
-            outputStream.write(opcodeBytes)
+                val opcodeBytes = ByteArray(1)
+                opcodeBytes[0] = opcode.value
+                outputStream.write(opcodeBytes)
 
-            if (data.isNotEmpty()) {
-                outputStream.write(data)
-            }
+                if (data.isNotEmpty()) {
+                    outputStream.write(data)
+                }
 
-            Log.d(TAG, "Sent $size bytes: (opcode: $opcode, body: $message).")
-        } catch (e: Throwable) {
-            Log.i(TAG, "Failed to send message.", e)
-            throw e
+                Log.d(TAG, "Sent $size bytes: (opcode: $opcode, body: $message).")
+            } catch (e: Throwable) {
+                Log.i(TAG, "Failed to send message.", e)
+                throw e
+            }
         }
     }
 
@@ -473,6 +542,7 @@ class FCastCastingDevice : CastingDevice {
         _started = false;
         //TODO: Kill and/or join thread?
         _thread = null;
+        _pingThread = null;
 
         val socket = _socket;
         val scopeIO = _scopeIO;
@@ -482,6 +552,8 @@ class FCastCastingDevice : CastingDevice {
 
             scopeIO.launch {
                 socket.close();
+                _inputStream?.close()
+                _outputStream?.close()
                 connectionState = CastConnectionState.DISCONNECTED;
                 scopeIO.cancel();
                 Logger.i(TAG, "Cancelled scopeIO with open socket.")
diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectedCastingDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectedCastingDialog.kt
index 300b0a665bc8a41b6f1581227a41a2965fa2336f..862f8333f19003e6af9c5b0d3dbfda8c448ea8dd 100644
--- a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectedCastingDialog.kt
+++ b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectedCastingDialog.kt
@@ -143,7 +143,9 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
 
         StateCasting.instance.onActiveDeviceDurationChanged.remove(this);
         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;
@@ -185,8 +187,10 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
         _sliderPosition.valueFrom = 0.0f;
         _sliderVolume.valueFrom = 0.0f;
         _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) {
             _layoutVolumeAdjustable.visibility = View.VISIBLE;
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
index 695edab30ab3159c1317e3773338d9ddca528a8c..a80b1aabd09d7a1ecc778c98d56c868f4d6dc243 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
@@ -145,7 +145,6 @@ import com.futo.polycentric.core.Opinion
 import com.futo.polycentric.core.toURLInfoSystemLinkUrl
 import com.google.protobuf.ByteString
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
@@ -1372,7 +1371,9 @@ class VideoDetailView : ConstraintLayout {
         val toResume = _videoResumePositionMilliseconds;
         _videoResumePositionMilliseconds = 0;
         loadCurrentVideo(toResume);
-        _player.setGestureSoundFactor(1.0f);
+        if (!Settings.instance.gestureControls.useSystemVolume) {
+            _player.setGestureSoundFactor(1.0f);
+        }
 
         updateQueueState();
 
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/topbar/SearchTopBarFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/topbar/SearchTopBarFragment.kt
index bb3af1aece5c33dae109e991e79409cb77737008..088655f954df1503c4b0c5c36515c86703e8b15c 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/topbar/SearchTopBarFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/topbar/SearchTopBarFragment.kt
@@ -13,17 +13,17 @@ import android.view.inputmethod.InputMethodManager
 import android.widget.EditText
 import android.widget.ImageButton
 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.stores.SearchHistoryStorage
 import com.futo.platformplayer.Settings
 import com.futo.platformplayer.UIDialogs
-import com.futo.platformplayer.api.media.PlatformID
 import com.futo.platformplayer.constructs.Event0
 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.stores.FragmentedStorage
+import com.futo.platformplayer.stores.SearchHistoryStorage
 
 class SearchTopBarFragment : TopFragment() {
     private val TAG = "SearchTopBarFragment"
@@ -54,11 +54,12 @@ class SearchTopBarFragment : TopFragment() {
 
     private val _searchDoneListener = object : TextView.OnEditorActionListener {
         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
 
-            onDone();
-            return true;
+            onDone()
+            return true
         }
     };
 
diff --git a/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt b/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt
index f500a87e6b6db91a6892585fc9eed4bc28b3736e..2fa89c8eef2fdb974c8b83b7031e1582f4a2d878 100644
--- a/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt
@@ -3,8 +3,10 @@ package com.futo.platformplayer.views.behavior
 import android.animation.Animator
 import android.animation.AnimatorSet
 import android.animation.ObjectAnimator
+import android.app.Activity
 import android.content.Context
 import android.graphics.drawable.Animatable
+import android.media.AudioManager
 import android.util.AttributeSet
 import android.view.GestureDetector
 import android.view.LayoutInflater
@@ -19,6 +21,7 @@ import androidx.core.animation.doOnEnd
 import androidx.core.animation.doOnStart
 import androidx.core.view.GestureDetectorCompat
 import com.futo.platformplayer.R
+import com.futo.platformplayer.Settings
 import com.futo.platformplayer.constructs.Event0
 import com.futo.platformplayer.constructs.Event1
 import com.futo.platformplayer.logging.Logger
@@ -59,6 +62,7 @@ class GestureControlView : LinearLayout {
     private val _progressSound: CircularProgressBar;
     private var _animatorSound: ObjectAnimator? = null;
     private var _brightnessFactor = 1.0f;
+    private var _originalBrightnessFactor = 1.0f;
     private var _adjustingBrightness: Boolean = false;
     private val _layoutControlsBrightness: FrameLayout;
     private val _progressBrightness: CircularProgressBar;
@@ -126,11 +130,11 @@ class GestureControlView : LinearLayout {
                     val rx = (p0.x + p1.x) / (2 * width);
                     val ry = (p0.y + p1.y) / (2 * height);
                     if (ry > 0.1 && ry < 0.9) {
-                        if (_isFullScreen && rx < 0.2) {
+                        if (Settings.instance.gestureControls.brightnessSlider && _isFullScreen && rx < 0.2) {
                             startAdjustingBrightness();
-                        } else if (_isFullScreen && rx > 0.8) {
+                        } else if (Settings.instance.gestureControls.volumeSlider && _isFullScreen && rx > 0.8) {
                             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) {
                                 startAdjustingFullscreenDown();
                             } else {
@@ -559,10 +563,34 @@ class GestureControlView : LinearLayout {
 
     fun setFullscreen(isFullScreen: Boolean) {
         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);
             onSoundAdjusted.emit(_soundFactor);
         } else {
-            onBrightnessAdjusted.emit(1.0f);
+            val c = context
+            if (c is Activity && Settings.instance.gestureControls.useSystemBrightness) {
+                onBrightnessAdjusted.emit(_originalBrightnessFactor);
+            } else {
+                onBrightnessAdjusted.emit(1.0f);
+            }
             //onSoundAdjusted.emit(1.0f);
             stopAdjustingBrightness();
             stopAdjustingSound();
diff --git a/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt b/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt
index b33c62dab5f4ff47c90255a2449bb666ae08610f..91631f1e2f98631b54f41bec8ce44a548d93ee84 100644
--- a/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt
@@ -24,8 +24,8 @@ import com.futo.platformplayer.casting.AirPlayCastingDevice
 import com.futo.platformplayer.casting.StateCasting
 import com.futo.platformplayer.constructs.Event0
 import com.futo.platformplayer.constructs.Event2
+import com.futo.platformplayer.formatDuration
 import com.futo.platformplayer.states.StatePlayer
-import com.futo.platformplayer.toHumanTime
 import com.futo.platformplayer.views.behavior.GestureControlView
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
@@ -252,8 +252,8 @@ class CastView : ConstraintLayout {
             .load(video.thumbnails.getHQThumbnail())
             .placeholder(R.drawable.placeholder_video_thumbnail)
             .into(_thumbnail);
-        _textPosition.text = position.toHumanTime(false);
-        _textDuration.text = video.duration.toHumanTime(false);
+        _textPosition.text = (position * 1000).formatDuration();
+        _textDuration.text = (video.duration * 1000).formatDuration();
         _timeBar.setPosition(position);
         _timeBar.setDuration(video.duration);
     }
@@ -261,7 +261,7 @@ class CastView : ConstraintLayout {
     @OptIn(UnstableApi::class)
     fun setTime(ms: Long) {
         updateCurrentChapter(ms);
-        _textPosition.text = ms.toHumanTime(true);
+        _textPosition.text = ms.formatDuration();
         _timeBar.setPosition(ms / 1000);
         StatePlayer.instance.updateMediaSessionPlaybackState(getPlaybackStateCompat(), ms);
     }
diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt
index 7e1d20e22a3f17034307fbb0a8b14284797f1924..2c7020260af5a8debfef3b2694ff657d9afd24f0 100644
--- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt
@@ -1,15 +1,18 @@
 package com.futo.platformplayer.views.video
 
+import android.app.Activity
 import android.content.Context
 import android.content.res.Resources
 import android.graphics.Color
 import android.graphics.drawable.Drawable
+import android.media.AudioManager
 import android.util.AttributeSet
 import android.util.Log
 import android.util.TypedValue
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.WindowManager
 import android.widget.FrameLayout
 import android.widget.ImageButton
 import android.widget.TextView
@@ -235,14 +238,29 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
 
         gestureControl.setupTouchArea(_layoutControls, background);
         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.onBrightnessAdjusted.subscribe {
-            if (it == 1.0f) {
-                _overlay_brightness.visibility = View.GONE;
+            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 {
-                _overlay_brightness.visibility = View.VISIBLE;
-                _overlay_brightness.setBackgroundColor(Color.valueOf(0.0f, 0.0f, 0.0f, (1.0f - it)).toArgb());
+                if (it == 1.0f) {
+                    _overlay_brightness.visibility = View.GONE;
+                } else {
+                    _overlay_brightness.visibility = View.VISIBLE;
+                    _overlay_brightness.setBackgroundColor(Color.valueOf(0.0f, 0.0f, 0.0f, (1.0f - it)).toArgb());
+                }
             }
         };
 
@@ -531,7 +549,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
             _videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
 
             _videoControls_fullscreen.show();
-            videoControls.hide();
+            videoControls.hideImmediately();
         }
         else {
             val lp = background.layoutParams as ConstraintLayout.LayoutParams;
@@ -543,7 +561,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
             _videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
 
             videoControls.show();
-            _videoControls_fullscreen.hide();
+            _videoControls_fullscreen.hideImmediately();
         }
 
         fitOrFill(fullScreen);
@@ -730,7 +748,6 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
         }
     }
 
-
     fun setGestureSoundFactor(soundFactor: Float) {
         gestureControl.setSoundFactor(soundFactor);
     }
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d4f1b6d18317a8c439090e1263d11fb79a579e03..64923346fac5b4e3e3e27174478029f928ce8e05 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -344,6 +344,17 @@
     <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="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="full_screen_portrait">Fullscreen portrait</string>
     <string name="allow_full_screen_portrait">Allow fullscreen portrait</string>