blob: 1c7d987afff290b3ee322d585ed179fc0b4981cf [file] [log] [blame]
Jay Aliomer0bd491a2020-03-16 14:34:10 -04001/*
2 * Copyright (C) 2020 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.systemui.screenrecord;
18
19import static android.content.Context.MEDIA_PROJECTION_SERVICE;
20
21import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.INTERNAL;
22import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC;
23import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC_AND_INTERNAL;
24
Jay Aliomer57e2aa42020-06-04 15:28:09 -040025import android.annotation.Nullable;
Jay Aliomer0bd491a2020-03-16 14:34:10 -040026import android.content.ContentResolver;
27import android.content.ContentValues;
28import android.content.Context;
Jay Aliomer57e2aa42020-06-04 15:28:09 -040029import android.graphics.Bitmap;
Jay Aliomer0bd491a2020-03-16 14:34:10 -040030import android.hardware.display.DisplayManager;
31import android.hardware.display.VirtualDisplay;
Jay Aliomer57e2aa42020-06-04 15:28:09 -040032import android.media.MediaCodecInfo;
Jay Aliomer0bd491a2020-03-16 14:34:10 -040033import android.media.MediaMuxer;
34import android.media.MediaRecorder;
Jay Aliomer57e2aa42020-06-04 15:28:09 -040035import android.media.ThumbnailUtils;
Jay Aliomer0bd491a2020-03-16 14:34:10 -040036import android.media.projection.IMediaProjection;
37import android.media.projection.IMediaProjectionManager;
38import android.media.projection.MediaProjection;
39import android.media.projection.MediaProjectionManager;
40import android.net.Uri;
41import android.os.IBinder;
42import android.os.RemoteException;
43import android.os.ServiceManager;
44import android.provider.MediaStore;
45import android.util.DisplayMetrics;
46import android.util.Log;
Jay Aliomer57e2aa42020-06-04 15:28:09 -040047import android.util.Size;
Jay Aliomer0bd491a2020-03-16 14:34:10 -040048import android.view.Surface;
49import android.view.WindowManager;
50
51import java.io.File;
52import java.io.IOException;
53import java.io.OutputStream;
54import java.nio.file.Files;
55import java.text.SimpleDateFormat;
56import java.util.Date;
57
58/**
59 * Recording screen and mic/internal audio
60 */
61public class ScreenMediaRecorder {
62 private static final int TOTAL_NUM_TRACKS = 1;
Jay Aliomer0bd491a2020-03-16 14:34:10 -040063 private static final int VIDEO_FRAME_RATE = 30;
Jay Aliomer246a25e2020-05-26 22:57:34 -040064 private static final int VIDEO_FRAME_RATE_TO_RESOLUTION_RATIO = 6;
65 private static final int AUDIO_BIT_RATE = 196000;
Jay Aliomer0bd491a2020-03-16 14:34:10 -040066 private static final int AUDIO_SAMPLE_RATE = 44100;
67 private static final int MAX_DURATION_MS = 60 * 60 * 1000;
68 private static final long MAX_FILESIZE_BYTES = 5000000000L;
69 private static final String TAG = "ScreenMediaRecorder";
70
71
72 private File mTempVideoFile;
73 private File mTempAudioFile;
74 private MediaProjection mMediaProjection;
75 private Surface mInputSurface;
76 private VirtualDisplay mVirtualDisplay;
77 private MediaRecorder mMediaRecorder;
78 private int mUser;
79 private ScreenRecordingMuxer mMuxer;
80 private ScreenInternalAudioRecorder mAudio;
81 private ScreenRecordingAudioSource mAudioSource;
82
83 private Context mContext;
84 MediaRecorder.OnInfoListener mListener;
85
86 public ScreenMediaRecorder(Context context,
87 int user, ScreenRecordingAudioSource audioSource,
88 MediaRecorder.OnInfoListener listener) {
89 mContext = context;
90 mUser = user;
91 mListener = listener;
92 mAudioSource = audioSource;
93 }
94
95 private void prepare() throws IOException, RemoteException {
96 //Setup media projection
97 IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);
98 IMediaProjectionManager mediaService =
99 IMediaProjectionManager.Stub.asInterface(b);
100 IMediaProjection proj = null;
101 proj = mediaService.createProjection(mUser, mContext.getPackageName(),
102 MediaProjectionManager.TYPE_SCREEN_CAPTURE, false);
103 IBinder projection = proj.asBinder();
104 mMediaProjection = new MediaProjection(mContext,
105 IMediaProjection.Stub.asInterface(projection));
106
107 File cacheDir = mContext.getCacheDir();
108 cacheDir.mkdirs();
109 mTempVideoFile = File.createTempFile("temp", ".mp4", cacheDir);
110
111 // Set up media recorder
112 mMediaRecorder = new MediaRecorder();
113
114 // Set up audio source
115 if (mAudioSource == MIC) {
Jay Aliomer246a25e2020-05-26 22:57:34 -0400116 mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT);
Jay Aliomer0bd491a2020-03-16 14:34:10 -0400117 }
118 mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
119
120 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
121
122
123 // Set up video
124 DisplayMetrics metrics = new DisplayMetrics();
125 WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
126 wm.getDefaultDisplay().getRealMetrics(metrics);
127 int screenWidth = metrics.widthPixels;
128 int screenHeight = metrics.heightPixels;
Jay Aliomer246a25e2020-05-26 22:57:34 -0400129 int refereshRate = (int) wm.getDefaultDisplay().getRefreshRate();
130 int vidBitRate = screenHeight * screenWidth * refereshRate / VIDEO_FRAME_RATE
131 * VIDEO_FRAME_RATE_TO_RESOLUTION_RATIO;
Jay Aliomer0bd491a2020-03-16 14:34:10 -0400132 mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
Jay Aliomer57e2aa42020-06-04 15:28:09 -0400133 mMediaRecorder.setVideoEncodingProfileLevel(
134 MediaCodecInfo.CodecProfileLevel.AVCProfileHigh,
135 MediaCodecInfo.CodecProfileLevel.AVCLevel42);
Jay Aliomer0bd491a2020-03-16 14:34:10 -0400136 mMediaRecorder.setVideoSize(screenWidth, screenHeight);
Jay Aliomer246a25e2020-05-26 22:57:34 -0400137 mMediaRecorder.setVideoFrameRate(refereshRate);
138 mMediaRecorder.setVideoEncodingBitRate(vidBitRate);
Jay Aliomer0bd491a2020-03-16 14:34:10 -0400139 mMediaRecorder.setMaxDuration(MAX_DURATION_MS);
140 mMediaRecorder.setMaxFileSize(MAX_FILESIZE_BYTES);
141
142 // Set up audio
143 if (mAudioSource == MIC) {
144 mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.HE_AAC);
145 mMediaRecorder.setAudioChannels(TOTAL_NUM_TRACKS);
146 mMediaRecorder.setAudioEncodingBitRate(AUDIO_BIT_RATE);
147 mMediaRecorder.setAudioSamplingRate(AUDIO_SAMPLE_RATE);
148 }
149
150 mMediaRecorder.setOutputFile(mTempVideoFile);
151 mMediaRecorder.prepare();
152 // Create surface
153 mInputSurface = mMediaRecorder.getSurface();
154 mVirtualDisplay = mMediaProjection.createVirtualDisplay(
155 "Recording Display",
156 screenWidth,
157 screenHeight,
158 metrics.densityDpi,
159 DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
160 mInputSurface,
161 null,
162 null);
163
164 mMediaRecorder.setOnInfoListener(mListener);
165 if (mAudioSource == INTERNAL ||
166 mAudioSource == MIC_AND_INTERNAL) {
167 mTempAudioFile = File.createTempFile("temp", ".aac",
168 mContext.getCacheDir());
169 mAudio = new ScreenInternalAudioRecorder(mTempAudioFile.getAbsolutePath(), mContext,
170 mMediaProjection, mAudioSource == MIC_AND_INTERNAL);
171 }
172
173 }
174
175 /**
176 * Start screen recording
177 */
178 void start() throws IOException, RemoteException {
179 Log.d(TAG, "start recording");
180 prepare();
181 mMediaRecorder.start();
182 recordInternalAudio();
183 }
184
185 /**
186 * End screen recording
187 */
188 void end() {
189 mMediaRecorder.stop();
190 mMediaProjection.stop();
191 mMediaRecorder.release();
192 mMediaRecorder = null;
193 mMediaProjection = null;
194 mInputSurface.release();
195 mVirtualDisplay.release();
196 stopInternalAudioRecording();
197
198 Log.d(TAG, "end recording");
199 }
200
201 private void stopInternalAudioRecording() {
202 if (mAudioSource == INTERNAL || mAudioSource == MIC_AND_INTERNAL) {
203 mAudio.end();
204 mAudio = null;
205 }
206 }
207
208 private void recordInternalAudio() {
209 if (mAudioSource == INTERNAL || mAudioSource == MIC_AND_INTERNAL) {
210 mAudio.start();
211 }
212 }
213
214 /**
215 * Store recorded video
216 */
Jay Aliomer57e2aa42020-06-04 15:28:09 -0400217 protected SavedRecording save() throws IOException {
Jay Aliomer0bd491a2020-03-16 14:34:10 -0400218 String fileName = new SimpleDateFormat("'screen-'yyyyMMdd-HHmmss'.mp4'")
219 .format(new Date());
220
221 ContentValues values = new ContentValues();
222 values.put(MediaStore.Video.Media.DISPLAY_NAME, fileName);
223 values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
224 values.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis());
225 values.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis());
226
227 ContentResolver resolver = mContext.getContentResolver();
228 Uri collectionUri = MediaStore.Video.Media.getContentUri(
229 MediaStore.VOLUME_EXTERNAL_PRIMARY);
230 Uri itemUri = resolver.insert(collectionUri, values);
231
232 Log.d(TAG, itemUri.toString());
233 if (mAudioSource == MIC_AND_INTERNAL || mAudioSource == INTERNAL) {
234 try {
235 Log.d(TAG, "muxing recording");
236 File file = File.createTempFile("temp", ".mp4",
237 mContext.getCacheDir());
238 mMuxer = new ScreenRecordingMuxer(MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4,
239 file.getAbsolutePath(),
240 mTempVideoFile.getAbsolutePath(),
241 mTempAudioFile.getAbsolutePath());
242 mMuxer.mux();
243 mTempVideoFile.delete();
244 mTempVideoFile = file;
245 } catch (IOException e) {
246 Log.e(TAG, "muxing recording " + e.getMessage());
247 e.printStackTrace();
248 }
249 }
250
251 // Add to the mediastore
252 OutputStream os = resolver.openOutputStream(itemUri, "w");
253 Files.copy(mTempVideoFile.toPath(), os);
254 os.close();
Jay Aliomer0bd491a2020-03-16 14:34:10 -0400255 if (mTempAudioFile != null) mTempAudioFile.delete();
Jay Aliomer57e2aa42020-06-04 15:28:09 -0400256 DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
257 Size size = new Size(metrics.widthPixels, metrics.heightPixels);
258 SavedRecording recording = new SavedRecording(itemUri, mTempVideoFile, size);
259 mTempVideoFile.delete();
260 return recording;
261 }
262
263 /**
264 * Object representing the recording
265 */
266 public class SavedRecording {
267
268 private Uri mUri;
269 private Bitmap mThumbnailBitmap;
270
271 protected SavedRecording(Uri uri, File file, Size thumbnailSize) {
272 mUri = uri;
273 try {
274 mThumbnailBitmap = ThumbnailUtils.createVideoThumbnail(
275 file, thumbnailSize, null);
276 } catch (IOException e) {
277 Log.e(TAG, "Error creating thumbnail", e);
278 }
279 }
280
281 public Uri getUri() {
282 return mUri;
283 }
284
285 public @Nullable Bitmap getThumbnail() {
286 return mThumbnailBitmap;
287 }
Jay Aliomer0bd491a2020-03-16 14:34:10 -0400288 }
289}