Merge "Screen recording sound options" into rvc-dev

This commit is contained in:
Jay Aliomer
2020-05-07 23:20:35 +00:00
committed by Android (Google) Code Review
15 changed files with 1045 additions and 190 deletions

View File

@@ -59,29 +59,16 @@
android:layout_gravity="center"
android:layout_weight="0"
android:layout_marginRight="@dimen/screenrecord_dialog_padding"/>
<LinearLayout
<Spinner
android:id="@+id/screen_recording_options"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_weight="1">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:text="@string/screenrecord_audio_label"
android:textColor="?android:attr/textColorPrimary"
android:textAppearance="?android:attr/textAppearanceMedium"/>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/audio_type"
android:text="@string/screenrecord_mic_label"
android:textAppearance="?android:attr/textAppearanceSmall"/>
</LinearLayout>
android:layout_height="48dp"
android:prompt="@string/screenrecord_audio_label"/>
<Switch
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_weight="0"
android:layout_weight="1"
android:layout_gravity="end"
android:id="@+id/screenrecord_audio_switch"/>
</LinearLayout>
@@ -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"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="vertical"
android:padding="10dp"
android:layout_weight="1">
<TextView
android:id="@+id/screen_recording_dialog_source_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary"/>
<TextView
android:id="@+id/screen_recording_dialog_source_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"/>
</LinearLayout>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="vertical"
android:layout_weight="1">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/screenrecord_audio_label"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary"/>
<TextView
android:id="@+id/screen_recording_dialog_source_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textColor="?android:attr/textColorSecondary"
android:textAppearance="?android:attr/textAppearanceSmall"/>
</LinearLayout>

View File

@@ -144,5 +144,10 @@
<!-- NotificationPanelView -->
<item type="id" name="notification_panel" />
<!-- Screen Recording -->
<item type="id" name="screen_recording_options" />
<item type="id" name="screen_recording_dialog_source_text" />
<item type="id" name="screen_recording_dialog_source_description" />
</resources>

View File

@@ -240,6 +240,8 @@
<!-- Notification title displayed for screen recording [CHAR LIMIT=50]-->
<string name="screenrecord_name">Screen Recorder</string>
<!-- Processing screen recoding video in the background [CHAR LIMIT=30]-->
<string name="screenrecord_background_processing_label">Processing screen recording</string>
<!-- Description of the screen recording notification channel [CHAR LIMIT=NONE]-->
<string name="screenrecord_channel_description">Ongoing notification for a screen record session</string>
<!-- Title for the screen prompting the user to begin recording their screen [CHAR LIMIT=NONE]-->

View File

@@ -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 {
}

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<ScreenRecordingAudioSource> 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,

View File

@@ -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<ScreenRecordingAudioSource> {
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<ScreenRecordingAudioSource> 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);
}
}
}

View File

@@ -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;
}

View File

@@ -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<Pair<MediaExtractor, Integer>, Integer> mExtractorIndexToMuxerIndex
= new ArrayMap<>();
private ArrayList<MediaExtractor> 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<MediaExtractor, Integer> 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();
}
}

View File

@@ -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.
*/