From 5c6e2fc8fc5fdbe1722d39eb40a1fbc0d1ae36dd Mon Sep 17 00:00:00 2001 From: sfan5 Date: Fri, 24 May 2024 21:51:06 +0200 Subject: [PATCH 01/78] SettingsActivity: hide non-functional back button on TV ??? --- app/src/main/java/is/xyz/mpv/config/SettingsActivity.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/is/xyz/mpv/config/SettingsActivity.kt b/app/src/main/java/is/xyz/mpv/config/SettingsActivity.kt index 07e602722..1b063e0cb 100644 --- a/app/src/main/java/is/xyz/mpv/config/SettingsActivity.kt +++ b/app/src/main/java/is/xyz/mpv/config/SettingsActivity.kt @@ -33,8 +33,8 @@ class SettingsActivity : PreferenceActivity() { * Set up the [android.app.ActionBar], if the API is available. */ private fun setupActionBar() { - val actionBar = actionBar - actionBar?.setDisplayHomeAsUpEnabled(true) + if (!packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) + actionBar?.setDisplayHomeAsUpEnabled(true) } /** From e6868ed335bf998b74ed8ecf509e1dcec13ae4f4 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Fri, 24 May 2024 22:08:42 +0200 Subject: [PATCH 02/78] app/build: increase version --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 5fce53b46..cc3f28a3b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 33 - versionCode 35 - versionName "2024-05-04-release" + versionCode 36 + versionName "2024-05-24-release" vectorDrawables.useSupportLibrary = true } From cf771faa024d82dfea1abfa9ac73302f9017b5f3 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sun, 2 Jun 2024 18:09:08 +0200 Subject: [PATCH 03/78] MPVActivity: switch to AudioManagerCompat --- app/src/main/java/is/xyz/mpv/MPVActivity.kt | 46 +++++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/is/xyz/mpv/MPVActivity.kt b/app/src/main/java/is/xyz/mpv/MPVActivity.kt index f3b552798..bc0dfff9d 100644 --- a/app/src/main/java/is/xyz/mpv/MPVActivity.kt +++ b/app/src/main/java/is/xyz/mpv/MPVActivity.kt @@ -38,6 +38,9 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams +import androidx.media.AudioAttributesCompat +import androidx.media.AudioFocusRequestCompat +import androidx.media.AudioManagerCompat import java.io.File import java.lang.IllegalArgumentException import kotlin.math.roundToInt @@ -65,6 +68,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse private var toast: Toast? = null private var audioManager: AudioManager? = null + private var audioFocusRequest: AudioFocusRequestCompat? = null private var audioFocusRestore: () -> Unit = {} private val psc = Utils.PlaybackStateCache() @@ -93,6 +97,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse } } + // Note that after Android 12 this is not necessarily called. private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { type -> Log.v(TAG, "Audio focus changed: $type") if (ignoreAudioFocus) @@ -305,15 +310,26 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager - volumeControlStream = AudioManager.STREAM_MUSIC + volumeControlStream = STREAM_TYPE - @Suppress("DEPRECATION") - val res = audioManager!!.requestAudioFocus( - audioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN - ) - if (res != AudioManager.AUDIOFOCUS_REQUEST_GRANTED && !ignoreAudioFocus) { - Log.w(TAG, "Audio focus not granted") - onloadCommands.add(arrayOf("set", "pause", "yes")) + // Handle audio focus + val req = with (AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN)) { + setAudioAttributes(with (AudioAttributesCompat.Builder()) { + // N.B.: libmpv may use different values in ao_audiotrack, but here we always pretend to be music. + setUsage(AudioAttributesCompat.USAGE_MEDIA) + setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC) + build() + }) + setOnAudioFocusChangeListener(audioFocusChangeListener) + build() + } + val res = AudioManagerCompat.requestAudioFocus(audioManager!!, req) + if (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + audioFocusRequest = req + } else { + Log.v(TAG, "Audio focus not granted") + if (!ignoreAudioFocus) + onloadCommands.add(arrayOf("set", "pause", "yes")) } } @@ -345,8 +361,10 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse } mediaSession = null - @Suppress("DEPRECATION") - audioManager?.abandonAudioFocus(audioFocusChangeListener) + audioFocusRequest?.let { + AudioManagerCompat.abandonAudioFocusRequest(audioManager!!, it) + } + audioFocusRequest = null // take the background service with us stopServiceRunnable.run() @@ -1772,11 +1790,11 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse initialSeek = psc.positionSec initialBright = Utils.getScreenBrightness(this) ?: 0.5f with (audioManager!!) { - initialVolume = getStreamVolume(AudioManager.STREAM_MUSIC) + initialVolume = getStreamVolume(STREAM_TYPE) maxVolume = if (isVolumeFixed) 0 else - getStreamMaxVolume(AudioManager.STREAM_MUSIC) + getStreamMaxVolume(STREAM_TYPE) } pausedForSeek = 0 @@ -1813,7 +1831,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse return val newVolume = (initialVolume + (diff * maxVolume).toInt()).coerceIn(0, maxVolume) val newVolumePercent = 100 * newVolume / maxVolume - audioManager!!.setStreamVolume(AudioManager.STREAM_MUSIC, newVolume, 0) + audioManager!!.setStreamVolume(STREAM_TYPE, newVolume, 0) gestureTextView.text = getString(R.string.ui_volume, newVolumePercent) } @@ -1867,5 +1885,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse private const val RCODE_LOAD_FILE = 1002 // action of result intent private const val RESULT_INTENT = "is.xyz.mpv.MPVActivity.result" + // stream type used with AudioManager + private const val STREAM_TYPE = AudioManager.STREAM_MUSIC } } From 12bfad77a5026357e1338cce622d3fd579022cfd Mon Sep 17 00:00:00 2001 From: sfan5 Date: Tue, 4 Jun 2024 15:36:18 +0200 Subject: [PATCH 04/78] MPVView: revert back to using aspect before filters Otherwise the automatic rotation will work "against" the user setting --video-rotate. This reverts to the old way of using the information as returned from decoding, so won't work for complex filter chains. fixes: ded0803a36f20b4de6fefb3cef4ff4fc3858bf30 closes #933 --- app/src/main/java/is/xyz/mpv/MPVActivity.kt | 6 +++--- app/src/main/java/is/xyz/mpv/MPVView.kt | 14 ++++++-------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/is/xyz/mpv/MPVActivity.kt b/app/src/main/java/is/xyz/mpv/MPVActivity.kt index bc0dfff9d..c1278c607 100644 --- a/app/src/main/java/is/xyz/mpv/MPVActivity.kt +++ b/app/src/main/java/is/xyz/mpv/MPVActivity.kt @@ -1576,7 +1576,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse if (initial || player.vid == -1) return - val ratio = player.getVideoOutAspect()?.toFloat() ?: 0f + val ratio = player.getVideoAspect()?.toFloat() ?: 0f if (ratio == 0f || ratio in (1f / ASPECT_RATIO_MIN) .. ASPECT_RATIO_MIN) { // video is square, let Android do what it wants requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED @@ -1603,7 +1603,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse } val params = with(PictureInPictureParams.Builder()) { - val aspect = player.getVideoOutAspect() ?: 0.0 + val aspect = player.getVideoAspect() ?: 0.0 setAspectRatio(Rational(aspect.times(10000).toInt(), 10000)) setActions(listOf(action1)) } @@ -1663,7 +1663,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse if (!activityIsForeground) return when (property) { "track-list" -> player.loadTracks() - "video-out-params/aspect", "video-out-params/rotate" -> { + "video-params/aspect", "video-params/rotate" -> { updateOrientation() updatePiPParams() } diff --git a/app/src/main/java/is/xyz/mpv/MPVView.kt b/app/src/main/java/is/xyz/mpv/MPVView.kt index d16540592..e8a4ebfc0 100644 --- a/app/src/main/java/is/xyz/mpv/MPVView.kt +++ b/app/src/main/java/is/xyz/mpv/MPVView.kt @@ -9,7 +9,6 @@ import android.os.Build import android.os.Environment import android.preference.PreferenceManager import android.view.* -import kotlin.math.abs import kotlin.reflect.KProperty internal class MPVView(context: Context, attrs: AttributeSet) : SurfaceView(context, attrs), SurfaceHolder.Callback { @@ -218,8 +217,8 @@ internal class MPVView(context: Context, attrs: AttributeSet) : SurfaceView(cont Property("track-list"), // observing double properties is not hooked up in the JNI code, but doing this // will restrict updates to when it actually changes - Property("video-out-params/aspect", MPV_FORMAT_DOUBLE), - Property("video-out-params/rotate", MPV_FORMAT_DOUBLE), + Property("video-params/aspect", MPV_FORMAT_DOUBLE), + Property("video-params/rotate", MPV_FORMAT_DOUBLE), // Property("playlist-pos", MPV_FORMAT_INT64), Property("playlist-count", MPV_FORMAT_INT64), @@ -341,14 +340,13 @@ internal class MPVView(context: Context, attrs: AttributeSet) : SurfaceView(cont get() = MPVLib.getPropertyDouble("estimated-vf-fps") /** - * Returns the video aspect ratio after video filters (before VO). - * Rotation is taken into account. + * Returns the video aspect ratio. Rotation is taken into account. */ - fun getVideoOutAspect(): Double? { - return MPVLib.getPropertyDouble("video-out-params/aspect")?.let { + fun getVideoAspect(): Double? { + return MPVLib.getPropertyDouble("video-params/aspect")?.let { if (it < 0.001) return 0.0 - val rot = MPVLib.getPropertyInt("video-out-params/rotate") ?: 0 + val rot = MPVLib.getPropertyInt("video-params/rotate") ?: 0 if (rot % 180 == 90) 1.0 / it else From 85942975cfac569c3765cb3c2db64e23bf637381 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Wed, 5 Jun 2024 18:57:54 +0200 Subject: [PATCH 05/78] MPVActivity: fix race condition with init and observer closes #935 --- app/src/main/java/is/xyz/mpv/MPVActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/is/xyz/mpv/MPVActivity.kt b/app/src/main/java/is/xyz/mpv/MPVActivity.kt index c1278c607..a7b38ed9f 100644 --- a/app/src/main/java/is/xyz/mpv/MPVActivity.kt +++ b/app/src/main/java/is/xyz/mpv/MPVActivity.kt @@ -294,8 +294,8 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse return } - player.initialize(applicationContext.filesDir.path, applicationContext.cacheDir.path) player.addObserver(this) + player.initialize(filesDir.path, cacheDir.path) player.playFile(filepath) binding.playbackSeekbar.setOnSeekBarChangeListener(seekBarChangeListener) From 080725b7387ea07ac9fc9ae13ce0b48304fdc675 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Wed, 5 Jun 2024 22:04:46 +0200 Subject: [PATCH 06/78] buildscripts: update harfbuzz, fribidi --- buildscripts/include/depinfo.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/buildscripts/include/depinfo.sh b/buildscripts/include/depinfo.sh index c66d3310b..3779abe76 100755 --- a/buildscripts/include/depinfo.sh +++ b/buildscripts/include/depinfo.sh @@ -11,8 +11,8 @@ v_sdk_build_tools=34.0.0 v_lua=5.2.4 v_unibreak=6.1 -v_harfbuzz=8.4.0 -v_fribidi=1.0.13 +v_harfbuzz=8.5.0 +v_fribidi=1.0.14 v_freetype=2-13-2 v_mbedtls=3.5.2 From d16ecf19d138428cba8b89c1cdf3b7a509aee622 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Wed, 5 Jun 2024 22:20:53 +0200 Subject: [PATCH 07/78] app/build: bump version another round on the review carousel --- app/build.gradle | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index cc3f28a3b..5597a56fa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,10 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 33 - versionCode 36 - versionName "2024-05-24-release" + + versionCode 37 + versionName "2024-06-05-release" + vectorDrawables.useSupportLibrary = true } From d57e3ce5a57bf324fc9fc28eeddaf7d2c082a635 Mon Sep 17 00:00:00 2001 From: Yevhen Popok Date: Sun, 9 Jun 2024 19:40:50 +0300 Subject: [PATCH 08/78] Update Ukrainian translation (#940) --- app/src/main/res/values-uk/arrays.xml | 54 +++++++-- app/src/main/res/values-uk/strings.xml | 145 ++++++++++++++----------- 2 files changed, 127 insertions(+), 72 deletions(-) diff --git a/app/src/main/res/values-uk/arrays.xml b/app/src/main/res/values-uk/arrays.xml index d32a0295b..e10591765 100644 --- a/app/src/main/res/values-uk/arrays.xml +++ b/app/src/main/res/values-uk/arrays.xml @@ -1,20 +1,54 @@ + + + Початкове + 16:9 + 16:10 + 4:3 + 2.35:1 + Панорамування/Cканування + + + Ніколи - Тільки з аудіофайлами - Завжди (також відеофайли) + Лише з аудіофайлами + Завжди (включно з відеофайлами) + + + + Автоматично + Типово ландшафтна + Типово портретна + Як на пристрої + - Вимкненно - CPU - GPU + Вимкнено + Процесор + Відеокарта + - Немає - FPS - stats.lua: General - stats.lua: Timings - stats.lua: Cache + Нічого + Кадри за секунду + stats.lua: Загальне + stats.lua: Таймінг + stats.lua: Кеш + + + + Нічого + Позиціювання + Гучність + Яскравість + + + + Нічого + Позиціювання + Відтворення/Пауза + Користувацьке diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 53b8bf578..33d2c856f 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -1,7 +1,10 @@ - + + mpv + Відтворити з mpv Налаштування OK Скасувати @@ -10,9 +13,14 @@ Ні Назад Вперед - Звук - Субтитри + Скинути + Аудіо + Субтитри Вимкнути + Основне + Додаткове + Відтворення + Пауза Звук: %d%% Яскравість: %d%% %1$s\n[%2$s] @@ -27,10 +35,10 @@ Швидкість відтворення - Не вказано жодного файлу, допобачення :D - Файл додано до плей-листа - %d елементи залишились в списку відтворення.\nВи точно хочете вийти? - Контрасність + Не надано жодного файлу, виходжу + Файл додано до списку відтворення + Залишилось елементів в списку відтворення: %d.\nВи дійсно хочете вийти? + Контрастність Яскравість відео Гамма Насиченість @@ -38,107 +46,120 @@ Затримка звуку Затримка субтитрів Показ усіх файлів - Показ тільки медіа файлів + Показ лише медіафайлів + Запамʼятати вибір для наступного запуску + Некоректний протокол - Змінити оріентацію - Відкрийте зовнішнє аудіо… - Відкрийте зовнішні субтитри… - Розширенні… - Програвати в фоні - Переключити статистику + Змінити орієнтацію + Відкрити зовнішнє аудіо… + Відкрити зовнішні субтитри… + Додатково… + Фонове відтворення + Показати/приховати статистику Розділ - Шукати за субтитрами + Позиціювання за субтитрами + Вибір файлів\n(застарілий) Налаштування - Відкрити посилання - Виберіть файл + Список відтворення + Відкрити адресу + Відкрити документ + Відкрити дерево документів + Вибір файлів + Вибір файлів (застарілий) Зовнішнє сховище - Увімкнути / вимкнути фільтр + Увімкнути/вимкнути фільтр - Головні + Загальне + Відео + Інтерфейс користувача + Жести дотику + Для розробника + Розширені - Шлях за замовчуванням до файлового менеджера + Типовий шлях для менеджера файлів - Мова звуку за замовчуванням - Виберіть мову (мови) звуку, яка буде вибрана за замовчуванням під час відтворення відео з кількома потоками звуку.\nЗазвичай використовують дво або трибуквенні коди мов. Кілька значень можна розділити комою. + Типова мова аудіо + Бажана до вибору мова (чи перелік мов) аудіо при відтворенні відео з декількома аудіопотоками.\nПрацюють дво- та трилітерні мовні коди. Значення можна розділяти комами. - Мова субтитрів за замовчуванням - Виберіть мову (мови) субтитрів, яка буде вибрана за замовчуванням під час відтворення відео з кількома потоками звуку.\nЗазвичай використовують дво або трибуквенні коди мов. Кілька значень можна розділити комою. + Типова мова субтитрів + Бажана до вибору мова (чи перелік мов) субтитрів при відтворенні відео з декількома субтитрами.\nПрацюють дво- та трилітерні мовні коди. Значення можна розділяти комами. Апаратне декодування - Якщо вибрано цей параметр, буде здійснено спробу апаратного декодування. + Спробувати апаратне декодування, в разі невдачі повернутися до програмного. Загалом, це підвищує ефективність. Фонове відтворення - Коли автоматично поновлювати відтворення у фоновому режимі + Визначає, чи потрібно продовжувати відтворення на фоні. - Зберегти позицію після виходу - Зберігання поточної позицію відтворення при виході. Коли той самий файл буде відтворено знову, відтворення почнеться з попередньої позиції. + Зберігати позицію при виході + Запамʼятати поточну позицію відтворення при виході. Коли той самий файл буде відкрито повторно, mpv повернеться до попереднього часу відтворення. - Орієнтація екрану - Вирішіть, в якій орієнтації mpv буде відтворювати альбомне або портретне відео + Орієнтація екрана + Визначає, в якій орієнтації mpv буде відтворювати ландшафтні чи портретні відео. - Відображення заголовка - Якщо вибрано, заголовок медіа чи імя файлу відео відображаєтиметься над елементами керування. Аудіофайли завжди відображатимуть їх назву, виконавця та альбом. + Показувати заголовки (для відео) + Показувати заголовок чи назву відеофайлу над елементами керування. Для аудіофайлів завжди будуть виводитись їхні метадані. - Показати елементи керування внизу екрана + Показувати елементи керування знизу екрана + Продовжувати відтворення, коли є додаткові вікна + Продовжувати відтворення, коли відкриті додаткові вікна (напр., діалог списку відтворення). - Сенсорні жести + Підтверджувати вихід, коли є список відтворення + Виводити діалог підтвердження виходу, коли завантажений список відтворення. - Горизонтальне перетягування + Більш плавне прокручування - Вертикальне перетягування (зліва) + Горизонтальне перетягування - Вертикальне перетягування (зправа) + Вертикальне перетягування (лівий бік) - Двічі натисніть (зліва) + Вертикальне перетягування (правий бік) - Двічі натисніть (по центру) + Подвійне натискання (зліва) - Двічі натисніть (зправа) + Подвійне натискання (по центру) + Подвійне натискання (справа) - Відео + Коли вибрано \"Користувацьке\", для жесту дотику можна призначити будь-яку команду, відредагувавши input.conf. Коди клавіш: 0x10001 (зліва), 0x10002 (по центру), 0x10003 (справа).\nНаприклад, можна призначити гортання на 6 секунд:\n\t\t0x10003 no-osd seek 6 Фільтр збільшення масштабу Фільтр зменшення масштабу - Обробка - Встановіть на чому обробляти відео + Розшарування + Виберіть режим розшарування відео. Інтерполяція - Зменште тремтіння, спричинене невідповідним FPS відео та частотою оновлення дисплея. + Зменшіть тремтіння, спричинене різницею між частотою кадрів відео та частотою оновлення дисплея. - Часовий інтерполяційний фільтр - Виберіть фільтр, який використовується для інтерполяції часової осі (кадрів) - Ці налаштування набувають чинності лише в тому випадку, якщо інтерполяція ввімкнена вище. + Тимчасовий фільтр інтерполяції + Виберіть фільтр для інтерполяції тимчасової осі (кадрів). + Ці налаштування працюють лише якщо увімкнено інтерполяцію. Низькоякісне декодування відео - Угода якості для швидкості і, таким чином плавне відтворення.\nСИЛЬНО ЗНИЖУЄ ЯКІСТЬ - - - Розробник - - Ігнорувати фокус аудіо - Якщо вибране відтворення не буде призупинено або зменшено гучність, коли інші програми одночасно відтворюють звук. + Покращує продуктивність, а отже і плавність відтворення, за рахунок погіршення якості.\nЗНАЧНО ПОГІРШУЄ ЯКІСТЬ - Показати статистику - Виберіть, яка статистика відображатиметься + Ігнорувати аудіофокус + Не призупиняти відтворення та не зменшувати гучність при паралельному відтворенні аудіо іншими застосунками. - Увімкнути налагодження OpenGL + Показ статистики + Виберіть, яку статистику показувати + Увімкнути зневадження OpenGL - Розширенні + Використовувати gpu-next + Використати новий бекенд обробки відео на основі libplacebo. Інформація про версію Редагувати mpv.conf - Ви можете безпосередньо редагувати конфігурацію mpv тут. + Ви можете відредагувати конфігурацію mpv безпосередньо тут. Редагувати input.conf - Ви можете редагувати input.conf тут, це в основному корисно для пультів дистанційного керування телевізором та клавіатур. \n\nВажливо: mpv-android має деякі закодовані клавіші, які тут не можна перевизначити, наприклад \'j \' для циклу субтитрів. + Тут ви можете відредагувати input.conf, здебільшого це корисно для пультів дистанційного керування телевізором та клавіатур.\n\nВажливо: в mpv-android є деякі жорстко прописані в коді клавіші, які не можна тут перепризначити, напр., \'j\' для гортання субтитрів. Параметр 1: @@ -147,6 +168,6 @@ Синхронізація відео: - Імя - Дозвіл на доступ до файлової системи відмовлено + Назва + Відмовлено у доступі до файлової системи From 7e8fad6b8ff5004717e9608d4ec0553af58301a1 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sat, 22 Jun 2024 14:24:41 +0200 Subject: [PATCH 09/78] DocumentPickerFragment: fix getParent crashing at root fixes: 12bfad77a5026357e1338cce622d3fd579022cfd --- .../main/java/is/xyz/filepicker/DocumentPickerFragment.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/is/xyz/filepicker/DocumentPickerFragment.java b/app/src/main/java/is/xyz/filepicker/DocumentPickerFragment.java index 44b5dc639..f5d944ee3 100644 --- a/app/src/main/java/is/xyz/filepicker/DocumentPickerFragment.java +++ b/app/src/main/java/is/xyz/filepicker/DocumentPickerFragment.java @@ -113,6 +113,10 @@ public String getName(@NonNull Uri path) { @NonNull @Override public Uri getParent(@NonNull Uri from) { + // root path is a tree and would error if given to getDocumentId(), catch that early + if (from.equals(getRoot())) + return getRoot(); + String docId = DocumentsContract.getDocumentId(from); Uri parent = mParents.get(docId); if (parent != null) From 943dac516b231048139f2181dd2c1b63d9c715f4 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Mon, 24 Jun 2024 23:19:04 +0200 Subject: [PATCH 10/78] all: update some documentation --- README.md | 16 ++++++++++---- buildscripts/README.md | 32 +++++++++++++--------------- buildscripts/include/download-sdk.sh | 15 +++++++------ docs/intent.html | 4 ++-- 4 files changed, 38 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 2137f7dc7..60470d75f 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,17 @@ mpv-android is a video player for Android based on [libmpv](https://github.com/m * Gesture-based seeking, volume/brightness control and more * libass support for styled subtitles * Secondary (or dual) subtitle support -* Advanced video settings (interpolation, debanding, scalers, ...) +* High-quality rendering with advanced settings (scalers, debanding, interpolation, ...) * Play network streams with the "Open URL" function * Background playback, Picture-in-Picture, keyboard input supported -Note that mpv-android is *not* a library you can embed into your app, but you can look here for inspiration. +### Library? + +mpv-android is **not** a library/module (AAR) you can import into your app. + +If you'd like to use libmpv in your app you can use our code as inspiration. The important parts are [`MPVLib`](app/src/main/java/is/xyz/mpv/MPVLib.java), [`MPVView`](app/src/main/java/is/xyz/mpv/MPVView.kt) and the [native code](app/src/main/jni/). -libmpv/ffmpeg is built by [these scripts](buildscripts/). +Native code is built by [these scripts](buildscripts/). ## Downloads @@ -26,6 +30,10 @@ You can download mpv-android from the [Releases section](https://github.com/mpv- [Get it on F-Droid](https://f-droid.org/packages/is.xyz.mpv) +**Note**: Android TV is supported, but only available on F-Droid or by installing the APK manually. + ## Building from source -Take a look at [README.md](buildscripts/README.md) inside the `buildscripts` directory. +Take a look at the [README](buildscripts/README.md) inside the `buildscripts` directory. + +Some other documentation can be found at this [link](http://mpv-android.github.io/mpv-android/). diff --git a/buildscripts/README.md b/buildscripts/README.md index b8d4690f6..2049071bd 100644 --- a/buildscripts/README.md +++ b/buildscripts/README.md @@ -1,12 +1,16 @@ # Building +Compiling the native parts is a process separate from Gradle and the app won't function if you skip this. + +This process is supported on Linux and macOS. Windows (or WSL) will **not** work. + ## Download dependencies `download.sh` will take care of installing the Android SDK, NDK and downloading the sources. If you're running on Debian/Ubuntu or RHEL/Fedora it will also install the necessary dependencies for you. -``` +```sh ./download.sh ``` @@ -17,19 +21,21 @@ A matching NDK version inside the SDK will be picked up automatically or downloa ## Build -``` +```sh ./buildall.sh ``` Run `buildall.sh` with `--clean` to clean the build directories before building. +For a guaranteed clean build also do a `rm -rf prefix` beforehand. Building for just 32-bit ARM (which is the default) is fine generally. However if you want to make use of AArch64 or are targeting Intel x86 devices, these architectures can be optionally be built into the same APK. -To do this run one (or both) of these commands **before** ./buildall.sh: -``` +To do this run one (or more) of these commands **before** ./buildall.sh: +```sh ./buildall.sh --arch arm64 mpv +./buildall.sh --arch x86 mpv ./buildall.sh --arch x86_64 mpv ``` @@ -37,7 +43,7 @@ To do this run one (or both) of these commands **before** ./buildall.sh: ## Getting logs -``` +```sh adb logcat # get all logs, useful when drivers/vendor libs output to logcat adb logcat -s "mpv" # get only mpv logs ``` @@ -46,7 +52,7 @@ adb logcat -s "mpv" # get only mpv logs If you've made changes to a single component (e.g. ffmpeg or mpv) and want a new build you can of course just run ./buildall.sh but it's also possible to just build a single component like this: -``` +```sh ./buildall.sh -n ffmpeg # optional: add --clean to build from a clean state ``` @@ -55,7 +61,7 @@ Note that you might need to be rebuild for other architectures (`--arch`) too de Afterwards, build mpv-android and install the apk: -``` +```sh ./buildall.sh -n adb install -r ../app/build/outputs/apk/debug/app-debug.apk ``` @@ -82,22 +88,14 @@ Also, debugging native code does not work from within the studio at the moment, You first need to rebuild mpv-android with gdbserver support: -``` +```sh NDK_DEBUG=1 ./buildall.sh -n adb install -r ../app/build/outputs/apk/debug/app-debug.apk ``` After that, ndk-gdb can be used to debug the app: -``` +```sh cd mpv-android/app/src/main/ ../../../buildscripts/sdk/android-ndk-r*/ndk-gdb --launch ``` - -# Credits, notes, etc - -Travis will create prebuilt prefixes whenever needed, see `build_prefix()` in `.travis.sh`. -These prefixes contain everything except mpv built for `armv7l` and are uploaded [here](https://github.com/mpv-android/prebuilt-prefixes/releases). - -These build scripts were created by @sfan5, thanks! - diff --git a/buildscripts/include/download-sdk.sh b/buildscripts/include/download-sdk.sh index e0c624e5c..5794b50d8 100755 --- a/buildscripts/include/download-sdk.sh +++ b/buildscripts/include/download-sdk.sh @@ -9,14 +9,17 @@ if [ "$os" == "linux" ]; then if [ $TRAVIS -eq 0 ]; then - hash yum &>/dev/null && { + if hash yum &>/dev/null; then sudo yum install autoconf pkgconfig libtool ninja-build \ - python3-pip python3-setuptools unzip wget; - python3 -m pip install meson; } - apt-get -v &>/dev/null && { + python3-pip unzip wget + pip3 install -U meson + elif apt-get -v &>/dev/null; then sudo apt-get install autoconf pkg-config libtool ninja-build \ - python3-pip python3-setuptools unzip; - python3 -m pip install meson; } + python3-pip unzip wget + pip3 install -U meson + else + echo "Note: dependencies were not installed, you have to do that manually." + fi fi if ! javac -version &>/dev/null; then diff --git a/docs/intent.html b/docs/intent.html index 07c918b99..1463161e0 100644 --- a/docs/intent.html +++ b/docs/intent.html @@ -14,7 +14,7 @@

Intent

  • data: URI with scheme rtmp, rtmps, rtp, rtsp, mms, mmst, mmsh, tcp, udp - (as supported by FFmpeg)
    + (as supported by FFmpeg)
    or
  • @@ -77,7 +77,7 @@

    Result Intent

    Notes

    -

    This API was inspired by the counterpart in MXPlayer.
    +

    This API was inspired by the counterpart in MXPlayer.
    Note that only Java code is powerful enough to use the full features of this specification or receive result intents.

    From 2b28598fd9f5ba8fd54652e3aee54b1b05ef936c Mon Sep 17 00:00:00 2001 From: Migz <12945959+9mdv@users.noreply.github.com> Date: Wed, 26 Jun 2024 19:00:08 +0800 Subject: [PATCH 11/78] app: declare android:installLocation to auto --- app/src/main/AndroidManifest.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ef9355bca..5e29bca37 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ - + Date: Mon, 15 Jul 2024 16:46:19 +0200 Subject: [PATCH 12/78] app: update Gradle and libraries --- app/build.gradle | 2 +- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 5597a56fa..eb57244c7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -73,7 +73,7 @@ android.applicationVariants.all { variant -> dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'androidx.media:media:1.7.0' } diff --git a/build.gradle b/build.gradle index 581b030c9..d2337c462 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:8.4.0' + classpath 'com.android.tools.build:gradle:8.5.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a80b22ce5..b82aa23a4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From e43a7283236faee26648d0f5e54f09ea1f114520 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Mon, 15 Jul 2024 16:46:45 +0200 Subject: [PATCH 13/78] app: raise target sdk to 34 --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 1 + app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.kt | 3 ++- app/src/main/java/is/xyz/mpv/FilePickerActivity.kt | 4 ++-- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index eb57244c7..5acc57a9c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,7 +10,7 @@ android { defaultConfig { minSdkVersion 21 - targetSdkVersion 33 + targetSdkVersion 34 versionCode 37 versionName "2024-06-05-release" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5e29bca37..afb232915 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ + Date: Mon, 15 Jul 2024 21:52:21 +0200 Subject: [PATCH 14/78] various: use more compat helpers --- .../is/xyz/filepicker/FilePickerFragment.java | 4 ++-- .../is/xyz/mpv/BackgroundPlaybackService.kt | 7 ++----- app/src/main/java/is/xyz/mpv/MPVActivity.kt | 17 +++++++---------- app/src/main/java/is/xyz/mpv/Utils.kt | 12 ++++++++++++ .../java/is/xyz/mpv/config/SettingsActivity.kt | 1 - 5 files changed, 23 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/is/xyz/filepicker/FilePickerFragment.java b/app/src/main/java/is/xyz/filepicker/FilePickerFragment.java index 4d05b4b5b..e080f8a99 100644 --- a/app/src/main/java/is/xyz/filepicker/FilePickerFragment.java +++ b/app/src/main/java/is/xyz/filepicker/FilePickerFragment.java @@ -11,13 +11,13 @@ import android.Manifest; import android.content.Context; import android.content.pm.PackageManager; -import android.net.Uri; import android.os.Build; import android.os.FileObserver; import android.util.Log; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.loader.content.AsyncTaskLoader; import androidx.core.content.ContextCompat; import androidx.loader.content.Loader; @@ -26,7 +26,6 @@ import java.io.FileFilter; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.List; /** @@ -37,6 +36,7 @@ public class FilePickerFragment extends AbstractFilePickerFragment { protected static final int PERMISSIONS_REQUEST_ID = 1001; protected static final String PERMISSION_PRE33 = Manifest.permission.WRITE_EXTERNAL_STORAGE; + @RequiresApi(33) protected static final String[] PERMISSIONS_POST33 = { Manifest.permission.READ_MEDIA_AUDIO, Manifest.permission.READ_MEDIA_IMAGES, diff --git a/app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.kt b/app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.kt index 6ce160289..7008cc7fb 100644 --- a/app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.kt +++ b/app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.kt @@ -5,7 +5,6 @@ import android.app.* import android.content.Context import android.content.Intent import android.graphics.Bitmap -import android.os.Build import android.os.IBinder import android.support.v4.media.session.MediaSessionCompat import android.util.Log @@ -14,6 +13,7 @@ import androidx.annotation.StringRes import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import androidx.core.app.PendingIntentCompat import androidx.core.app.ServiceCompat import androidx.media.app.NotificationCompat.MediaStyle @@ -46,10 +46,7 @@ class BackgroundPlaybackService : Service(), MPVLib.EventObserver { @SuppressLint("UnspecifiedImmutableFlag") private fun buildNotification(): Notification { val notificationIntent = Intent(this, MPVActivity::class.java) - val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) - PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE) - else - PendingIntent.getActivity(this, 0, notificationIntent, 0) + val pendingIntent = PendingIntentCompat.getActivity(this, 0, notificationIntent, 0, false) val builder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) diff --git a/app/src/main/java/is/xyz/mpv/MPVActivity.kt b/app/src/main/java/is/xyz/mpv/MPVActivity.kt index a7b38ed9f..d8747fb1a 100644 --- a/app/src/main/java/is/xyz/mpv/MPVActivity.kt +++ b/app/src/main/java/is/xyz/mpv/MPVActivity.kt @@ -451,10 +451,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse Log.v(TAG, "Resuming playback in background") stopServiceHandler.removeCallbacks(stopServiceRunnable) val serviceIntent = Intent(this, BackgroundPlaybackService::class.java) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - startForegroundService(serviceIntent) - else - startService(serviceIntent) + ContextCompat.startForegroundService(this, serviceIntent) } } @@ -972,20 +969,20 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse if (extras.getByte("decode_mode") == 2.toByte()) onloadCommands.add(arrayOf("set", "file-local-options/hwdec", "no")) if (extras.containsKey("subs")) { - val subList = extras.getParcelableArray("subs")?.mapNotNull { it as? Uri } ?: emptyList() - val subsToEnable = extras.getParcelableArray("subs.enable")?.mapNotNull { it as? Uri } ?: emptyList() + val subList = Utils.getParcelableArray(extras, "subs") + val subsToEnable = Utils.getParcelableArray(extras, "subs.enable") for (suburi in subList) { val subfile = resolveUri(suburi) ?: continue - val flag = if (subsToEnable.filter { it.compareTo(suburi) == 0 }.any()) "select" else "auto" + val flag = if (subsToEnable.any { it == suburi }) "select" else "auto" Log.v(TAG, "Adding subtitles from intent extras: $subfile") onloadCommands.add(arrayOf("sub-add", subfile, flag)) } } - if (extras.getInt("position", 0) > 0) { - val pos = extras.getInt("position", 0) / 1000f - onloadCommands.add(arrayOf("set", "start", pos.toString())) + extras.getInt("position", 0).let { + if (it > 0) + onloadCommands.add(arrayOf("set", "start", "${it / 1000f}")) } } diff --git a/app/src/main/java/is/xyz/mpv/Utils.kt b/app/src/main/java/is/xyz/mpv/Utils.kt index 3b8359ae5..c58800c5a 100644 --- a/app/src/main/java/is/xyz/mpv/Utils.kt +++ b/app/src/main/java/is/xyz/mpv/Utils.kt @@ -6,7 +6,9 @@ import android.content.Context import android.content.res.AssetManager import android.net.Uri import android.os.Build +import android.os.Bundle import android.os.Environment +import android.os.Parcelable import android.os.storage.StorageManager import android.provider.Settings import android.support.v4.media.MediaMetadataCompat @@ -19,6 +21,7 @@ import android.view.View import android.view.ViewGroup import android.widget.EditText import androidx.appcompat.app.AlertDialog +import androidx.core.os.BundleCompat import androidx.core.widget.addTextChangedListener import java.io.* import kotlin.math.abs @@ -393,6 +396,15 @@ internal object Utils { get() = editText.text.toString() } + @Suppress("UNCHECKED_CAST") + inline fun getParcelableArray(bundle: Bundle, key: String): Array { + val array = BundleCompat.getParcelableArray(bundle, key, T::class.java) + return if (array == null) + emptyArray() + else + array as Array + } + private const val TAG = "mpv" // This is used to filter files in the file picker, so it contains just about everything diff --git a/app/src/main/java/is/xyz/mpv/config/SettingsActivity.kt b/app/src/main/java/is/xyz/mpv/config/SettingsActivity.kt index 1b063e0cb..2fcd2b8f4 100644 --- a/app/src/main/java/is/xyz/mpv/config/SettingsActivity.kt +++ b/app/src/main/java/is/xyz/mpv/config/SettingsActivity.kt @@ -5,7 +5,6 @@ import `is`.xyz.mpv.R import android.content.Context import android.content.pm.PackageManager import android.content.res.Configuration -import android.os.Build import android.os.Bundle import android.preference.PreferenceActivity import android.preference.PreferenceFragment From 5538e6ec77728c566aefab4d7933e3d36d0b647f Mon Sep 17 00:00:00 2001 From: sfan5 Date: Mon, 15 Jul 2024 22:05:11 +0200 Subject: [PATCH 15/78] docs: extend intent code example --- docs/intent.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/intent.html b/docs/intent.html index 1463161e0..10fada294 100644 --- a/docs/intent.html +++ b/docs/intent.html @@ -51,6 +51,8 @@
    Kotlin
    val intent = Intent(Intent.ACTION_VIEW)
     intent.setDataAndType(Uri.parse("https://example.org/media.png"), "video/any")
     intent.setPackage("is.xyz.mpv")
    +val subtitle = Uri.parse("https://example.org/subtitle.srt")
    +intent.putExtra("subs", arrayOf<Uri>(subtitle))
     startActivity(intent)
    HTML (Chrome)
    <a href="intent://example.org/media.png#Intent;type=video/any;package=is.xyz.mpv;scheme=https;end;">Click me</a>
    From 5394087991ff70d01c5e9d375084c274e55cbd5e Mon Sep 17 00:00:00 2001 From: sfan5 Date: Mon, 15 Jul 2024 22:23:50 +0200 Subject: [PATCH 16/78] MPVActivity: allow specifying media title via intent --- app/src/main/java/is/xyz/mpv/MPVActivity.kt | 13 +++++++++++-- docs/intent.html | 5 ++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/is/xyz/mpv/MPVActivity.kt b/app/src/main/java/is/xyz/mpv/MPVActivity.kt index d8747fb1a..b9970a608 100644 --- a/app/src/main/java/is/xyz/mpv/MPVActivity.kt +++ b/app/src/main/java/is/xyz/mpv/MPVActivity.kt @@ -965,9 +965,13 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse if (extras == null) return + fun pushOption(key: String, value: String) { + onloadCommands.add(arrayOf("set", "file-local-options/${key}", value)) + } + // Refer to http://mpv-android.github.io/mpv-android/intent.html if (extras.getByte("decode_mode") == 2.toByte()) - onloadCommands.add(arrayOf("set", "file-local-options/hwdec", "no")) + pushOption("hwdec", "no") if (extras.containsKey("subs")) { val subList = Utils.getParcelableArray(extras, "subs") val subsToEnable = Utils.getParcelableArray(extras, "subs.enable") @@ -982,8 +986,13 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse } extras.getInt("position", 0).let { if (it > 0) - onloadCommands.add(arrayOf("set", "start", "${it / 1000f}")) + pushOption("start", "${it / 1000f}") + } + extras.getString("title", "").let { + if (!it.isNullOrEmpty()) + pushOption("force-media-title", it) } + // TODO: `headers` would be good, maybe `tls_verify` } // UI (Part 2) diff --git a/docs/intent.html b/docs/intent.html index 10fada294..c41f63901 100644 --- a/docs/intent.html +++ b/docs/intent.html @@ -28,7 +28,7 @@

    Intent

If you need to force an URL to be opened in mpv regardless of the file extension set the MIME type to video/any.
- extras: (optional)

+ extras: (all optional)

  • decode_mode (Byte): if set to 2, hardware decoding will be disabled @@ -42,6 +42,9 @@

    Intent

  • position (Int): starting point of video playback in milliseconds
  • +
  • + title (String): media title to show for this file +
From 56c193d800ff5f8d7716c4b4b689b46393dd7c8c Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sun, 21 Jul 2024 23:43:48 +0200 Subject: [PATCH 17/78] buildscripts: update ndk --- buildscripts/include/depinfo.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/buildscripts/include/depinfo.sh b/buildscripts/include/depinfo.sh index 3779abe76..8564ee7c9 100755 --- a/buildscripts/include/depinfo.sh +++ b/buildscripts/include/depinfo.sh @@ -4,8 +4,8 @@ # Make sure to keep v_ndk and v_ndk_n in sync, both are listed on the NDK download page v_sdk=11076708_latest -v_ndk=r26d -v_ndk_n=26.3.11579264 +v_ndk=r27 +v_ndk_n=27.0.12077973 v_sdk_platform=34 v_sdk_build_tools=34.0.0 From 7ae2b0fdc7f5a0948a1327191bf56798884f839b Mon Sep 17 00:00:00 2001 From: sfan5 Date: Mon, 22 Jul 2024 11:19:46 +0200 Subject: [PATCH 18/78] buildscripts: update pinned ffmpeg --- buildscripts/include/depinfo.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildscripts/include/depinfo.sh b/buildscripts/include/depinfo.sh index 8564ee7c9..bfb1458c4 100755 --- a/buildscripts/include/depinfo.sh +++ b/buildscripts/include/depinfo.sh @@ -37,7 +37,7 @@ dep_mpv_android=(mpv) ## Travis-related # pinned ffmpeg commit used by CI -v_travis_ffmpeg=n6.1.1 +v_travis_ffmpeg=n7.0.1 # filename used to uniquely identify a build prefix travis_tarball="prefix-ndk-${v_ndk}-lua-${v_lua}-unibreak-${v_unibreak}-harfbuzz-${v_harfbuzz}-fribidi-${v_fribidi}-freetype-${v_freetype}-mbedtls-${v_mbedtls}-ffmpeg-${v_travis_ffmpeg}.tgz" From 066a2b10ecfc45d99ed1aa4a6f3c5aa03cdc8ded Mon Sep 17 00:00:00 2001 From: sfan5 Date: Thu, 25 Jul 2024 16:26:13 +0200 Subject: [PATCH 19/78] SubTrackDialog: visually indicate primary/secondary closes #959 --- app/src/main/java/is/xyz/mpv/SubTrackDialog.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/is/xyz/mpv/SubTrackDialog.kt b/app/src/main/java/is/xyz/mpv/SubTrackDialog.kt index e4774dc9b..bf6df8a1b 100644 --- a/app/src/main/java/is/xyz/mpv/SubTrackDialog.kt +++ b/app/src/main/java/is/xyz/mpv/SubTrackDialog.kt @@ -5,6 +5,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.CheckedTextView +import androidx.core.content.ContextCompat import androidx.core.view.ViewCompat import androidx.recyclerview.widget.RecyclerView @@ -46,6 +47,11 @@ internal class SubTrackDialog(private val player: MPVView) { selectedMpvId = player.sid selectedMpvId2 = player.secondarySid + // this is what you get for not using a proper tab view... + val darkenDrawable = ContextCompat.getDrawable(binding.root.context, R.drawable.alpha_darken) + binding.primaryBtn.background = if (secondary) null else darkenDrawable + binding.secondaryBtn.background = if (secondary) darkenDrawable else null + // show primary/secondary toggle if applicable if (secondary || selectedMpvId2 != -1 || tracks.size > 2) { binding.buttonRow.visibility = View.VISIBLE From cc30506e012a49ac6721baab54ee2421ad468860 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Fri, 26 Jul 2024 12:37:32 +0200 Subject: [PATCH 20/78] MPVView: split into minimal base class --- README.md | 2 +- app/src/main/java/is/xyz/mpv/BaseMPVView.kt | 107 ++++++++++++++++++++ app/src/main/java/is/xyz/mpv/MPVView.kt | 82 ++------------- 3 files changed, 119 insertions(+), 72 deletions(-) create mode 100644 app/src/main/java/is/xyz/mpv/BaseMPVView.kt diff --git a/README.md b/README.md index 60470d75f..57ef45946 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ mpv-android is a video player for Android based on [libmpv](https://github.com/m mpv-android is **not** a library/module (AAR) you can import into your app. If you'd like to use libmpv in your app you can use our code as inspiration. -The important parts are [`MPVLib`](app/src/main/java/is/xyz/mpv/MPVLib.java), [`MPVView`](app/src/main/java/is/xyz/mpv/MPVView.kt) and the [native code](app/src/main/jni/). +The important parts are [`MPVLib`](app/src/main/java/is/xyz/mpv/MPVLib.java), [`BaseMPVView`](app/src/main/java/is/xyz/mpv/BaseMPVView.kt) and the [native code](app/src/main/jni/). Native code is built by [these scripts](buildscripts/). ## Downloads diff --git a/app/src/main/java/is/xyz/mpv/BaseMPVView.kt b/app/src/main/java/is/xyz/mpv/BaseMPVView.kt new file mode 100644 index 000000000..88d65cd75 --- /dev/null +++ b/app/src/main/java/is/xyz/mpv/BaseMPVView.kt @@ -0,0 +1,107 @@ +package `is`.xyz.mpv + +import android.content.Context +import android.util.AttributeSet +import android.util.Log +import android.view.SurfaceHolder +import android.view.SurfaceView + +// Contains only the essential code needed to get a picture on the screen + +abstract class BaseMPVView(context: Context, attrs: AttributeSet) : SurfaceView(context, attrs), SurfaceHolder.Callback { + /** + * Initialize libmpv. + * + * Call this once before the view is shown. + */ + fun initialize(configDir: String, cacheDir: String) { + MPVLib.create(context) + + /* set normal options (user-supplied config can override) */ + MPVLib.setOptionString("config", "yes") + MPVLib.setOptionString("config-dir", configDir) + for (opt in arrayOf("gpu-shader-cache-dir", "icc-cache-dir")) + MPVLib.setOptionString(opt, cacheDir) + initOptions() + + MPVLib.init() + + /* set hardcoded options */ + postInitOptions() + // would crash before the surface is attached + MPVLib.setOptionString("force-window", "no") + + holder.addCallback(this) + observeProperties() + } + + /** + * Deinitialize libmpv. + * + * Call this once before the view is destroyed. + */ + fun destroy() { + // Disable surface callbacks to avoid using unintialized mpv state + holder.removeCallback(this) + + MPVLib.destroy() + } + + protected abstract fun initOptions() + protected abstract fun postInitOptions() + + protected abstract fun observeProperties() + + private var filePath: String? = null + + /** + * Set the first file to be played once the player is ready. + */ + fun playFile(filePath: String) { + this.filePath = filePath + } + + private var voInUse: String = "gpu" + + /** + * Sets the VO to use. + * It is automatically disabled/enabled when the surface dis-/appears. + */ + fun setVo(vo: String) { + voInUse = vo + MPVLib.setOptionString("vo", vo) + } + + // Surface callbacks + + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { + MPVLib.setPropertyString("android-surface-size", "${width}x$height") + } + + override fun surfaceCreated(holder: SurfaceHolder) { + Log.w(TAG, "attaching surface") + MPVLib.attachSurface(holder.surface) + // This forces mpv to render subs/osd/whatever into our surface even if it would ordinarily not + MPVLib.setOptionString("force-window", "yes") + + if (filePath != null) { + MPVLib.command(arrayOf("loadfile", filePath as String)) + filePath = null + } else { + // We disable video output when the context disappears, enable it back + MPVLib.setPropertyString("vo", voInUse) + } + } + + override fun surfaceDestroyed(holder: SurfaceHolder) { + Log.w(TAG, "detaching surface") + MPVLib.setPropertyString("vo", "null") + MPVLib.setOptionString("force-window", "no") + MPVLib.detachSurface() + // FIXME: race condition here because detachSurface just sets a property and that is async + } + + companion object { + private const val TAG = "mpv" + } +} diff --git a/app/src/main/java/is/xyz/mpv/MPVView.kt b/app/src/main/java/is/xyz/mpv/MPVView.kt index e8a4ebfc0..e0868b64e 100644 --- a/app/src/main/java/is/xyz/mpv/MPVView.kt +++ b/app/src/main/java/is/xyz/mpv/MPVView.kt @@ -11,41 +11,18 @@ import android.preference.PreferenceManager import android.view.* import kotlin.reflect.KProperty -internal class MPVView(context: Context, attrs: AttributeSet) : SurfaceView(context, attrs), SurfaceHolder.Callback { - fun initialize(configDir: String, cacheDir: String) { - MPVLib.create(this.context) - MPVLib.setOptionString("config", "yes") - MPVLib.setOptionString("config-dir", configDir) - for (opt in arrayOf("gpu-shader-cache-dir", "icc-cache-dir")) - MPVLib.setOptionString(opt, cacheDir) - initOptions() // do this before init() so user-supplied config can override our choices - MPVLib.init() - /* Hardcoded options: */ - // we need to call write-watch-later manually - MPVLib.setOptionString("save-position-on-quit", "no") - // would crash before the surface is attached - MPVLib.setOptionString("force-window", "no") - // "no" wouldn't work and "yes" is not intended by the UI - MPVLib.setOptionString("idle", "once") - - holder.addCallback(this) - observeProperties() - } - - private var voInUse: String = "" - - private fun initOptions() { - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.context) +internal class MPVView(context: Context, attrs: AttributeSet) : BaseMPVView(context, attrs) { + override fun initOptions() { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) // apply phone-optimized defaults MPVLib.setOptionString("profile", "fast") // vo - val vo = if (sharedPreferences.getBoolean("gpu_next", false)) + setVo(if (sharedPreferences.getBoolean("gpu_next", false)) "gpu-next" else - "gpu" - voInUse = vo + "gpu") // hwdec val hwdec = if (sharedPreferences.getBoolean("hardware_decoding", true)) @@ -93,8 +70,6 @@ internal class MPVView(context: Context, attrs: AttributeSet) : SurfaceView(cont MPVLib.setOptionString(mpv_option, preference) } - // set more options - val debandMode = sharedPreferences.getString("video_debanding", "") if (debandMode == "gradfun") { // lower the default radius (16) to improve performance @@ -117,7 +92,6 @@ internal class MPVView(context: Context, attrs: AttributeSet) : SurfaceView(cont MPVLib.setOptionString("vd-lavc-skiploopfilter", "nonkey") } - MPVLib.setOptionString("vo", vo) MPVLib.setOptionString("gpu-context", "android") MPVLib.setOptionString("opengl-es", "yes") MPVLib.setOptionString("hwdec", hwdec) @@ -136,18 +110,12 @@ internal class MPVView(context: Context, attrs: AttributeSet) : SurfaceView(cont MPVLib.setOptionString("screenshot-directory", screenshotDir.path) } - private var filePath: String? = null - - fun playFile(filePath: String) { - this.filePath = filePath - } - - // Called when back button is pressed, or app is shutting down - fun destroy() { - // Disable surface callbacks to avoid using unintialized mpv state - holder.removeCallback(this) + override fun postInitOptions() { + // we need to call write-watch-later manually + MPVLib.setOptionString("save-position-on-quit", "no") - MPVLib.destroy() + // "no" wouldn't work and "yes" is not intended by the UI + MPVLib.setOptionString("idle", "once") } fun onPointerEvent(event: MotionEvent): Boolean { @@ -205,7 +173,7 @@ internal class MPVView(context: Context, attrs: AttributeSet) : SurfaceView(cont return true } - private fun observeProperties() { + override 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) val p = arrayOf( @@ -422,34 +390,6 @@ internal class MPVView(context: Context, attrs: AttributeSet) : SurfaceView(cont MPVLib.setPropertyBoolean("shuffle", newState) } - // Surface callbacks - - override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { - MPVLib.setPropertyString("android-surface-size", "${width}x$height") - } - - override fun surfaceCreated(holder: SurfaceHolder) { - Log.w(TAG, "attaching surface") - MPVLib.attachSurface(holder.surface) - // This forces mpv to render subs/osd/whatever into our surface even if it would ordinarily not - MPVLib.setOptionString("force-window", "yes") - - if (filePath != null) { - MPVLib.command(arrayOf("loadfile", filePath as String)) - filePath = null - } else { - // We disable video output when the context disappears, enable it back - MPVLib.setPropertyString("vo", voInUse) - } - } - - override fun surfaceDestroyed(holder: SurfaceHolder) { - Log.w(TAG, "detaching surface") - MPVLib.setPropertyString("vo", "null") - MPVLib.setOptionString("force-window", "no") - MPVLib.detachSurface() - } - companion object { private const val TAG = "mpv" } From 6b9e86c0cd793e633386dea39cfa635f109cadab Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sat, 27 Jul 2024 10:06:45 +0200 Subject: [PATCH 21/78] TouchGestures: do more to prevent NaNs no idea how this could possibly happen --- app/src/main/java/is/xyz/mpv/TouchGestures.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/is/xyz/mpv/TouchGestures.kt b/app/src/main/java/is/xyz/mpv/TouchGestures.kt index 2d6667160..b2782b357 100644 --- a/app/src/main/java/is/xyz/mpv/TouchGestures.kt +++ b/app/src/main/java/is/xyz/mpv/TouchGestures.kt @@ -62,6 +62,8 @@ internal class TouchGestures(private val observer: TouchGesturesObserver) { private var tapGestureRight : PropertyChange? = null fun setMetrics(width: Float, height: Float) { + if (arrayOf(width, height).any { it.isInfinite() || it.isNaN() }) + throw IllegalArgumentException() this.width = width this.height = height trigger = min(width, height) / TRIGGER_RATE @@ -187,7 +189,7 @@ internal class TouchGestures(private val observer: TouchGesturesObserver) { } fun onTouchEvent(e: MotionEvent): Boolean { - if (width == 0f || height == 0f) + if (width < 1 || height < 1) return false var gestureHandled = false val point = PointF(e.x, e.y) From 8997b77bd4895b984adfa91aee97320da29f667b Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sat, 27 Jul 2024 10:46:00 +0200 Subject: [PATCH 22/78] MPVView: use compat method to get display --- app/src/main/java/is/xyz/mpv/MPVView.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/is/xyz/mpv/MPVView.kt b/app/src/main/java/is/xyz/mpv/MPVView.kt index e0868b64e..702b7a083 100644 --- a/app/src/main/java/is/xyz/mpv/MPVView.kt +++ b/app/src/main/java/is/xyz/mpv/MPVView.kt @@ -1,14 +1,14 @@ package `is`.xyz.mpv import android.content.Context -import android.util.AttributeSet -import android.util.Log - -import `is`.xyz.mpv.MPVLib.mpvFormat.* import android.os.Build import android.os.Environment import android.preference.PreferenceManager +import android.util.AttributeSet +import android.util.Log import android.view.* +import androidx.core.content.ContextCompat +import `is`.xyz.mpv.MPVLib.mpvFormat.* import kotlin.reflect.KProperty internal class MPVView(context: Context, attrs: AttributeSet) : BaseMPVView(context, attrs) { @@ -32,8 +32,7 @@ internal class MPVView(context: Context, attrs: AttributeSet) : BaseMPVView(cont // vo: set display fps as reported by android if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager - val disp = wm.defaultDisplay + val disp = ContextCompat.getDisplayOrDefault(context) val refreshRate = disp.mode.refreshRate Log.v(TAG, "Display ${disp.displayId} reports FPS of $refreshRate") From cf259b433ad45154decaa31a2c8957d449469d0b Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sat, 27 Jul 2024 10:55:01 +0200 Subject: [PATCH 23/78] BackgroundPlaybackService: fix missing service type for newer API levels I could have sworn that I read somewhere startForeground() would read it from the manifest automatically. fixes: e43a7283236faee26648d0f5e54f09ea1f114520 --- .../is/xyz/mpv/BackgroundPlaybackService.kt | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.kt b/app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.kt index 7008cc7fb..e9f9d88c0 100644 --- a/app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.kt +++ b/app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.kt @@ -4,7 +4,9 @@ import android.annotation.SuppressLint import android.app.* import android.content.Context import android.content.Intent +import android.content.pm.ServiceInfo import android.graphics.Bitmap +import android.os.Build import android.os.IBinder import android.support.v4.media.session.MediaSessionCompat import android.util.Log @@ -43,7 +45,6 @@ class BackgroundPlaybackService : Service(), MPVLib.EventObserver { } } - @SuppressLint("UnspecifiedImmutableFlag") private fun buildNotification(): Notification { val notificationIntent = Intent(this, MPVActivity::class.java) val pendingIntent = PendingIntentCompat.getActivity(this, 0, notificationIntent, 0, false) @@ -95,6 +96,12 @@ class BackgroundPlaybackService : Service(), MPVLib.EventObserver { return builder.build() } + @SuppressLint("NotificationPermission") // not required for foreground service + private fun refreshNotification() { + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.notify(NOTIFICATION_ID, buildNotification()) + } + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { Log.v(TAG, "BackgroundPlaybackService: starting") @@ -107,7 +114,12 @@ class BackgroundPlaybackService : Service(), MPVLib.EventObserver { // create notification and turn this into a "foreground service" val notification = buildNotification() - ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, 0) + val type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK + } else { + 0 + } + ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, type) return START_NOT_STICKY // Android can't restart this service on its own } @@ -128,9 +140,7 @@ class BackgroundPlaybackService : Service(), MPVLib.EventObserver { if (property != "pause") return paused = value - - val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.notify(NOTIFICATION_ID, buildNotification()) + refreshNotification() } override fun eventProperty(property: String, value: Long) { } @@ -138,9 +148,7 @@ class BackgroundPlaybackService : Service(), MPVLib.EventObserver { override fun eventProperty(property: String, value: String) { if (!cachedMetadata.update(property, value)) return - - val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.notify(NOTIFICATION_ID, buildNotification()) + refreshNotification() } override fun event(eventId: Int) { From f327b16449e2bc3b05ff399d3cd5edcb8482b7a2 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sat, 27 Jul 2024 12:03:30 +0200 Subject: [PATCH 24/78] app: support 16 KB page sizes closes #952 --- app/src/main/jni/Application.mk | 1 + buildscripts/buildall.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/jni/Application.mk b/app/src/main/jni/Application.mk index 26ce72b34..53418cb46 100644 --- a/app/src/main/jni/Application.mk +++ b/app/src/main/jni/Application.mk @@ -14,3 +14,4 @@ endif APP_PLATFORM := android-21 APP_STL := c++_shared +APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true diff --git a/buildscripts/buildall.sh b/buildscripts/buildall.sh index 9754d4724..63ad28f79 100755 --- a/buildscripts/buildall.sh +++ b/buildscripts/buildall.sh @@ -53,7 +53,7 @@ loadarch () { export CC=$cc_triple-gcc export CXX=$cc_triple-g++ fi - export LDFLAGS="-Wl,-O1,--icf=safe" + export LDFLAGS="-Wl,-O1,--icf=safe -Wl,-z,max-page-size=16384" export AR=llvm-ar export RANLIB=llvm-ranlib } From 403ba733a17c44b405b92262cf1c102454c33c84 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sat, 27 Jul 2024 12:55:49 +0200 Subject: [PATCH 25/78] Utils: observe metadata not as individual keys but the entire property fixes #925 --- .../is/xyz/mpv/BackgroundPlaybackService.kt | 6 ++++- app/src/main/java/is/xyz/mpv/MPVActivity.kt | 19 +++++++++------ app/src/main/java/is/xyz/mpv/MPVView.kt | 6 ++--- app/src/main/java/is/xyz/mpv/Utils.kt | 24 +++++++++++++++---- 4 files changed, 39 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.kt b/app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.kt index e9f9d88c0..826257414 100644 --- a/app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.kt +++ b/app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.kt @@ -134,7 +134,11 @@ class BackgroundPlaybackService : Service(), MPVLib.EventObserver { /* Event observers */ - override fun eventProperty(property: String) { } + override fun eventProperty(property: String) { + if (!cachedMetadata.update(property)) + return + refreshNotification() + } override fun eventProperty(property: String, value: Boolean) { if (property != "pause") diff --git a/app/src/main/java/is/xyz/mpv/MPVActivity.kt b/app/src/main/java/is/xyz/mpv/MPVActivity.kt index b9970a608..11afc7855 100644 --- a/app/src/main/java/is/xyz/mpv/MPVActivity.kt +++ b/app/src/main/java/is/xyz/mpv/MPVActivity.kt @@ -1665,7 +1665,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse // mpv events - private fun eventPropertyUi(property: String) { + private fun eventPropertyUi(property: String, dummy: Any?, metaUpdated: Boolean) { if (!activityIsForeground) return when (property) { "track-list" -> player.loadTracks() @@ -1676,6 +1676,8 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse "video-format" -> updateAudioUI() "hwdec-current" -> updateDecoderButton() } + if (metaUpdated) + updateMetadataDisplay() } private fun eventPropertyUi(property: String, value: Boolean) { @@ -1694,12 +1696,12 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse } } - private fun eventPropertyUi(property: String, value: String, triggerMetaUpdate: Boolean) { + private fun eventPropertyUi(property: String, value: String, metaUpdated: Boolean) { if (!activityIsForeground) return when (property) { "speed" -> updateSpeedButton() } - if (triggerMetaUpdate) + if (metaUpdated) updateMetadataDisplay() } @@ -1709,6 +1711,9 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse } override fun eventProperty(property: String) { + val metaUpdated = psc.update(property) + if (metaUpdated) + updateMediaSession() if (property == "loop-file" || property == "loop-playlist") { mediaSession?.setRepeatMode(when (player.getRepeat()) { 2 -> PlaybackStateCompat.REPEAT_MODE_ONE @@ -1718,7 +1723,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse } if (!activityIsForeground) return - eventUiHandler.post { eventPropertyUi(property) } + eventUiHandler.post { eventPropertyUi(property, null, metaUpdated) } } override fun eventProperty(property: String, value: Boolean) { @@ -1744,12 +1749,12 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse } override fun eventProperty(property: String, value: String) { - val triggerMetaUpdate = psc.update(property, value) - if (triggerMetaUpdate) + val metaUpdated = psc.update(property, value) + if (metaUpdated) updateMediaSession() if (!activityIsForeground) return - eventUiHandler.post { eventPropertyUi(property, value, triggerMetaUpdate) } + eventUiHandler.post { eventPropertyUi(property, value, metaUpdated) } } override fun event(eventId: Int) { diff --git a/app/src/main/java/is/xyz/mpv/MPVView.kt b/app/src/main/java/is/xyz/mpv/MPVView.kt index 702b7a083..b3debaef3 100644 --- a/app/src/main/java/is/xyz/mpv/MPVView.kt +++ b/app/src/main/java/is/xyz/mpv/MPVView.kt @@ -182,8 +182,7 @@ internal class MPVView(context: Context, attrs: AttributeSet) : BaseMPVView(cont Property("paused-for-cache", MPV_FORMAT_FLAG), Property("speed", MPV_FORMAT_STRING), Property("track-list"), - // observing double properties is not hooked up in the JNI code, but doing this - // will restrict updates to when it actually changes + // note: updates with DOUBLE format are passed to Java like NONE Property("video-params/aspect", MPV_FORMAT_DOUBLE), Property("video-params/rotate", MPV_FORMAT_DOUBLE), // @@ -191,8 +190,7 @@ internal class MPVView(context: Context, attrs: AttributeSet) : BaseMPVView(cont Property("playlist-count", MPV_FORMAT_INT64), Property("video-format"), Property("media-title", MPV_FORMAT_STRING), - Property("metadata/by-key/Artist", MPV_FORMAT_STRING), - Property("metadata/by-key/Album", MPV_FORMAT_STRING), + Property("metadata"), Property("loop-playlist"), Property("loop-file"), Property("shuffle", MPV_FORMAT_FLAG), diff --git a/app/src/main/java/is/xyz/mpv/Utils.kt b/app/src/main/java/is/xyz/mpv/Utils.kt index c58800c5a..678a5b32f 100644 --- a/app/src/main/java/is/xyz/mpv/Utils.kt +++ b/app/src/main/java/is/xyz/mpv/Utils.kt @@ -201,15 +201,26 @@ internal object Utils { fun readAll() { mediaTitle = MPVLib.getPropertyString("media-title") - mediaArtist = MPVLib.getPropertyString("metadata/by-key/Artist") - mediaAlbum = MPVLib.getPropertyString("metadata/by-key/Album") + update("metadata") // read artist & album } + /** callback for properties of type MPV_FORMAT_NONE */ + fun update(property: String): Boolean { + // TODO?: maybe one day this could natively handle a MPV_FORMAT_NODE_MAP + if (property == "metadata") { + // If we observe individual keys libmpv won't notify us once they become + // unavailable, so we observe "metadata" and read both keys on trigger. + mediaArtist = MPVLib.getPropertyString("metadata/by-key/Artist") + mediaAlbum = MPVLib.getPropertyString("metadata/by-key/Album") + return true + } + return false + } + + /** callback for properties of type MPV_FORMAT_STRING */ fun update(property: String, value: String): Boolean { when (property) { "media-title" -> mediaTitle = value - "metadata/by-key/Artist" -> mediaArtist = value - "metadata/by-key/Album" -> mediaAlbum = value else -> return false } return true @@ -259,6 +270,11 @@ internal object Utils { /** duration in seconds */ val durationSec get() = (duration / 1000).toInt() + /** callback for properties of type MPV_FORMAT_NONE */ + fun update(property: String): Boolean { + return meta.update(property) + } + /** callback for properties of type MPV_FORMAT_STRING */ fun update(property: String, value: String): Boolean { if (meta.update(property, value)) From a6246267b0af93ae96a2d0a330a08ec7fbc5c424 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sat, 27 Jul 2024 16:28:33 +0200 Subject: [PATCH 26/78] jni: add native support for observing double properties --- .../is/xyz/mpv/BackgroundPlaybackService.kt | 2 ++ app/src/main/java/is/xyz/mpv/MPVActivity.kt | 19 +++++++++++--- app/src/main/java/is/xyz/mpv/MPVLib.java | 8 ++++++ app/src/main/java/is/xyz/mpv/MPVView.kt | 2 -- app/src/main/jni/event.cpp | 11 +++++--- app/src/main/jni/jni_utils.cpp | 17 ++++--------- app/src/main/jni/jni_utils.h | 25 +++++++++++++------ app/src/main/jni/property.cpp | 4 ++- 8 files changed, 58 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.kt b/app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.kt index 826257414..2221448b9 100644 --- a/app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.kt +++ b/app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.kt @@ -149,6 +149,8 @@ class BackgroundPlaybackService : Service(), MPVLib.EventObserver { override fun eventProperty(property: String, value: Long) { } + override fun eventProperty(property: String, value: Double) { } + override fun eventProperty(property: String, value: String) { if (!cachedMetadata.update(property, value)) return diff --git a/app/src/main/java/is/xyz/mpv/MPVActivity.kt b/app/src/main/java/is/xyz/mpv/MPVActivity.kt index 11afc7855..449965a12 100644 --- a/app/src/main/java/is/xyz/mpv/MPVActivity.kt +++ b/app/src/main/java/is/xyz/mpv/MPVActivity.kt @@ -1669,10 +1669,6 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse if (!activityIsForeground) return when (property) { "track-list" -> player.loadTracks() - "video-params/aspect", "video-params/rotate" -> { - updateOrientation() - updatePiPParams() - } "video-format" -> updateAudioUI() "hwdec-current" -> updateDecoderButton() } @@ -1696,6 +1692,16 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse } } + private fun eventPropertyUi(property: String, value: Double) { + if (!activityIsForeground) return + when (property) { + "video-params/aspect", "video-params/rotate" -> { + updateOrientation() + updatePiPParams() + } + } + } + private fun eventPropertyUi(property: String, value: String, metaUpdated: Boolean) { if (!activityIsForeground) return when (property) { @@ -1748,6 +1754,11 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse eventUiHandler.post { eventPropertyUi(property, value) } } + override fun eventProperty(property: String, value: Double) { + if (!activityIsForeground) return + eventUiHandler.post { eventPropertyUi(property, value) } + } + override fun eventProperty(property: String, value: String) { val metaUpdated = psc.update(property, value) if (metaUpdated) diff --git a/app/src/main/java/is/xyz/mpv/MPVLib.java b/app/src/main/java/is/xyz/mpv/MPVLib.java index d04aaa6f0..a6b4d9855 100644 --- a/app/src/main/java/is/xyz/mpv/MPVLib.java +++ b/app/src/main/java/is/xyz/mpv/MPVLib.java @@ -66,6 +66,13 @@ public static void eventProperty(String property, boolean value) { } } + public static void eventProperty(String property, double value) { + synchronized (observers) { + for (EventObserver o : observers) + o.eventProperty(property, value); + } + } + public static void eventProperty(String property, String value) { synchronized (observers) { for (EventObserver o : observers) @@ -108,6 +115,7 @@ public interface EventObserver { void eventProperty(@NonNull String property, long value); void eventProperty(@NonNull String property, boolean value); void eventProperty(@NonNull String property, @NonNull String value); + void eventProperty(@NonNull String property, double value); void event(int eventId); } diff --git a/app/src/main/java/is/xyz/mpv/MPVView.kt b/app/src/main/java/is/xyz/mpv/MPVView.kt index b3debaef3..31e593ccf 100644 --- a/app/src/main/java/is/xyz/mpv/MPVView.kt +++ b/app/src/main/java/is/xyz/mpv/MPVView.kt @@ -182,10 +182,8 @@ internal class MPVView(context: Context, attrs: AttributeSet) : BaseMPVView(cont Property("paused-for-cache", MPV_FORMAT_FLAG), Property("speed", MPV_FORMAT_STRING), Property("track-list"), - // note: updates with DOUBLE format are passed to Java like NONE Property("video-params/aspect", MPV_FORMAT_DOUBLE), Property("video-params/rotate", MPV_FORMAT_DOUBLE), - // Property("playlist-pos", MPV_FORMAT_INT64), Property("playlist-count", MPV_FORMAT_INT64), Property("video-format"), diff --git a/app/src/main/jni/event.cpp b/app/src/main/jni/event.cpp index 329a85d25..88101eea0 100644 --- a/app/src/main/jni/event.cpp +++ b/app/src/main/jni/event.cpp @@ -11,14 +11,19 @@ static void sendPropertyUpdateToJava(JNIEnv *env, mpv_event_property *prop) { jstring jvalue = NULL; switch (prop->format) { case MPV_FORMAT_NONE: - case MPV_FORMAT_DOUBLE: env->CallStaticVoidMethod(mpv_MPVLib, mpv_MPVLib_eventProperty_S, jprop); break; case MPV_FORMAT_FLAG: - env->CallStaticVoidMethod(mpv_MPVLib, mpv_MPVLib_eventProperty_Sb, jprop, *(int*)prop->data); + env->CallStaticVoidMethod(mpv_MPVLib, mpv_MPVLib_eventProperty_Sb, jprop, + (jboolean) (*(int*)prop->data != 0)); break; case MPV_FORMAT_INT64: - env->CallStaticVoidMethod(mpv_MPVLib, mpv_MPVLib_eventProperty_Sl, jprop, *(int64_t*)prop->data); + env->CallStaticVoidMethod(mpv_MPVLib, mpv_MPVLib_eventProperty_Sl, jprop, + (jlong) *(int64_t*)prop->data); + break; + case MPV_FORMAT_DOUBLE: + env->CallStaticVoidMethod(mpv_MPVLib, mpv_MPVLib_eventProperty_Sd, jprop, + (jdouble) *(double*)prop->data); break; case MPV_FORMAT_STRING: jvalue = env->NewStringUTF(*(const char**)prop->data); diff --git a/app/src/main/jni/jni_utils.cpp b/app/src/main/jni/jni_utils.cpp index 666b99459..f02878ff2 100644 --- a/app/src/main/jni/jni_utils.cpp +++ b/app/src/main/jni/jni_utils.cpp @@ -1,3 +1,4 @@ +#define UTIL_EXTERN #include "jni_utils.h" #include @@ -13,19 +14,10 @@ bool acquire_jni_env(JavaVM *vm, JNIEnv **env) } // Apparently it's considered slow to FindClass and GetMethodID every time we need them, -// so let's have a nice cache here -jclass java_Integer, java_Double, java_Boolean; -jmethodID java_Integer_init, java_Integer_intValue, java_Double_init, java_Double_doubleValue, java_Boolean_init, java_Boolean_booleanValue; -jmethodID java_GLSurfaceView_requestRender; +// so let's have a nice cache here. -jclass android_graphics_Bitmap, android_graphics_Bitmap_Config; -jmethodID android_graphics_Bitmap_createBitmap; -jfieldID android_graphics_Bitmap_Config_ARGB_8888; - -jclass mpv_MPVLib; -jmethodID mpv_MPVLib_eventProperty_S, mpv_MPVLib_eventProperty_Sb, mpv_MPVLib_eventProperty_Sl, mpv_MPVLib_eventProperty_SS, mpv_MPVLib_event, mpv_MPVLib_logMessage_SiS; - -void init_methods_cache(JNIEnv *env) { +void init_methods_cache(JNIEnv *env) +{ static bool methods_initialized = false; if (methods_initialized) return; @@ -52,6 +44,7 @@ void init_methods_cache(JNIEnv *env) { mpv_MPVLib_eventProperty_S = env->GetStaticMethodID(mpv_MPVLib, "eventProperty", "(Ljava/lang/String;)V"); // eventProperty(String) mpv_MPVLib_eventProperty_Sb = env->GetStaticMethodID(mpv_MPVLib, "eventProperty", "(Ljava/lang/String;Z)V"); // eventProperty(String, boolean) mpv_MPVLib_eventProperty_Sl = env->GetStaticMethodID(mpv_MPVLib, "eventProperty", "(Ljava/lang/String;J)V"); // eventProperty(String, long) + mpv_MPVLib_eventProperty_Sd = env->GetStaticMethodID(mpv_MPVLib, "eventProperty", "(Ljava/lang/String;D)V"); // eventProperty(String, double) mpv_MPVLib_eventProperty_SS = env->GetStaticMethodID(mpv_MPVLib, "eventProperty", "(Ljava/lang/String;Ljava/lang/String;)V"); // eventProperty(String, String) mpv_MPVLib_event = env->GetStaticMethodID(mpv_MPVLib, "event", "(I)V"); // event(int) mpv_MPVLib_logMessage_SiS = env->GetStaticMethodID(mpv_MPVLib, "logMessage", "(Ljava/lang/String;ILjava/lang/String;)V"); // logMessage(String, int, String) diff --git a/app/src/main/jni/jni_utils.h b/app/src/main/jni/jni_utils.h index 7d91df402..fcef14871 100644 --- a/app/src/main/jni/jni_utils.h +++ b/app/src/main/jni/jni_utils.h @@ -8,13 +8,22 @@ bool acquire_jni_env(JavaVM *vm, JNIEnv **env); void init_methods_cache(JNIEnv *env); -extern jclass java_Integer, java_Double, java_Boolean; -extern jmethodID java_Integer_init, java_Integer_intValue, java_Double_init, java_Double_doubleValue, java_Boolean_init, java_Boolean_booleanValue; -extern jmethodID java_GLSurfaceView_requestRender; +#ifndef UTIL_EXTERN +#define UTIL_EXTERN extern +#endif -extern jclass android_graphics_Bitmap, android_graphics_Bitmap_Config; -extern jmethodID android_graphics_Bitmap_createBitmap; -extern jfieldID android_graphics_Bitmap_Config_ARGB_8888; +UTIL_EXTERN jclass java_Integer, java_Double, java_Boolean; +UTIL_EXTERN jmethodID java_Integer_init, java_Integer_intValue, java_Double_init, java_Double_doubleValue, java_Boolean_init, java_Boolean_booleanValue; -extern jclass mpv_MPVLib; -extern jmethodID mpv_MPVLib_eventProperty_S, mpv_MPVLib_eventProperty_Sb, mpv_MPVLib_eventProperty_Sl, mpv_MPVLib_eventProperty_SS, mpv_MPVLib_event, mpv_MPVLib_logMessage_SiS; +UTIL_EXTERN jclass android_graphics_Bitmap, android_graphics_Bitmap_Config; +UTIL_EXTERN jmethodID android_graphics_Bitmap_createBitmap; +UTIL_EXTERN jfieldID android_graphics_Bitmap_Config_ARGB_8888; + +UTIL_EXTERN jclass mpv_MPVLib; +UTIL_EXTERN jmethodID mpv_MPVLib_eventProperty_S, + mpv_MPVLib_eventProperty_Sb, + mpv_MPVLib_eventProperty_Sl, + mpv_MPVLib_eventProperty_Sd, + mpv_MPVLib_eventProperty_SS, + mpv_MPVLib_event, + mpv_MPVLib_logMessage_SiS; diff --git a/app/src/main/jni/property.cpp b/app/src/main/jni/property.cpp index 8736be433..a4401681b 100644 --- a/app/src/main/jni/property.cpp +++ b/app/src/main/jni/property.cpp @@ -118,6 +118,8 @@ jni_func(void, observeProperty, jstring property, jint format) { if (!g_mpv) die("mpv is not initialized"); const char *prop = env->GetStringUTFChars(property, NULL); - mpv_observe_property(g_mpv, 0, prop, (mpv_format)format); + int result = mpv_observe_property(g_mpv, 0, prop, (mpv_format)format); + if (result < 0) + ALOGE("mpv_observe_property(%s) format %d returned error %s", prop, format, mpv_error_string(result)); env->ReleaseStringUTFChars(property, prop); } From 20fe0ddf037e0f2e3ffb611262e622712d34340c Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sat, 27 Jul 2024 16:44:13 +0200 Subject: [PATCH 27/78] MPVActivity: handle file duration with sub-second precision --- app/src/main/java/is/xyz/mpv/MPVActivity.kt | 16 ++++++++++------ app/src/main/java/is/xyz/mpv/MPVView.kt | 9 +++++---- app/src/main/java/is/xyz/mpv/Utils.kt | 14 ++++++++++++-- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/is/xyz/mpv/MPVActivity.kt b/app/src/main/java/is/xyz/mpv/MPVActivity.kt index 449965a12..cd2912360 100644 --- a/app/src/main/java/is/xyz/mpv/MPVActivity.kt +++ b/app/src/main/java/is/xyz/mpv/MPVActivity.kt @@ -84,7 +84,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { if (!fromUser) return - player.timePos = progress + player.timePos = progress.toDouble() updatePlaybackPos(progress) } @@ -1640,7 +1640,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse player.paused = false } override fun onSeekTo(pos: Long) { - player.timePos = (pos / 1000).toInt() + player.timePos = (pos / 1000.0) } override fun onSkipToNext() = playlistNext() override fun onSkipToPrevious() = playlistPrev() @@ -1686,8 +1686,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse private fun eventPropertyUi(property: String, value: Long) { if (!activityIsForeground) return when (property) { - "time-pos" -> updatePlaybackPos(value.toInt()) - "duration" -> updatePlaybackDuration(value.toInt()) + "time-pos" -> updatePlaybackPos(psc.positionSec) "playlist-pos", "playlist-count" -> updatePlaylistButtons() } } @@ -1695,6 +1694,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse private fun eventPropertyUi(property: String, value: Double) { if (!activityIsForeground) return when (property) { + "duration/full" -> updatePlaybackDuration(psc.durationSec) "video-params/aspect", "video-params/rotate" -> { updateOrientation() updatePiPParams() @@ -1755,6 +1755,9 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse } override fun eventProperty(property: String, value: Double) { + if (psc.update(property, value)) + updateMediaSession() + if (!activityIsForeground) return eventUiHandler.post { eventPropertyUi(property, value) } } @@ -1793,7 +1796,8 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse private var initialBright = 0f private var initialVolume = 0 private var maxVolume = 0 - private var pausedForSeek = 0 // 0 = initial, 1 = paused, 2 = was already paused + /** 0 = initial, 1 = paused, 2 = was already paused */ + private var pausedForSeek = 0 private fun fadeGestureText() { fadeHandler.removeCallbacks(fadeRunnable3) @@ -1838,7 +1842,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse val newPos = (initialSeek + diff.toInt()).coerceIn(0, duration) val newDiff = newPos - initialSeek if (smoothSeekGesture) { - player.timePos = newPos // (exact seek) + player.timePos = newPos.toDouble() // (exact seek) } else { // seek faster than assigning to timePos but less precise MPVLib.command(arrayOf("seek", newPos.toString(), "absolute+keyframes")) diff --git a/app/src/main/java/is/xyz/mpv/MPVView.kt b/app/src/main/java/is/xyz/mpv/MPVView.kt index 31e593ccf..ff94d23d7 100644 --- a/app/src/main/java/is/xyz/mpv/MPVView.kt +++ b/app/src/main/java/is/xyz/mpv/MPVView.kt @@ -177,7 +177,7 @@ internal class MPVView(context: Context, attrs: AttributeSet) : BaseMPVView(cont data class Property(val name: String, val format: Int = MPV_FORMAT_NONE) val p = arrayOf( Property("time-pos", MPV_FORMAT_INT64), - Property("duration", MPV_FORMAT_INT64), + Property("duration/full", MPV_FORMAT_DOUBLE), Property("pause", MPV_FORMAT_FLAG), Property("paused-for-cache", MPV_FORMAT_FLAG), Property("speed", MPV_FORMAT_STRING), @@ -280,10 +280,11 @@ internal class MPVView(context: Context, attrs: AttributeSet) : BaseMPVView(cont get() = MPVLib.getPropertyBoolean("pause") set(paused) = MPVLib.setPropertyBoolean("pause", paused!!) - var timePos: Int? - get() = MPVLib.getPropertyInt("time-pos") - set(progress) = MPVLib.setPropertyInt("time-pos", progress!!) + var timePos: Double? + get() = MPVLib.getPropertyDouble("time-pos/full") + set(progress) = MPVLib.setPropertyDouble("time-pos", progress!!) + /** name of currently active hardware decoder or "no" */ val hwdecActive: String get() = MPVLib.getPropertyString("hwdec-current") ?: "no" diff --git a/app/src/main/java/is/xyz/mpv/Utils.kt b/app/src/main/java/is/xyz/mpv/Utils.kt index 678a5b32f..4de3b229f 100644 --- a/app/src/main/java/is/xyz/mpv/Utils.kt +++ b/app/src/main/java/is/xyz/mpv/Utils.kt @@ -25,6 +25,8 @@ import androidx.core.os.BundleCompat import androidx.core.widget.addTextChangedListener import java.io.* import kotlin.math.abs +import kotlin.math.ceil +import kotlin.math.roundToInt internal object Utils { fun copyAssets(context: Context) { @@ -268,7 +270,7 @@ internal object Utils { /** playback position in seconds */ val positionSec get() = (position / 1000).toInt() /** duration in seconds */ - val durationSec get() = (duration / 1000).toInt() + val durationSec get() = (duration / 1000f).roundToInt() /** callback for properties of type MPV_FORMAT_NONE */ fun update(property: String): Boolean { @@ -300,7 +302,6 @@ internal object Utils { fun update(property: String, value: Long): Boolean { when (property) { "time-pos" -> position = value * 1000 - "duration" -> duration = value * 1000 "playlist-pos" -> playlistPos = value.toInt() "playlist-count" -> playlistCount = value.toInt() else -> return false @@ -308,6 +309,15 @@ internal object Utils { return true } + /** callback for properties of type MPV_FORMAT_DOUBLE */ + fun update(property: String, value: Double): Boolean { + when (property) { + "duration/full" -> duration = ceil(value * 1000.0).coerceAtLeast(0.0).toLong() + else -> return false + } + return true + } + private val mediaMetadataBuilder = MediaMetadataCompat.Builder() private val playbackStateBuilder = PlaybackStateCompat.Builder() From 9f1e977c0a6cce561322c3cbff7f17a9a0493e13 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sat, 27 Jul 2024 16:58:10 +0200 Subject: [PATCH 28/78] MPVActivity: make seek gesture work with sub-second precision --- app/src/main/java/is/xyz/mpv/MPVActivity.kt | 23 ++++++++++++--------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/is/xyz/mpv/MPVActivity.kt b/app/src/main/java/is/xyz/mpv/MPVActivity.kt index cd2912360..7a8275c16 100644 --- a/app/src/main/java/is/xyz/mpv/MPVActivity.kt +++ b/app/src/main/java/is/xyz/mpv/MPVActivity.kt @@ -1792,7 +1792,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse // Gesture handler - private var initialSeek = 0 + private var initialSeek = 0f private var initialBright = 0f private var initialVolume = 0 private var maxVolume = 0 @@ -1813,7 +1813,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse PropertyChange.Init -> { mightWantToToggleControls = false - initialSeek = psc.positionSec + initialSeek = (psc.position / 1000f) initialBright = Utils.getScreenBrightness(this) ?: 0.5f with (audioManager!!) { initialVolume = getStreamVolume(STREAM_TYPE) @@ -1830,8 +1830,8 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse } PropertyChange.Seek -> { // disable seeking when duration is unknown - val duration = psc.durationSec - if (duration == 0 || initialSeek < 0) + val duration = (psc.duration / 1000f) + if (duration == 0f || initialSeek < 0) return if (smoothSeekGesture && pausedForSeek == 0) { pausedForSeek = if (psc.pause) 2 else 1 @@ -1839,18 +1839,21 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse player.paused = true } - val newPos = (initialSeek + diff.toInt()).coerceIn(0, duration) - val newDiff = newPos - initialSeek + val newPosExact = (initialSeek + diff).coerceIn(0f, duration) + val newPos = newPosExact.roundToInt() + val newDiff = (newPosExact - initialSeek).roundToInt() if (smoothSeekGesture) { - player.timePos = newPos.toDouble() // (exact seek) + player.timePos = newPosExact.toDouble() // (exact seek) } else { // seek faster than assigning to timePos but less precise - MPVLib.command(arrayOf("seek", newPos.toString(), "absolute+keyframes")) + MPVLib.command(arrayOf("seek", "$newPosExact", "absolute+keyframes")) } - updatePlaybackPos(newPos) + // Note: don't call updatePlaybackPos() here because mpv will seek a timestamp + // actually present in the file, and not the exact one we specified. + val posText = Utils.prettyTime(newPos) val diffText = Utils.prettyTime(newDiff, true) - gestureTextView.text = getString(R.string.ui_seek_distance, Utils.prettyTime(newPos), diffText) + gestureTextView.text = getString(R.string.ui_seek_distance, posText, diffText) } PropertyChange.Volume -> { if (maxVolume == 0) From 66276e03e13437f54c409a9150669f0e47641faf Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sat, 27 Jul 2024 17:18:24 +0200 Subject: [PATCH 29/78] MPVActivity: increase precision of seek bar --- app/src/main/java/is/xyz/mpv/MPVActivity.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/is/xyz/mpv/MPVActivity.kt b/app/src/main/java/is/xyz/mpv/MPVActivity.kt index 7a8275c16..3919f8ee4 100644 --- a/app/src/main/java/is/xyz/mpv/MPVActivity.kt +++ b/app/src/main/java/is/xyz/mpv/MPVActivity.kt @@ -84,8 +84,8 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { if (!fromUser) return - player.timePos = progress.toDouble() - updatePlaybackPos(progress) + player.timePos = progress.toDouble() / SEEK_BAR_PRECISION + // Note: don't call updatePlaybackPos() here either } override fun onStartTrackingTouch(seekBar: SeekBar) { @@ -1507,7 +1507,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse Utils.prettyTime(-diff, true) } if (!userIsOperatingSeekbar) - binding.playbackSeekbar.progress = position + binding.playbackSeekbar.progress = position * SEEK_BAR_PRECISION // Note: do NOT add other update functions here just because this is called every second. // Use property observation instead. @@ -1518,7 +1518,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse if (!useTimeRemaining) binding.playbackDurationTxt.text = Utils.prettyTime(duration) if (!userIsOperatingSeekbar) - binding.playbackSeekbar.max = duration + binding.playbackSeekbar.max = duration * SEEK_BAR_PRECISION } private fun updatePlaybackStatus(paused: Boolean) { @@ -1916,5 +1916,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse private const val RESULT_INTENT = "is.xyz.mpv.MPVActivity.result" // stream type used with AudioManager private const val STREAM_TYPE = AudioManager.STREAM_MUSIC + // precision used by seekbar (1/s) + private const val SEEK_BAR_PRECISION = 2 } } From 461e9869613c5ecbb342121c2e02b55fb4b7f0db Mon Sep 17 00:00:00 2001 From: sfan5 Date: Thu, 1 Aug 2024 19:04:45 +0200 Subject: [PATCH 30/78] BackgroundPlaybackService: remove notification when done --- app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.kt b/app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.kt index 2221448b9..dbc871bff 100644 --- a/app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.kt +++ b/app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.kt @@ -127,6 +127,9 @@ class BackgroundPlaybackService : Service(), MPVLib.EventObserver { override fun onDestroy() { MPVLib.removeObserver(this) + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel(NOTIFICATION_ID) + Log.v(TAG, "BackgroundPlaybackService: destroyed") } From a8554af954b6eda9011a4145e8bc4b5fc2a356cf Mon Sep 17 00:00:00 2001 From: sfan5 Date: Thu, 1 Aug 2024 19:47:40 +0200 Subject: [PATCH 31/78] MPVActivity: fix keep-open left enabled by "play in background" --- app/src/main/java/is/xyz/mpv/MPVActivity.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/is/xyz/mpv/MPVActivity.kt b/app/src/main/java/is/xyz/mpv/MPVActivity.kt index 3919f8ee4..845f3f01f 100644 --- a/app/src/main/java/is/xyz/mpv/MPVActivity.kt +++ b/app/src/main/java/is/xyz/mpv/MPVActivity.kt @@ -1231,6 +1231,8 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse openPlaylistMenu(restoreState); false }, MenuItem(R.id.backgroundBtn) { + // restoring state may (un)pause so do that first + restoreState() backgroundPlayMode = "always" player.paused = false moveTaskToBack(true) From 3bd673ff7f56bc7ae978519bbeb3dda5137adc9d Mon Sep 17 00:00:00 2001 From: sfan5 Date: Thu, 1 Aug 2024 19:51:45 +0200 Subject: [PATCH 32/78] BaseMPVView: move a code fragment --- app/src/main/java/is/xyz/mpv/BaseMPVView.kt | 2 ++ app/src/main/java/is/xyz/mpv/MPVView.kt | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/is/xyz/mpv/BaseMPVView.kt b/app/src/main/java/is/xyz/mpv/BaseMPVView.kt index 88d65cd75..9e835c786 100644 --- a/app/src/main/java/is/xyz/mpv/BaseMPVView.kt +++ b/app/src/main/java/is/xyz/mpv/BaseMPVView.kt @@ -30,6 +30,8 @@ abstract class BaseMPVView(context: Context, attrs: AttributeSet) : SurfaceView( postInitOptions() // would crash before the surface is attached MPVLib.setOptionString("force-window", "no") + // need to idle at least once for playFile() logic to work + MPVLib.setOptionString("idle", "once") holder.addCallback(this) observeProperties() diff --git a/app/src/main/java/is/xyz/mpv/MPVView.kt b/app/src/main/java/is/xyz/mpv/MPVView.kt index ff94d23d7..ca80129df 100644 --- a/app/src/main/java/is/xyz/mpv/MPVView.kt +++ b/app/src/main/java/is/xyz/mpv/MPVView.kt @@ -112,9 +112,6 @@ internal class MPVView(context: Context, attrs: AttributeSet) : BaseMPVView(cont override fun postInitOptions() { // we need to call write-watch-later manually MPVLib.setOptionString("save-position-on-quit", "no") - - // "no" wouldn't work and "yes" is not intended by the UI - MPVLib.setOptionString("idle", "once") } fun onPointerEvent(event: MotionEvent): Boolean { From 32cbff3cedea73b4616b34542cb95bf1d00504cc Mon Sep 17 00:00:00 2001 From: sfan5 Date: Thu, 1 Aug 2024 20:50:06 +0200 Subject: [PATCH 33/78] MPVActivity: address some deprecations --- app/src/main/java/is/xyz/mpv/MPVActivity.kt | 36 +++++++++++++-------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/is/xyz/mpv/MPVActivity.kt b/app/src/main/java/is/xyz/mpv/MPVActivity.kt index 845f3f01f..be370cb2b 100644 --- a/app/src/main/java/is/xyz/mpv/MPVActivity.kt +++ b/app/src/main/java/is/xyz/mpv/MPVActivity.kt @@ -3,6 +3,7 @@ package `is`.xyz.mpv import `is`.xyz.mpv.databinding.PlayerBinding import android.animation.Animator import android.animation.AnimatorListenerAdapter +import android.annotation.SuppressLint import androidx.appcompat.app.AlertDialog import android.app.PictureInPictureParams import android.app.RemoteAction @@ -28,6 +29,7 @@ import android.view.ViewGroup.MarginLayoutParams import android.widget.Button import android.widget.SeekBar import android.widget.Toast +import androidx.activity.addCallback import androidx.annotation.IdRes import androidx.annotation.LayoutRes import androidx.annotation.StringRes @@ -196,6 +198,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse private var smoothSeekGesture = false /* * */ + @SuppressLint("ClickableViewAccessibility") private fun initListeners() { with (binding) { prevBtn.setOnClickListener { playlistPrev() } @@ -221,6 +224,12 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse prevBtn.setOnLongClickListener { openPlaylistMenu(pauseForDialog()); true } nextBtn.setOnLongClickListener { openPlaylistMenu(pauseForDialog()); true } cycleDecoderBtn.setOnLongClickListener { pickDecoder(); true } + + playbackSeekbar.setOnSeekBarChangeListener(seekBarChangeListener) + } + + player.setOnTouchListener { _, e -> + if (lockedUI) false else gestures.onTouchEvent(e) } ViewCompat.setOnApplyWindowInsetsListener(binding.outside) { _, windowInsets -> @@ -236,6 +245,14 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse } WindowInsetsCompat.CONSUMED } + + onBackPressedDispatcher.addCallback(this) { + onBackPressedImpl() + } + + addOnPictureInPictureModeChangedListener { info -> + onPiPModeChangedImpl(info.isInPictureInPictureMode) + } } private var playbackHasStarted = false @@ -298,12 +315,6 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse player.initialize(filesDir.path, cacheDir.path) player.playFile(filepath) - binding.playbackSeekbar.setOnSeekBarChangeListener(seekBarChangeListener) - - player.setOnTouchListener { _, e -> - if (lockedUI) false else gestures.onTouchEvent(e) - } - mediaSession = initMediaSession() updateMediaSession() BackgroundPlaybackService.mediaToken = mediaSession?.sessionToken @@ -825,7 +836,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse return unhandeled < 2 } - override fun onBackPressed() { + private fun onBackPressedImpl() { if (lockedUI) return showUnlockControls() @@ -857,7 +868,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val wm = windowManager.currentWindowMetrics gestures.setMetrics(wm.bounds.width().toFloat(), wm.bounds.height().toFloat()) - } else { + } else @Suppress("DEPRECATION") { val dm = DisplayMetrics() windowManager.defaultDisplay.getRealMetrics(dm) gestures.setMetrics(dm.widthPixels.toFloat(), dm.heightPixels.toFloat()) @@ -879,8 +890,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse } } - override fun onPictureInPictureModeChanged(state: Boolean) { - super.onPictureInPictureModeChanged(state) + private fun onPiPModeChangedImpl(state: Boolean) { Log.v(TAG, "onPiPModeChanged($state)") if (state) { lockedUI = true @@ -891,6 +901,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse unlockUI() // For whatever stupid reason Android provides no good detection for when PiP is exited // so we have to do this shit (https://stackoverflow.com/questions/43174507/#answer-56127742) + // FIXME: on Android 14 the activity just disappears into the void in this case if (activityIsStopped) { // audio-only detection doesn't work in this situation, I don't care to fix this: this.backgroundPlayMode = "never" @@ -913,8 +924,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse // Intent/Uri parsing private fun parsePathFromIntent(intent: Intent): String? { - val filepath: String? - filepath = when (intent.action) { + val filepath = when (intent.action) { Intent.ACTION_VIEW -> intent.data?.let { resolveUri(it) } Intent.ACTION_SEND -> intent.getStringExtra(Intent.EXTRA_TEXT)?.let { val uri = Uri.parse(it.trim()) @@ -1499,7 +1509,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse } } - fun updatePlaybackPos(position: Int) { + private fun updatePlaybackPos(position: Int) { binding.playbackPositionTxt.text = Utils.prettyTime(position) if (useTimeRemaining) { val diff = psc.durationSec - position From 73593cd39978eca912b359dcb13315eaafd8ed56 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sat, 3 Aug 2024 21:55:53 +0200 Subject: [PATCH 34/78] MPVActivity: treat NUMPAD_ENTER like ENTER or DPAD_CENTER closes #963 --- app/src/main/java/is/xyz/mpv/MPVActivity.kt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/is/xyz/mpv/MPVActivity.kt b/app/src/main/java/is/xyz/mpv/MPVActivity.kt index be370cb2b..b08d61514 100644 --- a/app/src/main/java/is/xyz/mpv/MPVActivity.kt +++ b/app/src/main/java/is/xyz/mpv/MPVActivity.kt @@ -754,6 +754,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse } return false } + // this runs when dpad nagivation is active: when (ev.keyCode) { KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_DOWN -> { @@ -779,7 +780,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse } return true } - KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_DPAD_CENTER -> { + KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_DPAD_CENTER -> { if (ev.action == KeyEvent.ACTION_UP) { val view = dpadButtons().elementAtOrNull(btnSelected) // 500ms appears to be the standard @@ -812,22 +813,26 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse var unhandeled = 0 when (event.unicodeChar.toChar()) { - // overrides a default binding: + // (overrides a default binding) 'j' -> cycleSub() '#' -> cycleAudio() else -> unhandeled++ } + // Note: dpad center is bound according to how Android TV apps should generally behave, + // see . + // Due to implementation inconsistencies enter and numpad enter need to perform the same + // function (issue #963). when (event.keyCode) { - // no default binding: + // (no default binding) KeyEvent.KEYCODE_CAPTIONS -> cycleSub() KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK -> cycleAudio() KeyEvent.KEYCODE_INFO -> toggleControls() KeyEvent.KEYCODE_MENU -> openTopMenu() KeyEvent.KEYCODE_GUIDE -> openTopMenu() - KeyEvent.KEYCODE_DPAD_CENTER -> player.cyclePause() + KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_DPAD_CENTER -> player.cyclePause() - // overrides a default binding: + // (overrides a default binding) KeyEvent.KEYCODE_ENTER -> player.cyclePause() else -> unhandeled++ From 11244c684a15beb44f0978962c3bbd666c55bf7d Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sun, 4 Aug 2024 12:23:07 +0200 Subject: [PATCH 35/78] buildscripts: address meson deprecation warning by just defining both --- buildscripts/buildall.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/buildscripts/buildall.sh b/buildscripts/buildall.sh index 63ad28f79..562080158 100755 --- a/buildscripts/buildall.sh +++ b/buildscripts/buildall.sh @@ -69,6 +69,11 @@ setup_prefix () { local cpu_family=${ndk_triple%%-*} [ "$cpu_family" == "i686" ] && cpu_family=x86 + if ! command -v pkg-config >/dev/null; then + echo "pkg-config not provided!" + return 1 + fi + # meson wants to be spoonfed this file, so create it ahead of time # also define: release build, static libs and no source downloads at runtime(!!!) cat >"$prefix_dir/crossfile.tmp" < Date: Wed, 7 Aug 2024 12:37:00 +0200 Subject: [PATCH 36/78] FilePickerActivity: replace deprecated onBackPressed --- app/src/main/java/is/xyz/mpv/FilePickerActivity.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/is/xyz/mpv/FilePickerActivity.kt b/app/src/main/java/is/xyz/mpv/FilePickerActivity.kt index cf52bcb72..a518a879b 100644 --- a/app/src/main/java/is/xyz/mpv/FilePickerActivity.kt +++ b/app/src/main/java/is/xyz/mpv/FilePickerActivity.kt @@ -14,6 +14,7 @@ import android.util.Log import android.view.* import android.widget.PopupMenu import android.widget.Toast +import androidx.activity.addCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity @@ -43,6 +44,10 @@ class FilePickerActivity : AppCompatActivity(), AbstractFilePickerFragment.OnFil setContentView(R.layout.activity_filepicker) supportActionBar?.title = "" + onBackPressedDispatcher.addCallback(this) { + onBackPressedImpl() + } + // The basic issue we have here is this: https://stackoverflow.com/questions/31190612/ // Some part of the view hierachy swallows the insets during fragment transitions // and it's impossible to invoke this calculation a second time (requestApplyInsets doesn't help). @@ -280,7 +285,7 @@ class FilePickerActivity : AppCompatActivity(), AbstractFilePickerFragment.OnFil } } - override fun onBackPressed() { + private fun onBackPressedImpl() { fragment?.apply { if (!isBackTop) { goUp() From 1ffcbf79b4dd7915876fd5ba09c162a933013775 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Wed, 7 Aug 2024 12:54:59 +0200 Subject: [PATCH 37/78] app: add activity for testing player intent behavior only compiled into debug builds --- app/src/debug/AndroidManifest.xml | 9 ++ .../java/is/xyz/mpv/IntentTestActivity.kt | 65 +++++++++++++++ .../debug/res/layout/activity_intent_test.xml | 83 +++++++++++++++++++ app/src/debug/res/values/strings.xml | 1 + .../java/is/xyz/mpv/MainScreenFragment.kt | 24 ++++++ 5 files changed, 182 insertions(+) create mode 100644 app/src/debug/AndroidManifest.xml create mode 100644 app/src/debug/java/is/xyz/mpv/IntentTestActivity.kt create mode 100644 app/src/debug/res/layout/activity_intent_test.xml create mode 100644 app/src/debug/res/values/strings.xml diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000..56ab76932 --- /dev/null +++ b/app/src/debug/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/debug/java/is/xyz/mpv/IntentTestActivity.kt b/app/src/debug/java/is/xyz/mpv/IntentTestActivity.kt new file mode 100644 index 000000000..4b32cb12d --- /dev/null +++ b/app/src/debug/java/is/xyz/mpv/IntentTestActivity.kt @@ -0,0 +1,65 @@ +package `is`.xyz.mpv + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import `is`.xyz.mpv.databinding.ActivityIntentTestBinding + +class IntentTestActivity : AppCompatActivity() { + private lateinit var binding: ActivityIntentTestBinding + + private val callback = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + updateText("resultCode: ${ActivityResult.resultCodeToString(it.resultCode)}\n") + val intent = it.data + if (intent != null) { + updateText("action: ${intent.action}\ndata: ${intent.data?.toString()}\n") + val extras = intent.extras + if (extras != null) { + for (key in extras.keySet()) { + val v = extras.get(key) + updateText("extras[$key] = $v\n") + } + } + } + } + + private var text = "" + + private fun updateText(append: String) { + text += append + runOnUiThread { + binding.info.text = this.text + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityIntentTestBinding.inflate(layoutInflater) + setContentView(binding.root) + + binding.button.setOnClickListener { + val uri = Uri.parse(binding.editText1.text.toString()) + if (uri.scheme.isNullOrEmpty()) + return@setOnClickListener + + val intent = Intent(Intent.ACTION_VIEW) + intent.setDataAndType(uri, "video/any") + intent.setPackage(packageName) + /*val subtitle = Uri.parse("https://example.org/subtitle.srt") + intent.putExtra("subs", arrayOf(subtitle))*/ + if (binding.switch2.isChecked) + intent.putExtra("decode_mode", 2.toByte()) + if (binding.switch3.isChecked) + intent.putExtra("title", "example text") + if (binding.seekBar2.progress > 0) + intent.putExtra("position", binding.seekBar2.progress * 1000) + callback.launch(intent) + + text = "" + updateText("launched!\n") + } + } +} diff --git a/app/src/debug/res/layout/activity_intent_test.xml b/app/src/debug/res/layout/activity_intent_test.xml new file mode 100644 index 000000000..ce81f3b47 --- /dev/null +++ b/app/src/debug/res/layout/activity_intent_test.xml @@ -0,0 +1,83 @@ + + + + + + + +