Add mediacodec mode (GPU) for video compressor
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
-1
@@ -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.
Binary file not shown.
+1
-1
@@ -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.
Binary file not shown.
+1
-1
@@ -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.
Binary file not shown.
+1
-1
@@ -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.
Binary file not shown.
@@ -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;
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user