1 /* 2 * Copyright (C) 2017 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.googlecode.android_scripting.facade.media; 18 19 import android.app.Service; 20 import android.content.Intent; 21 import android.media.MediaRecorder; 22 import android.net.Uri; 23 import android.provider.MediaStore; 24 import android.view.SurfaceHolder; 25 import android.view.SurfaceHolder.Callback; 26 import android.view.SurfaceView; 27 import android.view.WindowManager; 28 29 import com.googlecode.android_scripting.BaseApplication; 30 import com.googlecode.android_scripting.FutureActivityTaskExecutor; 31 import com.googlecode.android_scripting.Log; 32 import com.googlecode.android_scripting.facade.AndroidFacade; 33 import com.googlecode.android_scripting.facade.FacadeManager; 34 import com.googlecode.android_scripting.future.FutureActivityTask; 35 import com.googlecode.android_scripting.jsonrpc.RpcReceiver; 36 import com.googlecode.android_scripting.rpc.Rpc; 37 import com.googlecode.android_scripting.rpc.RpcDefault; 38 import com.googlecode.android_scripting.rpc.RpcOptional; 39 import com.googlecode.android_scripting.rpc.RpcParameter; 40 41 import java.io.File; 42 import java.io.IOException; 43 import java.lang.reflect.Field; 44 import java.util.concurrent.CountDownLatch; 45 import java.util.concurrent.TimeUnit; 46 47 /** 48 * A facade for recording media. 49 * 50 * Guidance notes: Use e.g. '/sdcard/file.ext' for your media destination file. A file extension of 51 * mpg will use the default settings for format and codec (often h263 which won't work with common 52 * PC media players). A file extension of mp4 or 3gp will use the appropriate format with the (more 53 * common) h264 codec. A video player such as QQPlayer (from the android market) plays both codecs 54 * and uses the composition matrix (embedded in the video file) to correct for image rotation. Many 55 * PC based media players ignore this matrix. Standard video sizes may be specified. 56 * 57 */ 58 public class MediaRecorderFacade extends RpcReceiver { 59 60 private final MediaRecorder mMediaRecorder = new MediaRecorder(); 61 private final Service mService; 62 63 public MediaRecorderFacade(FacadeManager manager) { 64 super(manager); 65 mService = manager.getService(); 66 } 67 68 @Rpc(description = "Records audio from the microphone and saves it to the given location.") 69 public void recorderStartMicrophone(@RpcParameter(name = "targetPath") String targetPath) 70 throws IOException { 71 startAudioRecording(targetPath, MediaRecorder.AudioSource.MIC); 72 } 73 74 @Rpc(description = "Records video from the camera and saves it to the given location. " 75 + "\nDuration specifies the maximum duration of the recording session. " 76 + "\nIf duration is 0 this method will return and the recording will only be stopped " 77 + "\nwhen recorderStop is called or when a scripts exits. " 78 + "\nOtherwise it will block for the time period equal to the duration argument." 79 + "\nvideoSize: 0=160x120, 1=320x240, 2=352x288, 3=640x480, 4=800x480.") 80 public void recorderStartVideo(@RpcParameter(name = "targetPath") String targetPath, 81 @RpcParameter(name = "duration") @RpcDefault("0") Integer duration, 82 @RpcParameter(name = "videoSize") @RpcDefault("1") Integer videoSize) throws Exception { 83 int ms = convertSecondsToMilliseconds(duration); 84 startVideoRecording(new File(targetPath), ms, videoSize); 85 } 86 87 private void startVideoRecording(File file, int milliseconds, int videoSize) throws Exception { 88 mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); 89 90 int audioSource = MediaRecorder.AudioSource.MIC; 91 try { 92 Field source = Class.forName("android.media.MediaRecorder$AudioSource").getField("CAMCORDER"); 93 source.getInt(null); 94 } catch (Exception e) { 95 Log.e(e); 96 } 97 int xSize; 98 int ySize; 99 switch (videoSize) { 100 case 0: 101 xSize = 160; 102 ySize = 120; 103 break; 104 case 1: 105 xSize = 320; 106 ySize = 240; 107 break; 108 case 2: 109 xSize = 352; 110 ySize = 288; 111 break; 112 case 3: 113 xSize = 640; 114 ySize = 480; 115 break; 116 case 4: 117 xSize = 800; 118 ySize = 480; 119 break; 120 default: 121 xSize = 320; 122 ySize = 240; 123 break; 124 } 125 126 mMediaRecorder.setAudioSource(audioSource); 127 String extension = file.toString().split("\\.")[1]; 128 if (extension.equals("mp4")) { 129 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); 130 mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); 131 mMediaRecorder.setVideoSize(xSize, ySize); 132 mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); 133 } else if (extension.equals("3gp")) { 134 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); 135 mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); 136 mMediaRecorder.setVideoSize(xSize, ySize); 137 mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); 138 } else { 139 140 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT); 141 mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT); 142 mMediaRecorder.setVideoSize(xSize, ySize); 143 mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT); 144 } 145 146 mMediaRecorder.setOutputFile(file.getAbsolutePath()); 147 if (milliseconds > 0) { 148 mMediaRecorder.setMaxDuration(milliseconds); 149 } 150 FutureActivityTask<Exception> prepTask = prepare(); 151 mMediaRecorder.start(); 152 if (milliseconds > 0) { 153 new CountDownLatch(1).await(milliseconds, TimeUnit.MILLISECONDS); 154 } 155 prepTask.finish(); 156 } 157 158 @Rpc(description = "Records video (and optionally audio) from the camera and saves it to the given location. " 159 + "\nDuration specifies the maximum duration of the recording session. " 160 + "\nIf duration is not provided this method will return immediately and the recording will only be stopped " 161 + "\nwhen recorderStop is called or when a scripts exits. " 162 + "\nOtherwise it will block for the time period equal to the duration argument.") 163 public void recorderCaptureVideo(@RpcParameter(name = "targetPath") String targetPath, 164 @RpcParameter(name = "duration") @RpcOptional Integer duration, 165 @RpcParameter(name = "recordAudio") @RpcDefault("true") Boolean recordAudio) throws Exception { 166 int ms = convertSecondsToMilliseconds(duration); 167 startVideoRecording(new File(targetPath), ms, recordAudio); 168 } 169 170 private void startVideoRecording(File file, int milliseconds, boolean withAudio) throws Exception { 171 mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); 172 if (withAudio) { 173 int audioSource = MediaRecorder.AudioSource.MIC; 174 try { 175 Field source = 176 Class.forName("android.media.MediaRecorder$AudioSource").getField("CAMCORDER"); 177 audioSource = source.getInt(null); 178 } catch (Exception e) { 179 Log.e(e); 180 } 181 mMediaRecorder.setAudioSource(audioSource); 182 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT); 183 mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT); 184 } else { 185 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT); 186 } 187 mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT); 188 mMediaRecorder.setOutputFile(file.getAbsolutePath()); 189 if (milliseconds > 0) { 190 mMediaRecorder.setMaxDuration(milliseconds); 191 } 192 FutureActivityTask<Exception> prepTask = prepare(); 193 mMediaRecorder.start(); 194 if (milliseconds > 0) { 195 new CountDownLatch(1).await(milliseconds, TimeUnit.MILLISECONDS); 196 } 197 prepTask.finish(); 198 } 199 200 private void startAudioRecording(String targetPath, int source) throws IOException { 201 mMediaRecorder.setAudioSource(source); 202 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT); 203 mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT); 204 mMediaRecorder.setOutputFile(targetPath); 205 mMediaRecorder.prepare(); 206 mMediaRecorder.start(); 207 } 208 209 @Rpc(description = "Stops a previously started recording.") 210 public void recorderStop() { 211 mMediaRecorder.stop(); 212 mMediaRecorder.reset(); 213 } 214 215 @Rpc(description = "Starts the video capture application to record a video and saves it to the specified path.") 216 public void startInteractiveVideoRecording(@RpcParameter(name = "path") final String path) { 217 Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); 218 File file = new File(path); 219 intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file)); 220 AndroidFacade facade = mManager.getReceiver(AndroidFacade.class); 221 facade.startActivityForResult(intent); 222 } 223 224 @Override 225 public void shutdown() { 226 mMediaRecorder.release(); 227 } 228 229 // TODO(damonkohler): This shares a lot of code with the CameraFacade. It's probably worth moving 230 // it there. 231 private FutureActivityTask<Exception> prepare() throws Exception { 232 FutureActivityTask<Exception> task = new FutureActivityTask<Exception>() { 233 @Override 234 public void onCreate() { 235 super.onCreate(); 236 final SurfaceView view = new SurfaceView(getActivity()); 237 getActivity().setContentView(view); 238 getActivity().getWindow().setSoftInputMode( 239 WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED); 240 view.getHolder().addCallback(new Callback() { 241 @Override 242 public void surfaceDestroyed(SurfaceHolder holder) { 243 } 244 245 @Override 246 public void surfaceCreated(SurfaceHolder holder) { 247 try { 248 mMediaRecorder.setPreviewDisplay(view.getHolder().getSurface()); 249 mMediaRecorder.prepare(); 250 setResult(null); 251 } catch (IOException e) { 252 setResult(e); 253 } 254 } 255 256 @Override 257 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 258 } 259 }); 260 } 261 }; 262 263 FutureActivityTaskExecutor taskExecutor = 264 ((BaseApplication) mService.getApplication()).getTaskExecutor(); 265 taskExecutor.execute(task); 266 267 Exception e = task.getResult(); 268 if (e != null) { 269 throw e; 270 } 271 return task; 272 } 273 274 private int convertSecondsToMilliseconds(Integer seconds) { 275 if (seconds == null) { 276 return 0; 277 } 278 return (int) (seconds * 1000L); 279 } 280 } 281