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.MediaMuxer; 34 import android.media.MediaRecorder; 35 import android.media.ThumbnailUtils; 36 import android.media.projection.IMediaProjection; 37 import android.media.projection.IMediaProjectionManager; 38 import android.media.projection.MediaProjection; 39 import android.media.projection.MediaProjectionManager; 40 import android.net.Uri; 41 import android.os.IBinder; 42 import android.os.RemoteException; 43 import android.os.ServiceManager; 44 import android.provider.MediaStore; 45 import android.util.DisplayMetrics; 46 import android.util.Log; 47 import android.util.Size; 48 import android.view.Surface; 49 import android.view.WindowManager; 50 51 import java.io.File; 52 import java.io.IOException; 53 import java.io.OutputStream; 54 import java.nio.file.Files; 55 import java.text.SimpleDateFormat; 56 import java.util.Date; 57 58 /** 59 * Recording screen and mic/internal audio 60 */ 61 public class ScreenMediaRecorder { 62 private static final int TOTAL_NUM_TRACKS = 1; 63 private static final int VIDEO_FRAME_RATE = 30; 64 private static final int VIDEO_FRAME_RATE_TO_RESOLUTION_RATIO = 6; 65 private static final int AUDIO_BIT_RATE = 196000; 66 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 ScreenMediaRecorder(Context context, int user, ScreenRecordingAudioSource audioSource, MediaRecorder.OnInfoListener listener)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 prepare()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) { 116 mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT); 117 } 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; 129 int refereshRate = (int) wm.getDefaultDisplay().getRefreshRate(); 130 int vidBitRate = screenHeight * screenWidth * refereshRate / VIDEO_FRAME_RATE 131 * VIDEO_FRAME_RATE_TO_RESOLUTION_RATIO; 132 mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); 133 mMediaRecorder.setVideoEncodingProfileLevel( 134 MediaCodecInfo.CodecProfileLevel.AVCProfileHigh, 135 MediaCodecInfo.CodecProfileLevel.AVCLevel42); 136 mMediaRecorder.setVideoSize(screenWidth, screenHeight); 137 mMediaRecorder.setVideoFrameRate(refereshRate); 138 mMediaRecorder.setVideoEncodingBitRate(vidBitRate); 139 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(), 170 mMediaProjection, mAudioSource == MIC_AND_INTERNAL); 171 } 172 173 } 174 175 /** 176 * Start screen recording 177 */ start()178 void start() throws IOException, RemoteException, IllegalStateException { 179 Log.d(TAG, "start recording"); 180 prepare(); 181 mMediaRecorder.start(); 182 recordInternalAudio(); 183 } 184 185 /** 186 * End screen recording 187 */ end()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 stopInternalAudioRecording()201 private void stopInternalAudioRecording() { 202 if (mAudioSource == INTERNAL || mAudioSource == MIC_AND_INTERNAL) { 203 mAudio.end(); 204 mAudio = null; 205 } 206 } 207 recordInternalAudio()208 private void recordInternalAudio() throws IllegalStateException { 209 if (mAudioSource == INTERNAL || mAudioSource == MIC_AND_INTERNAL) { 210 mAudio.start(); 211 } 212 } 213 214 /** 215 * Store recorded video 216 */ save()217 protected SavedRecording save() throws IOException { 218 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(); 255 if (mTempAudioFile != null) mTempAudioFile.delete(); 256 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 SavedRecording(Uri uri, File file, Size thumbnailSize)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 getUri()281 public Uri getUri() { 282 return mUri; 283 } 284 getThumbnail()285 public @Nullable Bitmap getThumbnail() { 286 return mThumbnailBitmap; 287 } 288 } 289 } 290