From 0bd491ae55a1b368469d4a41322c747fc6bae957 Mon Sep 17 00:00:00 2001 From: Jay Aliomer Date: Mon, 16 Mar 2020 14:34:10 -0400 Subject: [PATCH] Screen recording sound options Adding options to screen recording Internal and Mic audio combination Fixes: 146340420 Test: manual Change-Id: Ib3c3b6c78a4d075f22479897e7c9e93a29c4fd4f Change-Id: If8d49ebba02150580d8caf3117d293aedcd648ff --- .../res/layout/screen_record_dialog.xml | 28 +- .../screen_record_dialog_audio_source.xml | 37 +++ ...en_record_dialog_audio_source_selected.xml | 35 +++ packages/SystemUI/res/values/ids.xml | 5 + packages/SystemUI/res/values/strings.xml | 2 + .../dagger/qualifiers/LongRunning.java | 30 ++ .../screenrecord/RecordingController.java | 4 +- .../screenrecord/RecordingService.java | 240 +++++---------- .../ScreenInternalAudioRecorder.java | 290 ++++++++++++++++++ .../screenrecord/ScreenMediaRecorder.java | 248 +++++++++++++++ .../screenrecord/ScreenRecordDialog.java | 39 ++- .../screenrecord/ScreenRecordingAdapter.java | 124 ++++++++ .../ScreenRecordingAudioSource.java | 27 ++ .../screenrecord/ScreenRecordingMuxer.java | 104 +++++++ .../util/concurrency/ConcurrencyModule.java | 22 ++ 15 files changed, 1045 insertions(+), 190 deletions(-) create mode 100644 packages/SystemUI/res/layout/screen_record_dialog_audio_source.xml create mode 100644 packages/SystemUI/res/layout/screen_record_dialog_audio_source_selected.xml create mode 100644 packages/SystemUI/src/com/android/systemui/dagger/qualifiers/LongRunning.java create mode 100644 packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java create mode 100644 packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java create mode 100644 packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingAdapter.java create mode 100644 packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingAudioSource.java create mode 100644 packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingMuxer.java diff --git a/packages/SystemUI/res/layout/screen_record_dialog.xml b/packages/SystemUI/res/layout/screen_record_dialog.xml index df576d83323f1..fd9936f6b8ea2 100644 --- a/packages/SystemUI/res/layout/screen_record_dialog.xml +++ b/packages/SystemUI/res/layout/screen_record_dialog.xml @@ -59,29 +59,16 @@ android:layout_gravity="center" android:layout_weight="0" android:layout_marginRight="@dimen/screenrecord_dialog_padding"/> - - - - + android:layout_height="48dp" + android:prompt="@string/screenrecord_audio_label"/> @@ -102,7 +89,8 @@ android:id="@+id/screenrecord_taps_switch" android:text="@string/screenrecord_taps_label" android:textColor="?android:attr/textColorPrimary" - android:textAppearance="?android:attr/textAppearanceMedium"/> + android:textAppearance="?android:attr/textAppearanceSmall"/> + diff --git a/packages/SystemUI/res/layout/screen_record_dialog_audio_source.xml b/packages/SystemUI/res/layout/screen_record_dialog_audio_source.xml new file mode 100644 index 0000000000000..af6f9bb827f66 --- /dev/null +++ b/packages/SystemUI/res/layout/screen_record_dialog_audio_source.xml @@ -0,0 +1,37 @@ + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/layout/screen_record_dialog_audio_source_selected.xml b/packages/SystemUI/res/layout/screen_record_dialog_audio_source_selected.xml new file mode 100644 index 0000000000000..fabe9e2d44531 --- /dev/null +++ b/packages/SystemUI/res/layout/screen_record_dialog_audio_source_selected.xml @@ -0,0 +1,35 @@ + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 04640f418e975..8156e8dc9bf1c 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -144,5 +144,10 @@ + + + + + diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 2b3e661c4f7ea..62a242ab21fcf 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -240,6 +240,8 @@ Screen Recorder + + Processing screen recording Ongoing notification for a screen record session diff --git a/packages/SystemUI/src/com/android/systemui/dagger/qualifiers/LongRunning.java b/packages/SystemUI/src/com/android/systemui/dagger/qualifiers/LongRunning.java new file mode 100644 index 0000000000000..e90781b48f233 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dagger/qualifiers/LongRunning.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dagger.qualifiers; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Qualifier; + +@Qualifier +@Documented +@Retention(RUNTIME) +public @interface LongRunning { +} diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java index ae0a1c4d98225..b253635e9bfa7 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java @@ -137,7 +137,7 @@ public class RecordingController * Check if the recording is ongoing * @return */ - public boolean isRecording() { + public synchronized boolean isRecording() { return mIsRecording; } @@ -157,7 +157,7 @@ public class RecordingController * Update the current status * @param isRecording */ - public void updateState(boolean isRecording) { + public synchronized void updateState(boolean isRecording) { mIsRecording = isRecording; for (RecordingStateChangeCallback cb : mListeners) { if (isRecording) { diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java index 390ac0969b218..cf098d52fa91b 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java @@ -22,41 +22,27 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.ContentResolver; -import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.Icon; -import android.hardware.display.DisplayManager; -import android.hardware.display.VirtualDisplay; import android.media.MediaRecorder; -import android.media.projection.IMediaProjection; -import android.media.projection.IMediaProjectionManager; -import android.media.projection.MediaProjection; -import android.media.projection.MediaProjectionManager; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; -import android.os.ServiceManager; -import android.provider.MediaStore; import android.provider.Settings; import android.util.DisplayMetrics; import android.util.Log; import android.util.Size; -import android.view.Surface; -import android.view.WindowManager; import android.widget.Toast; import com.android.systemui.R; +import com.android.systemui.dagger.qualifiers.LongRunning; -import java.io.File; import java.io.IOException; -import java.io.OutputStream; -import java.nio.file.Files; -import java.text.SimpleDateFormat; -import java.util.Date; +import java.util.concurrent.Executor; import javax.inject.Inject; @@ -66,13 +52,15 @@ import javax.inject.Inject; public class RecordingService extends Service implements MediaRecorder.OnInfoListener { public static final int REQUEST_CODE = 2; - private static final int NOTIFICATION_ID = 1; + private static final int NOTIFICATION_RECORDING_ID = 4274; + private static final int NOTIFICATION_PROCESSING_ID = 4275; + private static final int NOTIFICATION_VIEW_ID = 4273; private static final String TAG = "RecordingService"; private static final String CHANNEL_ID = "screen_record"; private static final String EXTRA_RESULT_CODE = "extra_resultCode"; private static final String EXTRA_DATA = "extra_data"; private static final String EXTRA_PATH = "extra_path"; - private static final String EXTRA_USE_AUDIO = "extra_useAudio"; + private static final String EXTRA_AUDIO_SOURCE = "extra_useAudio"; private static final String EXTRA_SHOW_TAPS = "extra_showTaps"; private static final String ACTION_START = "com.android.systemui.screenrecord.START"; @@ -80,29 +68,19 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis private static final String ACTION_SHARE = "com.android.systemui.screenrecord.SHARE"; private static final String ACTION_DELETE = "com.android.systemui.screenrecord.DELETE"; - private static final int TOTAL_NUM_TRACKS = 1; - private static final int VIDEO_BIT_RATE = 10000000; - private static final int VIDEO_FRAME_RATE = 30; - private static final int AUDIO_BIT_RATE = 16; - private static final int AUDIO_SAMPLE_RATE = 44100; - private static final int MAX_DURATION_MS = 60 * 60 * 1000; - private static final long MAX_FILESIZE_BYTES = 5000000000L; - private final RecordingController mController; - private MediaProjection mMediaProjection; - private Surface mInputSurface; - private VirtualDisplay mVirtualDisplay; - private MediaRecorder mMediaRecorder; private Notification.Builder mRecordingNotificationBuilder; - private boolean mUseAudio; + private ScreenRecordingAudioSource mAudioSource; private boolean mShowTaps; private boolean mOriginalShowTaps; - private File mTempFile; + private ScreenMediaRecorder mRecorder; + private final Executor mLongExecutor; @Inject - public RecordingService(RecordingController controller) { + public RecordingService(RecordingController controller, @LongRunning Executor executor) { mController = controller; + mLongExecutor = executor; } /** @@ -113,16 +91,16 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis * android.content.Intent)} * @param data The data from {@link android.app.Activity#onActivityResult(int, int, * android.content.Intent)} - * @param useAudio True to enable microphone input while recording + * @param audioSource The ordinal value of the audio source + * {@link com.android.systemui.screenrecord.ScreenRecordingAudioSource} * @param showTaps True to make touches visible while recording */ - public static Intent getStartIntent(Context context, int resultCode, Intent data, - boolean useAudio, boolean showTaps) { + public static Intent getStartIntent(Context context, int resultCode, + int audioSource, boolean showTaps) { return new Intent(context, RecordingService.class) .setAction(ACTION_START) .putExtra(EXTRA_RESULT_CODE, resultCode) - .putExtra(EXTRA_DATA, data) - .putExtra(EXTRA_USE_AUDIO, useAudio) + .putExtra(EXTRA_AUDIO_SOURCE, audioSource) .putExtra(EXTRA_SHOW_TAPS, showTaps); } @@ -139,36 +117,31 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis switch (action) { case ACTION_START: - mUseAudio = intent.getBooleanExtra(EXTRA_USE_AUDIO, false); + mAudioSource = ScreenRecordingAudioSource + .values()[intent.getIntExtra(EXTRA_AUDIO_SOURCE, 0)]; + Log.d(TAG, "recording with audio source" + mAudioSource); mShowTaps = intent.getBooleanExtra(EXTRA_SHOW_TAPS, false); - try { - IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE); - IMediaProjectionManager mediaService = - IMediaProjectionManager.Stub.asInterface(b); - IMediaProjection proj = mediaService.createProjection(getUserId(), - getPackageName(), - MediaProjectionManager.TYPE_SCREEN_CAPTURE, false); - IBinder projection = proj.asBinder(); - if (projection == null) { - Log.e(TAG, "Projection was null"); - Toast.makeText(this, R.string.screenrecord_start_error, Toast.LENGTH_LONG) - .show(); - return Service.START_NOT_STICKY; - } - mMediaProjection = new MediaProjection(getApplicationContext(), - IMediaProjection.Stub.asInterface(projection)); - startRecording(); - } catch (RemoteException e) { - e.printStackTrace(); - Toast.makeText(this, R.string.screenrecord_start_error, Toast.LENGTH_LONG) - .show(); - return Service.START_NOT_STICKY; - } + + mOriginalShowTaps = Settings.System.getInt( + getApplicationContext().getContentResolver(), + Settings.System.SHOW_TOUCHES, 0) != 0; + + setTapsVisible(mShowTaps); + + mRecorder = new ScreenMediaRecorder( + getApplicationContext(), + getUserId(), + mAudioSource, + this + ); + startRecording(); break; case ACTION_STOP: stopRecording(); + notificationManager.cancel(NOTIFICATION_RECORDING_ID); saveRecording(notificationManager); + stopSelf(); break; case ACTION_SHARE: @@ -183,10 +156,10 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); // Remove notification - notificationManager.cancel(NOTIFICATION_ID); + notificationManager.cancel(NOTIFICATION_RECORDING_ID); startActivity(Intent.createChooser(shareIntent, shareLabel) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); break; case ACTION_DELETE: // Close quick shade @@ -202,7 +175,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis Toast.LENGTH_LONG).show(); // Remove notification - notificationManager.cancel(NOTIFICATION_ID); + notificationManager.cancel(NOTIFICATION_RECORDING_ID); Log.d(TAG, "Deleted recording " + uri); break; } @@ -224,70 +197,15 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis */ private void startRecording() { try { - File cacheDir = getCacheDir(); - cacheDir.mkdirs(); - mTempFile = File.createTempFile("temp", ".mp4", cacheDir); - Log.d(TAG, "Writing video output to: " + mTempFile.getAbsolutePath()); - - mOriginalShowTaps = 1 == Settings.System.getInt( - getApplicationContext().getContentResolver(), - Settings.System.SHOW_TOUCHES, 0); - setTapsVisible(mShowTaps); - - // Set up media recorder - mMediaRecorder = new MediaRecorder(); - if (mUseAudio) { - mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); - } - mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); - mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); - - // Set up video - DisplayMetrics metrics = new DisplayMetrics(); - WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); - wm.getDefaultDisplay().getRealMetrics(metrics); - int screenWidth = metrics.widthPixels; - int screenHeight = metrics.heightPixels; - mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); - mMediaRecorder.setVideoSize(screenWidth, screenHeight); - mMediaRecorder.setVideoFrameRate(VIDEO_FRAME_RATE); - mMediaRecorder.setVideoEncodingBitRate(VIDEO_BIT_RATE); - mMediaRecorder.setMaxDuration(MAX_DURATION_MS); - mMediaRecorder.setMaxFileSize(MAX_FILESIZE_BYTES); - - // Set up audio - if (mUseAudio) { - mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); - mMediaRecorder.setAudioChannels(TOTAL_NUM_TRACKS); - mMediaRecorder.setAudioEncodingBitRate(AUDIO_BIT_RATE); - mMediaRecorder.setAudioSamplingRate(AUDIO_SAMPLE_RATE); - } - - mMediaRecorder.setOutputFile(mTempFile); - mMediaRecorder.prepare(); - - // Create surface - mInputSurface = mMediaRecorder.getSurface(); - mVirtualDisplay = mMediaProjection.createVirtualDisplay( - "Recording Display", - screenWidth, - screenHeight, - metrics.densityDpi, - DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, - mInputSurface, - null, - null); - - mMediaRecorder.setOnInfoListener(this); - mMediaRecorder.start(); + mRecorder.start(); mController.updateState(true); - } catch (IOException e) { - Log.e(TAG, "Error starting screen recording: " + e.getMessage()); + createRecordingNotification(); + } catch (IOException | RemoteException e) { + Toast.makeText(this, + R.string.screenrecord_start_error, Toast.LENGTH_LONG) + .show(); e.printStackTrace(); - throw new RuntimeException(e); } - - createRecordingNotification(); } private void createRecordingNotification() { @@ -306,7 +224,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, res.getString(R.string.screenrecord_name)); - String notificationTitle = mUseAudio + String notificationTitle = mAudioSource == ScreenRecordingAudioSource.NONE ? res.getString(R.string.screenrecord_ongoing_screen_and_audio) : res.getString(R.string.screenrecord_ongoing_screen_only); @@ -323,9 +241,10 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis this, REQUEST_CODE, getStopIntent(this), PendingIntent.FLAG_UPDATE_CURRENT)) .addExtras(extras); - notificationManager.notify(NOTIFICATION_ID, mRecordingNotificationBuilder.build()); + notificationManager.notify(NOTIFICATION_RECORDING_ID, + mRecordingNotificationBuilder.build()); Notification notification = mRecordingNotificationBuilder.build(); - startForeground(NOTIFICATION_ID, notification); + startForeground(NOTIFICATION_RECORDING_ID, notification); } private Notification createSaveNotification(Uri uri) { @@ -392,47 +311,38 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis private void stopRecording() { setTapsVisible(mOriginalShowTaps); - mMediaRecorder.stop(); - mMediaRecorder.release(); - mMediaRecorder = null; - mMediaProjection.stop(); - mMediaProjection = null; - mInputSurface.release(); - mVirtualDisplay.release(); - stopSelf(); + mRecorder.end(); mController.updateState(false); } private void saveRecording(NotificationManager notificationManager) { - String fileName = new SimpleDateFormat("'screen-'yyyyMMdd-HHmmss'.mp4'") - .format(new Date()); + Resources res = getApplicationContext().getResources(); + String notificationTitle = mAudioSource == ScreenRecordingAudioSource.NONE + ? res.getString(R.string.screenrecord_ongoing_screen_only) + : res.getString(R.string.screenrecord_ongoing_screen_and_audio); + Notification.Builder builder = new Notification.Builder(getApplicationContext(), CHANNEL_ID) + .setContentTitle(notificationTitle) + .setContentText( + getResources().getString(R.string.screenrecord_background_processing_label)) + .setSmallIcon(R.drawable.ic_screenrecord); + notificationManager.notify(NOTIFICATION_PROCESSING_ID, builder.build()); - ContentValues values = new ContentValues(); - values.put(MediaStore.Video.Media.DISPLAY_NAME, fileName); - values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4"); - values.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis()); - values.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis()); - - ContentResolver resolver = getContentResolver(); - Uri collectionUri = MediaStore.Video.Media.getContentUri( - MediaStore.VOLUME_EXTERNAL_PRIMARY); - Uri itemUri = resolver.insert(collectionUri, values); - - try { - // Add to the mediastore - OutputStream os = resolver.openOutputStream(itemUri, "w"); - Files.copy(mTempFile.toPath(), os); - os.close(); - - Notification notification = createSaveNotification(itemUri); - notificationManager.notify(NOTIFICATION_ID, notification); - - mTempFile.delete(); - } catch (IOException e) { - Log.e(TAG, "Error saving screen recording: " + e.getMessage()); - Toast.makeText(this, R.string.screenrecord_delete_error, Toast.LENGTH_LONG) - .show(); - } + mLongExecutor.execute(() -> { + try { + Log.d(TAG, "saving recording"); + Notification notification = createSaveNotification(mRecorder.save()); + if (!mController.isRecording()) { + Log.d(TAG, "showing saved notification"); + notificationManager.notify(NOTIFICATION_VIEW_ID, notification); + } + } catch (IOException e) { + Log.e(TAG, "Error saving screen recording: " + e.getMessage()); + Toast.makeText(this, R.string.screenrecord_delete_error, Toast.LENGTH_LONG) + .show(); + } finally { + notificationManager.cancel(NOTIFICATION_PROCESSING_ID); + } + }); } private void setTapsVisible(boolean turnOn) { diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java new file mode 100644 index 0000000000000..752f4fddf24bd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenrecord; + +import android.content.Context; +import android.media.AudioAttributes; +import android.media.AudioFormat; +import android.media.AudioPlaybackCaptureConfiguration; +import android.media.AudioRecord; +import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaFormat; +import android.media.MediaMuxer; +import android.media.MediaRecorder; +import android.media.projection.MediaProjection; +import android.util.Log; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Recording internal audio + */ +public class ScreenInternalAudioRecorder { + private static String TAG = "ScreenAudioRecorder"; + private static final int TIMEOUT = 500; + private final Context mContext; + private AudioRecord mAudioRecord; + private AudioRecord mAudioRecordMic; + private Config mConfig = new Config(); + private Thread mThread; + private MediaProjection mMediaProjection; + private MediaCodec mCodec; + private long mPresentationTime; + private long mTotalBytes; + private MediaMuxer mMuxer; + private String mOutFile; + private boolean mMic; + + private int mTrackId = -1; + + public ScreenInternalAudioRecorder(String outFile, Context context, + MediaProjection mp, boolean includeMicInput) throws IOException { + mMic = includeMicInput; + mOutFile = outFile; + mMuxer = new MediaMuxer(outFile, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); + mContext = context; + mMediaProjection = mp; + Log.d(TAG, "creating audio file " + outFile); + setupSimple(); + } + /** + * Audio recoding configuration + */ + public static class Config { + public int channelOutMask = AudioFormat.CHANNEL_OUT_MONO; + public int channelInMask = AudioFormat.CHANNEL_IN_MONO; + public int encoding = AudioFormat.ENCODING_PCM_16BIT; + public int sampleRate = 44100; + public int bitRate = 196000; + public int bufferSizeBytes = 1 << 17; + public boolean privileged = true; + public boolean legacy_app_looback = false; + + @Override + public String toString() { + return "channelMask=" + channelOutMask + + "\n encoding=" + encoding + + "\n sampleRate=" + sampleRate + + "\n bufferSize=" + bufferSizeBytes + + "\n privileged=" + privileged + + "\n legacy app looback=" + legacy_app_looback; + } + + } + + private void setupSimple() throws IOException { + int size = AudioRecord.getMinBufferSize( + mConfig.sampleRate, mConfig.channelInMask, + mConfig.encoding) * 2; + + Log.d(TAG, "audio buffer size: " + size); + + AudioFormat format = new AudioFormat.Builder() + .setEncoding(mConfig.encoding) + .setSampleRate(mConfig.sampleRate) + .setChannelMask(mConfig.channelOutMask) + .build(); + + AudioPlaybackCaptureConfiguration playbackConfig = + new AudioPlaybackCaptureConfiguration.Builder(mMediaProjection) + .addMatchingUsage(AudioAttributes.USAGE_MEDIA) + .addMatchingUsage(AudioAttributes.USAGE_UNKNOWN) + .addMatchingUsage(AudioAttributes.USAGE_GAME) + .build(); + + mAudioRecord = new AudioRecord.Builder() + .setAudioFormat(format) + .setAudioPlaybackCaptureConfig(playbackConfig) + .build(); + + if (mMic) { + mAudioRecordMic = new AudioRecord(MediaRecorder.AudioSource.VOICE_COMMUNICATION, + mConfig.sampleRate, AudioFormat.CHANNEL_IN_MONO, mConfig.encoding, size); + } + + mCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC); + MediaFormat medFormat = MediaFormat.createAudioFormat( + MediaFormat.MIMETYPE_AUDIO_AAC, mConfig.sampleRate, 1); + medFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, + MediaCodecInfo.CodecProfileLevel.AACObjectLC); + medFormat.setInteger(MediaFormat.KEY_BIT_RATE, mConfig.bitRate); + medFormat.setInteger(MediaFormat.KEY_PCM_ENCODING, mConfig.encoding); + mCodec.configure(medFormat, + null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + + mThread = new Thread(() -> { + short[] bufferInternal = null; + short[] bufferMic = null; + byte[] buffer = null; + + if (mMic) { + bufferInternal = new short[size / 2]; + bufferMic = new short[size / 2]; + } else { + buffer = new byte[size]; + } + + while (true) { + int readBytes = 0; + int readShortsInternal = 0; + int readShortsMic = 0; + if (mMic) { + readShortsInternal = mAudioRecord.read(bufferInternal, 0, + bufferInternal.length); + readShortsMic = mAudioRecordMic.read(bufferMic, 0, bufferMic.length); + readBytes = Math.min(readShortsInternal, readShortsMic) * 2; + buffer = addAndConvertBuffers(bufferInternal, readShortsInternal, bufferMic, + readShortsMic); + } else { + readBytes = mAudioRecord.read(buffer, 0, buffer.length); + } + + //exit the loop when at end of stream + if (readBytes < 0) { + Log.e(TAG, "read error " + readBytes + + ", shorts internal: " + readShortsInternal + + ", shorts mic: " + readShortsMic); + break; + } + encode(buffer, readBytes); + } + endStream(); + }); + } + + private byte[] addAndConvertBuffers(short[] a1, int a1Limit, short[] a2, int a2Limit) { + int size = Math.max(a1Limit, a2Limit); + if (size < 0) return new byte[0]; + byte[] buff = new byte[size * 2]; + for (int i = 0; i < size; i++) { + int sum; + if (i > a1Limit) { + sum = a2[i]; + } else if (i > a2Limit) { + sum = a1[i]; + } else { + sum = (int) a1[i] + (int) a2[i]; + } + + if (sum > Short.MAX_VALUE) sum = Short.MAX_VALUE; + if (sum < Short.MIN_VALUE) sum = Short.MIN_VALUE; + int byteIndex = i * 2; + buff[byteIndex] = (byte) (sum & 0xff); + buff[byteIndex + 1] = (byte) ((sum >> 8) & 0xff); + } + return buff; + } + + private void encode(byte[] buffer, int readBytes) { + int offset = 0; + while (readBytes > 0) { + int totalBytesRead = 0; + int bufferIndex = mCodec.dequeueInputBuffer(TIMEOUT); + if (bufferIndex < 0) { + writeOutput(); + return; + } + ByteBuffer buff = mCodec.getInputBuffer(bufferIndex); + buff.clear(); + int bufferSize = buff.capacity(); + int bytesToRead = readBytes > bufferSize ? bufferSize : readBytes; + totalBytesRead += bytesToRead; + readBytes -= bytesToRead; + buff.put(buffer, offset, bytesToRead); + offset += bytesToRead; + mCodec.queueInputBuffer(bufferIndex, 0, bytesToRead, mPresentationTime, 0); + mTotalBytes += totalBytesRead; + mPresentationTime = 1000000L * (mTotalBytes / 2) / mConfig.sampleRate; + + writeOutput(); + } + } + + private void endStream() { + int bufferIndex = mCodec.dequeueInputBuffer(TIMEOUT); + mCodec.queueInputBuffer(bufferIndex, 0, 0, mPresentationTime, + MediaCodec.BUFFER_FLAG_END_OF_STREAM); + writeOutput(); + } + + private void writeOutput() { + while (true) { + MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); + int bufferIndex = mCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT); + if (bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + mTrackId = mMuxer.addTrack(mCodec.getOutputFormat()); + mMuxer.start(); + continue; + } + if (bufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) { + break; + } + if (mTrackId < 0) return; + ByteBuffer buff = mCodec.getOutputBuffer(bufferIndex); + + if (!((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0 + && bufferInfo.size != 0)) { + mMuxer.writeSampleData(mTrackId, buff, bufferInfo); + } + mCodec.releaseOutputBuffer(bufferIndex, false); + } + } + + /** + * start recording + */ + public void start() { + if (mThread != null) { + Log.e(TAG, "a recording is being done in parallel or stop is not called"); + } + mAudioRecord.startRecording(); + if (mMic) mAudioRecordMic.startRecording(); + Log.d(TAG, "channel count " + mAudioRecord.getChannelCount()); + mCodec.start(); + if (mAudioRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) { + Log.e(TAG, "Error starting audio recording"); + return; + } + mThread.start(); + } + + /** + * end recording + */ + public void end() { + mAudioRecord.stop(); + if (mMic) { + mAudioRecordMic.stop(); + } + mAudioRecord.release(); + if (mMic) { + mAudioRecordMic.release(); + } + try { + mThread.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + mCodec.stop(); + mCodec.release(); + mMuxer.stop(); + mMuxer.release(); + mThread = null; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java new file mode 100644 index 0000000000000..c967648c544eb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenrecord; + +import static android.content.Context.MEDIA_PROJECTION_SERVICE; + +import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.INTERNAL; +import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC; +import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC_AND_INTERNAL; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.hardware.display.VirtualDisplay; +import android.media.MediaMuxer; +import android.media.MediaRecorder; +import android.media.projection.IMediaProjection; +import android.media.projection.IMediaProjectionManager; +import android.media.projection.MediaProjection; +import android.media.projection.MediaProjectionManager; +import android.net.Uri; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.provider.MediaStore; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Surface; +import android.view.WindowManager; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Recording screen and mic/internal audio + */ +public class ScreenMediaRecorder { + private static final int TOTAL_NUM_TRACKS = 1; + private static final int VIDEO_BIT_RATE = 10000000; + private static final int VIDEO_FRAME_RATE = 30; + private static final int AUDIO_BIT_RATE = 16; + private static final int AUDIO_SAMPLE_RATE = 44100; + private static final int MAX_DURATION_MS = 60 * 60 * 1000; + private static final long MAX_FILESIZE_BYTES = 5000000000L; + private static final String TAG = "ScreenMediaRecorder"; + + + private File mTempVideoFile; + private File mTempAudioFile; + private MediaProjection mMediaProjection; + private Surface mInputSurface; + private VirtualDisplay mVirtualDisplay; + private MediaRecorder mMediaRecorder; + private int mUser; + private ScreenRecordingMuxer mMuxer; + private ScreenInternalAudioRecorder mAudio; + private ScreenRecordingAudioSource mAudioSource; + + private Context mContext; + MediaRecorder.OnInfoListener mListener; + + public ScreenMediaRecorder(Context context, + int user, ScreenRecordingAudioSource audioSource, + MediaRecorder.OnInfoListener listener) { + mContext = context; + mUser = user; + mListener = listener; + mAudioSource = audioSource; + } + + private void prepare() throws IOException, RemoteException { + //Setup media projection + IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE); + IMediaProjectionManager mediaService = + IMediaProjectionManager.Stub.asInterface(b); + IMediaProjection proj = null; + proj = mediaService.createProjection(mUser, mContext.getPackageName(), + MediaProjectionManager.TYPE_SCREEN_CAPTURE, false); + IBinder projection = proj.asBinder(); + mMediaProjection = new MediaProjection(mContext, + IMediaProjection.Stub.asInterface(projection)); + + File cacheDir = mContext.getCacheDir(); + cacheDir.mkdirs(); + mTempVideoFile = File.createTempFile("temp", ".mp4", cacheDir); + + // Set up media recorder + mMediaRecorder = new MediaRecorder(); + + // Set up audio source + if (mAudioSource == MIC) { + mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); + } + mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); + + mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); + + + // Set up video + DisplayMetrics metrics = new DisplayMetrics(); + WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + wm.getDefaultDisplay().getRealMetrics(metrics); + int screenWidth = metrics.widthPixels; + int screenHeight = metrics.heightPixels; + mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); + mMediaRecorder.setVideoSize(screenWidth, screenHeight); + mMediaRecorder.setVideoFrameRate(VIDEO_FRAME_RATE); + mMediaRecorder.setVideoEncodingBitRate(VIDEO_BIT_RATE); + mMediaRecorder.setMaxDuration(MAX_DURATION_MS); + mMediaRecorder.setMaxFileSize(MAX_FILESIZE_BYTES); + + // Set up audio + if (mAudioSource == MIC) { + mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.HE_AAC); + mMediaRecorder.setAudioChannels(TOTAL_NUM_TRACKS); + mMediaRecorder.setAudioEncodingBitRate(AUDIO_BIT_RATE); + mMediaRecorder.setAudioSamplingRate(AUDIO_SAMPLE_RATE); + } + + mMediaRecorder.setOutputFile(mTempVideoFile); + mMediaRecorder.prepare(); + // Create surface + mInputSurface = mMediaRecorder.getSurface(); + mVirtualDisplay = mMediaProjection.createVirtualDisplay( + "Recording Display", + screenWidth, + screenHeight, + metrics.densityDpi, + DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, + mInputSurface, + null, + null); + + mMediaRecorder.setOnInfoListener(mListener); + if (mAudioSource == INTERNAL || + mAudioSource == MIC_AND_INTERNAL) { + mTempAudioFile = File.createTempFile("temp", ".aac", + mContext.getCacheDir()); + mAudio = new ScreenInternalAudioRecorder(mTempAudioFile.getAbsolutePath(), mContext, + mMediaProjection, mAudioSource == MIC_AND_INTERNAL); + } + + } + + /** + * Start screen recording + */ + void start() throws IOException, RemoteException { + Log.d(TAG, "start recording"); + prepare(); + mMediaRecorder.start(); + recordInternalAudio(); + } + + /** + * End screen recording + */ + void end() { + mMediaRecorder.stop(); + mMediaProjection.stop(); + mMediaRecorder.release(); + mMediaRecorder = null; + mMediaProjection = null; + mInputSurface.release(); + mVirtualDisplay.release(); + stopInternalAudioRecording(); + + Log.d(TAG, "end recording"); + } + + private void stopInternalAudioRecording() { + if (mAudioSource == INTERNAL || mAudioSource == MIC_AND_INTERNAL) { + mAudio.end(); + mAudio = null; + } + } + + private void recordInternalAudio() { + if (mAudioSource == INTERNAL || mAudioSource == MIC_AND_INTERNAL) { + mAudio.start(); + } + } + + /** + * Store recorded video + */ + Uri save() throws IOException { + String fileName = new SimpleDateFormat("'screen-'yyyyMMdd-HHmmss'.mp4'") + .format(new Date()); + + ContentValues values = new ContentValues(); + values.put(MediaStore.Video.Media.DISPLAY_NAME, fileName); + values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4"); + values.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis()); + values.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis()); + + ContentResolver resolver = mContext.getContentResolver(); + Uri collectionUri = MediaStore.Video.Media.getContentUri( + MediaStore.VOLUME_EXTERNAL_PRIMARY); + Uri itemUri = resolver.insert(collectionUri, values); + + Log.d(TAG, itemUri.toString()); + if (mAudioSource == MIC_AND_INTERNAL || mAudioSource == INTERNAL) { + try { + Log.d(TAG, "muxing recording"); + File file = File.createTempFile("temp", ".mp4", + mContext.getCacheDir()); + mMuxer = new ScreenRecordingMuxer(MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4, + file.getAbsolutePath(), + mTempVideoFile.getAbsolutePath(), + mTempAudioFile.getAbsolutePath()); + mMuxer.mux(); + mTempVideoFile.delete(); + mTempVideoFile = file; + } catch (IOException e) { + Log.e(TAG, "muxing recording " + e.getMessage()); + e.printStackTrace(); + } + } + + // Add to the mediastore + OutputStream os = resolver.openOutputStream(itemUri, "w"); + Files.copy(mTempVideoFile.toPath(), os); + os.close(); + mTempVideoFile.delete(); + if (mTempAudioFile != null) mTempAudioFile.delete(); + return itemUri; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java index 26973d092209c..c247328078a7a 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java @@ -16,17 +16,30 @@ package com.android.systemui.screenrecord; +import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.INTERNAL; +import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC; +import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC_AND_INTERNAL; +import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.NONE; + import android.app.Activity; import android.app.PendingIntent; import android.os.Bundle; +import android.util.Log; import android.view.Gravity; +import android.view.View; import android.view.ViewGroup; import android.view.Window; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; import android.widget.Button; +import android.widget.Spinner; import android.widget.Switch; import com.android.systemui.R; +import java.util.ArrayList; +import java.util.List; + import javax.inject.Inject; /** @@ -35,10 +48,15 @@ import javax.inject.Inject; public class ScreenRecordDialog extends Activity { private static final long DELAY_MS = 3000; private static final long INTERVAL_MS = 1000; + private static final String TAG = "ScreenRecordDialog"; private final RecordingController mController; - private Switch mAudioSwitch; private Switch mTapsSwitch; + private Switch mAudioSwitch; + private Spinner mOptions; + private List mModes; + private int mSelected; + @Inject public ScreenRecordDialog(RecordingController controller) { @@ -68,17 +86,32 @@ public class ScreenRecordDialog extends Activity { finish(); }); + mModes = new ArrayList<>(); + mModes.add(INTERNAL); + mModes.add(MIC); + mModes.add(MIC_AND_INTERNAL); + mAudioSwitch = findViewById(R.id.screenrecord_audio_switch); mTapsSwitch = findViewById(R.id.screenrecord_taps_switch); + mOptions = findViewById(R.id.screen_recording_options); + ArrayAdapter a = new ScreenRecordingAdapter(getApplicationContext(), + android.R.layout.simple_spinner_dropdown_item, + mModes); + a.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mOptions.setAdapter(a); + } private void requestScreenCapture() { - boolean useAudio = mAudioSwitch.isChecked(); boolean showTaps = mTapsSwitch.isChecked(); + ScreenRecordingAudioSource audioMode = mAudioSwitch.isChecked() + ? (ScreenRecordingAudioSource) mOptions.getSelectedItem() + : NONE; PendingIntent startIntent = PendingIntent.getForegroundService(this, RecordingService.REQUEST_CODE, RecordingService.getStartIntent( - ScreenRecordDialog.this, RESULT_OK, null, useAudio, showTaps), + ScreenRecordDialog.this, RESULT_OK, + audioMode.ordinal(), showTaps), PendingIntent.FLAG_UPDATE_CURRENT); PendingIntent stopIntent = PendingIntent.getService(this, RecordingService.REQUEST_CODE, diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingAdapter.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingAdapter.java new file mode 100644 index 0000000000000..2e0e746594b4d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingAdapter.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenrecord; + +import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.INTERNAL; +import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC; +import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC_AND_INTERNAL; + +import android.content.Context; +import android.content.res.Resources; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.systemui.R; + +import java.util.List; + +/** + * Screen recording view adapter + */ +public class ScreenRecordingAdapter extends ArrayAdapter { + private LinearLayout mSelectedMic; + private LinearLayout mSelectedInternal; + private LinearLayout mSelectedMicAndInternal; + private LinearLayout mMicOption; + private LinearLayout mMicAndInternalOption; + private LinearLayout mInternalOption; + + public ScreenRecordingAdapter(Context context, int resource, + List objects) { + super(context, resource, objects); + initViews(); + } + + private void initViews() { + mSelectedInternal = getSelected(R.string.screenrecord_device_audio_label); + mSelectedMic = getSelected(R.string.screenrecord_mic_label); + mSelectedMicAndInternal = getSelected(R.string.screenrecord_device_audio_and_mic_label); + + mMicOption = getOption(R.string.screenrecord_mic_label, Resources.ID_NULL); + mMicOption.removeViewAt(1); + + mMicAndInternalOption = getOption( + R.string.screenrecord_device_audio_and_mic_label, Resources.ID_NULL); + mMicAndInternalOption.removeViewAt(1); + + mInternalOption = getOption(R.string.screenrecord_device_audio_label, + R.string.screenrecord_device_audio_description); + } + + private LinearLayout getOption(int label, int description) { + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + LinearLayout layout = (LinearLayout) inflater + .inflate(R.layout.screen_record_dialog_audio_source, null, false); + ((TextView) layout.findViewById(R.id.screen_recording_dialog_source_text)) + .setText(label); + if (description != Resources.ID_NULL) + ((TextView) layout.findViewById(R.id.screen_recording_dialog_source_description)) + .setText(description); + return layout; + } + + private LinearLayout getSelected(int label) { + LayoutInflater inflater = LayoutInflater.from(getContext()); + LinearLayout layout = (LinearLayout) inflater + .inflate(R.layout.screen_record_dialog_audio_source_selected, null, false); + ((TextView) layout.findViewById(R.id.screen_recording_dialog_source_text)) + .setText(label); + return layout; + } + + private void setDescription(LinearLayout layout, int description) { + if (description != Resources.ID_NULL) { + ((TextView) layout.getChildAt(1)).setText(description); + } + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + switch (getItem(position)) { + case INTERNAL: + return mInternalOption; + case MIC_AND_INTERNAL: + return mMicAndInternalOption; + case MIC: + return mMicOption; + default: + return super.getDropDownView(position, convertView, parent); + } + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + switch (getItem(position)) { + case INTERNAL: + return mSelectedInternal; + case MIC_AND_INTERNAL: + return mSelectedMicAndInternal; + case MIC: + return mSelectedMic; + default: + return super.getView(position, convertView, parent); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingAudioSource.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingAudioSource.java new file mode 100644 index 0000000000000..ee11865a03a16 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingAudioSource.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenrecord; + +/** + * Audio sources + */ +public enum ScreenRecordingAudioSource { + NONE, + INTERNAL, + MIC, + MIC_AND_INTERNAL; +} diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingMuxer.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingMuxer.java new file mode 100644 index 0000000000000..7ffcfd46a1a48 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingMuxer.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenrecord; + +import android.media.MediaCodec; +import android.media.MediaExtractor; +import android.media.MediaMuxer; +import android.util.ArrayMap; +import android.util.Log; +import android.util.Pair; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; + +/** + * Mixing audio and video tracks + */ +public class ScreenRecordingMuxer { + // size of a memory page for cache coherency + private static final int BUFFER_SIZE = 1024 * 4096; + private String[] mFiles; + private String mOutFile; + private int mFormat; + private ArrayMap, Integer> mExtractorIndexToMuxerIndex + = new ArrayMap<>(); + private ArrayList mExtractors = new ArrayList<>(); + + private static String TAG = "ScreenRecordingMuxer"; + public ScreenRecordingMuxer(@MediaMuxer.Format int format, String outfileName, + String... inputFileNames) { + mFiles = inputFileNames; + mOutFile = outfileName; + mFormat = format; + Log.d(TAG, "out: " + mOutFile + " , in: " + mFiles[0]); + } + + /** + * RUN IN THE BACKGROUND THREAD! + */ + public void mux() throws IOException { + MediaMuxer muxer = null; + muxer = new MediaMuxer(mOutFile, mFormat); + // Add extractors + for (String file: mFiles) { + MediaExtractor extractor = new MediaExtractor(); + try { + extractor.setDataSource(file); + } catch (IOException e) { + Log.e(TAG, "error creating extractor: " + file); + e.printStackTrace(); + continue; + } + Log.d(TAG, file + " track count: " + extractor.getTrackCount()); + mExtractors.add(extractor); + for (int i = 0; i < extractor.getTrackCount(); i++) { + int muxId = muxer.addTrack(extractor.getTrackFormat(i)); + Log.d(TAG, "created extractor format" + extractor.getTrackFormat(i).toString()); + mExtractorIndexToMuxerIndex.put(Pair.create(extractor, i), muxId); + } + } + + muxer.start(); + for (Pair pair: mExtractorIndexToMuxerIndex.keySet()) { + MediaExtractor extractor = pair.first; + extractor.selectTrack(pair.second); + int muxId = mExtractorIndexToMuxerIndex.get(pair); + Log.d(TAG, "track format: " + extractor.getTrackFormat(pair.second)); + extractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC); + ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); + MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); + int offset; + while (true) { + offset = buffer.arrayOffset(); + info.size = extractor.readSampleData(buffer, offset); + if (info.size < 0) break; + info.presentationTimeUs = extractor.getSampleTime(); + info.flags = extractor.getSampleFlags(); + muxer.writeSampleData(muxId, buffer, info); + extractor.advance(); + } + } + + for (MediaExtractor extractor: mExtractors) { + extractor.release(); + } + muxer.stop(); + muxer.release(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java index 8acfbf2b69961..7729965b56c4e 100644 --- a/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java @@ -23,6 +23,7 @@ import android.os.Looper; import android.os.Process; import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.LongRunning; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.UiBackground; @@ -50,6 +51,17 @@ public abstract class ConcurrencyModule { return thread.getLooper(); } + /** Long running tasks Looper */ + @Provides + @Singleton + @LongRunning + public static Looper provideLongRunningLooper() { + HandlerThread thread = new HandlerThread("SysUiLng", + Process.THREAD_PRIORITY_BACKGROUND); + thread.start(); + return thread.getLooper(); + } + /** Main Looper */ @Provides @Main @@ -88,6 +100,16 @@ public abstract class ConcurrencyModule { return new ExecutorImpl(looper); } + /** + * Provide a Long running Executor by default. + */ + @Provides + @Singleton + @LongRunning + public static Executor provideLongRunningExecutor(@LongRunning Looper looper) { + return new ExecutorImpl(looper); + } + /** * Provide a Background-Thread Executor. */