Merge "Screen recording sound options" into rvc-dev
This commit is contained in:
@@ -59,29 +59,16 @@
|
|||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:layout_weight="0"
|
android:layout_weight="0"
|
||||||
android:layout_marginRight="@dimen/screenrecord_dialog_padding"/>
|
android:layout_marginRight="@dimen/screenrecord_dialog_padding"/>
|
||||||
<LinearLayout
|
<Spinner
|
||||||
|
android:id="@+id/screen_recording_options"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="48dp"
|
||||||
android:orientation="vertical"
|
android:prompt="@string/screenrecord_audio_label"/>
|
||||||
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>
|
|
||||||
<Switch
|
<Switch
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="48dp"
|
android:layout_height="48dp"
|
||||||
android:layout_weight="0"
|
android:layout_weight="1"
|
||||||
|
android:layout_gravity="end"
|
||||||
android:id="@+id/screenrecord_audio_switch"/>
|
android:id="@+id/screenrecord_audio_switch"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
@@ -102,7 +89,8 @@
|
|||||||
android:id="@+id/screenrecord_taps_switch"
|
android:id="@+id/screenrecord_taps_switch"
|
||||||
android:text="@string/screenrecord_taps_label"
|
android:text="@string/screenrecord_taps_label"
|
||||||
android:textColor="?android:attr/textColorPrimary"
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
android:textAppearance="?android:attr/textAppearanceMedium"/>
|
android:textAppearance="?android:attr/textAppearanceSmall"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -144,5 +144,10 @@
|
|||||||
|
|
||||||
<!-- NotificationPanelView -->
|
<!-- NotificationPanelView -->
|
||||||
<item type="id" name="notification_panel" />
|
<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>
|
</resources>
|
||||||
|
|
||||||
|
|||||||
@@ -240,6 +240,8 @@
|
|||||||
|
|
||||||
<!-- Notification title displayed for screen recording [CHAR LIMIT=50]-->
|
<!-- Notification title displayed for screen recording [CHAR LIMIT=50]-->
|
||||||
<string name="screenrecord_name">Screen Recorder</string>
|
<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]-->
|
<!-- Description of the screen recording notification channel [CHAR LIMIT=NONE]-->
|
||||||
<string name="screenrecord_channel_description">Ongoing notification for a screen record session</string>
|
<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]-->
|
<!-- Title for the screen prompting the user to begin recording their screen [CHAR LIMIT=NONE]-->
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
}
|
||||||
@@ -137,7 +137,7 @@ public class RecordingController
|
|||||||
* Check if the recording is ongoing
|
* Check if the recording is ongoing
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public boolean isRecording() {
|
public synchronized boolean isRecording() {
|
||||||
return mIsRecording;
|
return mIsRecording;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ public class RecordingController
|
|||||||
* Update the current status
|
* Update the current status
|
||||||
* @param isRecording
|
* @param isRecording
|
||||||
*/
|
*/
|
||||||
public void updateState(boolean isRecording) {
|
public synchronized void updateState(boolean isRecording) {
|
||||||
mIsRecording = isRecording;
|
mIsRecording = isRecording;
|
||||||
for (RecordingStateChangeCallback cb : mListeners) {
|
for (RecordingStateChangeCallback cb : mListeners) {
|
||||||
if (isRecording) {
|
if (isRecording) {
|
||||||
|
|||||||
@@ -22,41 +22,27 @@ import android.app.NotificationManager;
|
|||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.drawable.Icon;
|
import android.graphics.drawable.Icon;
|
||||||
import android.hardware.display.DisplayManager;
|
|
||||||
import android.hardware.display.VirtualDisplay;
|
|
||||||
import android.media.MediaRecorder;
|
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.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.os.ServiceManager;
|
|
||||||
import android.provider.MediaStore;
|
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Size;
|
import android.util.Size;
|
||||||
import android.view.Surface;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.android.systemui.R;
|
import com.android.systemui.R;
|
||||||
|
import com.android.systemui.dagger.qualifiers.LongRunning;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.util.concurrent.Executor;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
@@ -66,13 +52,15 @@ import javax.inject.Inject;
|
|||||||
public class RecordingService extends Service implements MediaRecorder.OnInfoListener {
|
public class RecordingService extends Service implements MediaRecorder.OnInfoListener {
|
||||||
public static final int REQUEST_CODE = 2;
|
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 TAG = "RecordingService";
|
||||||
private static final String CHANNEL_ID = "screen_record";
|
private static final String CHANNEL_ID = "screen_record";
|
||||||
private static final String EXTRA_RESULT_CODE = "extra_resultCode";
|
private static final String EXTRA_RESULT_CODE = "extra_resultCode";
|
||||||
private static final String EXTRA_DATA = "extra_data";
|
private static final String EXTRA_DATA = "extra_data";
|
||||||
private static final String EXTRA_PATH = "extra_path";
|
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 EXTRA_SHOW_TAPS = "extra_showTaps";
|
||||||
|
|
||||||
private static final String ACTION_START = "com.android.systemui.screenrecord.START";
|
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_SHARE = "com.android.systemui.screenrecord.SHARE";
|
||||||
private static final String ACTION_DELETE = "com.android.systemui.screenrecord.DELETE";
|
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 final RecordingController mController;
|
||||||
private MediaProjection mMediaProjection;
|
|
||||||
private Surface mInputSurface;
|
|
||||||
private VirtualDisplay mVirtualDisplay;
|
|
||||||
private MediaRecorder mMediaRecorder;
|
|
||||||
private Notification.Builder mRecordingNotificationBuilder;
|
private Notification.Builder mRecordingNotificationBuilder;
|
||||||
|
|
||||||
private boolean mUseAudio;
|
private ScreenRecordingAudioSource mAudioSource;
|
||||||
private boolean mShowTaps;
|
private boolean mShowTaps;
|
||||||
private boolean mOriginalShowTaps;
|
private boolean mOriginalShowTaps;
|
||||||
private File mTempFile;
|
private ScreenMediaRecorder mRecorder;
|
||||||
|
private final Executor mLongExecutor;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public RecordingService(RecordingController controller) {
|
public RecordingService(RecordingController controller, @LongRunning Executor executor) {
|
||||||
mController = controller;
|
mController = controller;
|
||||||
|
mLongExecutor = executor;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -113,16 +91,16 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
|
|||||||
* android.content.Intent)}
|
* android.content.Intent)}
|
||||||
* @param data The data from {@link android.app.Activity#onActivityResult(int, int,
|
* @param data The data from {@link android.app.Activity#onActivityResult(int, int,
|
||||||
* android.content.Intent)}
|
* 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
|
* @param showTaps True to make touches visible while recording
|
||||||
*/
|
*/
|
||||||
public static Intent getStartIntent(Context context, int resultCode, Intent data,
|
public static Intent getStartIntent(Context context, int resultCode,
|
||||||
boolean useAudio, boolean showTaps) {
|
int audioSource, boolean showTaps) {
|
||||||
return new Intent(context, RecordingService.class)
|
return new Intent(context, RecordingService.class)
|
||||||
.setAction(ACTION_START)
|
.setAction(ACTION_START)
|
||||||
.putExtra(EXTRA_RESULT_CODE, resultCode)
|
.putExtra(EXTRA_RESULT_CODE, resultCode)
|
||||||
.putExtra(EXTRA_DATA, data)
|
.putExtra(EXTRA_AUDIO_SOURCE, audioSource)
|
||||||
.putExtra(EXTRA_USE_AUDIO, useAudio)
|
|
||||||
.putExtra(EXTRA_SHOW_TAPS, showTaps);
|
.putExtra(EXTRA_SHOW_TAPS, showTaps);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,36 +117,31 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
|
|||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case ACTION_START:
|
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);
|
mShowTaps = intent.getBooleanExtra(EXTRA_SHOW_TAPS, false);
|
||||||
try {
|
|
||||||
IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);
|
mOriginalShowTaps = Settings.System.getInt(
|
||||||
IMediaProjectionManager mediaService =
|
getApplicationContext().getContentResolver(),
|
||||||
IMediaProjectionManager.Stub.asInterface(b);
|
Settings.System.SHOW_TOUCHES, 0) != 0;
|
||||||
IMediaProjection proj = mediaService.createProjection(getUserId(),
|
|
||||||
getPackageName(),
|
setTapsVisible(mShowTaps);
|
||||||
MediaProjectionManager.TYPE_SCREEN_CAPTURE, false);
|
|
||||||
IBinder projection = proj.asBinder();
|
mRecorder = new ScreenMediaRecorder(
|
||||||
if (projection == null) {
|
getApplicationContext(),
|
||||||
Log.e(TAG, "Projection was null");
|
getUserId(),
|
||||||
Toast.makeText(this, R.string.screenrecord_start_error, Toast.LENGTH_LONG)
|
mAudioSource,
|
||||||
.show();
|
this
|
||||||
return Service.START_NOT_STICKY;
|
);
|
||||||
}
|
startRecording();
|
||||||
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;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ACTION_STOP:
|
case ACTION_STOP:
|
||||||
stopRecording();
|
stopRecording();
|
||||||
|
notificationManager.cancel(NOTIFICATION_RECORDING_ID);
|
||||||
saveRecording(notificationManager);
|
saveRecording(notificationManager);
|
||||||
|
stopSelf();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ACTION_SHARE:
|
case ACTION_SHARE:
|
||||||
@@ -183,10 +156,10 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
|
|||||||
sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
|
sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
|
||||||
|
|
||||||
// Remove notification
|
// Remove notification
|
||||||
notificationManager.cancel(NOTIFICATION_ID);
|
notificationManager.cancel(NOTIFICATION_RECORDING_ID);
|
||||||
|
|
||||||
startActivity(Intent.createChooser(shareIntent, shareLabel)
|
startActivity(Intent.createChooser(shareIntent, shareLabel)
|
||||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
|
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
|
||||||
break;
|
break;
|
||||||
case ACTION_DELETE:
|
case ACTION_DELETE:
|
||||||
// Close quick shade
|
// Close quick shade
|
||||||
@@ -202,7 +175,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
|
|||||||
Toast.LENGTH_LONG).show();
|
Toast.LENGTH_LONG).show();
|
||||||
|
|
||||||
// Remove notification
|
// Remove notification
|
||||||
notificationManager.cancel(NOTIFICATION_ID);
|
notificationManager.cancel(NOTIFICATION_RECORDING_ID);
|
||||||
Log.d(TAG, "Deleted recording " + uri);
|
Log.d(TAG, "Deleted recording " + uri);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -224,70 +197,15 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
|
|||||||
*/
|
*/
|
||||||
private void startRecording() {
|
private void startRecording() {
|
||||||
try {
|
try {
|
||||||
File cacheDir = getCacheDir();
|
mRecorder.start();
|
||||||
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();
|
|
||||||
mController.updateState(true);
|
mController.updateState(true);
|
||||||
} catch (IOException e) {
|
createRecordingNotification();
|
||||||
Log.e(TAG, "Error starting screen recording: " + e.getMessage());
|
} catch (IOException | RemoteException e) {
|
||||||
|
Toast.makeText(this,
|
||||||
|
R.string.screenrecord_start_error, Toast.LENGTH_LONG)
|
||||||
|
.show();
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createRecordingNotification();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createRecordingNotification() {
|
private void createRecordingNotification() {
|
||||||
@@ -306,7 +224,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
|
|||||||
extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
|
extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
|
||||||
res.getString(R.string.screenrecord_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_and_audio)
|
||||||
: res.getString(R.string.screenrecord_ongoing_screen_only);
|
: 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),
|
this, REQUEST_CODE, getStopIntent(this),
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT))
|
PendingIntent.FLAG_UPDATE_CURRENT))
|
||||||
.addExtras(extras);
|
.addExtras(extras);
|
||||||
notificationManager.notify(NOTIFICATION_ID, mRecordingNotificationBuilder.build());
|
notificationManager.notify(NOTIFICATION_RECORDING_ID,
|
||||||
|
mRecordingNotificationBuilder.build());
|
||||||
Notification notification = mRecordingNotificationBuilder.build();
|
Notification notification = mRecordingNotificationBuilder.build();
|
||||||
startForeground(NOTIFICATION_ID, notification);
|
startForeground(NOTIFICATION_RECORDING_ID, notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Notification createSaveNotification(Uri uri) {
|
private Notification createSaveNotification(Uri uri) {
|
||||||
@@ -392,47 +311,38 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
|
|||||||
|
|
||||||
private void stopRecording() {
|
private void stopRecording() {
|
||||||
setTapsVisible(mOriginalShowTaps);
|
setTapsVisible(mOriginalShowTaps);
|
||||||
mMediaRecorder.stop();
|
mRecorder.end();
|
||||||
mMediaRecorder.release();
|
|
||||||
mMediaRecorder = null;
|
|
||||||
mMediaProjection.stop();
|
|
||||||
mMediaProjection = null;
|
|
||||||
mInputSurface.release();
|
|
||||||
mVirtualDisplay.release();
|
|
||||||
stopSelf();
|
|
||||||
mController.updateState(false);
|
mController.updateState(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveRecording(NotificationManager notificationManager) {
|
private void saveRecording(NotificationManager notificationManager) {
|
||||||
String fileName = new SimpleDateFormat("'screen-'yyyyMMdd-HHmmss'.mp4'")
|
Resources res = getApplicationContext().getResources();
|
||||||
.format(new Date());
|
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();
|
mLongExecutor.execute(() -> {
|
||||||
values.put(MediaStore.Video.Media.DISPLAY_NAME, fileName);
|
try {
|
||||||
values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
|
Log.d(TAG, "saving recording");
|
||||||
values.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis());
|
Notification notification = createSaveNotification(mRecorder.save());
|
||||||
values.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis());
|
if (!mController.isRecording()) {
|
||||||
|
Log.d(TAG, "showing saved notification");
|
||||||
ContentResolver resolver = getContentResolver();
|
notificationManager.notify(NOTIFICATION_VIEW_ID, notification);
|
||||||
Uri collectionUri = MediaStore.Video.Media.getContentUri(
|
}
|
||||||
MediaStore.VOLUME_EXTERNAL_PRIMARY);
|
} catch (IOException e) {
|
||||||
Uri itemUri = resolver.insert(collectionUri, values);
|
Log.e(TAG, "Error saving screen recording: " + e.getMessage());
|
||||||
|
Toast.makeText(this, R.string.screenrecord_delete_error, Toast.LENGTH_LONG)
|
||||||
try {
|
.show();
|
||||||
// Add to the mediastore
|
} finally {
|
||||||
OutputStream os = resolver.openOutputStream(itemUri, "w");
|
notificationManager.cancel(NOTIFICATION_PROCESSING_ID);
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setTapsVisible(boolean turnOn) {
|
private void setTapsVisible(boolean turnOn) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,17 +16,30 @@
|
|||||||
|
|
||||||
package com.android.systemui.screenrecord;
|
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.Activity;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
|
import android.widget.AdapterView;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
|
import android.widget.Spinner;
|
||||||
import android.widget.Switch;
|
import android.widget.Switch;
|
||||||
|
|
||||||
import com.android.systemui.R;
|
import com.android.systemui.R;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,10 +48,15 @@ import javax.inject.Inject;
|
|||||||
public class ScreenRecordDialog extends Activity {
|
public class ScreenRecordDialog extends Activity {
|
||||||
private static final long DELAY_MS = 3000;
|
private static final long DELAY_MS = 3000;
|
||||||
private static final long INTERVAL_MS = 1000;
|
private static final long INTERVAL_MS = 1000;
|
||||||
|
private static final String TAG = "ScreenRecordDialog";
|
||||||
|
|
||||||
private final RecordingController mController;
|
private final RecordingController mController;
|
||||||
private Switch mAudioSwitch;
|
|
||||||
private Switch mTapsSwitch;
|
private Switch mTapsSwitch;
|
||||||
|
private Switch mAudioSwitch;
|
||||||
|
private Spinner mOptions;
|
||||||
|
private List<ScreenRecordingAudioSource> mModes;
|
||||||
|
private int mSelected;
|
||||||
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public ScreenRecordDialog(RecordingController controller) {
|
public ScreenRecordDialog(RecordingController controller) {
|
||||||
@@ -68,17 +86,32 @@ public class ScreenRecordDialog extends Activity {
|
|||||||
finish();
|
finish();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mModes = new ArrayList<>();
|
||||||
|
mModes.add(INTERNAL);
|
||||||
|
mModes.add(MIC);
|
||||||
|
mModes.add(MIC_AND_INTERNAL);
|
||||||
|
|
||||||
mAudioSwitch = findViewById(R.id.screenrecord_audio_switch);
|
mAudioSwitch = findViewById(R.id.screenrecord_audio_switch);
|
||||||
mTapsSwitch = findViewById(R.id.screenrecord_taps_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() {
|
private void requestScreenCapture() {
|
||||||
boolean useAudio = mAudioSwitch.isChecked();
|
|
||||||
boolean showTaps = mTapsSwitch.isChecked();
|
boolean showTaps = mTapsSwitch.isChecked();
|
||||||
|
ScreenRecordingAudioSource audioMode = mAudioSwitch.isChecked()
|
||||||
|
? (ScreenRecordingAudioSource) mOptions.getSelectedItem()
|
||||||
|
: NONE;
|
||||||
PendingIntent startIntent = PendingIntent.getForegroundService(this,
|
PendingIntent startIntent = PendingIntent.getForegroundService(this,
|
||||||
RecordingService.REQUEST_CODE,
|
RecordingService.REQUEST_CODE,
|
||||||
RecordingService.getStartIntent(
|
RecordingService.getStartIntent(
|
||||||
ScreenRecordDialog.this, RESULT_OK, null, useAudio, showTaps),
|
ScreenRecordDialog.this, RESULT_OK,
|
||||||
|
audioMode.ordinal(), showTaps),
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
PendingIntent stopIntent = PendingIntent.getService(this,
|
PendingIntent stopIntent = PendingIntent.getService(this,
|
||||||
RecordingService.REQUEST_CODE,
|
RecordingService.REQUEST_CODE,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ import android.os.Looper;
|
|||||||
import android.os.Process;
|
import android.os.Process;
|
||||||
|
|
||||||
import com.android.systemui.dagger.qualifiers.Background;
|
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.Main;
|
||||||
import com.android.systemui.dagger.qualifiers.UiBackground;
|
import com.android.systemui.dagger.qualifiers.UiBackground;
|
||||||
|
|
||||||
@@ -50,6 +51,17 @@ public abstract class ConcurrencyModule {
|
|||||||
return thread.getLooper();
|
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 */
|
/** Main Looper */
|
||||||
@Provides
|
@Provides
|
||||||
@Main
|
@Main
|
||||||
@@ -88,6 +100,16 @@ public abstract class ConcurrencyModule {
|
|||||||
return new ExecutorImpl(looper);
|
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.
|
* Provide a Background-Thread Executor.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user