diff --git a/app/build.gradle b/app/build.gradle index d185c0e..8bf2625 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "net.yrom.screenrecorder" minSdkVersion 21 targetSdkVersion 26 - versionCode 9 - versionName '2.0' + versionCode 10 + versionName '2.1' } compileOptions { diff --git a/app/src/main/java/net/yrom/screenrecorder/AudioEncodeConfig.java b/app/src/main/java/net/yrom/screenrecorder/AudioEncodeConfig.java index 6b4eb45..f9c9b17 100644 --- a/app/src/main/java/net/yrom/screenrecorder/AudioEncodeConfig.java +++ b/app/src/main/java/net/yrom/screenrecorder/AudioEncodeConfig.java @@ -16,7 +16,6 @@ package net.yrom.screenrecorder; -import android.media.MediaCodecInfo; import android.media.MediaFormat; import java.util.Objects; @@ -31,21 +30,35 @@ public class AudioEncodeConfig { final int bitRate; final int sampleRate; final int channelCount; + final int profile; public AudioEncodeConfig(String codecName, String mimeType, - int bitRate, int sampleRate, int channelCount) { + int bitRate, int sampleRate, int channelCount, int profile) { this.codecName = codecName; this.mimeType = Objects.requireNonNull(mimeType); this.bitRate = bitRate; this.sampleRate = sampleRate; this.channelCount = channelCount; + this.profile = profile; } MediaFormat toFormat() { MediaFormat format = MediaFormat.createAudioFormat(mimeType, sampleRate, channelCount); - format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectHE); + format.setInteger(MediaFormat.KEY_AAC_PROFILE, profile); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); //format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 4096 * 4); return format; } + + @Override + public String toString() { + return "AudioEncodeConfig{" + + "codecName='" + codecName + '\'' + + ", mimeType='" + mimeType + '\'' + + ", bitRate=" + bitRate + + ", sampleRate=" + sampleRate + + ", channelCount=" + channelCount + + ", profile=" + profile + + '}'; + } } diff --git a/app/src/main/java/net/yrom/screenrecorder/MainActivity.java b/app/src/main/java/net/yrom/screenrecorder/MainActivity.java index 2a7848d..937134b 100644 --- a/app/src/main/java/net/yrom/screenrecorder/MainActivity.java +++ b/app/src/main/java/net/yrom/screenrecorder/MainActivity.java @@ -38,6 +38,7 @@ import android.os.StrictMode; import android.preference.PreferenceManager; import android.util.Log; +import android.util.Range; import android.view.View; import android.widget.ArrayAdapter; import android.widget.Button; @@ -51,6 +52,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Date; +import java.util.List; import java.util.Locale; import static android.Manifest.permission.RECORD_AUDIO; @@ -65,14 +67,20 @@ public class MainActivity extends Activity { // members below will be initialized in onCreate() private MediaProjectionManager mMediaProjectionManager; private Button mButton; - private NamedSpinner mResolution; - private NamedSpinner mFramerate; + private NamedSpinner mVieoResolution; + private NamedSpinner mVideoFramerate; private NamedSpinner mIFrameInterval; - private NamedSpinner mBitrate; - private NamedSpinner mCodec; - private NamedSpinner mProfileLevel; + private NamedSpinner mVideoBitrate; + private NamedSpinner mAudioBitrate; + private NamedSpinner mAudioSampleRate; + private NamedSpinner mAudioChannelCount; + private NamedSpinner mVideoCodec; + private NamedSpinner mAudioCodec; + private NamedSpinner mVideoProfileLevel; + private NamedSpinner mAudioProfile; private NamedSpinner mOrientation; - private MediaCodecInfo[] mCodecInfos; // avc codecs + private MediaCodecInfo[] mAvcCodecInfos; // avc codecs + private MediaCodecInfo[] mAacCodecInfos; // aac codecs private Notifications mNotifications; /** @@ -88,51 +96,22 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_main); mMediaProjectionManager = (MediaProjectionManager) getApplicationContext().getSystemService(MEDIA_PROJECTION_SERVICE); mNotifications = new Notifications(getApplicationContext()); - - mButton = findViewById(R.id.record_button); - mButton.setOnClickListener(this::onButtonClick); - - mCodec = findViewById(R.id.media_codec); - mResolution = findViewById(R.id.resolution); - mFramerate = findViewById(R.id.framerate); - mIFrameInterval = findViewById(R.id.iframe_interval); - mBitrate = findViewById(R.id.bitrate); - mProfileLevel = findViewById(R.id.avc_profile); - mOrientation = findViewById(R.id.orientation); - - if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { - mOrientation.setSelectedPosition(1); - } - restoreSelections(); - - mCodec.setOnItemSelectedListener((view, position) -> onCodecSelected(view.getSelectedItem())); - mResolution.setOnItemSelectedListener((view, position) -> { - if (position == 0) return; - onResolutionChanged(position, view.getSelectedItem()); - }); - mFramerate.setOnItemSelectedListener((view, position) -> { - if (position == 0) return; - onFramerateChanged(position, view.getSelectedItem()); - }); - mBitrate.setOnItemSelectedListener((view, position) -> { - if (position == 0) return; - onBitrateChanged(position, view.getSelectedItem()); - }); - mOrientation.setOnItemSelectedListener((view, position) -> { - if (position == 0) return; - onOrientationChanged(position, view.getSelectedItem()); - }); - + bindViews(); Utils.findEncodersByTypeAsync(VIDEO_AVC, infos -> { logCodecInfos(infos, VIDEO_AVC); - mCodecInfos = infos; - SpinnerAdapter codecsAdapter = createCodecsAdapter(mCodecInfos); - mCodec.setAdapter(codecsAdapter); + mAvcCodecInfos = infos; + SpinnerAdapter codecsAdapter = createCodecsAdapter(mAvcCodecInfos); + mVideoCodec.setAdapter(codecsAdapter); + restoreSelections(mVideoCodec, mVieoResolution, mVideoFramerate, mIFrameInterval, mVideoBitrate); }); Utils.findEncodersByTypeAsync(AUDIO_AAC, infos -> { logCodecInfos(infos, AUDIO_AAC); + mAacCodecInfos = infos; + SpinnerAdapter codecsAdapter = createCodecsAdapter(mAacCodecInfos); + mAudioCodec.setAdapter(codecsAdapter); + restoreSelections(mAudioCodec, mAudioChannelCount); }); } @@ -162,71 +141,96 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { return; } - final String codec = getSelectedCodec(); - if (codec == null) { - toast("There is no media-codec"); + VideoEncodeConfig video = createVideoConfig(); + AudioEncodeConfig audio = createAudioConfig(); + if (video == null || audio == null) { + toast("Create ScreenRecorder failure"); mediaProjection.stop(); return; } + File dir = getSavingDir(); if (!dir.exists() && !dir.mkdirs()) { cancelRecorder(); return; } - - // video size - int[] selectedWithHeight = getSelectedWithHeight(); - boolean isLandscape = isLandscape(); - int width = selectedWithHeight[isLandscape ? 0 : 1]; - int height = selectedWithHeight[isLandscape ? 1 : 0]; - int framerate = getSelectedFramerate(); - int iframe = getSelectedIFrameInterval(); - int bitrate = getSelectedBitrate(); - MediaCodecInfo.CodecProfileLevel profileLevel = getSelectedProfileLevel(); - SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US); final File file = new File(dir, "Screen-" + format.format(new Date()) - + "-" + width + "x" + height + ".mp4"); - mRecorder = new ScreenRecorder(new VideoEncodeConfig(width, height, bitrate, - framerate, iframe, codec, VIDEO_AVC, profileLevel), - //TODO: audio encode config - new AudioEncodeConfig(null, AUDIO_AAC, 300_000, 44100, 2), - 1, mediaProjection, file.getAbsolutePath()); - mRecorder.setCallback(new ScreenRecorder.Callback() { - long startTime = 0; - - @Override - public void onStop(Throwable error) { - runOnUiThread(() -> stopRecorder()); - if (error != null) { - toast("Recorder error ! See logcat for more details"); - error.printStackTrace(); - file.delete(); - } + + "-" + video.width + "x" + video.height + ".mp4"); + Log.d("@@", "Create recorder with :" + video + " \n " + audio + "\n " + file); + mRecorder = newRecorder(mediaProjection, video, audio, file); + if (hasPermissions()) { + startRecorder(); + } else { + cancelRecorder(); + } + } + } + private ScreenRecorder newRecorder(MediaProjection mediaProjection, VideoEncodeConfig video, + AudioEncodeConfig audio, File output) { + ScreenRecorder r = new ScreenRecorder(video, audio, + 1, mediaProjection, output.getAbsolutePath()); + r.setCallback(new ScreenRecorder.Callback() { + long startTime = 0; + + @Override + public void onStop(Throwable error) { + runOnUiThread(() -> stopRecorder()); + if (error != null) { + toast("Recorder error ! See logcat for more details"); + error.printStackTrace(); + output.delete(); } + } - @Override - public void onStart() { - mNotifications.recording(0); - } + @Override + public void onStart() { + mNotifications.recording(0); + } - @Override - public void onRecording(long presentationTimeUs) { - if (startTime <= 0) { - startTime = presentationTimeUs; - } - long time = (presentationTimeUs - startTime) / 1000; - mNotifications.recording(time); + @Override + public void onRecording(long presentationTimeUs) { + if (startTime <= 0) { + startTime = presentationTimeUs; } - }); - if (hasPermissions()) { - startRecorder(); - } else { - cancelRecorder(); + long time = (presentationTimeUs - startTime) / 1000; + mNotifications.recording(time); } + }); + return r; + } + + private AudioEncodeConfig createAudioConfig() { + String codec = getSelectedAudioCodec(); + if (codec == null) { + return null; } + int bitrate = getSelectedAudioBitrate(); + int samplerate = getSelectedAudioSampleRate(); + int channelCount = getSelectedAudioChannelCount(); + int profile = getSelectedAudioProfile(); + + return new AudioEncodeConfig(codec, AUDIO_AAC, bitrate, samplerate, channelCount, profile); + } + private VideoEncodeConfig createVideoConfig() { + final String codec = getSelectedVideoCodec(); + if (codec == null) { + // no selected codec ?? + return null; + } + // video size + int[] selectedWithHeight = getSelectedWithHeight(); + boolean isLandscape = isLandscape(); + int width = selectedWithHeight[isLandscape ? 0 : 1]; + int height = selectedWithHeight[isLandscape ? 1 : 0]; + int framerate = getSelectedFramerate(); + int iframe = getSelectedIFrameInterval(); + int bitrate = getSelectedVideoBitrate(); + MediaCodecInfo.CodecProfileLevel profileLevel = getSelectedProfileLevel(); + return new VideoEncodeConfig(width, height, bitrate, + framerate, iframe, codec, VIDEO_AVC, profileLevel); } private static File getSavingDir() { @@ -246,11 +250,6 @@ public void onRequestPermissionsResult(int requestCode, String[] permissions, in } } - private void startCaptureIntent() { - Intent captureIntent = mMediaProjectionManager.createScreenCaptureIntent(); - startActivityForResult(captureIntent, REQUEST_MEDIA_PROJECTION); - } - @Override protected void onDestroy() { super.onDestroy(); @@ -259,6 +258,52 @@ protected void onDestroy() { mNotifications = null; } + private void startCaptureIntent() { + Intent captureIntent = mMediaProjectionManager.createScreenCaptureIntent(); + startActivityForResult(captureIntent, REQUEST_MEDIA_PROJECTION); + } + + private void bindViews() { + mButton = findViewById(R.id.record_button); + mButton.setOnClickListener(this::onButtonClick); + + mVideoCodec = findViewById(R.id.video_codec); + mVieoResolution = findViewById(R.id.resolution); + mVideoFramerate = findViewById(R.id.framerate); + mIFrameInterval = findViewById(R.id.iframe_interval); + mVideoBitrate = findViewById(R.id.video_bitrate); + mOrientation = findViewById(R.id.orientation); + + mAudioCodec = findViewById(R.id.audio_codec); + mVideoProfileLevel = findViewById(R.id.avc_profile); + mAudioBitrate = findViewById(R.id.audio_bitrate); + mAudioSampleRate = findViewById(R.id.sample_rate); + mAudioProfile = findViewById(R.id.aac_profile); + mAudioChannelCount = findViewById(R.id.audio_channel_count); + + if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { + mOrientation.setSelectedPosition(1); + } + + mVideoCodec.setOnItemSelectedListener((view, position) -> onVideoCodecSelected(view.getSelectedItem())); + mAudioCodec.setOnItemSelectedListener((view, position) -> onAudioCodecSelected(view.getSelectedItem())); + mVieoResolution.setOnItemSelectedListener((view, position) -> { + if (position == 0) return; + onResolutionChanged(position, view.getSelectedItem()); + }); + mVideoFramerate.setOnItemSelectedListener((view, position) -> { + if (position == 0) return; + onFramerateChanged(position, view.getSelectedItem()); + }); + mVideoBitrate.setOnItemSelectedListener((view, position) -> { + if (position == 0) return; + onBitrateChanged(position, view.getSelectedItem()); + }); + mOrientation.setOnItemSelectedListener((view, position) -> { + if (position == 0) return; + onOrientationChanged(position, view.getSelectedItem()); + }); + } private void onButtonClick(View v) { if (mRecorder != null) { @@ -326,8 +371,8 @@ private boolean hasPermissions() { } private void onResolutionChanged(int selectedPosition, String resolution) { - String codecName = getSelectedCodec(); - MediaCodecInfo codec = getCodecInfo(codecName); + String codecName = getSelectedVideoCodec(); + MediaCodecInfo codec = getVideoCodecInfo(codecName); if (codec == null) return; MediaCodecInfo.CodecCapabilities capabilities = codec.getCapabilitiesForType(VIDEO_AVC); MediaCodecInfo.VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities(); @@ -340,22 +385,22 @@ private void onResolutionChanged(int selectedPosition, String resolution) { double selectedFramerate = getSelectedFramerate(); int resetPos = Math.max(selectedPosition - 1, 0); if (!videoCapabilities.isSizeSupported(width, height)) { - mResolution.setSelectedPosition(resetPos); + mVieoResolution.setSelectedPosition(resetPos); toast("codec '%s' unsupported size %dx%d (%s)", codecName, width, height, mOrientation.getSelectedItem()); Log.w("@@", codecName + " height range: " + videoCapabilities.getSupportedHeights() + "\n width range: " + videoCapabilities.getSupportedHeights()); } else if (!videoCapabilities.areSizeAndRateSupported(width, height, selectedFramerate)) { - mResolution.setSelectedPosition(resetPos); + mVieoResolution.setSelectedPosition(resetPos); toast("codec '%s' unsupported size %dx%d(%s)\nwith framerate %d", codecName, width, height, mOrientation.getSelectedItem(), (int) selectedFramerate); } } private void onBitrateChanged(int selectedPosition, String bitrate) { - String codecName = getSelectedCodec(); - MediaCodecInfo codec = getCodecInfo(codecName); + String codecName = getSelectedVideoCodec(); + MediaCodecInfo codec = getVideoCodecInfo(codecName); if (codec == null) return; MediaCodecInfo.CodecCapabilities capabilities = codec.getCapabilitiesForType(VIDEO_AVC); MediaCodecInfo.VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities(); @@ -363,7 +408,7 @@ private void onBitrateChanged(int selectedPosition, String bitrate) { int resetPos = Math.max(selectedPosition - 1, 0); if (!videoCapabilities.getBitrateRange().contains(selectedBitrate)) { - mBitrate.setSelectedPosition(resetPos); + mVideoBitrate.setSelectedPosition(resetPos); toast("codec '%s' unsupported bitrate %d", codecName, selectedBitrate); Log.w("@@", codecName + " bitrate range: " + videoCapabilities.getBitrateRange()); @@ -371,8 +416,8 @@ private void onBitrateChanged(int selectedPosition, String bitrate) { } private void onOrientationChanged(int selectedPosition, String orientation) { - String codecName = getSelectedCodec(); - MediaCodecInfo codec = getCodecInfo(codecName); + String codecName = getSelectedVideoCodec(); + MediaCodecInfo codec = getVideoCodecInfo(codecName); if (codec == null) return; MediaCodecInfo.CodecCapabilities capabilities = codec.getCapabilitiesForType(VIDEO_AVC); MediaCodecInfo.VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities(); @@ -380,9 +425,9 @@ private void onOrientationChanged(int selectedPosition, String orientation) { boolean isLandscape = selectedPosition == 1; int width = selectedWithHeight[isLandscape ? 0 : 1]; int height = selectedWithHeight[isLandscape ? 1 : 0]; - int resetPos = Math.max(mResolution.getSelectedItemPosition() - 1, 0); + int resetPos = Math.max(mVieoResolution.getSelectedItemPosition() - 1, 0); if (!videoCapabilities.isSizeSupported(width, height)) { - mResolution.setSelectedPosition(resetPos); + mVieoResolution.setSelectedPosition(resetPos); toast("codec '%s' unsupported size %dx%d (%s)", codecName, width, height, orientation); return; @@ -397,8 +442,8 @@ private void onOrientationChanged(int selectedPosition, String orientation) { } private void onFramerateChanged(int selectedPosition, String rate) { - String codecName = getSelectedCodec(); - MediaCodecInfo codec = getCodecInfo(codecName); + String codecName = getSelectedVideoCodec(); + MediaCodecInfo codec = getVideoCodecInfo(codecName); if (codec == null) return; MediaCodecInfo.CodecCapabilities capabilities = codec.getCapabilitiesForType(VIDEO_AVC); MediaCodecInfo.VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities(); @@ -410,45 +455,46 @@ private void onFramerateChanged(int selectedPosition, String rate) { int resetPos = Math.max(selectedPosition - 1, 0); if (!videoCapabilities.getSupportedFrameRates().contains(selectedFramerate)) { - mFramerate.setSelectedPosition(resetPos); + mVideoFramerate.setSelectedPosition(resetPos); toast("codec '%s' unsupported framerate %d", codecName, selectedFramerate); } else if (!videoCapabilities.areSizeAndRateSupported(width, height, selectedFramerate)) { - mFramerate.setSelectedPosition(resetPos); + mVideoFramerate.setSelectedPosition(resetPos); toast("codec '%s' unsupported size %dx%d\nwith framerate %d", codecName, width, height, selectedFramerate); } } - private void onCodecSelected(String codecName) { - MediaCodecInfo codec = getCodecInfo(codecName); + private void onVideoCodecSelected(String codecName) { + MediaCodecInfo codec = getVideoCodecInfo(codecName); if (codec == null) { - mProfileLevel.setAdapter(null); + mVideoProfileLevel.setAdapter(null); return; } MediaCodecInfo.CodecCapabilities capabilities = codec.getCapabilitiesForType(VIDEO_AVC); - resetProfileLevelAdapter(capabilities); + resetAvcProfileLevelAdapter(capabilities); } - private void resetProfileLevelAdapter(MediaCodecInfo.CodecCapabilities capabilities) { - MediaCodecInfo.CodecProfileLevel[] levels = capabilities.profileLevels; - if (levels == null || levels.length == 0) { - mProfileLevel.setEnabled(false); + + private void resetAvcProfileLevelAdapter(MediaCodecInfo.CodecCapabilities capabilities) { + MediaCodecInfo.CodecProfileLevel[] profiles = capabilities.profileLevels; + if (profiles == null || profiles.length == 0) { + mVideoProfileLevel.setEnabled(false); return; } - mProfileLevel.setEnabled(true); - String[] profileLevels = new String[levels.length + 1]; + mVideoProfileLevel.setEnabled(true); + String[] profileLevels = new String[profiles.length + 1]; profileLevels[0] = "Default"; - for (int i = 0; i < levels.length; i++) { - profileLevels[i + 1] = Utils.avcProfileLevelToString(levels[i]); + for (int i = 0; i < profiles.length; i++) { + profileLevels[i + 1] = Utils.avcProfileLevelToString(profiles[i]); } - SpinnerAdapter old = mProfileLevel.getAdapter(); + SpinnerAdapter old = mVideoProfileLevel.getAdapter(); if (old == null || !(old instanceof ArrayAdapter)) { ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, new ArrayList<>()); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); adapter.addAll(profileLevels); - mProfileLevel.setAdapter(adapter); + mVideoProfileLevel.setAdapter(adapter); } else { ArrayAdapter adapter = (ArrayAdapter) old; adapter.setNotifyOnChange(false); @@ -458,15 +504,102 @@ private void resetProfileLevelAdapter(MediaCodecInfo.CodecCapabilities capabilit } } + private void onAudioCodecSelected(String codecName) { + MediaCodecInfo codec = getAudioCodecInfo(codecName); + if (codec == null) { + mAudioProfile.setAdapter(null); + mAudioSampleRate.setAdapter(null); + mAudioBitrate.setAdapter(null); + return; + } + MediaCodecInfo.CodecCapabilities capabilities = codec.getCapabilitiesForType(AUDIO_AAC); + + resetAudioBitrateAdapter(capabilities); + resetSampleRateAdapter(capabilities); + resetAacProfileAdapter(capabilities); + restoreSelections(mAudioBitrate, mAudioSampleRate, mAudioProfile); + } + + private void resetAacProfileAdapter(MediaCodecInfo.CodecCapabilities capabilities) { + String[] profiles = Utils.aacProfiles(); + SpinnerAdapter old = mAudioProfile.getAdapter(); + if (old == null || !(old instanceof ArrayAdapter)) { + ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, new ArrayList<>()); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + adapter.addAll(profiles); + mAudioProfile.setAdapter(adapter); + } else { + ArrayAdapter adapter = (ArrayAdapter) old; + adapter.setNotifyOnChange(false); + adapter.clear(); + adapter.addAll(profiles); + adapter.notifyDataSetChanged(); + } + + } + + private void resetSampleRateAdapter(MediaCodecInfo.CodecCapabilities capabilities) { + int[] sampleRates = capabilities.getAudioCapabilities().getSupportedSampleRates(); + List rates = new ArrayList<>(sampleRates.length); + int preferred = -1; + for (int i = 0; i < sampleRates.length; i++) { + int sampleRate = sampleRates[i]; + if (sampleRate == 44100) { + preferred = i; + } + rates.add(sampleRate); + } + + SpinnerAdapter old = mAudioSampleRate.getAdapter(); + if (old == null || !(old instanceof ArrayAdapter)) { + ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, new ArrayList<>()); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + adapter.addAll(rates); + mAudioSampleRate.setAdapter(adapter); + } else { + ArrayAdapter adapter = (ArrayAdapter) old; + adapter.setNotifyOnChange(false); + adapter.clear(); + adapter.addAll(rates); + adapter.notifyDataSetChanged(); + } + mAudioSampleRate.setSelectedPosition(preferred); + } + + private void resetAudioBitrateAdapter(MediaCodecInfo.CodecCapabilities capabilities) { + Range bitrateRange = capabilities.getAudioCapabilities().getBitrateRange(); + int lower = Math.max(bitrateRange.getLower() / 1000, 80); + int upper = bitrateRange.getUpper() / 1000; + List rates = new ArrayList<>(); + for (int rate = lower; rate < upper; rate += lower) { + rates.add(rate); + } + rates.add(upper); + + SpinnerAdapter old = mAudioBitrate.getAdapter(); + if (old == null || !(old instanceof ArrayAdapter)) { + ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, new ArrayList<>()); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + adapter.addAll(rates); + mAudioBitrate.setAdapter(adapter); + } else { + ArrayAdapter adapter = (ArrayAdapter) old; + adapter.setNotifyOnChange(false); + adapter.clear(); + adapter.addAll(rates); + adapter.notifyDataSetChanged(); + } + mAudioSampleRate.setSelectedPosition(rates.size() / 2); + } - private MediaCodecInfo getCodecInfo(String codecName) { + private MediaCodecInfo getVideoCodecInfo(String codecName) { if (codecName == null) return null; - if (mCodecInfos == null) { - mCodecInfos = Utils.findEncodersByType(VIDEO_AVC); + if (mAvcCodecInfos == null) { + mAvcCodecInfos = Utils.findEncodersByType(VIDEO_AVC); } MediaCodecInfo codec = null; - for (int i = 0; i < mCodecInfos.length; i++) { - MediaCodecInfo info = mCodecInfos[i]; + for (int i = 0; i < mAvcCodecInfos.length; i++) { + MediaCodecInfo info = mAvcCodecInfos[i]; if (info.getName().equals(codecName)) { codec = info; break; @@ -476,8 +609,25 @@ private MediaCodecInfo getCodecInfo(String codecName) { return codec; } - private String getSelectedCodec() { - return mCodec == null ? null : mCodec.getSelectedItem(); + private MediaCodecInfo getAudioCodecInfo(String codecName) { + if (codecName == null) return null; + if (mAacCodecInfos == null) { + mAacCodecInfos = Utils.findEncodersByType(AUDIO_AAC); + } + MediaCodecInfo codec = null; + for (int i = 0; i < mAacCodecInfos.length; i++) { + MediaCodecInfo info = mAacCodecInfos[i]; + if (info.getName().equals(codecName)) { + codec = info; + break; + } + } + if (codec == null) return null; + return codec; + } + + private String getSelectedVideoCodec() { + return mVideoCodec == null ? null : mVideoCodec.getSelectedItem(); } private SpinnerAdapter createCodecsAdapter(MediaCodecInfo[] codecInfos) { @@ -491,13 +641,13 @@ private boolean isLandscape() { } private int getSelectedFramerate() { - if (mFramerate == null) throw new IllegalStateException(); - return Integer.parseInt(mFramerate.getSelectedItem()); + if (mVideoFramerate == null) throw new IllegalStateException(); + return Integer.parseInt(mVideoFramerate.getSelectedItem()); } - private int getSelectedBitrate() { - if (mBitrate == null) throw new IllegalStateException(); - String selectedItem = mBitrate.getSelectedItem(); //kbps + private int getSelectedVideoBitrate() { + if (mVideoBitrate == null) throw new IllegalStateException(); + String selectedItem = mVideoBitrate.getSelectedItem(); //kbps return Integer.parseInt(selectedItem) * 1000; } @@ -506,18 +656,47 @@ private int getSelectedIFrameInterval() { } private MediaCodecInfo.CodecProfileLevel getSelectedProfileLevel() { - return mProfileLevel != null ? Utils.toProfileLevel(mProfileLevel.getSelectedItem()) : null; + return mVideoProfileLevel != null ? Utils.toProfileLevel(mVideoProfileLevel.getSelectedItem()) : null; } private int[] getSelectedWithHeight() { - if (mResolution == null) throw new IllegalStateException(); - String selected = mResolution.getSelectedItem(); + if (mVieoResolution == null) throw new IllegalStateException(); + String selected = mVieoResolution.getSelectedItem(); String[] xes = selected.split("x"); if (xes.length != 2) throw new IllegalArgumentException(); return new int[]{Integer.parseInt(xes[0]), Integer.parseInt(xes[1])}; } + private String getSelectedAudioCodec() { + return mAudioCodec == null ? null : mAudioCodec.getSelectedItem(); + } + + private int getSelectedAudioBitrate() { + if (mAudioBitrate == null) throw new IllegalStateException(); + Integer selectedItem = mAudioBitrate.getSelectedItem(); + return selectedItem * 1000; // bps + } + + private int getSelectedAudioSampleRate() { + if (mAudioSampleRate == null) throw new IllegalStateException(); + Integer selectedItem = mAudioSampleRate.getSelectedItem(); + return selectedItem; + } + + private int getSelectedAudioProfile() { + if (mAudioProfile == null) throw new IllegalStateException(); + String selectedItem = mAudioProfile.getSelectedItem(); + MediaCodecInfo.CodecProfileLevel profileLevel = Utils.toProfileLevel(selectedItem); + return profileLevel == null ? MediaCodecInfo.CodecProfileLevel.AACObjectMain : profileLevel.profile; + } + + private int getSelectedAudioChannelCount() { + if (mAudioChannelCount == null) throw new IllegalStateException(); + String selectedItem = mAudioChannelCount.getSelectedItem().toString(); + return Integer.parseInt(selectedItem); + } + private void toast(String message, Object... args) { Toast toast = Toast.makeText(this, (args.length == 0) ? message : String.format(Locale.US, message, args), @@ -578,23 +757,32 @@ private static void logCodecInfos(MediaCodecInfo[] codecInfos, String mimeType) } } - private void restoreSelections() { + private void restoreSelections(NamedSpinner... spinners) { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - restoreSelectionFromPreferences(preferences, this.mResolution); - restoreSelectionFromPreferences(preferences, this.mFramerate); - restoreSelectionFromPreferences(preferences, this.mIFrameInterval); - restoreSelectionFromPreferences(preferences, this.mBitrate); + for (NamedSpinner spinner : spinners) { + restoreSelectionFromPreferences(preferences, spinner); + } } private void saveSelections() { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); SharedPreferences.Editor edit = preferences.edit(); - saveSelectionToPreferences(edit, this.mResolution); - saveSelectionToPreferences(edit, this.mFramerate); - saveSelectionToPreferences(edit, this.mIFrameInterval); - saveSelectionToPreferences(edit, this.mBitrate); + for (NamedSpinner spinner : new NamedSpinner[]{ + mVieoResolution, + mVideoFramerate, + mIFrameInterval, + mVideoBitrate, + mAudioBitrate, + mAudioSampleRate, + mAudioChannelCount, + mVideoCodec, + mAudioCodec, + mAudioProfile, + }) { + saveSelectionToPreferences(edit, spinner); + } edit.apply(); } @@ -602,15 +790,17 @@ private void saveSelectionToPreferences(SharedPreferences.Editor preferences, Na int resId = spinner.getId(); String key = getResources().getResourceEntryName(resId); int selectedItemPosition = spinner.getSelectedItemPosition(); - preferences.putInt(key, selectedItemPosition); + if (selectedItemPosition >= 0) { + preferences.putInt(key, selectedItemPosition); + } } private void restoreSelectionFromPreferences(SharedPreferences preferences, NamedSpinner spinner) { int resId = spinner.getId(); String key = getResources().getResourceEntryName(resId); - int resolution = preferences.getInt(key, -1); - if (resolution != -1) { - spinner.setSelectedPosition(resolution); + int value = preferences.getInt(key, 0); + if (value >= 0 && spinner.getAdapter() != null) { + spinner.setSelectedPosition(value); } } diff --git a/app/src/main/java/net/yrom/screenrecorder/Utils.java b/app/src/main/java/net/yrom/screenrecorder/Utils.java index b6905d1..dcac5a0 100644 --- a/app/src/main/java/net/yrom/screenrecorder/Utils.java +++ b/app/src/main/java/net/yrom/screenrecorder/Utils.java @@ -81,6 +81,7 @@ static MediaCodecInfo[] findEncodersByType(String mimeType) { } + static SparseArray sAACProfiles = new SparseArray<>(); static SparseArray sAVCProfiles = new SparseArray<>(); static SparseArray sAVCLevels = new SparseArray<>(); @@ -90,7 +91,7 @@ static MediaCodecInfo[] findEncodersByType(String mimeType) { */ static String avcProfileLevelToString(MediaCodecInfo.CodecProfileLevel avcProfileLevel) { if (sAVCProfiles.size() == 0 || sAVCLevels.size() == 0) { - initAVCProfileLevels(); + initProfileLevels(); } String profile = null, level = null; int i = sAVCProfiles.indexOfKey(avcProfileLevel.profile); @@ -112,17 +113,34 @@ static String avcProfileLevelToString(MediaCodecInfo.CodecProfileLevel avcProfil return profile + '-' + level; } + static String[] aacProfiles() { + if (sAACProfiles.size() == 0) { + initProfileLevels(); + } + String[] profiles = new String[sAACProfiles.size()]; + for (int i = 0; i < sAACProfiles.size(); i++) { + profiles[i] = sAACProfiles.valueAt(i); + } + return profiles; + } + static MediaCodecInfo.CodecProfileLevel toProfileLevel(String str) { - if (sAVCProfiles.size() == 0 || sAVCLevels.size() == 0) { - initAVCProfileLevels(); + if (sAVCProfiles.size() == 0 || sAVCLevels.size() == 0 || sAACProfiles.size() == 0) { + initProfileLevels(); } + String profile = str; + String level = null; int i = str.indexOf('-'); - if (i < 0) return null; - String profile = str.substring(0, i); - String level = str.substring(i + 1); + if (i > 0) { // AVC profile has level + profile = str.substring(0, i); + level = str.substring(i + 1); + } + MediaCodecInfo.CodecProfileLevel res = new MediaCodecInfo.CodecProfileLevel(); if (profile.startsWith("AVC")) { res.profile = keyOfValue(sAVCProfiles, profile); + } else if (profile.startsWith("AAC")) { + res.profile = keyOfValue(sAACProfiles, profile); } else { try { res.profile = Integer.parseInt(profile); @@ -130,16 +148,20 @@ static MediaCodecInfo.CodecProfileLevel toProfileLevel(String str) { return null; } } - if (level.startsWith("AVC")) { - res.level = keyOfValue(sAVCLevels, level); - } else { - try { - res.level = Integer.parseInt(level); - } catch (NumberFormatException e) { - return null; + + if (level != null) { + if (level.startsWith("AVC")) { + res.level = keyOfValue(sAVCLevels, level); + } else { + try { + res.level = Integer.parseInt(level); + } catch (NumberFormatException e) { + return null; + } } } - return res.profile > 0 && res.level > 0 ? res : null; + + return res.profile > 0 && res.level >= 0 ? res : null; } private static int keyOfValue(SparseArray array, T value) { @@ -153,7 +175,7 @@ private static int keyOfValue(SparseArray array, T value) { return -1; } - private static void initAVCProfileLevels() { + private static void initProfileLevels() { Field[] fields = MediaCodecInfo.CodecProfileLevel.class.getFields(); for (Field f : fields) { if ((f.getModifiers() & (Modifier.STATIC | Modifier.FINAL)) == 0) { @@ -165,6 +187,8 @@ private static void initAVCProfileLevels() { target = sAVCProfiles; } else if (name.startsWith("AVCLevel")) { target = sAVCLevels; + } else if (name.startsWith("AACObject")) { + target = sAACProfiles; } else { continue; } diff --git a/app/src/main/java/net/yrom/screenrecorder/VideoEncodeConfig.java b/app/src/main/java/net/yrom/screenrecorder/VideoEncodeConfig.java index 039a12c..1ec944b 100644 --- a/app/src/main/java/net/yrom/screenrecorder/VideoEncodeConfig.java +++ b/app/src/main/java/net/yrom/screenrecorder/VideoEncodeConfig.java @@ -36,15 +36,14 @@ public class VideoEncodeConfig { final MediaCodecInfo.CodecProfileLevel codecProfileLevel; /** - * - * @param codecName selected codec name, maybe null - * @param mimeType video MIME type, cannot be null + * @param codecName selected codec name, maybe null + * @param mimeType video MIME type, cannot be null * @param codecProfileLevel profile level for video encoder nullable */ public VideoEncodeConfig(int width, int height, int bitrate, - int framerate, int iframeInterval, - String codecName, String mimeType, - MediaCodecInfo.CodecProfileLevel codecProfileLevel) { + int framerate, int iframeInterval, + String codecName, String mimeType, + MediaCodecInfo.CodecProfileLevel codecProfileLevel) { this.width = width; this.height = height; this.bitrate = bitrate; @@ -70,4 +69,17 @@ MediaFormat toFormat() { return format; } + @Override + public String toString() { + return "VideoEncodeConfig{" + + "width=" + width + + ", height=" + height + + ", bitrate=" + bitrate + + ", framerate=" + framerate + + ", iframeInterval=" + iframeInterval + + ", codecName='" + codecName + '\'' + + ", mimeType='" + mimeType + '\'' + + ", codecProfileLevel=" + (codecProfileLevel == null ? "" : Utils.avcProfileLevelToString(codecProfileLevel)) + + '}'; + } } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index ec3ca7f..4c317c6 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -7,73 +7,129 @@ android:paddingStart="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin"> - - - + - + android:orientation="vertical"> - - android:layout_marginTop="8dp" - android:entries="@array/bitrates" /> + - - android:layout_marginTop="8dp" - android:entries="@array/framerates" /> - - android:layout_marginTop="8dp" - android:entries="@array/iframeintervals" /> - + + + + + + + + + + + + + + + + + + + - android:layout_marginTop="8dp" - android:entries="@array/orientations" /> - + +