diff --git a/.github/ISSUE_TEMPLATE/1_bug_report.md b/.github/ISSUE_TEMPLATE/1_bug_report.md
index 0e3d28227..79044deb0 100644
--- a/.github/ISSUE_TEMPLATE/1_bug_report.md
+++ b/.github/ISSUE_TEMPLATE/1_bug_report.md
@@ -28,7 +28,7 @@ The Android SDK includes a tool named [`logcat`](https://developer.android.com/s
You will need a computer to view these logs.
Logs pertaining to mpv can be collected like this:
- adb logcat -s 'mpv:*' '*:F'
+ adb logcat -s mpv '*:F'
You can attach text files on Github directly or use sites like https://0x0.st/ to upload your logs.
Depending on the nature of the bug, a log file might not be required and you can *omit this section*. If in doubt provide one, it will help us find possible issues later.
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 4e20d7cb9..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-language: generic
-os: linux
-dist: jammy
-notifications:
- email: false
-
-addons:
- apt:
- packages:
- - autoconf
- - pkg-config
- - libtool
- - ninja-build
- - python3-pip
- - python3-setuptools
- - openjdk-17-jdk-headless
-
-env:
- global:
- - JAVA_HOME: '/usr/lib/jvm/java-17-openjdk-amd64'
-
-before_install:
- - sudo pip3 install meson
- - CACHE_MODE=github buildscripts/.travis.sh install
-script:
- - buildscripts/.travis.sh build
-
-before_cache:
- - find $HOME/.gradle/caches -maxdepth 3 -name '*.lock' -delete
- - rm -rf $HOME/.gradle/caches/journal-1/
-cache:
- directories:
- - $HOME/.gradle/caches/
- - $HOME/.gradle/wrapper/
diff --git a/README.md b/README.md
index 1bff59181..b68e89b73 100644
--- a/README.md
+++ b/README.md
@@ -12,13 +12,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.
-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/).
+### 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), [`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
@@ -28,6 +32,10 @@ You can download mpv-android from the [Releases section](https://github.com/mpv-
[ ](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/app/build.gradle b/app/build.gradle
index 5efd73cf1..8febad07f 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -11,9 +11,9 @@ android {
defaultConfig {
minSdkVersion 21
- targetSdkVersion 33
- versionCode 35
- versionName "1.15.n"
+ targetSdkVersion 34
+ versionCode 40
+ versionName "1.16.n"
vectorDrawables.useSupportLibrary = true
}
@@ -69,13 +69,13 @@ android {
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'
}
def getVersionName = { ->
- return "1.15.n"
+ return "1.16.n"
}
def getGroupId = { ->
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..2d4686c91
--- /dev/null
+++ b/app/src/debug/java/is/xyz/mpv/IntentTestActivity.kt
@@ -0,0 +1,71 @@
+package `is`.xyz.mpv
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.util.Base64
+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)
+ if (binding.switch1.isChecked) {
+ val subMime = "application/x-subrip"
+ val subData = "1\n00:00:00,000 --> 00:00:10,000\nHello World\n\n"
+ val subUri = Uri.parse("data:${subMime};base64," + Base64.encodeToString(subData.toByteArray(), Base64.NO_WRAP))
+ intent.putExtra("subs", arrayOf(subUri))
+ intent.putExtra("subs.enable", arrayOf(subUri))
+ }
+ 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..9b8d1fb2a
--- /dev/null
+++ b/app/src/debug/res/layout/activity_intent_test.xml
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml
new file mode 100644
index 000000000..7abc06d3b
--- /dev/null
+++ b/app/src/debug/res/values/strings.xml
@@ -0,0 +1 @@
+
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0b0160750..bbd2e91d8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,6 +1,6 @@
+ android:versionCode="40"
+ android:versionName="1.16.n" >
diff --git a/app/src/main/assets/cacert.pem b/app/src/main/assets/cacert.pem
index f78a6101a..86d6cd80c 100644
--- a/app/src/main/assets/cacert.pem
+++ b/app/src/main/assets/cacert.pem
@@ -1,7 +1,7 @@
##
## Bundle of CA Root Certificates
##
-## Certificate data from Mozilla as of: Mon Mar 11 15:25:27 2024 GMT
+## Certificate data from Mozilla as of: Tue Jul 2 03:12:04 2024 GMT
##
## This is a bundle of X.509 certificates of public Certificate Authorities
## (CA). These were automatically extracted from Mozilla's root certificates
@@ -14,7 +14,7 @@
## Just configure this file as the SSLCACertificateFile.
##
## Conversion done with mk-ca-bundle.pl version 1.29.
-## SHA256: 4d96bd539f4719e9ace493757afbe4a23ee8579de1c97fbebc50bba3c12e8c1e
+## SHA256: 456ff095dde6dd73354c5c28c73d9c06f53b61a803963414cb91a1d92945cdd3
##
@@ -2600,36 +2600,6 @@ vLtoURMMA/cVi4RguYv/Uo7njLwcAjA8+RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+
CAezNIm8BZ/3Hobui3A=
-----END CERTIFICATE-----
-GLOBALTRUST 2020
-================
------BEGIN CERTIFICATE-----
-MIIFgjCCA2qgAwIBAgILWku9WvtPilv6ZeUwDQYJKoZIhvcNAQELBQAwTTELMAkGA1UEBhMCQVQx
-IzAhBgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVT
-VCAyMDIwMB4XDTIwMDIxMDAwMDAwMFoXDTQwMDYxMDAwMDAwMFowTTELMAkGA1UEBhMCQVQxIzAh
-BgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVTVCAy
-MDIwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAri5WrRsc7/aVj6B3GyvTY4+ETUWi
-D59bRatZe1E0+eyLinjF3WuvvcTfk0Uev5E4C64OFudBc/jbu9G4UeDLgztzOG53ig9ZYybNpyrO
-VPu44sB8R85gfD+yc/LAGbaKkoc1DZAoouQVBGM+uq/ufF7MpotQsjj3QWPKzv9pj2gOlTblzLmM
-CcpL3TGQlsjMH/1WljTbjhzqLL6FLmPdqqmV0/0plRPwyJiT2S0WR5ARg6I6IqIoV6Lr/sCMKKCm
-fecqQjuCgGOlYx8ZzHyyZqjC0203b+J+BlHZRYQfEs4kUmSFC0iAToexIiIwquuuvuAC4EDosEKA
-A1GqtH6qRNdDYfOiaxaJSaSjpCuKAsR49GiKweR6NrFvG5Ybd0mN1MkGco/PU+PcF4UgStyYJ9OR
-JitHHmkHr96i5OTUawuzXnzUJIBHKWk7buis/UDr2O1xcSvy6Fgd60GXIsUf1DnQJ4+H4xj04KlG
-DfV0OoIu0G4skaMxXDtG6nsEEFZegB31pWXogvziB4xiRfUg3kZwhqG8k9MedKZssCz3AwyIDMvU
-clOGvGBG85hqwvG/Q/lwIHfKN0F5VVJjjVsSn8VoxIidrPIwq7ejMZdnrY8XD2zHc+0klGvIg5rQ
-mjdJBKuxFshsSUktq6HQjJLyQUp5ISXbY9e2nKd+Qmn7OmMCAwEAAaNjMGEwDwYDVR0TAQH/BAUw
-AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFNwuH9FhN3nkq9XVsxJxaD1qaJwiMB8GA1Ud
-IwQYMBaAFNwuH9FhN3nkq9XVsxJxaD1qaJwiMA0GCSqGSIb3DQEBCwUAA4ICAQCR8EICaEDuw2jA
-VC/f7GLDw56KoDEoqoOOpFaWEhCGVrqXctJUMHytGdUdaG/7FELYjQ7ztdGl4wJCXtzoRlgHNQIw
-4Lx0SsFDKv/bGtCwr2zD/cuz9X9tAy5ZVp0tLTWMstZDFyySCstd6IwPS3BD0IL/qMy/pJTAvoe9
-iuOTe8aPmxadJ2W8esVCgmxcB9CpwYhgROmYhRZf+I/KARDOJcP5YBugxZfD0yyIMaK9MOzQ0MAS
-8cE54+X1+NZK3TTN+2/BT+MAi1bikvcoskJ3ciNnxz8RFbLEAwW+uxF7Cr+obuf/WEPPm2eggAe2
-HcqtbepBEX4tdJP7wry+UUTF72glJ4DjyKDUEuzZpTcdN3y0kcra1LGWge9oXHYQSa9+pTeAsRxS
-vTOBTI/53WXZFM2KJVj04sWDpQmQ1GwUY7VA3+vA/MRYfg0UFodUJ25W5HCEuGwyEn6CMUO+1918
-oa2u1qsgEu8KwxCMSZY13At1XrFP1U80DhEgB3VDRemjEdqso5nCtnkn4rnvyOL2NSl6dPrFf4IF
-YqYK6miyeUcGbvJXqBUzxvd4Sj1Ce2t+/vdG6tHrju+IaFvowdlxfv1k7/9nR4hYJS8+hge9+6jl
-gqispdNpQ80xiEmEU5LAsTkbOYMBMMTyqfrQA71yN2BWHzZ8vTmR9W0Nv3vXkg==
------END CERTIFICATE-----
-
ANF Secure Server Root CA
=========================
-----BEGIN CERTIFICATE-----
@@ -3579,3 +3549,20 @@ wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+ljX273CXE2whJdV/LItM3z7gLfEdxquVeE
HVlNjM7IDiPCtyaaEBRx/pOyiriA8A4QntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0
o82bNSQ3+pCTE4FCxpgmdTdmQRCsu/WU48IxK63nI1bMNSWSs1A=
-----END CERTIFICATE-----
+
+FIRMAPROFESIONAL CA ROOT-A WEB
+==============================
+-----BEGIN CERTIFICATE-----
+MIICejCCAgCgAwIBAgIQMZch7a+JQn81QYehZ1ZMbTAKBggqhkjOPQQDAzBuMQswCQYDVQQGEwJF
+UzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UEYQwPVkFURVMtQTYyNjM0MDY4
+MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENBIFJPT1QtQSBXRUIwHhcNMjIwNDA2MDkwMTM2
+WhcNNDcwMzMxMDkwMTM2WjBuMQswCQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25h
+bCBTQTEYMBYGA1UEYQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFM
+IENBIFJPT1QtQSBXRUIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARHU+osEaR3xyrq89Zfe9MEkVz6
+iMYiuYMQYneEMy3pA4jU4DP37XcsSmDq5G+tbbT4TIqk5B/K6k84Si6CcyvHZpsKjECcfIr28jlg
+st7L7Ljkb+qbXbdTkBgyVcUgt5SjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUk+FD
+Y1w8ndYn81LsF7Kpryz3dvgwHQYDVR0OBBYEFJPhQ2NcPJ3WJ/NS7Beyqa8s93b4MA4GA1UdDwEB
+/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjAdfKR7w4l1M+E7qUW/Runpod3JIha3RxEL2Jq68cgL
+cFBTApFwhVmpHqTm6iMxoAACMQD94vizrxa5HnPEluPBMBnYfubDl94cT7iJLzPrSA8Z94dGXSaQ
+pYXFuXqUPoeovQA=
+-----END CERTIFICATE-----
diff --git a/app/src/main/assets/subfont.ttf b/app/src/main/assets/subfont.ttf
index 56d013f81..9659afc65 100644
Binary files a/app/src/main/assets/subfont.ttf and b/app/src/main/assets/subfont.ttf differ
diff --git a/app/src/main/java/is/xyz/filepicker/DocumentPickerFragment.java b/app/src/main/java/is/xyz/filepicker/DocumentPickerFragment.java
index 44b5dc639..31678c772 100644
--- a/app/src/main/java/is/xyz/filepicker/DocumentPickerFragment.java
+++ b/app/src/main/java/is/xyz/filepicker/DocumentPickerFragment.java
@@ -9,6 +9,8 @@
import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.util.Predicate;
import androidx.loader.content.AsyncTaskLoader;
import androidx.loader.content.Loader;
@@ -31,9 +33,10 @@ public class DocumentPickerFragment extends AbstractFilePickerFragment {
// The structure of the file picker assumes that only the file URIs matter and you can
// grab additional info for free afterwards. This is not the case with the documents API so we
// have to work around it.
- final HashMap mLastRead;
+ private final HashMap mLastRead;
// maps document ID of directories to parent path
- final HashMap mParents;
+ private final HashMap mParents;
+ protected Predicate mFilterPredicate;
public DocumentPickerFragment(@NonNull Uri root) {
mRoot = root;
@@ -61,6 +64,25 @@ public static boolean isTreeUsable(@NonNull Context context, @NonNull Uri treeUr
}
}
+ /**
+ * This method is used to set the filter that determines the documents to be shown
+ *
+ * @param predicate filter implementation or null
+ */
+ public void setFilterPredicate(@Nullable Predicate predicate) {
+ this.mFilterPredicate = predicate;
+ refresh(mCurrentPath);
+ }
+
+ /**
+ * Returns the filter that determines the documents to be shown
+ *
+ * @return filter implementation or null
+ */
+ public @Nullable Predicate getFilterPredicate() {
+ return this.mFilterPredicate;
+ }
+
@Override
public boolean isDir(@NonNull Uri path) {
Document doc = mLastRead.get(path);
@@ -69,6 +91,7 @@ public boolean isDir(@NonNull Uri path) {
}
// retrieve the data uncached (not supposed to happen)
+ Log.w(TAG, "isDir(): uncached read");
return isDir(requireContext(), path);
}
@@ -113,6 +136,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)
@@ -167,14 +194,17 @@ public List loadInBackground() {
ArrayList files = new ArrayList<>();
final int i1 = c.getColumnIndex(cols[0]), i2 = c.getColumnIndex(cols[1]), i3 = c.getColumnIndex(cols[2]);
while (c.moveToNext()) {
- // TODO later: support FileFilter equivalent here
final String docId = c.getString(i1);
final boolean isDir = c.getString(i2).equals(DocumentsContract.Document.MIME_TYPE_DIR);
- files.add(new Document(
+ final Document doc = new Document(
DocumentsContract.buildDocumentUriUsingTree(root, docId),
isDir,
c.getString(i3)
- ));
+ );
+ if (mFilterPredicate != null && !mFilterPredicate.test(doc))
+ continue;
+ files.add(doc);
+
// There is no generic way to get a parent directory for another directory and this
// can't be solved via mLastRead either, since by the time someone asks getParent()
// we're already inside the new directory. Not to mention that this would be insufficient
@@ -209,7 +239,7 @@ protected void onStartLoading() {
* Class that represents a document.
* Wrapper around a content:// URI but with extra information provided at no extra cost (cached).
*/
- private static class Document implements Comparable {
+ public static class Document implements Comparable {
private final @NonNull Uri uri;
private final boolean isDir;
private final @NonNull String displayName;
@@ -220,6 +250,20 @@ private Document(@NonNull Uri uri, boolean dir, @NonNull String name) {
displayName = name;
}
+ @NonNull
+ public Uri getUri() {
+ return uri;
+ }
+
+ public boolean isDirectory() {
+ return isDir;
+ }
+
+ @NonNull
+ public String getDisplayName() {
+ return displayName;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
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 7903a3115..b13de5c93 100644
--- a/app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.kt
+++ b/app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.kt
@@ -4,6 +4,7 @@ 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
@@ -14,6 +15,8 @@ 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
/*
@@ -42,13 +45,9 @@ 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)
@@ -97,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")
@@ -109,7 +114,12 @@ class BackgroundPlaybackService : Service(), MPVLib.EventObserver {
// create notification and turn this into a "foreground service"
val notification = buildNotification()
- startForeground(NOTIFICATION_ID, notification)
+ 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
}
@@ -117,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")
}
@@ -124,25 +137,27 @@ 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")
return
paused = value
-
- val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- notificationManager.notify(NOTIFICATION_ID, buildNotification())
+ refreshNotification()
}
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
-
- val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- notificationManager.notify(NOTIFICATION_ID, buildNotification())
+ refreshNotification()
}
override fun event(eventId: Int) {
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..a6bf7826d
--- /dev/null
+++ b/app/src/main/java/is/xyz/mpv/BaseMPVView.kt
@@ -0,0 +1,109 @@
+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, logLvl: String = "v", vo: String = "gpu") {
+ MPVLib.create(context, logLvl)
+
+ /* 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(vo)
+
+ MPVLib.init()
+
+ /* set hardcoded options */
+ 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()
+ }
+
+ /**
+ * 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(vo: String)
+ 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 = ""
+
+ /**
+ * 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/FilePickerActivity.kt b/app/src/main/java/is/xyz/mpv/FilePickerActivity.kt
index de6906118..796017fc8 100644
--- a/app/src/main/java/is/xyz/mpv/FilePickerActivity.kt
+++ b/app/src/main/java/is/xyz/mpv/FilePickerActivity.kt
@@ -14,11 +14,14 @@ 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
+import androidx.core.util.Predicate
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
+import `is`.xyz.filepicker.DocumentPickerFragment
import `is`.xyz.filepicker.FilePickerFragment
import `is`.xyz.mpv.databinding.FragmentFilepickerChoiceBinding
import java.io.File
@@ -43,6 +46,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).
@@ -94,6 +101,20 @@ class FilePickerActivity : AppCompatActivity(), AbstractFilePickerFragment.OnFil
lastSeenInsets?.let { recycler.onApplyWindowInsets(lastSeenInsets) }
}
+ private fun getFilterState(): Boolean {
+ with (PreferenceManager.getDefaultSharedPreferences(this)) {
+ // naming is a legacy leftover
+ return getBoolean("MainActivity_filter_state", false)
+ }
+ }
+
+ private fun saveFilterState(enabled: Boolean) {
+ with (PreferenceManager.getDefaultSharedPreferences(this).edit()) {
+ this.putBoolean("MainActivity_filter_state", enabled)
+ apply()
+ }
+ }
+
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array,
grantResults: IntArray
@@ -107,14 +128,22 @@ class FilePickerActivity : AppCompatActivity(), AbstractFilePickerFragment.OnFil
}
}
+ private fun inflateOptionsMenu(menu: Menu) {
+ menuInflater.inflate(R.menu.menu_filepicker, menu)
+ // document picker does not have a concept of storages
+ if (fragment == null)
+ menu.findItem(R.id.action_external_storage).isVisible = false
+ }
+
override fun onCreateOptionsMenu(menu: Menu): Boolean {
- if (fragment == null) // no menu in doc picker mode
- return true
val uiModeManager = getSystemService(UI_MODE_SERVICE) as UiModeManager
- if (uiModeManager.currentModeType != Configuration.UI_MODE_TYPE_TELEVISION)
- menuInflater.inflate(R.menu.menu_filepicker, menu)
- else
- menu.add(Menu.NONE, Menu.NONE, Menu.NONE, "...") // dummy menu item to indicate presence
+ if (uiModeManager.currentModeType != Configuration.UI_MODE_TYPE_TELEVISION) {
+ inflateOptionsMenu(menu)
+ } else {
+ // add a dummy menu item so the menu icon shows up, even though you can't use it on TV.
+ // it is instead opened via dpad keys
+ menu.add(Menu.NONE, Menu.NONE, Menu.NONE, "...")
+ }
return true
}
@@ -143,20 +172,20 @@ class FilePickerActivity : AppCompatActivity(), AbstractFilePickerFragment.OnFil
return true
}
R.id.action_file_filter -> {
- val old: Boolean
- with (fragment!!) {
+ var old = false
+ fragment?.apply {
old = filterPredicate != null
filterPredicate = if (!old) MEDIA_FILE_FILTER else null
}
+ fragment2?.apply {
+ old = filterPredicate != null
+ filterPredicate = if (!old) MEDIA_DOC_FILTER else null
+ }
with (Toast.makeText(this, "", Toast.LENGTH_SHORT)) {
setText(if (!old) R.string.notice_show_media_files else R.string.notice_show_all_files)
show()
}
- // remember state for next time
- with (PreferenceManager.getDefaultSharedPreferences(this).edit()) {
- this.putBoolean("${PREF_PREFIX}filter_state", !old)
- apply()
- }
+ saveFilterState(!old)
return true
}
else -> return false
@@ -183,9 +212,8 @@ class FilePickerActivity : AppCompatActivity(), AbstractFilePickerFragment.OnFil
Log.v(TAG, "FilePickerActivity: showing file picker")
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
- if (sharedPrefs.getBoolean("${PREF_PREFIX}filter_state", false)) {
+ if (getFilterState())
fragment!!.filterPredicate = MEDIA_FILE_FILTER
- }
var defaultPathStr = intent.getStringExtra("default_path")
if (defaultPathStr.isNullOrEmpty()) {
@@ -218,13 +246,11 @@ class FilePickerActivity : AppCompatActivity(), AbstractFilePickerFragment.OnFil
}
}
- override fun dispatchKeyEvent(ev: KeyEvent?): Boolean {
+ override fun dispatchKeyEvent(ev: KeyEvent): Boolean {
// If up is pressed at the header element display the usual options menu as a popup menu
// to make it usable on Android TV.
var openMenu = false
- if (fragment == null) {
- // only for file picker
- } else if (ev?.action == KeyEvent.ACTION_DOWN && ev.keyCode == KeyEvent.KEYCODE_DPAD_UP) {
+ if (ev.action == KeyEvent.ACTION_DOWN && ev.keyCode == KeyEvent.KEYCODE_DPAD_UP) {
val recycler: RecyclerView = findViewById(android.R.id.list)
val holder = try {
window.currentFocus?.let { recycler.getChildViewHolder(it) }
@@ -238,7 +264,7 @@ class FilePickerActivity : AppCompatActivity(), AbstractFilePickerFragment.OnFil
setOnMenuItemClickListener {
this@FilePickerActivity.onOptionsItemSelected(it)
}
- inflate(R.menu.menu_filepicker)
+ this@FilePickerActivity.inflateOptionsMenu(menu)
show()
}
return true
@@ -259,6 +285,9 @@ class FilePickerActivity : AppCompatActivity(), AbstractFilePickerFragment.OnFil
}
}
+ if (getFilterState())
+ fragment2!!.filterPredicate = MEDIA_DOC_FILTER
+
with (supportFragmentManager.beginTransaction()) {
setReorderingAllowed(true)
add(R.id.fragment_container_view, fragment2!!, null)
@@ -280,7 +309,7 @@ class FilePickerActivity : AppCompatActivity(), AbstractFilePickerFragment.OnFil
}
}
- override fun onBackPressed() {
+ private fun onBackPressedImpl() {
fragment?.apply {
if (!isBackTop) {
goUp()
@@ -373,6 +402,15 @@ class FilePickerActivity : AppCompatActivity(), AbstractFilePickerFragment.OnFil
}
}
+ private val MEDIA_DOC_FILTER = Predicate { doc ->
+ if (doc.isDirectory) {
+ true
+ } else {
+ val ext = doc.displayName.substringAfterLast('.', "")
+ Utils.MEDIA_EXTENSIONS.contains(ext.lowercase())
+ }
+ }
+
// values for "skip" in intent
const val URL_DIALOG = 0
const val FILE_PICKER = 1
diff --git a/app/src/main/java/is/xyz/mpv/MPVActivity.kt b/app/src/main/java/is/xyz/mpv/MPVActivity.kt
index 3087b7abd..78a560b7b 100644
--- a/app/src/main/java/is/xyz/mpv/MPVActivity.kt
+++ b/app/src/main/java/is/xyz/mpv/MPVActivity.kt
@@ -3,11 +3,14 @@ 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
+import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
+import android.content.IntentFilter
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.content.res.ColorStateList
@@ -28,6 +31,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
@@ -38,6 +42,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 +72,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()
@@ -80,8 +88,8 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
if (!fromUser)
return
- player.timePos = progress
- updatePlaybackPos(progress)
+ player.timePos = progress.toDouble() / SEEK_BAR_PRECISION
+ // Note: don't call updatePlaybackPos() here either
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
@@ -90,9 +98,21 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse
override fun onStopTrackingTouch(seekBar: SeekBar) {
userIsOperatingSeekbar = false
+ showControls() // re-trigger display timeout
}
}
+ private val becomingNoisyReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ if (intent?.action == AudioManager.ACTION_AUDIO_BECOMING_NOISY) {
+ // effects are the identical
+ audioFocusChangeListener.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS)
+ }
+ }
+ }
+ private var becomingNoisyReceiverRegistered = false
+
+ // 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)
@@ -191,6 +211,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse
private var smoothSeekGesture = false
/* * */
+ @SuppressLint("ClickableViewAccessibility")
private fun initListeners() {
with (binding) {
prevBtn.setOnClickListener { playlistPrev() }
@@ -216,6 +237,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 ->
@@ -231,6 +258,14 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse
}
WindowInsetsCompat.CONSUMED
}
+
+ onBackPressedDispatcher.addCallback(this) {
+ onBackPressedImpl()
+ }
+
+ addOnPictureInPictureModeChangedListener { info ->
+ onPiPModeChangedImpl(info.isInPictureInPictureMode)
+ }
}
private var playbackHasStarted = false
@@ -289,31 +324,36 @@ 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)
-
- player.setOnTouchListener { _, e ->
- if (lockedUI) false else gestures.onTouchEvent(e)
- }
-
mediaSession = initMediaSession()
updateMediaSession()
BackgroundPlaybackService.mediaToken = mediaSession?.sessionToken
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 +385,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()
@@ -379,8 +421,8 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse
private fun isPlayingAudioOnly(): Boolean {
if (player.aid == -1)
return false
- val fmt = MPVLib.getPropertyString("video-format")
- return fmt.isNullOrEmpty() || arrayOf("mjpeg", "png", "bmp").indexOf(fmt) != -1
+ val image = MPVLib.getPropertyString("current-tracks/video/image")
+ return image.isNullOrEmpty() || image == "yes"
}
private fun shouldBackground(): Boolean {
@@ -433,10 +475,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)
}
}
@@ -458,7 +497,8 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse
this.backgroundPlayMode = getString("background_play", R.string.pref_background_play_default)
this.noUIPauseMode = getString("no_ui_pause", R.string.pref_no_ui_pause_default)
this.shouldSavePosition = prefs.getBoolean("save_position", false)
- this.autoRotationMode = getString("auto_rotation", R.string.pref_auto_rotation_default)
+ if (this.autoRotationMode != "manual") // don't reset
+ this.autoRotationMode = getString("auto_rotation", R.string.pref_auto_rotation_default)
this.controlsAtBottom = prefs.getBoolean("bottom_controls", true)
this.showMediaTitle = prefs.getBoolean("display_media_title", false)
this.useTimeRemaining = prefs.getBoolean("use_time_remaining", false)
@@ -565,11 +605,10 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse
private fun controlsShouldBeVisible(): Boolean {
if (lockedUI)
return false
- // If either the audio UI is active or a button is selected for dpad navigation
- // the controls should never hide
- return useAudioUI || btnSelected != -1
+ return useAudioUI || btnSelected != -1 || userIsOperatingSeekbar
}
+ /** Make controls visible, also controls the timeout until they fade. */
private fun showControls() {
if (lockedUI) {
Log.w(TAG, "cannot show UI in locked mode")
@@ -603,6 +642,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse
fadeHandler.postDelayed(fadeRunnable, CONTROLS_DISPLAY_TIMEOUT)
}
+ /** Hide controls instantly */
fun hideControls() {
if (controlsShouldBeVisible())
return
@@ -616,18 +656,23 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse
insetsController.hide(WindowInsetsCompat.Type.systemBars())
}
- private fun hideControlsDelayed() {
+ /** Start fading out the controls */
+ private fun hideControlsFade() {
fadeHandler.removeCallbacks(fadeRunnable)
fadeHandler.post(fadeRunnable)
}
+ /**
+ * Toggle visibility of controls (if allowed)
+ * @return future visibility state
+ */
private fun toggleControls(): Boolean {
if (lockedUI)
return false
if (controlsShouldBeVisible())
return true
return if (binding.controls.visibility == View.VISIBLE && !fadeRunnable.hasStarted) {
- hideControlsDelayed()
+ hideControlsFade()
false
} else {
showControls()
@@ -728,13 +773,14 @@ 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 -> {
if (ev.action == KeyEvent.ACTION_DOWN) { // deactivate dpad navigation
btnSelected = -1
updateSelectedDpadButton()
- hideControlsDelayed()
+ hideControlsFade()
}
return true
}
@@ -753,7 +799,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
@@ -786,22 +832,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++
@@ -810,7 +860,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse
return unhandeled < 2
}
- override fun onBackPressed() {
+ private fun onBackPressedImpl() {
if (lockedUI)
return showUnlockControls()
@@ -842,7 +892,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())
@@ -864,8 +914,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
@@ -876,6 +925,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"
@@ -898,8 +948,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())
@@ -914,6 +963,8 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse
val filepath = when (data.scheme) {
"file" -> data.path
"content" -> openContentFd(data)
+ // mpv supports data URIs but needs data:// to pass it through correctly
+ "data" -> "data://${data.schemeSpecificPart}"
"http", "https", "rtmp", "rtmps", "rtp", "rtsp", "mms", "mmst", "mmsh", "tcp", "udp", "lavf"
-> data.toString()
else -> null
@@ -950,25 +1001,34 @@ 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 = 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)
+ 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)
@@ -1129,7 +1189,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse
private fun lockUI() {
lockedUI = true
- hideControlsDelayed()
+ hideControlsFade()
}
private fun unlockUI() {
@@ -1207,6 +1267,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)
@@ -1240,15 +1302,17 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse
MPVLib.command(arrayOf("add", "chapter", "1")); true
},
MenuItem(R.id.advancedBtn) { openAdvancedMenu(restoreState); false },
- MenuItem(R.id.orientationBtn) { this.cycleOrientation(); true }
+ MenuItem(R.id.orientationBtn) {
+ autoRotationMode = "manual"
+ cycleOrientation()
+ true
+ }
)
if (player.aid == -1)
hiddenButtons.add(R.id.backgroundBtn)
if (MPVLib.getPropertyInt("chapter-list/count") ?: 0 == 0)
hiddenButtons.add(R.id.rowChapter)
- if (autoRotationMode == "auto")
- hiddenButtons.add(R.id.orientationBtn)
/******/
genericMenu(R.layout.dialog_top_menu, buttons, hiddenButtons, restoreState)
@@ -1473,7 +1537,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
@@ -1483,7 +1547,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.
@@ -1494,7 +1558,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) {
@@ -1502,10 +1566,17 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse
binding.playBtn.setImageResource(r)
updatePiPParams()
- if (paused)
+ if (paused) {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
- else
+ if (becomingNoisyReceiverRegistered)
+ unregisterReceiver(becomingNoisyReceiver)
+ becomingNoisyReceiverRegistered = false
+ } else {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+ if (!becomingNoisyReceiverRegistered)
+ registerReceiver(becomingNoisyReceiver, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY))
+ becomingNoisyReceiverRegistered = true
+ }
}
private fun updateDecoderButton() {
@@ -1558,7 +1629,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
@@ -1585,7 +1656,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))
}
@@ -1600,6 +1671,29 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse
// Media Session handling
+ private val mediaSessionCallback = object : MediaSessionCompat.Callback() {
+ override fun onPause() {
+ player.paused = true
+ }
+ override fun onPlay() {
+ player.paused = false
+ }
+ override fun onSeekTo(pos: Long) {
+ player.timePos = (pos / 1000.0)
+ }
+ override fun onSkipToNext() = playlistNext()
+ override fun onSkipToPrevious() = playlistPrev()
+ override fun onSetRepeatMode(repeatMode: Int) {
+ MPVLib.setPropertyString("loop-playlist",
+ if (repeatMode == PlaybackStateCompat.REPEAT_MODE_ALL) "inf" else "no")
+ MPVLib.setPropertyString("loop-file",
+ if (repeatMode == PlaybackStateCompat.REPEAT_MODE_ONE) "inf" else "no")
+ }
+ override fun onSetShuffleMode(shuffleMode: Int) {
+ player.changeShuffle(false, shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL)
+ }
+ }
+
private fun initMediaSession(): MediaSessionCompat {
/*
https://developer.android.com/guide/topics/media-apps/working-with-a-media-session
@@ -1608,28 +1702,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse
*/
val session = MediaSessionCompat(this, TAG)
session.setFlags(0)
- session.setCallback(object : MediaSessionCompat.Callback() {
- override fun onPause() {
- player.paused = true
- }
- override fun onPlay() {
- player.paused = false
- }
- override fun onSeekTo(pos: Long) {
- player.timePos = (pos / 1000).toInt()
- }
- override fun onSkipToNext() = playlistNext()
- override fun onSkipToPrevious() = playlistPrev()
- override fun onSetRepeatMode(repeatMode: Int) {
- MPVLib.setPropertyString("loop-playlist",
- if (repeatMode == PlaybackStateCompat.REPEAT_MODE_ALL) "inf" else "no")
- MPVLib.setPropertyString("loop-file",
- if (repeatMode == PlaybackStateCompat.REPEAT_MODE_ONE) "inf" else "no")
- }
- override fun onSetShuffleMode(shuffleMode: Int) {
- player.changeShuffle(false, shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL)
- }
- })
+ session.setCallback(mediaSessionCallback)
return session
}
@@ -1641,17 +1714,15 @@ 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()
- "video-out-params/aspect", "video-out-params/rotate" -> {
- updateOrientation()
- updatePiPParams()
- }
- "video-format" -> updateAudioUI()
+ "current-tracks/video/image" -> updateAudioUI()
"hwdec-current" -> updateDecoderButton()
}
+ if (metaUpdated)
+ updateMetadataDisplay()
}
private fun eventPropertyUi(property: String, value: Boolean) {
@@ -1664,18 +1735,28 @@ 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()
}
}
- private fun eventPropertyUi(property: String, value: String, triggerMetaUpdate: Boolean) {
+ 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()
+ }
+ }
+ }
+
+ private fun eventPropertyUi(property: String, value: String, metaUpdated: Boolean) {
if (!activityIsForeground) return
when (property) {
"speed" -> updateSpeedButton()
}
- if (triggerMetaUpdate)
+ if (metaUpdated)
updateMetadataDisplay()
}
@@ -1685,6 +1766,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
@@ -1694,7 +1778,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) {
@@ -1719,13 +1803,21 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse
eventUiHandler.post { eventPropertyUi(property, value) }
}
+ override fun eventProperty(property: String, value: Double) {
+ if (psc.update(property, value))
+ updateMediaSession()
+
+ if (!activityIsForeground) return
+ eventUiHandler.post { eventPropertyUi(property, value) }
+ }
+
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) {
@@ -1736,8 +1828,7 @@ class MPVActivity : AppCompatActivity(), MPVLib.EventObserver, TouchGesturesObse
for (c in onloadCommands)
MPVLib.command(c)
if (this.statsLuaMode > 0 && !playbackHasStarted) {
- MPVLib.command(arrayOf("script-binding", "stats/display-stats-toggle"))
- MPVLib.command(arrayOf("script-binding", "stats/${this.statsLuaMode}"))
+ MPVLib.command(arrayOf("script-binding", "stats/display-page-${this.statsLuaMode}-toggle"))
}
playbackHasStarted = true
@@ -1751,11 +1842,12 @@ 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
- 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)
@@ -1771,14 +1863,14 @@ 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(AudioManager.STREAM_MUSIC)
+ initialVolume = getStreamVolume(STREAM_TYPE)
maxVolume = if (isVolumeFixed)
0
else
- getStreamMaxVolume(AudioManager.STREAM_MUSIC)
+ getStreamMaxVolume(STREAM_TYPE)
}
pausedForSeek = 0
@@ -1788,8 +1880,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
@@ -1797,25 +1889,28 @@ 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 // (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)
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)
}
@@ -1869,5 +1964,9 @@ 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
+ // precision used by seekbar (1/s)
+ private const val SEEK_BAR_PRECISION = 2
}
}
diff --git a/app/src/main/java/is/xyz/mpv/MPVLib.java b/app/src/main/java/is/xyz/mpv/MPVLib.java
index f5fae9474..20166101c 100644
--- a/app/src/main/java/is/xyz/mpv/MPVLib.java
+++ b/app/src/main/java/is/xyz/mpv/MPVLib.java
@@ -32,6 +32,7 @@ public class MPVLib {
public static native Bitmap grabThumbnail(int dimension);
+ // FIXME: get methods are actually nullable
public static native Integer getPropertyInt(@NonNull String property);
public static native void setPropertyInt(@NonNull String property, @NonNull Integer value);
public static native Double getPropertyDouble(@NonNull String property);
@@ -66,6 +67,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)
@@ -115,6 +123,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);
void efEvent(String err);
}
diff --git a/app/src/main/java/is/xyz/mpv/MPVView.kt b/app/src/main/java/is/xyz/mpv/MPVView.kt
index c4fab17e0..492e3e778 100644
--- a/app/src/main/java/is/xyz/mpv/MPVView.kt
+++ b/app/src/main/java/is/xyz/mpv/MPVView.kt
@@ -1,48 +1,26 @@
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 kotlin.math.abs
+import androidx.core.content.ContextCompat
+import `is`.xyz.mpv.MPVLib.mpvFormat.*
import kotlin.reflect.KProperty
-class MPVView(context: Context, attrs: AttributeSet) : SurfaceView(context, attrs), SurfaceHolder.Callback {
- fun initialize(configDir: String, cacheDir: String, logLvl: String = "v", vo: String = "gpu") {
- MPVLib.create(this.context, logLvl)
- 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(vo) // 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 = ""
+class MPVView(context: Context, attrs: AttributeSet) : BaseMPVView(context, attrs) {
- private fun initOptions(vo: String) {
- val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.context)
+ override fun initOptions(vo: String) {
+ val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
// apply phone-optimized defaults
MPVLib.setOptionString("profile", "fast")
// vo
- voInUse = vo
+ setVo(vo)
// hwdec
val hwdec = if (sharedPreferences.getBoolean("hardware_decoding", true))
@@ -52,8 +30,7 @@ class MPVView(context: Context, attrs: AttributeSet) : SurfaceView(context, attr
// 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")
@@ -90,8 +67,6 @@ class MPVView(context: Context, attrs: AttributeSet) : SurfaceView(context, attr
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
@@ -114,7 +89,6 @@ class MPVView(context: Context, attrs: AttributeSet) : SurfaceView(context, attr
MPVLib.setOptionString("vd-lavc-skiploopfilter", "nonkey")
}
- MPVLib.setOptionString("vo", vo)
MPVLib.setOptionString("gpu-context", "android")
MPVLib.setOptionString("opengl-es", "yes")
MPVLib.setOptionString("hwdec", hwdec)
@@ -129,20 +103,13 @@ class MPVView(context: Context, attrs: AttributeSet) : SurfaceView(context, attr
val screenshotDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
screenshotDir.mkdirs()
MPVLib.setOptionString("screenshot-directory", screenshotDir.path)
+ // workaround for
+ MPVLib.setOptionString("vd-lavc-film-grain", "cpu")
}
- 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)
-
- MPVLib.destroy()
+ override fun postInitOptions() {
+ // we need to call write-watch-later manually
+ MPVLib.setOptionString("save-position-on-quit", "no")
}
fun onPointerEvent(event: MotionEvent): Boolean {
@@ -200,12 +167,12 @@ class MPVView(context: Context, attrs: AttributeSet) : SurfaceView(context, attr
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(
Property("time-pos", MPV_FORMAT_INT64),
- Property("duration", MPV_FORMAT_INT64),
+ Property("duration/full", MPV_FORMAT_INT64),
Property("demuxer-cache-time", MPV_FORMAT_INT64),
Property("paused-for-cache", MPV_FORMAT_FLAG),
Property("seeking", MPV_FORMAT_FLAG),
@@ -214,17 +181,13 @@ class MPVView(context: Context, attrs: AttributeSet) : SurfaceView(context, attr
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
- 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),
- Property("video-format"),
+ Property("current-tracks/video/image"),
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),
@@ -319,10 +282,11 @@ class MPVView(context: Context, attrs: AttributeSet) : SurfaceView(context, attr
val duration: Int?
get() = MPVLib.getPropertyInt("duration")
- 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"
@@ -347,21 +311,17 @@ class MPVView(context: Context, attrs: AttributeSet) : SurfaceView(context, attr
val videoH: Int?
get() = MPVLib.getPropertyInt("video-params/h")
- val videoAspect: Double?
- get() = MPVLib.getPropertyDouble("video-params/aspect")
-
val videoOutRotation: Int?
get() = MPVLib.getPropertyInt("video-out-params/rotate")
/**
- * 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 videoAspect?.let {
+ fun getVideoAspect(): Double? {
+ return MPVLib.getPropertyDouble("video-params/aspect")?.let {
if (it < 0.001)
return 0.0
- val rot = videoOutRotation ?: 0
+ val rot = MPVLib.getPropertyInt("video-params/rotate") ?: 0
if (rot % 180 == 90)
1.0 / it
else
@@ -437,34 +397,6 @@ class MPVView(context: Context, attrs: AttributeSet) : SurfaceView(context, attr
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"
}
diff --git a/app/src/main/java/is/xyz/mpv/MainScreenFragment.kt b/app/src/main/java/is/xyz/mpv/MainScreenFragment.kt
index 5acffabcd..ebb82e27a 100644
--- a/app/src/main/java/is/xyz/mpv/MainScreenFragment.kt
+++ b/app/src/main/java/is/xyz/mpv/MainScreenFragment.kt
@@ -6,6 +6,7 @@ import `is`.xyz.mpv.databinding.FragmentMainScreenBinding
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
+import android.content.res.Configuration
import android.net.Uri
import android.os.Bundle
import android.preference.PreferenceManager
@@ -13,6 +14,8 @@ import android.util.Log
import android.view.View
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AlertDialog
+import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
class MainScreenFragment : Fragment(R.layout.fragment_main_screen) {
@@ -98,6 +101,34 @@ class MainScreenFragment : Fragment(R.layout.fragment_main_screen) {
saveChoice("") // will reset
startActivity(Intent(context, SettingsActivity::class.java))
}
+
+ if (BuildConfig.DEBUG) {
+ binding.settingsBtn.setOnLongClickListener { showDebugMenu(); true }
+ }
+
+ onConfigurationChanged(view.resources.configuration)
+ }
+
+ private fun showDebugMenu() {
+ assert(BuildConfig.DEBUG)
+ val context = requireContext()
+ with (AlertDialog.Builder(context)) {
+ setItems(DEBUG_ACTIVITIES) { dialog, idx ->
+ dialog.dismiss()
+ val intent = Intent(Intent.ACTION_MAIN)
+ intent.setClassName(context, "${context.packageName}.${DEBUG_ACTIVITIES[idx]}")
+ startActivity(intent)
+ }
+ create().show()
+ }
+ }
+
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ super.onConfigurationChanged(newConfig)
+ // phone screens are too small to show the action buttons alongside the logo
+ if (!Utils.isXLargeTablet(requireContext())) {
+ binding.logo.isVisible = newConfig.orientation != Configuration.ORIENTATION_LANDSCAPE
+ }
}
override fun onResume() {
@@ -173,5 +204,10 @@ class MainScreenFragment : Fragment(R.layout.fragment_main_screen) {
companion object {
private const val TAG = "mpv"
+
+ // list of debug or testing activities that can be launched
+ private val DEBUG_ACTIVITIES = arrayOf(
+ "IntentTestActivity"
+ )
}
}
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
diff --git a/app/src/main/java/is/xyz/mpv/TouchGestures.kt b/app/src/main/java/is/xyz/mpv/TouchGestures.kt
index 2d6667160..e731e9e2c 100644
--- a/app/src/main/java/is/xyz/mpv/TouchGestures.kt
+++ b/app/src/main/java/is/xyz/mpv/TouchGestures.kt
@@ -4,6 +4,7 @@ import android.content.SharedPreferences
import android.content.res.Resources
import android.graphics.PointF
import android.os.SystemClock
+import android.util.Log
import android.view.MotionEvent
import kotlin.math.*
@@ -48,10 +49,10 @@ internal class TouchGestures(private val observer: TouchGesturesObserver) {
// last non-throttled processed position
private var lastPos = PointF()
- private var width: Float = 0f
- private var height: Float = 0f
+ private var width = 0f
+ private var height = 0f
// minimum movement which triggers a Control state
- private var trigger: Float = 0f
+ private var trigger = 0f
// which property change should be invoked where
private var gestureHoriz = State.Down
@@ -61,13 +62,22 @@ internal class TouchGestures(private val observer: TouchGesturesObserver) {
private var tapGestureCenter : PropertyChange? = null
private var tapGestureRight : PropertyChange? = null
+ private inline fun checkFloat(vararg n: Float) {
+ if (n.any { it.isInfinite() || it.isNaN() })
+ throw IllegalArgumentException()
+ }
+ private inline fun checkFloat(p: PointF) = checkFloat(p.x, p.y)
+
fun setMetrics(width: Float, height: Float) {
+ checkFloat(width, height)
this.width = width
this.height = height
trigger = min(width, height) / TRIGGER_RATE
}
companion object {
+ private const val TAG = "mpv"
+
// ratio for trigger, 1/Xth of minimum dimension
// for tap gestures this is the distance that must *not* be moved for it to trigger
private const val TRIGGER_RATE = 30
@@ -129,6 +139,7 @@ internal class TouchGestures(private val observer: TouchGesturesObserver) {
return false
lastPos.set(p)
+ checkFloat(initialPos)
val dx = p.x - initialPos.x
val dy = p.y - initialPos.y
val dr = if (stateDirection == 0) (dx / width) else (-dy / height)
@@ -187,25 +198,27 @@ internal class TouchGestures(private val observer: TouchGesturesObserver) {
}
fun onTouchEvent(e: MotionEvent): Boolean {
- if (width == 0f || height == 0f)
+ if (width < 1 || height < 1) {
+ Log.w(TAG, "TouchGestures: width or height not set!")
return false
+ }
var gestureHandled = false
val point = PointF(e.x, e.y)
+ checkFloat(point)
when (e.action) {
MotionEvent.ACTION_UP -> {
gestureHandled = processMovement(point) or processTap(point)
if (state != State.Down)
sendPropertyChange(PropertyChange.Finalize, 0f)
state = State.Up
- return gestureHandled
}
MotionEvent.ACTION_DOWN -> {
// deadzone on top/bottom
if (e.y < height * DEADZONE / 100 || e.y > height * (100 - DEADZONE) / 100)
return false
+ initialPos.set(point)
processTap(point)
- initialPos = point
- lastPos.set(initialPos)
+ lastPos.set(point)
state = State.Down
// always return true on ACTION_DOWN to continue receiving events
gestureHandled = true
diff --git a/app/src/main/java/is/xyz/mpv/Utils.kt b/app/src/main/java/is/xyz/mpv/Utils.kt
index 3861285fd..3f25cc7ce 100644
--- a/app/src/main/java/is/xyz/mpv/Utils.kt
+++ b/app/src/main/java/is/xyz/mpv/Utils.kt
@@ -4,9 +4,12 @@ import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.res.AssetManager
+import android.content.res.Configuration
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,9 +22,12 @@ 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
+import kotlin.math.ceil
+import kotlin.math.roundToInt
object Utils {
fun copyAssets(context: Context) {
@@ -198,15 +204,26 @@ 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
@@ -254,7 +271,12 @@ 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 {
+ return meta.update(property)
+ }
/** callback for properties of type MPV_FORMAT_STRING
*/
fun update(property: String, value: String): Boolean {
@@ -281,7 +303,6 @@ 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
@@ -289,6 +310,15 @@ 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()
@@ -393,6 +423,22 @@ object Utils {
get() = editText.text.toString()
}
+ inline fun getParcelableArray(bundle: Bundle, key: String): Array {
+ val array = BundleCompat.getParcelableArray(bundle, key, T::class.java)
+ return if (array == null)
+ emptyArray()
+ else // the result is not T[] nor castable because BundleCompat is stupid
+ array.mapNotNull { it as? T }.toTypedArray()
+ }
+
+ /**
+ * Helper method to determine if the device has an extra-large screen. For
+ * example, 10" tablets are extra-large.
+ */
+ fun isXLargeTablet(context: Context): Boolean {
+ return context.resources.configuration.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK >= Configuration.SCREENLAYOUT_SIZE_XLARGE
+ }
+
private const val TAG = "mpv"
// This is used to filter files in the file picker, so it contains just about everything
@@ -427,7 +473,7 @@ object Utils {
// cf. AndroidManifest.xml and MPVActivity.resolveUri()
val PROTOCOLS = setOf(
- "file", "content", "http", "https",
+ "file", "content", "http", "https", "data",
"rtmp", "rtmps", "rtp", "rtsp", "mms", "mmst", "mmsh", "tcp", "udp", "lavf"
)
@@ -439,9 +485,9 @@ object Utils {
)
val VERSIONS = Versions(
- mpv = "%MPV_VERSION%",
- buildDate = "%DATE%",
- libPlacebo = "%LIBPLACEBO_VERSION%",
- ffmpeg = "%FFMPEG_VERSION%",
+ mpv = "",
+ buildDate = "",
+ libPlacebo = "",
+ ffmpeg = "n7.1",
)
}
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..4a2b3bdba 100644
--- a/app/src/main/java/is/xyz/mpv/config/SettingsActivity.kt
+++ b/app/src/main/java/is/xyz/mpv/config/SettingsActivity.kt
@@ -5,11 +5,11 @@ 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
import android.view.MenuItem
+import `is`.xyz.mpv.Utils
/**
* A [PreferenceActivity] that presents a set of application settings. On
@@ -33,15 +33,15 @@ 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)
}
/**
* {@inheritDoc}
*/
override fun onIsMultiPane(): Boolean {
- return isXLargeTablet(this)
+ return Utils.isXLargeTablet(this)
}
/**
@@ -192,14 +192,4 @@ class SettingsActivity : PreferenceActivity() {
return super.onOptionsItemSelected(item)
}
}
-
- companion object {
- /**
- * Helper method to determine if the device has an extra-large screen. For
- * example, 10" tablets are extra-large.
- */
- private fun isXLargeTablet(context: Context): Boolean {
- return context.resources.configuration.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK >= Configuration.SCREENLAYOUT_SIZE_XLARGE
- }
- }
}
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/app/src/main/jni/event.cpp b/app/src/main/jni/event.cpp
index a9474251b..d58dd6a0d 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 c782e6e2e..7484cbd63 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_efEvent, 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_efEvent = env->GetStaticMethodID(mpv_MPVLib, "efEvent", "(Ljava/lang/String;)V"); // efEvent(String)
diff --git a/app/src/main/jni/jni_utils.h b/app/src/main/jni/jni_utils.h
index da21bdf65..c3e5a047c 100644
--- a/app/src/main/jni/jni_utils.h
+++ b/app/src/main/jni/jni_utils.h
@@ -8,13 +8,23 @@
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_efEvent, 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_efEvent,
+ mpv_MPVLib_logMessage_SiS;
diff --git a/app/src/main/jni/property.cpp b/app/src/main/jni/property.cpp
index bbe51e916..eabe6ab4e 100644
--- a/app/src/main/jni/property.cpp
+++ b/app/src/main/jni/property.cpp
@@ -126,6 +126,8 @@ jni_func(void, observeProperty, jstring property, jint format) {
return;
}
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);
}
diff --git a/app/src/main/res/drawable/mpv_monochrome.xml b/app/src/main/res/drawable/mpv_monochrome.xml
new file mode 100644
index 000000000..b570d9b0a
--- /dev/null
+++ b/app/src/main/res/drawable/mpv_monochrome.xml
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_slider.xml b/app/src/main/res/layout/dialog_slider.xml
index b3918b438..3eb7ef020 100644
--- a/app/src/main/res/layout/dialog_slider.xml
+++ b/app/src/main/res/layout/dialog_slider.xml
@@ -31,7 +31,6 @@
diff --git a/app/src/main/res/layout/fragment_filepicker_choice.xml b/app/src/main/res/layout/fragment_filepicker_choice.xml
index 19c55c119..bce2d8a59 100644
--- a/app/src/main/res/layout/fragment_filepicker_choice.xml
+++ b/app/src/main/res/layout/fragment_filepicker_choice.xml
@@ -16,7 +16,6 @@
+
\ No newline at end of file
diff --git a/app/src/main/res/values-es/arrays.xml b/app/src/main/res/values-es/arrays.xml
index a3d094198..2688ec7e5 100644
--- a/app/src/main/res/values-es/arrays.xml
+++ b/app/src/main/res/values-es/arrays.xml
@@ -3,11 +3,11 @@
- Original
+ - Panscan
- 16:9
- 16:10
- 4:3
- 2.35:1
- - Pan/Scan
diff --git a/app/src/main/res/values-it/arrays.xml b/app/src/main/res/values-it/arrays.xml
index 7fe3db64e..57c6bb38c 100644
--- a/app/src/main/res/values-it/arrays.xml
+++ b/app/src/main/res/values-it/arrays.xml
@@ -3,11 +3,11 @@
- Originale
+ - Panscan
- 16:9
- 16:10
- 4:3
- 2.35:1
- - Pan/Scan
diff --git a/app/src/main/res/values-ja/arrays.xml b/app/src/main/res/values-ja/arrays.xml
index 9d97bbc83..9a5f4fc5f 100644
--- a/app/src/main/res/values-ja/arrays.xml
+++ b/app/src/main/res/values-ja/arrays.xml
@@ -2,11 +2,11 @@
- オリジナル
+ - Panscan
- 16:9
- 16:10
- 4:3
- 2.35:1
- - Pan/Scan
diff --git a/app/src/main/res/values-pl/arrays.xml b/app/src/main/res/values-pl/arrays.xml
index 39f57d5f2..5716c74ba 100644
--- a/app/src/main/res/values-pl/arrays.xml
+++ b/app/src/main/res/values-pl/arrays.xml
@@ -3,11 +3,11 @@
- Oryginał
+ - Pan/Scan
- 16:9
- 16:10
- 4:3
- 2.35:1
- - Pan/Scan
diff --git a/app/src/main/res/values-ru/arrays.xml b/app/src/main/res/values-ru/arrays.xml
index 2f4b7e3fd..9f0c99957 100644
--- a/app/src/main/res/values-ru/arrays.xml
+++ b/app/src/main/res/values-ru/arrays.xml
@@ -2,11 +2,11 @@
- Оригинальное
+ - Panscan
- 16:9
- 16:10
- 4:3
- 2.35:1
- - Pan/Scan
- Никогда
diff --git a/app/src/main/res/values-tr/arrays.xml b/app/src/main/res/values-tr/arrays.xml
index f94a43422..fa22fb075 100644
--- a/app/src/main/res/values-tr/arrays.xml
+++ b/app/src/main/res/values-tr/arrays.xml
@@ -3,11 +3,11 @@
- Özgün
+ - Panscan
- 16:9
- 16:10
- 4:3
- 2.35:1
- - Pan/Scan
diff --git a/app/src/main/res/values-uk/arrays.xml b/app/src/main/res/values-uk/arrays.xml
index d32a0295b..256c5e394 100644
--- a/app/src/main/res/values-uk/arrays.xml
+++ b/app/src/main/res/values-uk/arrays.xml
@@ -1,20 +1,54 @@
+
+
+ - Початкове
+ - Panscan
+ - 16:9
+ - 16:10
+ - 4:3
+ - 2.35:1
+
+
+
- Ніколи
- - Тільки з аудіофайлами
- - Завжди (також відеофайли)
+ - Лише з аудіофайлами
+ - Завжди (включно з відеофайлами)
+
+
+
+ - Автоматично
+ - Типово ландшафтна
+ - Типово портретна
+ - Як на пристрої
+
- - Вимкненно
- - 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 @@
Синхронізація відео:
- Імя
- Дозвіл на доступ до файлової системи відмовлено
+ Назва
+ Відмовлено у доступі до файлової системи
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 67a0660e4..7ea80ca05 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -153,7 +153,7 @@
高级
使用 gpu-next
- 使用崭新的视频渲染后端,基于 libplacebo。
+ 使用新的视频渲染后端,基于 libplacebo。
版本信息
编辑 mpv.conf
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 94d58d579..782a75ce6 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -3,20 +3,20 @@
- Original
+ - Panscan
- 16:9
- 16:10
- 4:3
- 2.35:1
- - Pan/Scan
- -1
+ - panscan
- 16:9
- 16:10
- 4:3
- 2.35
- - panscan
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 975087f07..237cb7bcb 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -167,7 +167,7 @@
Enable OpenGL debugging
Use gpu-next
- Use brand-new video rendering backend, based on libplacebo.
+ Use new video rendering backend, based on libplacebo.
Version information
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/buildscripts/README.md b/buildscripts/README.md
index b8d4690f6..65e423a79 100644
--- a/buildscripts/README.md
+++ b/buildscripts/README.md
@@ -1,35 +1,43 @@
# Building
+Compiling the native parts is a process separate from Gradle and the app won't work 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
```
-If you already have the Android SDK installed you can symlink `android-sdk-linux` to your SDK root
-before running the script, it will still install the necessary SDK packages.
+If you already have the Android SDK installed you can symlink `android-sdk-linux` to your SDK root before running the script and the necessary SDK packages will still be installed.
+
+A matching NDK version (inside the SDK) will be picked up automatically or downloaded and installed otherwise.
-A matching NDK version inside the SDK will be picked up automatically or downloaded/installed otherwise.
+### NixOS
+
+You need to add the dependencies manually. This nix-shell worked for me: `nix-shell -p autoconf pkg-config libtool ninja python3Packages.pip python3Packages.setuptools python3Packages.jsonschema unzip nasm wget meson openjdk21_headless automake`
## Build
-```
+```sh
./buildall.sh
```
Run `buildall.sh` with `--clean` to clean the build directories before building.
+For a guaranteed clean build also run `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.
+By default this will build only for 32-bit ARM (`armv7l`).
+You probably want to build for AArch64 too, and perhaps Intel x86.
-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,67 +45,41 @@ 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
+adb logcat -s mpv # get only mpv logs
```
## Rebuilding a single component
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
```
-Note that you might need to be rebuild for other architectures (`--arch`) too depending on your device.
+Note that you might need to rebuild for other architectures (`--arch`) too depending on your device.
Afterwards, build mpv-android and install the apk:
-```
+```sh
./buildall.sh -n
-adb install -r ../app/build/outputs/apk/debug/app-debug.apk
+adb install -r ../app/build/outputs/apk/default/debug/app-default-universal-debug.apk
```
## Using Android Studio
You can use Android Studio to develop the Java part of the codebase. Before using it, make sure to build the project at least once by following the steps in the **Build** section.
-You should point Android Studio to existing SDK installation at `mpv-android/buildscripts/sdk/android-sdk-linux`. Then click "Open an existing Android Studio project" and select `mpv-android`.
+You should point Android Studio to existing SDK installation at `mpv-android/buildscripts/sdk/android-sdk-linux`.
+Then click "Open an existing Android Studio project" and select `mpv-android`.
-If Android Studio complains about project sync failing (`Error:Exception thrown while executing model rule: NdkComponentModelPlugin.Rules#createNativeBuildModel`), go to "File -> Project Structure -> SDK Location" and set "Android NDK Location" to `mpv-android/buildscripts/sdk/android-ndk-rVERSION`.
+Note that if you build from Android Studio only the Java/Kotlin part will be built.
+If you make any changes to libraries (ffmpeg, mpv, ...) or mpv-android native code (`app/src/main/jni/*`), first rebuild native code with:
-Note that if you build from Android Studio only the Java part will be built. If you make any changes to libraries (ffmpeg, mpv, ...) or mpv-android native code (`app/src/main/jni/*`), first rebuild native code with:
-
-```
+```sh
./buildall.sh -n
```
then build the project from Android Studio.
-
-Also, debugging native code does not work from within the studio at the moment, you will have to use gdb for that.
-
-## Debugging native code with gdb
-
-You first need to rebuild mpv-android with gdbserver support:
-
-```
-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:
-
-```
-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/buildall.sh b/buildscripts/buildall.sh
index 9df90a74e..01d18120e 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
}
@@ -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" <"$CACHE_FOLDER/id.txt"
+ echo "$ci_tarball" >"$CACHE_FOLDER/id.txt"
fi
}
export WGET="wget --progress=bar:force"
-if [ "$1" == "install" ]; then
+if [ "$1" = "export" ]; then
+ # export variable with unique cache identifier
+ echo "CACHE_IDENTIFIER=$ci_tarball"
+ exit 0
+elif [ "$1" = "install" ]; then
+ # install deps
if [[ -n "$ANDROID_HOME" && -d "$ANDROID_HOME" ]]; then
msg "Linking existing SDK"
mkdir -p sdk
@@ -63,7 +59,7 @@ if [ "$1" == "install" ]; then
fi
msg "Fetching SDK + NDK"
- TRAVIS=1 ./include/download-sdk.sh
+ IN_CI=1 ./include/download-sdk.sh
msg "Fetching mpv"
mkdir -p deps/mpv
@@ -75,7 +71,8 @@ if [ "$1" == "install" ]; then
mkdir -p prefix
fetch_prefix || build_prefix
exit 0
-elif [ "$1" == "build" ]; then
+elif [ "$1" = "build" ]; then
+ # run build
:
else
exit 1
diff --git a/buildscripts/include/depinfo.sh b/buildscripts/include/depinfo.sh
index f20b6fe16..4a51945c9 100755
--- a/buildscripts/include/depinfo.sh
+++ b/buildscripts/include/depinfo.sh
@@ -4,20 +4,20 @@
# 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=r27c
+v_ndk_n=27.2.12479018
v_sdk_platform=34
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.14
-v_freetype=2-13-2
-v_mbedtls=3.5.1
-v_libxml2=2.12.6
-v_ffmpeg=n7.0
-v_mpv=4d32db21c50db8cd9f2e7925c4b37f1490d85963
+v_harfbuzz=10.1.0
+v_fribidi=1.0.16
+v_freetype=2-13-3
+v_mbedtls=3.6.2
+v_libxml2=2.13.5
+v_ffmpeg=n7.1
+v_mpv=baf528069a584c04686262ef6f76e2c9232adba3
## Dependency tree
@@ -38,10 +38,10 @@ dep_mpv=(ffmpeg libass lua libplacebo)
dep_mpv_android=(mpv)
-## Travis-related
+## for CI workflow
-# pinned ffmpeg commit used by CI
-v_travis_ffmpeg=n6.1.1
+# pinned ffmpeg revision
+v_ci_ffmpeg=n7.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"
+ci_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_ci_ffmpeg}.tgz"
diff --git a/buildscripts/include/download-deps.sh b/buildscripts/include/download-deps.sh
index 593fbab2d..c31a3f6d9 100755
--- a/buildscripts/include/download-deps.sh
+++ b/buildscripts/include/download-deps.sh
@@ -2,13 +2,17 @@
. ./include/depinfo.sh
-[ -z "$TRAVIS" ] && TRAVIS=0
-[ -z "$WGET" ] && WGET="wget -q"
+[ -z "$IN_CI" ] && IN_CI=0
+[ -z "$WGET" ] && WGET=wget
mkdir -p deps && cd deps
# mbedtls
-[ ! -d mbedtls ] && git clone --recurse-submodules https://github.com/Mbed-TLS/mbedtls.git -b v$v_mbedtls --depth 1 --shallow-submodules
+if [ ! -d mbedtls ]; then
+ mkdir mbedtls
+ $WGET https://github.com/Mbed-TLS/mbedtls/releases/download/mbedtls-$v_mbedtls/mbedtls-$v_mbedtls.tar.bz2 -O - | \
+ tar -xj -C mbedtls --strip-components=1
+fi
#libxml2
if [ ! -d libxml2 ]; then
diff --git a/buildscripts/include/download-sdk.sh b/buildscripts/include/download-sdk.sh
index 262e2daf4..353a726fd 100755
--- a/buildscripts/include/download-sdk.sh
+++ b/buildscripts/include/download-sdk.sh
@@ -4,19 +4,20 @@
. ./include/path.sh # load $os var
-[ -z "$TRAVIS" ] && TRAVIS=0 # skip steps not required for CI?
-[ -z "$WGET" ] && WGET="wget -q" # possibility of calling wget differently
+[ -z "$IN_CI" ] && IN_CI=0 # skip steps not required for CI?
+[ -z "$WGET" ] && WGET=wget # possibility of calling wget differently
if [ "$os" == "linux" ]; then
- if [ $TRAVIS -eq 0 ]; then
- hash yum &>/dev/null && {
+ if [ $IN_CI -eq 0 ]; then
+ if hash yum &>/dev/null; then
sudo yum install autoconf pkgconfig libtool ninja-build \
- python3-pip python3-setuptools python3-jsonschema unzip wget nasm;
- python3 -m pip install meson; }
- apt-get -v &>/dev/null && {
+ python3-pip python3-setuptools python3-jsonschema unzip wget nasm meson
+ elif apt-get -v &>/dev/null; then
sudo apt-get install autoconf pkg-config libtool ninja-build \
- python3-pip python3-setuptools python3-jsonschema unzip nasm;
- python3 -m pip install meson; }
+ python3-pip python3-setuptools python3-jsonschema unzip nasm wget meson
+ else
+ echo "Note: dependencies were not installed, you have to do that manually."
+ fi
fi
if ! javac -version &>/dev/null; then
@@ -30,7 +31,7 @@ if [ "$os" == "linux" ]; then
os_ndk="linux"
elif [ "$os" == "mac" ]; then
- if [ $TRAVIS -eq 0 ]; then
+ if [ $IN_CI -eq 0 ]; then
if ! hash brew 2>/dev/null; then
echo "Error: brew not found. You need to install Homebrew: https://brew.sh/"
exit 255
diff --git a/buildscripts/scripts/mpv-android.sh b/buildscripts/scripts/mpv-android.sh
index 4022482a0..bc20d4ec9 100755
--- a/buildscripts/scripts/mpv-android.sh
+++ b/buildscripts/scripts/mpv-android.sh
@@ -41,8 +41,11 @@ $BUILD/scripts/write_versions.sh $ndk_suffix
PREFIX32=$prefix32 PREFIX64=$prefix64 PREFIX_X64=$prefix_x64 PREFIX_X86=$prefix_x86 \
ndk-build -C app/src/main -j$cores
-targets=(assembleDebug assembleRelease)
-[ -n "$BUNDLE" ] && targets+=(bundleRelease)
+targets=(assembleDebug)
+if [ -z "$DONT_BUILD_RELEASE" ]; then
+ targets+=(assembleRelease)
+ [ -n "$BUNDLE" ] && targets+=(bundleRelease)
+fi
./gradlew "${targets[@]}"
if [ -n "$ANDROID_SIGNING_KEY" ]; then
diff --git a/docs/intent.html b/docs/intent.html
index 07c918b99..c41f63901 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
@@ -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
+
@@ -51,6 +54,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>
@@ -77,7 +82,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.