From c5f80c80b36daf93d5cb31d7fb46438eea76a134 Mon Sep 17 00:00:00 2001 From: Marcel Dopita Date: Sat, 21 Dec 2024 01:04:58 +0100 Subject: [PATCH] Remove FFmpeg dependency * Custom frame rate detection * No chapter detection --- .github/workflows/android-build-apk.yml | 4 +- README.md | 6 - app/build.gradle | 22 +--- .../java/com/brouken/player/UtilsFeature.java | 106 ------------------ .../java/com/brouken/player/UtilsFeature.java | 14 --- .../com/brouken/player/CustomPlayerView.java | 7 -- .../com/brouken/player/PlayerActivity.java | 26 +---- .../com/brouken/player/SettingsActivity.java | 1 - .../main/java/com/brouken/player/Utils.java | 75 +++++++++++++ 9 files changed, 82 insertions(+), 179 deletions(-) delete mode 100644 app/src/full/java/com/brouken/player/UtilsFeature.java delete mode 100644 app/src/lite/java/com/brouken/player/UtilsFeature.java diff --git a/.github/workflows/android-build-apk.yml b/.github/workflows/android-build-apk.yml index 3be40ac0..fbff953b 100644 --- a/.github/workflows/android-build-apk.yml +++ b/.github/workflows/android-build-apk.yml @@ -29,7 +29,7 @@ jobs: - uses: softprops/action-gh-release@v2 with: files: | - app/build/outputs/apk/latestUniversalFull/release/*.apk - app/build/outputs/apk/legacyUniversalFull/release/*.apk + app/build/outputs/apk/latestUniversal/release/*.apk + app/build/outputs/apk/legacyUniversal/release/*.apk prerelease: true draft: true diff --git a/README.md b/README.md index e42060bf..2dec8bbf 100644 --- a/README.md +++ b/README.md @@ -146,12 +146,6 @@ If your device has a touchscreen you can use the pinch-to-zoom gesture or just t Just pause and resume playback once again. -### Why is the APK so big? - -The APK available here contains native libraries for all supported architectures (`armeabi-v7a`/`armeabi-v7a-neon`/`arm64-v8a`/`x86`/`x86_64`), which is what takes the most space. Although Just Player relies mostly on device decoders, it packs _FFmpeg_ for some advanced features (video chapters and frame rate detection). - -Please note that installs and updates made through Google Play are significantly smaller thanks to Android App Bundles and delta updates. - ## Other open source Android video players Here's a comparison table presenting all available and significant open source video players for Android I was able to find. Just Player is something like ~~80%~~ 90% feature complete. It will probably never have dozens of options or some rich media library UI. It will never truly compete with feature rich VLC. It just attempts to provide functional feature set and motive others to create greater players based on amazing ExoPlayer. diff --git a/app/build.gradle b/app/build.gradle index 81ba0fcc..7202c8b5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,7 +29,7 @@ android { //proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } - flavorDimensions "targetSdk", "distribution", "feature" + flavorDimensions "targetSdk", "distribution" productFlavors { latest { dimension "targetSdk" @@ -50,38 +50,25 @@ android { accrescent { dimension "distribution" } - full { - dimension "feature" - } - lite { - dimension "feature" - versionNameSuffix "-lite" - } } variantFilter { variant -> def names = variant.flavors*.name - if ((names.contains("legacy") || names.contains("lite")) && (names.contains("amazon") || names.contains("accrescent"))) { + if (names.contains("legacy") && (names.contains("amazon") || names.contains("accrescent"))) { setIgnore(true) } } applicationVariants.all { variant -> - if (variant.buildType.name == "release" && variant.flavorName == "latestUniversalFull") { + if (variant.buildType.name == "release" && variant.flavorName == "latestUniversal") { variant.outputs.all { output -> output.outputFileName = "${archivesBaseName}.apk" } } - if (variant.buildType.name == "release" && variant.flavorName == "legacyUniversalFull") { + if (variant.buildType.name == "release" && variant.flavorName == "legacyUniversal") { variant.outputs.all { output -> output.outputFileName = "${archivesBaseName}-legacy.apk" } } } - androidComponents { - onVariants(selector().withFlavor("distribution", "amazon"), { - packaging.dex.useLegacyPackaging.set(false) - packaging.jniLibs.excludes.add('lib/armeabi-v7a/*_neon.so') - }) - } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -117,7 +104,6 @@ dependencies { implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0' implementation "androidx.core:core:$androidxCoreVersion" - fullImplementation 'com.arthenica:ffmpeg-kit-https:6.0-2.LTS' implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.preference:preference:1.2.1' implementation 'com.squareup.okhttp3:okhttp:4.12.0' diff --git a/app/src/full/java/com/brouken/player/UtilsFeature.java b/app/src/full/java/com/brouken/player/UtilsFeature.java deleted file mode 100644 index 75cf6563..00000000 --- a/app/src/full/java/com/brouken/player/UtilsFeature.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.brouken.player; - -import android.app.Activity; -import android.content.ContentResolver; -import android.net.Uri; -import android.os.Build; - -import androidx.media3.common.Format; -import androidx.media3.ui.PlayerControlView; - -import com.arthenica.ffmpegkit.Chapter; -import com.arthenica.ffmpegkit.FFmpegKitConfig; -import com.arthenica.ffmpegkit.FFprobeKit; -import com.arthenica.ffmpegkit.MediaInformation; -import com.arthenica.ffmpegkit.MediaInformationSession; -import com.arthenica.ffmpegkit.StreamInformation; - -import java.util.List; - -public class UtilsFeature { - - private static MediaInformation getMediaInformation(final Activity activity, final Uri uri) { - String path; - if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { - try { - path = FFmpegKitConfig.getSafParameterForRead(activity, uri); - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } else if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { - // TODO: FFprobeKit doesn't accept encoded uri (like %20) (?!) - path = uri.getSchemeSpecificPart(); - } else { - path = uri.toString(); - } - MediaInformationSession mediaInformationSession = FFprobeKit.getMediaInformation(path); - return mediaInformationSession.getMediaInformation(); - } - - public static boolean switchFrameRate(final PlayerActivity activity, final Uri uri, final boolean play) { - // preferredDisplayModeId only available on SDK 23+ - // ExoPlayer already uses Surface.setFrameRate() on Android 11+ - if (Build.VERSION.SDK_INT >= 23) { - if (activity.frameRateSwitchThread != null) { - activity.frameRateSwitchThread.interrupt(); - } - activity.frameRateSwitchThread = new Thread(() -> { - // Use ffprobe as ExoPlayer doesn't detect video frame rate for lots of videos - // and has different precision than ffprobe (so do not mix that) - float frameRate = Format.NO_VALUE; - MediaInformation mediaInformation = getMediaInformation(activity, uri); - if (mediaInformation == null) { - activity.runOnUiThread(() -> { - Utils.playIfCan(activity, play); - }); - return; - } - List streamInformations = mediaInformation.getStreams(); - for (StreamInformation streamInformation : streamInformations) { - if (streamInformation.getType().equals("video")) { - String averageFrameRate = streamInformation.getAverageFrameRate(); - if (averageFrameRate.contains("/")) { - String[] vals = averageFrameRate.split("/"); - frameRate = Float.parseFloat(vals[0]) / Float.parseFloat(vals[1]); - break; - } - } - } - Utils.handleFrameRate(activity, frameRate, play); - }); - activity.frameRateSwitchThread.start(); - return true; - } else { - return false; - } - } - - public static void markChapters(final PlayerActivity activity, final Uri uri, PlayerControlView controlView) { - if (activity.chaptersThread != null) { - activity.chaptersThread.interrupt(); - } - activity.chaptersThread = new Thread(() -> { - MediaInformation mediaInformation = getMediaInformation(activity, uri); - if (mediaInformation == null) - return; - final List chapters = mediaInformation.getChapters(); - final long[] starts = new long[chapters.size()]; - final boolean[] played = new boolean[chapters.size()]; - - for (int i = 0; i < chapters.size(); i++) { - Chapter chapter = chapters.get(i); - final long start = chapter.getStart(); - if (start > 0) { - starts[i] = start / 1_000_000; - played[i] = true; - } - } - activity.chapterStarts = starts; - activity.runOnUiThread(() -> controlView.setExtraAdGroupMarkers(starts, played)); - }); - activity.chaptersThread.start(); - } - - -} diff --git a/app/src/lite/java/com/brouken/player/UtilsFeature.java b/app/src/lite/java/com/brouken/player/UtilsFeature.java deleted file mode 100644 index 1fa4f3f3..00000000 --- a/app/src/lite/java/com/brouken/player/UtilsFeature.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.brouken.player; - -import android.net.Uri; - -import androidx.media3.ui.PlayerControlView; - -public class UtilsFeature { - - public static boolean switchFrameRate(final PlayerActivity activity, final Uri uri, final boolean play) { - return false; - } - - public static void markChapters(final PlayerActivity activity, final Uri uri, PlayerControlView controlView) {} -} diff --git a/app/src/main/java/com/brouken/player/CustomPlayerView.java b/app/src/main/java/com/brouken/player/CustomPlayerView.java index 41fe062b..a1dcfa40 100644 --- a/app/src/main/java/com/brouken/player/CustomPlayerView.java +++ b/app/src/main/java/com/brouken/player/CustomPlayerView.java @@ -7,7 +7,6 @@ import android.os.Build; import android.util.AttributeSet; import android.view.GestureDetector; -import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; @@ -258,12 +257,6 @@ public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float PlayerActivity.player.seekTo(position); } } - for (long start : PlayerActivity.chapterStarts) { - if ((seekLastPosition < start && position >= start) || (seekLastPosition > start && position <= start)) { - performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK); - } - } - seekLastPosition = position; String message = Utils.formatMilisSign(seekChange); if (!isControllerFullyVisible()) { message += "\n" + Utils.formatMilis(position); diff --git a/app/src/main/java/com/brouken/player/PlayerActivity.java b/app/src/main/java/com/brouken/player/PlayerActivity.java index b5209496..a5ce619c 100644 --- a/app/src/main/java/com/brouken/player/PlayerActivity.java +++ b/app/src/main/java/com/brouken/player/PlayerActivity.java @@ -39,7 +39,6 @@ import android.util.DisplayMetrics; import android.util.Rational; import android.util.TypedValue; -import android.view.HapticFeedbackConstants; import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; @@ -183,9 +182,6 @@ public class PlayerActivity extends Activity { public static boolean locked = false; private Thread nextUriThread; public Thread frameRateSwitchThread; - public Thread chaptersThread; - private long lastScrubbingPosition; - public static long[] chapterStarts; public static boolean restoreControllerTimeout = false; public static boolean shortControllerTimeout = false; @@ -351,7 +347,6 @@ public void onScrubStart(TimeBar timeBar, long position) { if (restorePlayState) { player.pause(); } - lastScrubbingPosition = position; scrubbingNoticeable = false; isScrubbing = true; frameRendered = true; @@ -364,12 +359,6 @@ public void onScrubStart(TimeBar timeBar, long position) { @Override public void onScrubMove(TimeBar timeBar, long position) { reportScrubbing(position); - for (long start : chapterStarts) { - if ((lastScrubbingPosition < start && position >= start) || (lastScrubbingPosition > start && position <= start)) { - playerView.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK); - } - } - lastScrubbingPosition = position; } @Override @@ -1256,8 +1245,6 @@ public void initializePlayer() { locked = false; - chapterStarts = new long[0]; - if (haveMedia) { if (isNetworkUri) { timeBar.setBufferedColor(DefaultTimeBar.DEFAULT_BUFFERED_COLOR); @@ -1343,8 +1330,6 @@ public void initializePlayer() { nextUriThread.start(); } - UtilsFeature.markChapters(this, mPrefs.mediaUri, controlView); - player.setHandleAudioBecomingNoisy(!isTvBox); // mediaSession.setActive(true); } else { @@ -1464,15 +1449,6 @@ public void onPlaybackStateChanged(int state) { final long position = player.getCurrentPosition(); if (position + 4000 >= duration) { isNearEnd = true; - } else { - // Last chapter is probably "Credits" chapter - final int chapters = chapterStarts.length; - if (chapters > 1) { - final long lastChapter = chapterStarts[chapters - 1]; - if (duration - lastChapter < duration / 10 && position > lastChapter) { - isNearEnd = true; - } - } } } setEndControlsVisible(haveMedia && (state == Player.STATE_ENDED || isNearEnd)); @@ -1544,7 +1520,7 @@ public void onDisplayChanged(int displayId) { } displayManager.registerDisplayListener(displayListener, null); } - switched = UtilsFeature.switchFrameRate(PlayerActivity.this, mPrefs.mediaUri, play); + switched = Utils.switchFrameRate(PlayerActivity.this, mPrefs.mediaUri, play); } if (!switched) { if (displayManager != null) { diff --git a/app/src/main/java/com/brouken/player/SettingsActivity.java b/app/src/main/java/com/brouken/player/SettingsActivity.java index 761b61fe..3c0e7a29 100644 --- a/app/src/main/java/com/brouken/player/SettingsActivity.java +++ b/app/src/main/java/com/brouken/player/SettingsActivity.java @@ -78,7 +78,6 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { } Preference preferenceFrameRateMatching = findPreference("frameRateMatching"); if (preferenceFrameRateMatching != null) { - preferenceFrameRateMatching.setVisible(BuildConfig.FLAVOR_feature.equals("full")); preferenceFrameRateMatching.setEnabled(Build.VERSION.SDK_INT >= 23); } ListPreference listPreferenceFileAccess = findPreference("fileAccess"); diff --git a/app/src/main/java/com/brouken/player/Utils.java b/app/src/main/java/com/brouken/player/Utils.java index 8a02f5c4..160c65fe 100644 --- a/app/src/main/java/com/brouken/player/Utils.java +++ b/app/src/main/java/com/brouken/player/Utils.java @@ -19,6 +19,8 @@ import android.content.res.Resources; import android.database.Cursor; import android.media.AudioManager; +import android.media.MediaExtractor; +import android.media.MediaFormat; import android.media.MediaScannerConnection; import android.net.Uri; import android.os.Build; @@ -785,4 +787,77 @@ public static void scanMediaStorage(Context context) { } MediaScannerConnection.scanFile(context, storagePaths.toArray(new String[0]), new String[]{"*/*"}, null); } + + public static float getFrameRate(Context context, Uri videoUri) { + MediaExtractor mediaExtractor = new MediaExtractor(); + ArrayList timestamps = new ArrayList<>(); + float frameRate = Format.NO_VALUE; + int ignoreSamples = 30; + try { + mediaExtractor.setDataSource(context, videoUri, null); + for (int i = 0; i < mediaExtractor.getTrackCount(); i++) { + MediaFormat format = mediaExtractor.getTrackFormat(i); + String mimeType = format.getString(MediaFormat.KEY_MIME); + if (mimeType != null && mimeType.startsWith("video/")) { + mediaExtractor.selectTrack(i); + while (timestamps.size() < 1000 + ignoreSamples) { + long timestamp = mediaExtractor.getSampleTime(); + if (timestamp < 0) { + break; + } + timestamps.add(timestamp); + mediaExtractor.advance(); + } + break; + } + } + Collections.sort(timestamps); + long totalFrameDuration = 0; + for (int i = 1; i < (timestamps.size() - ignoreSamples); i++) { + totalFrameDuration += (timestamps.get(i) - timestamps.get(i - 1)); + } + if (timestamps.size() > 1) { + float averageFrameDuration = (float) totalFrameDuration / (timestamps.size() - ignoreSamples - 1); + frameRate = 1_000_000f / averageFrameDuration; + if (frameRate > 23.95f && frameRate < 23.988f) { + frameRate = 24000f / 1001f; + } else if (frameRate > 23.988 && frameRate < 24.1) { + frameRate = 24f; + } else if (frameRate > 24.9 && frameRate < 25.1) { + frameRate = 25f; + } else if (frameRate > 29.95f && frameRate < 29.985) { + frameRate = 30000f / 1001f; + } else if (frameRate > 29.985 && frameRate < 30.1) { + frameRate = 30f; + } else if (frameRate > 49.9f && frameRate < 50.1) { + frameRate = 50f; + } else if (frameRate > 59.9f && frameRate < 59.97) { + frameRate = 60000f / 1001f; + } + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + mediaExtractor.release(); + } + return frameRate; + } + + public static boolean switchFrameRate(final PlayerActivity activity, final Uri uri, final boolean play) { + // preferredDisplayModeId only available on SDK 23+ + // ExoPlayer already uses Surface.setFrameRate() on Android 11+ + if (Build.VERSION.SDK_INT >= 23) { + if (activity.frameRateSwitchThread != null) { + activity.frameRateSwitchThread.interrupt(); + } + activity.frameRateSwitchThread = new Thread(() -> { + float frameRate = getFrameRate(activity, uri); + Utils.handleFrameRate(activity, frameRate, play); + }); + activity.frameRateSwitchThread.start(); + return true; + } else { + return false; + } + } }