• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
17 package com.android.systemui.screenrecord;
18 
19 import static android.content.Context.MEDIA_PROJECTION_SERVICE;
20 
21 import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.INTERNAL;
22 import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC;
23 import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC_AND_INTERNAL;
24 
25 import android.annotation.Nullable;
26 import android.content.ContentResolver;
27 import android.content.ContentValues;
28 import android.content.Context;
29 import android.graphics.Bitmap;
30 import android.hardware.display.DisplayManager;
31 import android.hardware.display.VirtualDisplay;
32 import android.media.MediaCodecInfo;
33 import android.media.MediaCodecList;
34 import android.media.MediaFormat;
35 import android.media.MediaMuxer;
36 import android.media.MediaRecorder;
37 import android.media.ThumbnailUtils;
38 import android.media.projection.IMediaProjection;
39 import android.media.projection.IMediaProjectionManager;
40 import android.media.projection.MediaProjection;
41 import android.media.projection.MediaProjectionManager;
42 import android.net.Uri;
43 import android.os.IBinder;
44 import android.os.RemoteException;
45 import android.os.ServiceManager;
46 import android.provider.MediaStore;
47 import android.util.DisplayMetrics;
48 import android.util.Log;
49 import android.util.Size;
50 import android.view.Surface;
51 import android.view.WindowManager;
52 
53 import java.io.File;
54 import java.io.IOException;
55 import java.io.OutputStream;
56 import java.nio.file.Files;
57 import java.text.SimpleDateFormat;
58 import java.util.Date;
59 
60 /**
61  * Recording screen and mic/internal audio
62  */
63 public class ScreenMediaRecorder {
64     private static final int TOTAL_NUM_TRACKS = 1;
65     private static final int VIDEO_FRAME_RATE = 30;
66     private static final int VIDEO_FRAME_RATE_TO_RESOLUTION_RATIO = 6;
67     private static final int AUDIO_BIT_RATE = 196000;
68     private static final int AUDIO_SAMPLE_RATE = 44100;
69     private static final int MAX_DURATION_MS = 60 * 60 * 1000;
70     private static final long MAX_FILESIZE_BYTES = 5000000000L;
71     private static final String TAG = "ScreenMediaRecorder";
72 
73 
74     private File mTempVideoFile;
75     private File mTempAudioFile;
76     private MediaProjection mMediaProjection;
77     private Surface mInputSurface;
78     private VirtualDisplay mVirtualDisplay;
79     private MediaRecorder mMediaRecorder;
80     private int mUser;
81     private ScreenRecordingMuxer mMuxer;
82     private ScreenInternalAudioRecorder mAudio;
83     private ScreenRecordingAudioSource mAudioSource;
84 
85     private Context mContext;
86     MediaRecorder.OnInfoListener mListener;
87 
ScreenMediaRecorder(Context context, int user, ScreenRecordingAudioSource audioSource, MediaRecorder.OnInfoListener listener)88     public ScreenMediaRecorder(Context context,
89             int user, ScreenRecordingAudioSource audioSource,
90             MediaRecorder.OnInfoListener listener) {
91         mContext = context;
92         mUser = user;
93         mListener = listener;
94         mAudioSource = audioSource;
95     }
96 
prepare()97     private void prepare() throws IOException, RemoteException, RuntimeException {
98         //Setup media projection
99         IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);
100         IMediaProjectionManager mediaService =
101                 IMediaProjectionManager.Stub.asInterface(b);
102         IMediaProjection proj = null;
103         proj = mediaService.createProjection(mUser, mContext.getPackageName(),
104                     MediaProjectionManager.TYPE_SCREEN_CAPTURE, false);
105         IBinder projection = proj.asBinder();
106         mMediaProjection = new MediaProjection(mContext,
107                 IMediaProjection.Stub.asInterface(projection));
108 
109         File cacheDir = mContext.getCacheDir();
110         cacheDir.mkdirs();
111         mTempVideoFile = File.createTempFile("temp", ".mp4", cacheDir);
112 
113         // Set up media recorder
114         mMediaRecorder = new MediaRecorder();
115 
116         // Set up audio source
117         if (mAudioSource == MIC) {
118             mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT);
119         }
120         mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
121 
122         mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
123 
124 
125         // Set up video
126         DisplayMetrics metrics = new DisplayMetrics();
127         WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
128         wm.getDefaultDisplay().getRealMetrics(metrics);
129         int refreshRate = (int) wm.getDefaultDisplay().getRefreshRate();
130         int[] dimens = getSupportedSize(metrics.widthPixels, metrics.heightPixels, refreshRate);
131         int width = dimens[0];
132         int height = dimens[1];
133         refreshRate = dimens[2];
134         int vidBitRate = width * height * refreshRate / VIDEO_FRAME_RATE
135                 * VIDEO_FRAME_RATE_TO_RESOLUTION_RATIO;
136         mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
137         mMediaRecorder.setVideoEncodingProfileLevel(
138                 MediaCodecInfo.CodecProfileLevel.AVCProfileHigh,
139                 MediaCodecInfo.CodecProfileLevel.AVCLevel3);
140         mMediaRecorder.setVideoSize(width, height);
141         mMediaRecorder.setVideoFrameRate(refreshRate);
142         mMediaRecorder.setVideoEncodingBitRate(vidBitRate);
143         mMediaRecorder.setMaxDuration(MAX_DURATION_MS);
144         mMediaRecorder.setMaxFileSize(MAX_FILESIZE_BYTES);
145 
146         // Set up audio
147         if (mAudioSource == MIC) {
148             mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.HE_AAC);
149             mMediaRecorder.setAudioChannels(TOTAL_NUM_TRACKS);
150             mMediaRecorder.setAudioEncodingBitRate(AUDIO_BIT_RATE);
151             mMediaRecorder.setAudioSamplingRate(AUDIO_SAMPLE_RATE);
152         }
153 
154         mMediaRecorder.setOutputFile(mTempVideoFile);
155         mMediaRecorder.prepare();
156         // Create surface
157         mInputSurface = mMediaRecorder.getSurface();
158         mVirtualDisplay = mMediaProjection.createVirtualDisplay(
159                 "Recording Display",
160                 width,
161                 height,
162                 metrics.densityDpi,
163                 DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
164                 mInputSurface,
165                 null,
166                 null);
167 
168         mMediaRecorder.setOnInfoListener(mListener);
169         if (mAudioSource == INTERNAL ||
170                 mAudioSource == MIC_AND_INTERNAL) {
171             mTempAudioFile = File.createTempFile("temp", ".aac",
172                     mContext.getCacheDir());
173             mAudio = new ScreenInternalAudioRecorder(mTempAudioFile.getAbsolutePath(),
174                     mMediaProjection, mAudioSource == MIC_AND_INTERNAL);
175         }
176 
177     }
178 
179     /**
180      * Find the highest supported screen resolution and refresh rate for the given dimensions on
181      * this device, up to actual size and given rate.
182      * If possible this will return the same values as given, but values may be smaller on some
183      * devices.
184      *
185      * @param screenWidth Actual pixel width of screen
186      * @param screenHeight Actual pixel height of screen
187      * @param refreshRate Desired refresh rate
188      * @return array with supported width, height, and refresh rate
189      */
getSupportedSize(final int screenWidth, final int screenHeight, int refreshRate)190     private int[] getSupportedSize(final int screenWidth, final int screenHeight, int refreshRate) {
191         double maxScale = 0;
192 
193         MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
194         MediaCodecInfo.VideoCapabilities maxInfo = null;
195         for (MediaCodecInfo codec : codecList.getCodecInfos()) {
196             String videoType = MediaFormat.MIMETYPE_VIDEO_AVC;
197             String[] types = codec.getSupportedTypes();
198             for (String t : types) {
199                 if (!t.equalsIgnoreCase(videoType)) {
200                     continue;
201                 }
202                 MediaCodecInfo.CodecCapabilities capabilities =
203                         codec.getCapabilitiesForType(videoType);
204                 if (capabilities != null && capabilities.getVideoCapabilities() != null) {
205                     MediaCodecInfo.VideoCapabilities vc = capabilities.getVideoCapabilities();
206 
207                     int width = vc.getSupportedWidths().getUpper();
208                     int height = vc.getSupportedHeights().getUpper();
209 
210                     int screenWidthAligned = screenWidth;
211                     if (screenWidthAligned % vc.getWidthAlignment() != 0) {
212                         screenWidthAligned -= (screenWidthAligned % vc.getWidthAlignment());
213                     }
214                     int screenHeightAligned = screenHeight;
215                     if (screenHeightAligned % vc.getHeightAlignment() != 0) {
216                         screenHeightAligned -= (screenHeightAligned % vc.getHeightAlignment());
217                     }
218 
219                     if (width >= screenWidthAligned && height >= screenHeightAligned
220                             && vc.isSizeSupported(screenWidthAligned, screenHeightAligned)) {
221                         // Desired size is supported, now get the rate
222                         int maxRate = vc.getSupportedFrameRatesFor(screenWidthAligned,
223                                 screenHeightAligned).getUpper().intValue();
224 
225                         if (maxRate < refreshRate) {
226                             refreshRate = maxRate;
227                         }
228                         Log.d(TAG, "Screen size supported at rate " + refreshRate);
229                         return new int[]{screenWidthAligned, screenHeightAligned, refreshRate};
230                     }
231 
232                     // Otherwise, continue searching
233                     double scale = Math.min(((double) width / screenWidth),
234                             ((double) height / screenHeight));
235                     if (scale > maxScale) {
236                         maxScale = Math.min(1, scale);
237                         maxInfo = vc;
238                     }
239                 }
240             }
241         }
242 
243         // Resize for max supported size
244         int scaledWidth = (int) (screenWidth * maxScale);
245         int scaledHeight = (int) (screenHeight * maxScale);
246         if (scaledWidth % maxInfo.getWidthAlignment() != 0) {
247             scaledWidth -= (scaledWidth % maxInfo.getWidthAlignment());
248         }
249         if (scaledHeight % maxInfo.getHeightAlignment() != 0) {
250             scaledHeight -= (scaledHeight % maxInfo.getHeightAlignment());
251         }
252 
253         // Find max supported rate for size
254         int maxRate = maxInfo.getSupportedFrameRatesFor(scaledWidth, scaledHeight)
255                 .getUpper().intValue();
256         if (maxRate < refreshRate) {
257             refreshRate = maxRate;
258         }
259 
260         Log.d(TAG, "Resized by " + maxScale + ": " + scaledWidth + ", " + scaledHeight
261                 + ", " + refreshRate);
262         return new int[]{scaledWidth, scaledHeight, refreshRate};
263     }
264 
265     /**
266     * Start screen recording
267     */
start()268     void start() throws IOException, RemoteException, RuntimeException {
269         Log.d(TAG, "start recording");
270         prepare();
271         mMediaRecorder.start();
272         recordInternalAudio();
273     }
274 
275     /**
276      * End screen recording
277      */
end()278     void end() {
279         mMediaRecorder.stop();
280         mMediaRecorder.release();
281         mInputSurface.release();
282         mVirtualDisplay.release();
283         mMediaProjection.stop();
284         mMediaRecorder = null;
285         mMediaProjection = null;
286         stopInternalAudioRecording();
287 
288         Log.d(TAG, "end recording");
289     }
290 
stopInternalAudioRecording()291     private void stopInternalAudioRecording() {
292         if (mAudioSource == INTERNAL || mAudioSource == MIC_AND_INTERNAL) {
293             mAudio.end();
294             mAudio = null;
295         }
296     }
297 
recordInternalAudio()298     private  void recordInternalAudio() throws IllegalStateException {
299         if (mAudioSource == INTERNAL || mAudioSource == MIC_AND_INTERNAL) {
300             mAudio.start();
301         }
302     }
303 
304     /**
305      * Store recorded video
306      */
save()307     protected SavedRecording save() throws IOException {
308         String fileName = new SimpleDateFormat("'screen-'yyyyMMdd-HHmmss'.mp4'")
309                 .format(new Date());
310 
311         ContentValues values = new ContentValues();
312         values.put(MediaStore.Video.Media.DISPLAY_NAME, fileName);
313         values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
314         values.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis());
315         values.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis());
316 
317         ContentResolver resolver = mContext.getContentResolver();
318         Uri collectionUri = MediaStore.Video.Media.getContentUri(
319                 MediaStore.VOLUME_EXTERNAL_PRIMARY);
320         Uri itemUri = resolver.insert(collectionUri, values);
321 
322         Log.d(TAG, itemUri.toString());
323         if (mAudioSource == MIC_AND_INTERNAL || mAudioSource == INTERNAL) {
324             try {
325                 Log.d(TAG, "muxing recording");
326                 File file = File.createTempFile("temp", ".mp4",
327                         mContext.getCacheDir());
328                 mMuxer = new ScreenRecordingMuxer(MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4,
329                         file.getAbsolutePath(),
330                         mTempVideoFile.getAbsolutePath(),
331                         mTempAudioFile.getAbsolutePath());
332                 mMuxer.mux();
333                 mTempVideoFile.delete();
334                 mTempVideoFile = file;
335             } catch (IOException e) {
336                 Log.e(TAG, "muxing recording " + e.getMessage());
337                 e.printStackTrace();
338             }
339         }
340 
341         // Add to the mediastore
342         OutputStream os = resolver.openOutputStream(itemUri, "w");
343         Files.copy(mTempVideoFile.toPath(), os);
344         os.close();
345         if (mTempAudioFile != null) mTempAudioFile.delete();
346         DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
347         Size size = new Size(metrics.widthPixels, metrics.heightPixels);
348         SavedRecording recording = new SavedRecording(itemUri, mTempVideoFile, size);
349         mTempVideoFile.delete();
350         return recording;
351     }
352 
353     /**
354     * Object representing the recording
355     */
356     public class SavedRecording {
357 
358         private Uri mUri;
359         private Bitmap mThumbnailBitmap;
360 
SavedRecording(Uri uri, File file, Size thumbnailSize)361         protected SavedRecording(Uri uri, File file, Size thumbnailSize) {
362             mUri = uri;
363             try {
364                 mThumbnailBitmap = ThumbnailUtils.createVideoThumbnail(
365                         file, thumbnailSize, null);
366             } catch (IOException e) {
367                 Log.e(TAG, "Error creating thumbnail", e);
368             }
369         }
370 
getUri()371         public Uri getUri() {
372             return mUri;
373         }
374 
getThumbnail()375         public @Nullable Bitmap getThumbnail() {
376             return mThumbnailBitmap;
377         }
378     }
379 }
380