Skip to content

Commit

Permalink
[feature] Support custom MPV input.conf
Browse files Browse the repository at this point in the history
  • Loading branch information
SkyD666 committed May 26, 2024
1 parent c266a1b commit ce976c9
Show file tree
Hide file tree
Showing 13 changed files with 214 additions and 26 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ android {
minSdk = 24
targetSdk = 34
versionCode = 16
versionName = "1.1-beta34"
versionName = "1.1-beta35"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/com/skyd/anivu/base/BaseComposeActivity.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.skyd.anivu.base

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
Expand All @@ -15,7 +15,7 @@ import com.skyd.anivu.ui.theme.AniVuTheme
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
open class BaseComposeActivity : ComponentActivity() {
open class BaseComposeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.skyd.anivu.model.preference.player

import com.skyd.anivu.config.Const
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.io.File

object MpvInputConfigPreference {
private var value: String? = null

fun put(scope: CoroutineScope, value: String) {
this.value = value
scope.launch(Dispatchers.IO) {
File(Const.MPV_CONFIG_DIR, "input.conf")
.apply { if (!exists()) createNewFile() }
.writeText(value)
}
}

fun getValue(): String = value ?: runBlocking(Dispatchers.IO) {
value = File(Const.MPV_CONFIG_DIR, "input.conf")
.apply { if (!exists()) createNewFile() }
.readText()
value.orEmpty()
}
}
14 changes: 12 additions & 2 deletions app/src/main/java/com/skyd/anivu/ui/activity/PlayActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import android.view.WindowManager
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.DisposableEffect
Expand All @@ -16,6 +17,7 @@ import androidx.core.util.Consumer
import com.skyd.anivu.base.BaseComposeActivity
import com.skyd.anivu.ext.savePictureToMediaStore
import com.skyd.anivu.ui.component.showToast
import com.skyd.anivu.ui.mpv.MPVView
import com.skyd.anivu.ui.mpv.PlayerView
import com.skyd.anivu.ui.mpv.copyAssetsForMpv
import java.io.File
Expand All @@ -26,7 +28,7 @@ class PlayActivity : BaseComposeActivity() {
const val VIDEO_URI_KEY = "videoUri"
}


private var player: MPVView? = null
private lateinit var picture: File
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
Expand Down Expand Up @@ -63,7 +65,8 @@ class PlayActivity : BaseComposeActivity() {
onSaveScreenshot = {
picture = it
saveScreenshot()
}
},
onPlayerChanged = { player = it }
)
}
}
Expand All @@ -82,4 +85,11 @@ class PlayActivity : BaseComposeActivity() {
requestPermissionLauncher.launch(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}

override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (player?.onKey(event) == true) {
return true
}
return super.dispatchKeyEvent(event)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ fun TextFieldDialog(
errorText: String = "",
dismissText: String = stringResource(R.string.cancel),
confirmText: String = stringResource(R.string.ok),
enableConfirm: (String) -> Boolean = { it.isNotBlank() },
onValueChange: (String) -> Unit = {},
onDismissRequest: () -> Unit = {},
onConfirm: (String) -> Unit = {},
Expand Down Expand Up @@ -66,15 +67,15 @@ fun TextFieldDialog(
},
confirmButton = {
TextButton(
enabled = value.isNotBlank(),
enabled = enableConfirm(value),
onClick = {
focusManager.clearFocus()
onConfirm(value)
}
) {
Text(
text = confirmText,
color = if (value.isNotBlank()) {
color = if (enableConfirm(value)) {
Color.Unspecified
} else {
MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ fun FeedScreen() {
}

val windowWidth = with(density) { currentWindowSize().width.toDp() }
val feedListWidth by remember(windowWidth) { mutableStateOf(windowWidth * 0.31f) }
val feedListWidth by remember(windowWidth) { mutableStateOf(windowWidth * 0.335f) }

ListDetailPaneScaffold(
modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing.only(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.view.ViewGroup
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Keyboard
import androidx.compose.material.icons.outlined.PlayCircle
import androidx.compose.material.icons.rounded.DeveloperBoard
import androidx.compose.material3.Scaffold
Expand All @@ -27,6 +28,7 @@ import com.skyd.anivu.R
import com.skyd.anivu.base.BaseComposeFragment
import com.skyd.anivu.model.preference.player.HardwareDecodePreference
import com.skyd.anivu.model.preference.player.MpvConfigPreference
import com.skyd.anivu.model.preference.player.MpvInputConfigPreference
import com.skyd.anivu.ui.component.AniVuTopBar
import com.skyd.anivu.ui.component.AniVuTopBarStyle
import com.skyd.anivu.ui.component.BaseSettingsItem
Expand All @@ -52,6 +54,8 @@ fun PlayerConfigAdvancedScreen() {
val scope = rememberCoroutineScope()
var mpvConfEditDialogValue by rememberSaveable { mutableStateOf("") }
var openMpvConfEditDialog by rememberSaveable { mutableStateOf(false) }
var mpvInputConfEditDialogValue by rememberSaveable { mutableStateOf("") }
var openMpvInputConfEditDialog by rememberSaveable { mutableStateOf(false) }

Scaffold(
topBar = {
Expand Down Expand Up @@ -94,6 +98,17 @@ fun PlayerConfigAdvancedScreen() {
}
)
}
item {
BaseSettingsItem(
icon = rememberVectorPainter(Icons.Outlined.Keyboard),
text = stringResource(id = R.string.player_config_advanced_screen_mpv_input_config),
descriptionText = null,
onClick = {
mpvInputConfEditDialogValue = MpvInputConfigPreference.getValue()
openMpvInputConfEditDialog = true
}
)
}
}

TextFieldDialog(
Expand All @@ -107,7 +122,23 @@ fun PlayerConfigAdvancedScreen() {
)
openMpvConfEditDialog = false
},
enableConfirm = { true },
onDismissRequest = { openMpvConfEditDialog = false },
)

TextFieldDialog(
visible = openMpvInputConfEditDialog,
value = mpvInputConfEditDialogValue,
onValueChange = { mpvInputConfEditDialogValue = it },
onConfirm = {
MpvInputConfigPreference.put(
scope = scope,
value = it,
)
openMpvInputConfEditDialog = false
},
enableConfirm = { true },
onDismissRequest = { openMpvInputConfEditDialog = false },
)
}
}
70 changes: 70 additions & 0 deletions app/src/main/java/com/skyd/anivu/ui/mpv/KeyMapping.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.skyd.anivu.ui.mpv

import android.util.SparseArray
import android.view.KeyEvent

internal object KeyMapping {
val map: SparseArray<String> = SparseArray()

init {
// cf. https://github.com/mpv-player/mpv/blob/master/input/keycodes.h
map.put(KeyEvent.KEYCODE_SPACE, "SPACE")
map.put(KeyEvent.KEYCODE_ENTER, "ENTER")
map.put(KeyEvent.KEYCODE_TAB, "TAB")
map.put(KeyEvent.KEYCODE_DEL, "BS")
map.put(KeyEvent.KEYCODE_FORWARD_DEL, "DEL")
map.put(KeyEvent.KEYCODE_INSERT, "INS")
map.put(KeyEvent.KEYCODE_MOVE_HOME, "HOME")
map.put(KeyEvent.KEYCODE_MOVE_END, "END")
map.put(KeyEvent.KEYCODE_PAGE_UP, "PGUP")
map.put(KeyEvent.KEYCODE_PAGE_DOWN, "PGDWN")
map.put(KeyEvent.KEYCODE_ESCAPE, "ESC")
map.put(KeyEvent.KEYCODE_SYSRQ, "PRINT")

map.put(KeyEvent.KEYCODE_DPAD_RIGHT, "RIGHT")
map.put(KeyEvent.KEYCODE_DPAD_LEFT, "LEFT")
map.put(KeyEvent.KEYCODE_DPAD_DOWN, "DOWN")
map.put(KeyEvent.KEYCODE_DPAD_UP, "UP")

// not bound, let the OS handle these:
map.put(KeyEvent.KEYCODE_MEDIA_PLAY, "PLAYONLY")
map.put(KeyEvent.KEYCODE_MEDIA_PAUSE, "PAUSEONLY")
map.put(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, "PLAYPAUSE")
map.put(KeyEvent.KEYCODE_MEDIA_STOP, "STOP")
map.put(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, "FORWARD")
map.put(KeyEvent.KEYCODE_MEDIA_REWIND, "REWIND")
map.put(KeyEvent.KEYCODE_MEDIA_NEXT, "NEXT")
map.put(KeyEvent.KEYCODE_MEDIA_PREVIOUS, "PREV")
map.put(KeyEvent.KEYCODE_MEDIA_RECORD, "RECORD")
map.put(KeyEvent.KEYCODE_CHANNEL_UP, "CHANNEL_UP")
map.put(KeyEvent.KEYCODE_CHANNEL_DOWN, "CHANNEL_DOWN")
map.put(KeyEvent.KEYCODE_ZOOM_IN, "ZOOMIN")
map.put(KeyEvent.KEYCODE_ZOOM_OUT, "ZOOMOUT")

map.put(KeyEvent.KEYCODE_F1, "F1")
map.put(KeyEvent.KEYCODE_F2, "F2")
map.put(KeyEvent.KEYCODE_F3, "F3")
map.put(KeyEvent.KEYCODE_F4, "F4")
map.put(KeyEvent.KEYCODE_F5, "F5")
map.put(KeyEvent.KEYCODE_F6, "F6")
map.put(KeyEvent.KEYCODE_F7, "F7")
map.put(KeyEvent.KEYCODE_F8, "F8")
map.put(KeyEvent.KEYCODE_F9, "F9")
map.put(KeyEvent.KEYCODE_F10, "F10")
map.put(KeyEvent.KEYCODE_F11, "F11")
map.put(KeyEvent.KEYCODE_F12, "F12")

map.put(KeyEvent.KEYCODE_NUMPAD_0, "KP0")
map.put(KeyEvent.KEYCODE_NUMPAD_1, "KP1")
map.put(KeyEvent.KEYCODE_NUMPAD_2, "KP2")
map.put(KeyEvent.KEYCODE_NUMPAD_3, "KP3")
map.put(KeyEvent.KEYCODE_NUMPAD_4, "KP4")
map.put(KeyEvent.KEYCODE_NUMPAD_5, "KP5")
map.put(KeyEvent.KEYCODE_NUMPAD_6, "KP6")
map.put(KeyEvent.KEYCODE_NUMPAD_7, "KP7")
map.put(KeyEvent.KEYCODE_NUMPAD_8, "KP8")
map.put(KeyEvent.KEYCODE_NUMPAD_9, "KP9")
map.put(KeyEvent.KEYCODE_NUMPAD_DOT, "KP_DEC")
map.put(KeyEvent.KEYCODE_NUMPAD_ENTER, "KP_ENTER")
}
}
43 changes: 42 additions & 1 deletion app/src/main/java/com/skyd/anivu/ui/mpv/MPVView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.util.Log
import android.view.KeyCharacterMap
import android.view.KeyEvent
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.WindowManager
Expand Down Expand Up @@ -47,7 +49,7 @@ class MPVView(context: Context, attrs: AttributeSet?) : SurfaceView(context, att
vo: String = "gpu",
) {
if (initialized) return
synchronized(MPVView::class) {
synchronized(this) {
if (initialized) return
initialized = true
}
Expand Down Expand Up @@ -118,6 +120,45 @@ class MPVView(context: Context, attrs: AttributeSet?) : SurfaceView(context, att
MPVLib.destroy()
}

fun onKey(event: KeyEvent): Boolean {
if (event.action == KeyEvent.ACTION_MULTIPLE)
return false
if (KeyEvent.isModifierKey(event.keyCode))
return false

var mapped = KeyMapping.map.get(event.keyCode)
if (mapped == null) {
// Fallback to produced glyph
if (!event.isPrintingKey) {
if (event.repeatCount == 0) {
Log.d(TAG, "Unmapped non-printable key ${event.keyCode}")
}
return false
}

val ch = event.unicodeChar
if (ch.and(KeyCharacterMap.COMBINING_ACCENT) != 0) {
return false // dead key
}
mapped = ch.toChar().toString()
}

if (event.repeatCount > 0)
return true // eat event but ignore it, mpv has its own key repeat

val mod: MutableList<String> = mutableListOf()
event.isShiftPressed && mod.add("shift")
event.isCtrlPressed && mod.add("ctrl")
event.isAltPressed && mod.add("alt")
event.isMetaPressed && mod.add("meta")

val action = if (event.action == KeyEvent.ACTION_DOWN) "keydown" else "keyup"
mod.add(mapped)
MPVLib.command(arrayOf(action, mod.joinToString("+")))

return true
}

private fun observeProperties() {
// This observes all properties needed by MPVView, MPVActivity or other classes
data class Property(val name: String, val format: Int = MPV_FORMAT_NONE)
Expand Down
34 changes: 17 additions & 17 deletions app/src/main/java/com/skyd/anivu/ui/mpv/PlayerUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,24 @@ internal fun Uri.resolveUri(context: Context): String? {
return filepath
}

private fun Uri.openContentFd(context: Context): String? {
val resolver = context.contentResolver
val fd = try {
resolver.openFileDescriptor(this, "r")!!.detachFd()
} catch (e: Exception) {
Log.e("openContentFd", "Failed to open content fd: $e")
return null
}
// See if we skip the indirection and read the real file directly
val path = findRealPath(fd)
if (path != null) {
Log.v("openContentFd", "Found real file path: $path")
ParcelFileDescriptor.adoptFd(fd).close() // we don't need that anymore
return path
private fun Uri.openContentFd(context: Context): String? =
context.contentResolver.openFileDescriptor(this, "r")!!.use { fileDescriptor ->
val fd = try {
fileDescriptor.detachFd()
} catch (e: Exception) {
Log.e("openContentFd", "Failed to open content fd: $e")
return@use null
}
// See if we skip the indirection and read the real file directly
val path = findRealPath(fd)
if (path != null) {
Log.v("openContentFd", "Found real file path: $path")
ParcelFileDescriptor.adoptFd(fd).close() // we don't need that anymore
return@use path
}
// Else, pass the fd to mpv
return@use "fd://${fd}"
}
// Else, pass the fd to mpv
return "fd://${fd}"
}

fun findRealPath(fd: Int): String? {
var ins: InputStream? = null
Expand Down
Loading

0 comments on commit ce976c9

Please sign in to comment.