Add mediacodec mode (GPU) for video compressor

This commit is contained in:
2026-06-12 11:20:13 +07:00
parent 26602a4698
commit bbb0fe0d34
185 changed files with 17183 additions and 421 deletions
+3
View File
@@ -99,6 +99,9 @@ target_link_libraries(kcompressor
-Wl,--end-group
# Dibutuhkan oleh FFmpeg MediaCodec encoder/decoder:
# h264_mediacodec / hevc_mediacodec
mediandk
android
log
z
+7 -2
View File
@@ -726,8 +726,13 @@ int compress_image_fd_impl(JNIEnv *env,
callback_progress(env, callback, 92, "Menyimpan gambar...");
if (!write_all_fd(output_fd, encoded)) {
callback_progress(env, callback, 0, "Gagal menulis output gambar");
return -5;
callback_progress(env, callback, 0, is_cancel_requested() ? "Canceled" : "Gagal menulis output gambar");
return is_cancel_requested() ? -125 : -5;
}
if (is_cancel_requested()) {
callback_progress(env, callback, 0, "Canceled");
return -125;
}
callback_progress(env, callback, 100, "Done");
+29 -1
View File
@@ -5,6 +5,10 @@
#include "video_compressor.h"
#include "image_compressor.h"
extern "C" {
#include <libavcodec/avcodec.h>
}
static std::string jstring_to_string(JNIEnv *env, jstring value, const char *fallback) {
if (!env || !value) {
return fallback ? std::string(fallback) : std::string();
@@ -20,6 +24,21 @@ static std::string jstring_to_string(JNIEnv *env, jstring value, const char *fal
return result;
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_kikyps_kcompressor_NativeCompressor_isVideoEncoderAvailable(JNIEnv *env,
jclass,
jstring encoderName) {
std::string encoder = jstring_to_string(env, encoderName, "");
if (encoder.empty()) {
return JNI_FALSE;
}
const AVCodec *codec = avcodec_find_encoder_by_name(encoder.c_str());
return codec ? JNI_TRUE : JNI_FALSE;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_kikyps_kcompressor_NativeCompressor_cancelCurrentJob(JNIEnv *, jclass) {
@@ -35,6 +54,7 @@ Java_com_kikyps_kcompressor_NativeCompressor_compressFd(JNIEnv *env,
jint targetShortSide,
jint inputRotationDegrees,
jint crf,
jint videoBitrate,
jstring encoderName,
jstring preset,
jstring audioMode,
@@ -44,7 +64,10 @@ Java_com_kikyps_kcompressor_NativeCompressor_compressFd(JNIEnv *env,
std::string preset_str = jstring_to_string(env, preset, "medium");
std::string audio_mode = jstring_to_string(env, audioMode, "copy");
if (encoder != "libx264" && encoder != "libx265") {
if (encoder != "libx264" &&
encoder != "libx265" &&
encoder != "h264_mediacodec" &&
encoder != "hevc_mediacodec") {
encoder = "libx264";
}
@@ -53,6 +76,10 @@ Java_com_kikyps_kcompressor_NativeCompressor_compressFd(JNIEnv *env,
if (crf < 0) crf = 26;
if (crf > 51) crf = 51;
if (videoBitrate < 0) {
videoBitrate = 0;
}
if (audio_mode != "copy" && audio_mode != "aac" && audio_mode != "mute") {
audio_mode = "copy";
}
@@ -70,6 +97,7 @@ Java_com_kikyps_kcompressor_NativeCompressor_compressFd(JNIEnv *env,
static_cast<int>(targetShortSide),
static_cast<int>(inputRotationDegrees),
static_cast<int>(crf),
static_cast<int>(videoBitrate),
encoder,
preset_str,
audio_mode,
@@ -1,5 +1,5 @@
/* Automatically generated by version.sh, do not manually edit! */
#ifndef AVUTIL_FFVERSION_H
#define AVUTIL_FFVERSION_H
#define FFMPEG_VERSION "N-124951-g5f998e304d"
#define FFMPEG_VERSION "N-124985-g2cc7b87bdb"
#endif /* AVUTIL_FFVERSION_H */
@@ -1,5 +1,5 @@
/* Automatically generated by version.sh, do not manually edit! */
#ifndef AVUTIL_FFVERSION_H
#define AVUTIL_FFVERSION_H
#define FFMPEG_VERSION "N-124951-g5f998e304d"
#define FFMPEG_VERSION "N-124985-g2cc7b87bdb"
#endif /* AVUTIL_FFVERSION_H */
@@ -1,5 +1,5 @@
/* Automatically generated by version.sh, do not manually edit! */
#ifndef AVUTIL_FFVERSION_H
#define AVUTIL_FFVERSION_H
#define FFMPEG_VERSION "N-124951-g5f998e304d"
#define FFMPEG_VERSION "N-124985-g2cc7b87bdb"
#endif /* AVUTIL_FFVERSION_H */
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,5 @@
/* Automatically generated by version.sh, do not manually edit! */
#ifndef AVUTIL_FFVERSION_H
#define AVUTIL_FFVERSION_H
#define FFMPEG_VERSION "N-124951-g5f998e304d"
#define FFMPEG_VERSION "N-124985-g2cc7b87bdb"
#endif /* AVUTIL_FFVERSION_H */
+137 -7
View File
@@ -482,6 +482,65 @@ static AVRational choose_fps(AVStream *stream, AVCodecContext *dec_ctx) {
return AVRational{30, 1};
}
static bool is_mediacodec_video_encoder_name(const std::string &encoder_name) {
return encoder_name == "h264_mediacodec" ||
encoder_name == "hevc_mediacodec";
}
static bool encoder_supports_pix_fmt(const AVCodec *encoder, enum AVPixelFormat pix_fmt) {
if (!encoder || !encoder->pix_fmts) {
// Banyak encoder tidak mengisi pix_fmts. Anggap boleh, biarkan avcodec_open2 yang validasi.
return true;
}
for (const enum AVPixelFormat *p = encoder->pix_fmts; *p != AV_PIX_FMT_NONE; ++p) {
if (*p == pix_fmt) {
return true;
}
}
return false;
}
static int64_t estimate_mediacodec_video_bitrate(int width,
int height,
AVRational fps,
int crf,
const std::string &encoder_name) {
double fps_value = 30.0;
if (fps.num > 0 && fps.den > 0) {
fps_value = av_q2d(fps);
}
if (fps_value < 15.0) fps_value = 15.0;
if (fps_value > 60.0) fps_value = 60.0;
double bits_per_pixel = encoder_name == "hevc_mediacodec" ? 0.055 : 0.075;
double quality_factor;
if (crf <= 22) {
quality_factor = 1.35;
} else if (crf <= 26) {
quality_factor = 1.00;
} else if (crf <= 30) {
quality_factor = 0.78;
} else {
quality_factor = 0.58;
}
double bitrate = static_cast<double>(width) *
static_cast<double>(height) *
fps_value *
bits_per_pixel *
quality_factor;
if (bitrate < 400000.0) bitrate = 400000.0;
if (bitrate > 60000000.0) bitrate = 60000000.0;
return static_cast<int64_t>(bitrate);
}
static bool is_audio_copy_supported_in_mp4(enum AVCodecID codec_id) {
return codec_id == AV_CODEC_ID_AAC ||
codec_id == AV_CODEC_ID_MP3 ||
@@ -692,6 +751,7 @@ int compress_video_fd_impl(JNIEnv *env,
int target_short_side,
int input_rotation_degrees,
int crf,
int video_bitrate,
const std::string &encoder_name,
const std::string &preset,
const std::string &audio_mode,
@@ -827,9 +887,35 @@ int compress_video_fd_impl(JNIEnv *env,
{
const AVCodec *encoder = avcodec_find_encoder_by_name(encoder_name.c_str());
const bool video_encoder_hardware = is_mediacodec_video_encoder_name(encoder_name);
if (!encoder) {
ret = AVERROR_ENCODER_NOT_FOUND;
LOGE("Encoder not found: %s", encoder_name.c_str());
if (video_encoder_hardware) {
callback_progress(
env,
callback,
0,
"GPU encoder tidak tersedia. Build FFmpeg harus enable MediaCodec."
);
}
goto cleanup;
}
if (video_encoder_hardware &&
!encoder_supports_pix_fmt(encoder, AV_PIX_FMT_YUV420P)) {
ret = AVERROR(ENOSYS);
LOGE("GPU MediaCodec encoder tidak support input YUV420P pada build/device ini: %s",
encoder_name.c_str());
callback_progress(
env,
callback,
0,
"GPU encoder tidak support input YUV420P. Gunakan CPU."
);
goto cleanup;
}
@@ -890,9 +976,35 @@ int compress_video_fd_impl(JNIEnv *env,
enc_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
enc_ctx->time_base = time_base;
enc_ctx->framerate = fps;
enc_ctx->gop_size = 60;
enc_ctx->max_b_frames = 2;
enc_ctx->bit_rate = 0;
enc_ctx->gop_size = std::max(30, static_cast<int>(std::llround(av_q2d(fps) * 2.0)));
enc_ctx->max_b_frames = video_encoder_hardware ? 0 : 2;
if (video_encoder_hardware) {
enc_ctx->bit_rate = video_bitrate > 0
? static_cast<int64_t>(video_bitrate)
: estimate_mediacodec_video_bitrate(
dst_w,
dst_h,
fps,
crf,
encoder_name
);
if (enc_ctx->bit_rate < 400000) {
enc_ctx->bit_rate = 400000;
}
if (enc_ctx->bit_rate > 100000000) {
enc_ctx->bit_rate = 100000000;
}
LOGD("MediaCodec encoder=%s bitrate=%lld mode=%s",
encoder_name.c_str(),
static_cast<long long>(enc_ctx->bit_rate),
video_bitrate > 0 ? "manual" : "auto");
} else {
enc_ctx->bit_rate = 0;
}
if (in_video_stream->sample_aspect_ratio.num > 0 &&
in_video_stream->sample_aspect_ratio.den > 0) {
@@ -906,17 +1018,34 @@ int compress_video_fd_impl(JNIEnv *env,
std::string crf_str = std::to_string(crf);
if (enc_ctx->priv_data) {
av_opt_set(enc_ctx->priv_data, "preset", preset.c_str(), 0);
av_opt_set(enc_ctx->priv_data, "crf", crf_str.c_str(), 0);
if (video_encoder_hardware) {
// MediaCodec tidak memakai preset/CRF seperti libx264/libx265.
// Gunakan bitrate otomatis dan VBR jika encoder/device mendukung.
av_opt_set(enc_ctx->priv_data, "bitrate_mode", "vbr", 0);
av_opt_set_int(enc_ctx->priv_data, "ndk_codec", 1, 0);
} else {
av_opt_set(enc_ctx->priv_data, "preset", preset.c_str(), 0);
av_opt_set(enc_ctx->priv_data, "crf", crf_str.c_str(), 0);
if (encoder_name == "libx265") {
av_opt_set(enc_ctx->priv_data, "x265-params", "log-level=error", 0);
if (encoder_name == "libx265") {
av_opt_set(enc_ctx->priv_data, "x265-params", "log-level=error", 0);
}
}
}
ret = avcodec_open2(enc_ctx, encoder, nullptr);
if (ret < 0) {
LOGE("avcodec_open2 encoder failed: %s", av_err_to_string(ret).c_str());
if (video_encoder_hardware) {
callback_progress(
env,
callback,
0,
"GPU encoder gagal dibuka. Device/build mungkin tidak support."
);
}
goto cleanup;
}
@@ -926,6 +1055,7 @@ int compress_video_fd_impl(JNIEnv *env,
goto cleanup;
}
out_video_stream->codecpar->codec_tag = 0;
out_video_stream->time_base = enc_ctx->time_base;
stream_mapping[video_stream_index] = out_video_stream->index;
+1
View File
@@ -9,6 +9,7 @@ int compress_video_fd_impl(JNIEnv *env,
int target_short_side,
int input_rotation_degrees,
int crf,
int video_bitrate,
const std::string &encoder_name,
const std::string &preset,
const std::string &audio_mode,
@@ -10,6 +10,8 @@ import android.graphics.BitmapFactory;
import android.media.MediaMetadataRetriever;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -48,6 +50,8 @@ public class MainActivity extends Activity {
static final String EXTRA_AUDIO_MODE = "audio_mode";
static final String EXTRA_AUDIO_LABEL = "audio_label";
static final String EXTRA_AUDIO_BITRATE = "audio_bitrate";
static final String EXTRA_VIDEO_BITRATE = "video_bitrate";
static final String EXTRA_VIDEO_BITRATE_LABEL = "video_bitrate_label";
static final String EXTRA_JOB_TYPE = "job_type";
static final String EXTRA_INPUT_MIME = "input_mime";
static final String EXTRA_IMAGE_OUTPUT_FORMAT = "image_output_format";
@@ -62,6 +66,7 @@ public class MainActivity extends Activity {
private static final String KEY_CRF = "crf";
private static final String KEY_TARGET_SHORT = "target_short";
private static final String KEY_AUDIO_INDEX = "audio_index";
private static final String KEY_VIDEO_BITRATE_INDEX = "video_bitrate_index";
private static final String KEY_SETTINGS_EXPANDED = "settings_expanded";
private static final String KEY_IMAGE_FORMAT_INDEX = "image_format_index";
private static final String KEY_IMAGE_TARGET_SHORT = "image_target_short";
@@ -109,6 +114,9 @@ public class MainActivity extends Activity {
private Spinner spResolution;
private Spinner spPreset;
private Spinner spAudio;
private Spinner spVideoBitrate;
private View layoutCpuVideoSettings;
private View layoutGpuVideoSettings;
private SeekBar seekCrf;
private Spinner spImageFormat;
@@ -216,13 +224,35 @@ public class MainActivity extends Activity {
}
}
private static final class BitrateOption {
final String label;
final int bitrate;
BitrateOption(String label, int bitrate) {
this.label = label;
this.bitrate = bitrate;
}
@Override
public String toString() {
return label;
}
}
private static final class CodecOption {
final String label;
final String encoderName;
final boolean hardware;
CodecOption(String label, String encoderName) {
this(label, encoderName, false);
}
CodecOption(String label, String encoderName, boolean hardware) {
this.label = label;
this.encoderName = encoderName;
this.hardware = hardware;
}
@Override
@@ -269,6 +299,9 @@ public class MainActivity extends Activity {
spResolution = findViewById(R.id.spResolution);
spPreset = findViewById(R.id.spPreset);
spAudio = findViewById(R.id.spAudio);
spVideoBitrate = findViewById(R.id.spVideoBitrate);
layoutCpuVideoSettings = findViewById(R.id.layoutCpuVideoSettings);
layoutGpuVideoSettings = findViewById(R.id.layoutGpuVideoSettings);
seekCrf = findViewById(R.id.seekCrf);
spImageFormat = findViewById(R.id.spImageFormat);
@@ -347,10 +380,190 @@ public class MainActivity extends Activity {
};
}
private void setupSpinners() {
private ArrayList<CodecOption> buildVideoCodecOptions() {
ArrayList<CodecOption> codecItems = new ArrayList<>();
codecItems.add(new CodecOption("H.264 / AVC", "libx264"));
codecItems.add(new CodecOption("H.265 / HEVC", "libx265"));
codecItems.add(new CodecOption("H.264 / AVC (CPU)", "libx264", false));
codecItems.add(new CodecOption("H.265 / HEVC (CPU)", "libx265", false));
if (isGpuVideoEncoderUsable("video/avc", "h264_mediacodec")) {
codecItems.add(new CodecOption("H.264 / AVC (Hardware GPU)", "h264_mediacodec", true));
}
if (isGpuVideoEncoderUsable("video/hevc", "hevc_mediacodec")) {
codecItems.add(new CodecOption("H.265 / HEVC (Hardware GPU)", "hevc_mediacodec", true));
}
return codecItems;
}
private boolean isGpuVideoEncoderUsable(String mime, String ffmpegEncoderName) {
// Harus dua-duanya tersedia:
// 1. Device punya hardware MediaCodec encoder untuk MIME ini.
// 2. libkcompressor/FFmpeg build punya encoder wrapper h264_mediacodec/hevc_mediacodec.
return deviceSupportsHardwareVideoEncoder(mime) &&
NativeCompressor.isVideoEncoderAvailable(ffmpegEncoderName);
}
private boolean deviceSupportsHardwareVideoEncoder(String mime) {
if (mime == null || mime.length() == 0) {
return false;
}
try {
MediaCodecInfo[] codecInfos;
if (Build.VERSION.SDK_INT >= 21) {
MediaCodecList list = new MediaCodecList(MediaCodecList.ALL_CODECS);
codecInfos = list.getCodecInfos();
} else {
return false;
}
for (MediaCodecInfo info : codecInfos) {
if (info == null || !info.isEncoder()) {
continue;
}
boolean supportsMime = false;
String[] types = info.getSupportedTypes();
if (types != null) {
for (String type : types) {
if (type != null && type.equalsIgnoreCase(mime)) {
supportsMime = true;
break;
}
}
}
if (!supportsMime) {
continue;
}
if (isLikelyHardwareCodec(info)) {
return true;
}
}
} catch (Throwable ignored) {
}
return false;
}
private boolean isLikelyHardwareCodec(MediaCodecInfo info) {
if (info == null) {
return false;
}
if (Build.VERSION.SDK_INT >= 29) {
try {
return info.isHardwareAccelerated() && !info.isSoftwareOnly();
} catch (Throwable ignored) {
// Fallback ke cek nama untuk device/vendor yang implementasinya bermasalah.
}
}
String name = info.getName();
if (name == null) {
return false;
}
String lower = name.toLowerCase(Locale.US);
// Exclude software codec umum.
if (lower.startsWith("omx.google.") ||
lower.startsWith("c2.android.") ||
lower.contains(".sw.") ||
lower.contains("software") ||
lower.contains("ffmpeg")) {
return false;
}
// Vendor codec biasanya hardware.
return lower.startsWith("omx.") ||
lower.startsWith("c2.") ||
lower.contains("qcom") ||
lower.contains("exynos") ||
lower.contains("mtk") ||
lower.contains("mediatek") ||
lower.contains("kirin") ||
lower.contains("hisi") ||
lower.contains("sprd") ||
lower.contains("unisoc");
}
private void selectBestCodecForSelectedVideo(VideoInfo info) {
if (spCodec == null || spCodec.getAdapter() == null) {
return;
}
String originalMime = info != null ? info.videoMime : null;
String preferredEncoder = choosePreferredEncoderForOriginalMime(originalMime);
int preferredIndex = findCodecIndexByEncoder(preferredEncoder);
if (preferredIndex < 0) {
preferredIndex = findCodecIndexByEncoder("libx264");
}
if (preferredIndex < 0 && spCodec.getAdapter().getCount() > 0) {
preferredIndex = 0;
}
if (preferredIndex >= 0) {
spCodec.setSelection(preferredIndex);
}
}
private String choosePreferredEncoderForOriginalMime(String originalMime) {
if (originalMime != null) {
if ("video/avc".equalsIgnoreCase(originalMime)) {
if (findCodecIndexByEncoder("h264_mediacodec") >= 0) {
return "h264_mediacodec";
}
return "libx264";
}
if ("video/hevc".equalsIgnoreCase(originalMime) ||
"video/h265".equalsIgnoreCase(originalMime)) {
if (findCodecIndexByEncoder("hevc_mediacodec") >= 0) {
return "hevc_mediacodec";
}
return "libx265";
}
}
// Untuk codec input selain H.264/H.265 seperti AV1, VP9, MPEG4, dll:
// prioritas aman = H.264 Hardware GPU kalau tersedia, lalu H.264 CPU.
if (findCodecIndexByEncoder("h264_mediacodec") >= 0) {
return "h264_mediacodec";
}
return "libx264";
}
private int findCodecIndexByEncoder(String encoderName) {
if (spCodec == null || spCodec.getAdapter() == null || encoderName == null) {
return -1;
}
for (int i = 0; i < spCodec.getAdapter().getCount(); i++) {
Object item = spCodec.getAdapter().getItem(i);
if (item instanceof CodecOption) {
CodecOption option = (CodecOption) item;
if (encoderName.equals(option.encoderName)) {
return i;
}
}
}
return -1;
}
private void setupSpinners() {
ArrayList<CodecOption> codecItems = buildVideoCodecOptions();
ArrayAdapter<CodecOption> codecAdapter = makeDarkAdapter(codecItems);
codecAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
@@ -385,10 +598,36 @@ public class MainActivity extends Activity {
spAudio.setAdapter(audioAdapter);
spAudio.setSelection(clamp(prefs.getInt(KEY_AUDIO_INDEX, 0), 0, audioItems.size() - 1));
ArrayList<BitrateOption> bitrateItems = new ArrayList<>();
bitrateItems.add(new BitrateOption("Auto", 0));
bitrateItems.add(new BitrateOption("500 Kbps", 500000));
bitrateItems.add(new BitrateOption("800 Kbps", 800000));
bitrateItems.add(new BitrateOption("1 Mbps", 1000000));
bitrateItems.add(new BitrateOption("1.5 Mbps", 1500000));
bitrateItems.add(new BitrateOption("2 Mbps", 2000000));
bitrateItems.add(new BitrateOption("2.5 Mbps", 2500000));
bitrateItems.add(new BitrateOption("3 Mbps", 3000000));
bitrateItems.add(new BitrateOption("4 Mbps", 4000000));
bitrateItems.add(new BitrateOption("5 Mbps", 5000000));
bitrateItems.add(new BitrateOption("6 Mbps", 6000000));
bitrateItems.add(new BitrateOption("8 Mbps", 8000000));
bitrateItems.add(new BitrateOption("10 Mbps", 10000000));
bitrateItems.add(new BitrateOption("12 Mbps", 12000000));
bitrateItems.add(new BitrateOption("16 Mbps", 16000000));
bitrateItems.add(new BitrateOption("20 Mbps", 20000000));
bitrateItems.add(new BitrateOption("25 Mbps", 25000000));
bitrateItems.add(new BitrateOption("35 Mbps", 35000000));
ArrayAdapter<BitrateOption> bitrateAdapter = makeDarkAdapter(bitrateItems);
bitrateAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spVideoBitrate.setAdapter(bitrateAdapter);
spVideoBitrate.setSelection(clamp(prefs.getInt(KEY_VIDEO_BITRATE_INDEX, 0), 0, bitrateItems.size() - 1));
AdapterView.OnItemSelectedListener saveListener = new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
saveSettingsOnly();
syncAdvancedVideoSettingsForSelectedCodec();
updateCompressionSummary();
}
@@ -400,6 +639,8 @@ public class MainActivity extends Activity {
spPreset.setOnItemSelectedListener(saveListener);
spResolution.setOnItemSelectedListener(saveListener);
spAudio.setOnItemSelectedListener(saveListener);
spVideoBitrate.setOnItemSelectedListener(saveListener);
syncAdvancedVideoSettingsForSelectedCodec();
}
private void setupCrfSlider() {
@@ -635,17 +876,44 @@ public class MainActivity extends Activity {
layoutAdvancedSettings.setVisibility(expanded ? View.VISIBLE : View.GONE);
}
syncAdvancedVideoSettingsForSelectedCodec();
if (txtSettingsToggle != null) {
txtSettingsToggle.setText(expanded ? "Pengaturan lanjutan ▲" : "Pengaturan lanjutan ▼");
}
}
private boolean selectedVideoCodecIsGpu() {
Object codecObj = spCodec != null ? spCodec.getSelectedItem() : null;
return codecObj instanceof CodecOption && ((CodecOption) codecObj).hardware;
}
private void syncAdvancedVideoSettingsForSelectedCodec() {
boolean gpuCodec = selectedVideoCodecIsGpu();
if (layoutCpuVideoSettings != null) {
layoutCpuVideoSettings.setVisibility(gpuCodec ? View.GONE : View.VISIBLE);
}
if (layoutGpuVideoSettings != null) {
layoutGpuVideoSettings.setVisibility(gpuCodec ? View.VISIBLE : View.GONE);
}
}
private BitrateOption getSelectedVideoBitrateOption() {
Object item = spVideoBitrate != null ? spVideoBitrate.getSelectedItem() : null;
if (item instanceof BitrateOption) {
return (BitrateOption) item;
}
return new BitrateOption("Auto", 0);
}
private void updateCompressionSummary() {
if (txtCompressionSummary == null) {
return;
}
String codecLabel = "H.264 / AVC";
String codecLabel = "H.264 / AVC (CPU)";
Object codecObj = spCodec != null ? spCodec.getSelectedItem() : null;
if (codecObj instanceof CodecOption) {
codecLabel = ((CodecOption) codecObj).label;
@@ -663,16 +931,28 @@ public class MainActivity extends Activity {
int crf = seekCrf != null ? CRF_MIN + seekCrf.getProgress() : DEFAULT_CRF;
boolean hardwareCodec = false;
if (codecObj instanceof CodecOption) {
hardwareCodec = ((CodecOption) codecObj).hardware;
}
String audioLabel = "Original / Copy";
Object audioObj = spAudio != null ? spAudio.getSelectedItem() : null;
if (audioObj instanceof AudioOption) {
audioLabel = ((AudioOption) audioObj).label;
}
txtCompressionSummary.setText(
"Lanjutan: CRF " + crf + " • Preset " + presetLabel +
" • Audio " + audioLabel
);
if (hardwareCodec) {
BitrateOption bitrateOption = getSelectedVideoBitrateOption();
txtCompressionSummary.setText(
"Lanjutan: Bitrate " + bitrateOption.label + " • Audio " + audioLabel
);
} else {
txtCompressionSummary.setText(
"Lanjutan: CRF " + crf + " • Preset " + presetLabel +
" • Audio " + audioLabel
);
}
}
private void setResolutionOptions(VideoInfo info) {
@@ -821,6 +1101,7 @@ public class MainActivity extends Activity {
if (inputUri != null) {
currentVideoInfo = readVideoInfo(inputUri);
setResolutionOptions(currentVideoInfo);
selectBestCodecForSelectedVideo(currentVideoInfo);
txtSelected.setText("Video dipilih");
txtVideoInfo.setText(makeVideoInfoText(currentVideoInfo, inputUri));
@@ -1314,8 +1595,12 @@ public class MainActivity extends Activity {
CodecOption codec = (CodecOption) spCodec.getSelectedItem();
String encoderName = codec != null ? codec.encoderName : "libx264";
String codecLabel = codec != null ? codec.label : "H.264 / AVC";
String codecLabel = codec != null ? codec.label : "H.264 / AVC (CPU)";
String preset = String.valueOf(spPreset.getSelectedItem());
boolean gpuCodec = codec != null && codec.hardware;
BitrateOption videoBitrateOption = gpuCodec ? getSelectedVideoBitrateOption() : new BitrateOption("Auto", 0);
int videoBitrate = videoBitrateOption.bitrate;
String videoBitrateLabel = videoBitrateOption.label;
ResolutionOption resOption = (ResolutionOption) spResolution.getSelectedItem();
String resolutionLabel = resOption != null ? resOption.label : "Original";
@@ -1334,6 +1619,8 @@ public class MainActivity extends Activity {
intent.putExtra(EXTRA_VIDEO_ROTATION, currentVideoInfo != null ? currentVideoInfo.rotation : 0);
intent.putExtra(EXTRA_RESOLUTION_LABEL, resolutionLabel);
intent.putExtra(EXTRA_CRF, crf);
intent.putExtra(EXTRA_VIDEO_BITRATE, videoBitrate);
intent.putExtra(EXTRA_VIDEO_BITRATE_LABEL, videoBitrateLabel);
intent.putExtra(EXTRA_ENCODER_NAME, encoderName);
intent.putExtra(EXTRA_CODEC_LABEL, codecLabel);
intent.putExtra(EXTRA_PRESET, preset);
@@ -1407,6 +1694,7 @@ public class MainActivity extends Activity {
.putInt(KEY_CRF, crf)
.putInt(KEY_TARGET_SHORT, targetShortSide)
.putInt(KEY_AUDIO_INDEX, spAudio != null ? spAudio.getSelectedItemPosition() : 0)
.putInt(KEY_VIDEO_BITRATE_INDEX, spVideoBitrate != null ? spVideoBitrate.getSelectedItemPosition() : 0)
.apply();
}
@@ -27,6 +27,7 @@ public final class NativeCompressor {
int targetShortSide,
int inputRotationDegrees,
int crf,
int videoBitrate,
String encoderName,
String preset,
String audioMode,
@@ -62,5 +63,15 @@ public final class NativeCompressor {
ProgressCallback callback
);
/**
* Cek apakah FFmpeg build native memiliki encoder tertentu.
* Contoh:
* libx264
* libx265
* h264_mediacodec
* hevc_mediacodec
*/
public static native boolean isVideoEncoderAvailable(String encoderName);
public static native void cancelCurrentJob();
}
@@ -60,6 +60,8 @@ public class ProcessingActivity extends Activity {
private int targetShortSide;
private int inputRotationDegrees;
private int crf;
private int videoBitrate;
private String videoBitrateLabel;
private String encoderName;
private String codecLabel;
private String resolutionLabel;
@@ -174,6 +176,8 @@ public class ProcessingActivity extends Activity {
targetShortSide = getIntent().getIntExtra(MainActivity.EXTRA_TARGET_SHORT, 0);
inputRotationDegrees = getIntent().getIntExtra(MainActivity.EXTRA_VIDEO_ROTATION, 0);
crf = getIntent().getIntExtra(MainActivity.EXTRA_CRF, 26);
videoBitrate = getIntent().getIntExtra(MainActivity.EXTRA_VIDEO_BITRATE, 0);
videoBitrateLabel = getIntent().getStringExtra(MainActivity.EXTRA_VIDEO_BITRATE_LABEL);
encoderName = getIntent().getStringExtra(MainActivity.EXTRA_ENCODER_NAME);
codecLabel = getIntent().getStringExtra(MainActivity.EXTRA_CODEC_LABEL);
resolutionLabel = getIntent().getStringExtra(MainActivity.EXTRA_RESOLUTION_LABEL);
@@ -193,6 +197,7 @@ public class ProcessingActivity extends Activity {
if (codecLabel == null) codecLabel = "H.264 / AVC";
if (resolutionLabel == null) resolutionLabel = "Original";
if (preset == null) preset = "medium";
if (videoBitrateLabel == null) videoBitrateLabel = videoBitrate > 0 ? formatVideoBitrate(videoBitrate) : "Auto";
if (audioMode == null) audioMode = "copy";
if (audioLabel == null) audioLabel = "Original / Copy";
@@ -215,6 +220,16 @@ public class ProcessingActivity extends Activity {
return;
}
if (isGpuEncoder(encoderName)) {
txtConfig.setText(
"Codec : " + codecLabel + "\n" +
"Resolusi : " + resolutionLabel + "\n" +
"Bitrate : " + videoBitrateLabel + "\n" +
"Audio : " + audioLabel
);
return;
}
txtConfig.setText(
"Codec : " + codecLabel + "\n" +
"Resolusi : " + resolutionLabel + "\n" +
@@ -256,7 +271,7 @@ public class ProcessingActivity extends Activity {
String outputName = imageJob
? makeImageOutputName(outputFormat, targetShortSide, imageQuality)
: makeVideoOutputName(encoderName, targetShortSide, crf);
: makeVideoOutputName(encoderName, targetShortSide, crf, videoBitrate);
outputPfd = createOutputFile(outputName, outputMime, imageJob);
if (outputPfd == null) {
@@ -285,6 +300,7 @@ public class ProcessingActivity extends Activity {
targetShortSide,
inputRotationDegrees,
crf,
videoBitrate,
encoderName,
preset,
audioMode,
@@ -303,9 +319,9 @@ public class ProcessingActivity extends Activity {
handler.removeCallbacks(elapsedRunnable);
handler.removeCallbacks(smoothProgressRunnable);
allowScreenOffAfterProcessing();
txtStatus.setText("Dibatalkan");
txtStatus.setText(imageJob ? "Gambar dibatalkan" : "Video dibatalkan");
txtElapsed.setText("Total waktu: " + formatElapsed(endTimeMs - startTimeMs));
toast("Compress dibatalkan");
toast(imageJob ? "Compress gambar dibatalkan" : "Compress video dibatalkan");
finish();
});
return;
@@ -397,9 +413,14 @@ public class ProcessingActivity extends Activity {
return;
}
String title = imageJob ? "Batalkan kompresi gambar?" : "Batalkan kompresi video?";
String message = imageJob
? "Output gambar sementara akan dihapus."
: "Output video sementara akan dihapus.";
new AlertDialog.Builder(this)
.setTitle("Batalkan kompresi?")
.setMessage("File output sementara akan dihapus.")
.setTitle(title)
.setMessage(message)
.setNegativeButton("Tidak", null)
.setPositiveButton("Batalkan", (dialog, which) -> cancelCompression())
.show();
@@ -411,8 +432,10 @@ public class ProcessingActivity extends Activity {
}
cancelRequested = true;
txtStatus.setText("Membatalkan...");
txtStatus.setText(imageJob ? "Membatalkan gambar..." : "Membatalkan video...");
btnCancel.setEnabled(false);
// Berlaku untuk video dan image karena keduanya memakai flag cancel native yang sama.
NativeCompressor.cancelCurrentJob();
}
@@ -560,12 +583,34 @@ public class ProcessingActivity extends Activity {
}
}
private String makeVideoOutputName(String encoderName, int targetShortSide, int crf) {
String codec = encoderName.equals("libx265") ? "hevc" : "avc";
private String makeVideoOutputName(String encoderName, int targetShortSide, int crf, int videoBitrate) {
String codec = (encoderName != null && (encoderName.contains("265") || encoderName.contains("hevc")))
? "hevc"
: "avc";
String res = targetShortSide <= 0 ? "original" : targetShortSide + "p";
String mode = isGpuEncoder(encoderName)
? (videoBitrate > 0 ? "gpu_" + (videoBitrate / 1000000) + "mbps" : "gpu_auto")
: "crf" + crf;
String stamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date());
return "KCompressor_" + codec + "_" + res + "_crf" + crf + "_" + stamp + ".mp4";
return "KCompressor_" + codec + "_" + res + "_" + mode + "_" + stamp + ".mp4";
}
private boolean isGpuEncoder(String encoderName) {
return encoderName != null &&
(encoderName.equals("h264_mediacodec") || encoderName.equals("hevc_mediacodec"));
}
private String formatVideoBitrate(int bitrate) {
if (bitrate <= 0) {
return "Auto";
}
if (bitrate >= 1000000) {
return (bitrate / 1000000) + " Mbps";
}
return (bitrate / 1000) + " Kbps";
}
private String makeImageOutputName(String format, int targetShortSide, int quality) {
@@ -160,35 +160,68 @@
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/txtCrfValue"
<LinearLayout
android:id="@+id/layoutCpuVideoSettings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="CRF 26 (default 26) • Seimbang"
android:textColor="#9CA3AF"
android:textSize="13sp" />
android:orientation="vertical">
<SeekBar
android:id="@+id/seekCrf"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="2dp" />
<TextView
android:id="@+id/txtCrfValue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="CRF 26 (default 26) • Seimbang"
android:textColor="#9CA3AF"
android:textSize="13sp" />
<TextView
<SeekBar
android:id="@+id/seekCrf"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="2dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Preset Video"
android:textColor="#9CA3AF"
android:textSize="13sp" />
<Spinner
android:id="@+id/spPreset"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="4dp"
android:background="@drawable/bg_spinner_dark"
android:paddingStart="12dp"
android:paddingEnd="12dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/layoutGpuVideoSettings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Preset Video"
android:textColor="#9CA3AF"
android:textSize="13sp" />
android:orientation="vertical"
android:visibility="gone">
<Spinner
android:id="@+id/spPreset"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="4dp"
android:background="@drawable/bg_spinner_dark"
android:paddingStart="12dp"
android:paddingEnd="12dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Bitrate Video"
android:textColor="#9CA3AF"
android:textSize="13sp" />
<Spinner
android:id="@+id/spVideoBitrate"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="4dp"
android:background="@drawable/bg_spinner_dark"
android:paddingStart="12dp"
android:paddingEnd="12dp" />
</LinearLayout>
<TextView
android:layout_width="match_parent"