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