MediaEncoderFilter.java revision 6b9780efb2b34058f24e462ae54e6a5b6df85e46
1/* 2 * Copyright (C) 2011 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 18package android.filterpacks.videosink; 19 20import android.content.Context; 21import android.filterfw.core.Filter; 22import android.filterfw.core.FilterContext; 23import android.filterfw.core.Frame; 24import android.filterfw.core.FrameFormat; 25import android.filterfw.core.FrameManager; 26import android.filterfw.core.GenerateFieldPort; 27import android.filterfw.core.GenerateFinalPort; 28import android.filterfw.core.GLFrame; 29import android.filterfw.core.KeyValueMap; 30import android.filterfw.core.MutableFrameFormat; 31import android.filterfw.core.NativeFrame; 32import android.filterfw.core.Program; 33import android.filterfw.core.ShaderProgram; 34import android.filterfw.format.ImageFormat; 35import android.filterfw.geometry.Point; 36import android.filterfw.geometry.Quad; 37import android.os.ConditionVariable; 38import android.media.MediaRecorder; 39import android.media.CamcorderProfile; 40import android.filterfw.core.GLEnvironment; 41 42import java.io.IOException; 43import java.io.FileDescriptor; 44import java.util.List; 45import java.util.Set; 46 47import android.util.Log; 48 49/** @hide */ 50public class MediaEncoderFilter extends Filter { 51 52 /** User-visible parameters */ 53 54 /** Recording state. When set to false, recording will stop, or will not 55 * start if not yet running the graph. Instead, frames are simply ignored. 56 * When switched back to true, recording will restart. This allows a single 57 * graph to both provide preview and to record video. If this is false, 58 * recording settings can be updated while the graph is running. 59 */ 60 @GenerateFieldPort(name = "recording", hasDefault = true) 61 private boolean mRecording = true; 62 63 /** Filename to save the output. */ 64 @GenerateFieldPort(name = "outputFile", hasDefault = true) 65 private String mOutputFile = new String("/sdcard/MediaEncoderOut.mp4"); 66 67 /** File Descriptor to save the output. */ 68 @GenerateFieldPort(name = "outputFileDescriptor", hasDefault = true) 69 private FileDescriptor mFd = null; 70 71 /** Input audio source. If not set, no audio will be recorded. 72 * Select from the values in MediaRecorder.AudioSource 73 */ 74 @GenerateFieldPort(name = "audioSource", hasDefault = true) 75 private int mAudioSource = NO_AUDIO_SOURCE; 76 77 /** Media recorder info listener, which needs to implement 78 * MediaRecorder.OnInfoListener. Set this to receive notifications about 79 * recording events. 80 */ 81 @GenerateFieldPort(name = "infoListener", hasDefault = true) 82 private MediaRecorder.OnInfoListener mInfoListener = null; 83 84 /** Media recorder error listener, which needs to implement 85 * MediaRecorder.OnErrorListener. Set this to receive notifications about 86 * recording errors. 87 */ 88 @GenerateFieldPort(name = "errorListener", hasDefault = true) 89 private MediaRecorder.OnErrorListener mErrorListener = null; 90 91 /** Media recording done callback, which needs to implement OnRecordingDoneListener. 92 * Set this to finalize media upon completion of media recording. 93 */ 94 @GenerateFieldPort(name = "recordingDoneListener", hasDefault = true) 95 private OnRecordingDoneListener mRecordingDoneListener = null; 96 97 /** Orientation hint. Used for indicating proper video playback orientation. 98 * Units are in degrees of clockwise rotation, valid values are (0, 90, 180, 99 * 270). 100 */ 101 @GenerateFieldPort(name = "orientationHint", hasDefault = true) 102 private int mOrientationHint = 0; 103 104 /** Camcorder profile to use. Select from the profiles available in 105 * android.media.CamcorderProfile. If this field is set, it overrides 106 * settings to width, height, framerate, outputFormat, and videoEncoder. 107 */ 108 @GenerateFieldPort(name = "recordingProfile", hasDefault = true) 109 private CamcorderProfile mProfile = null; 110 111 /** Frame width to be encoded, defaults to 320. 112 * Actual received frame size has to match this */ 113 @GenerateFieldPort(name = "width", hasDefault = true) 114 private int mWidth = 320; 115 116 /** Frame height to to be encoded, defaults to 240. 117 * Actual received frame size has to match */ 118 @GenerateFieldPort(name = "height", hasDefault = true) 119 private int mHeight = 240; 120 121 /** Stream framerate to encode the frames at. 122 * By default, frames are encoded at 30 FPS*/ 123 @GenerateFieldPort(name = "framerate", hasDefault = true) 124 private int mFps = 30; 125 126 /** The output format to encode the frames in. 127 * Choose an output format from the options in 128 * android.media.MediaRecorder.OutputFormat */ 129 @GenerateFieldPort(name = "outputFormat", hasDefault = true) 130 private int mOutputFormat = MediaRecorder.OutputFormat.MPEG_4; 131 132 /** The videoencoder to encode the frames with. 133 * Choose a videoencoder from the options in 134 * android.media.MediaRecorder.VideoEncoder */ 135 @GenerateFieldPort(name = "videoEncoder", hasDefault = true) 136 private int mVideoEncoder = MediaRecorder.VideoEncoder.H264; 137 138 /** The input region to read from the frame. The corners of this quad are 139 * mapped to the output rectangle. The input frame ranges from (0,0)-(1,1), 140 * top-left to bottom-right. The corners of the quad are specified in the 141 * order bottom-left, bottom-right, top-left, top-right. 142 */ 143 @GenerateFieldPort(name = "inputRegion", hasDefault = true) 144 private Quad mSourceRegion; 145 146 /** Sets the maximum filesize (in bytes) of the recording session. 147 * By default, it will be 0 and will be passed on to the MediaRecorder 148 * If the limit is zero or negative, MediaRecorder will disable the limit*/ 149 @GenerateFieldPort(name = "maxFileSize", hasDefault = true) 150 private long mMaxFileSize = 0; 151 152 // End of user visible parameters 153 154 private static final int NO_AUDIO_SOURCE = -1; 155 156 private int mSurfaceId; 157 private ShaderProgram mProgram; 158 private GLFrame mScreen; 159 160 private boolean mRecordingActive = false; 161 162 private boolean mLogVerbose; 163 private static final String TAG = "MediaEncoderFilter"; 164 165 // Our hook to the encoder 166 private MediaRecorder mMediaRecorder; 167 168 /** Callback to be called when media recording completes. */ 169 170 public interface OnRecordingDoneListener { 171 public void onRecordingDone(); 172 } 173 174 public MediaEncoderFilter(String name) { 175 super(name); 176 Point bl = new Point(0, 0); 177 Point br = new Point(1, 0); 178 Point tl = new Point(0, 1); 179 Point tr = new Point(1, 1); 180 mSourceRegion = new Quad(bl, br, tl, tr); 181 mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE); 182 } 183 184 @Override 185 public void setupPorts() { 186 // Add input port- will accept RGBA GLFrames 187 addMaskedInputPort("videoframe", ImageFormat.create(ImageFormat.COLORSPACE_RGBA, 188 FrameFormat.TARGET_GPU)); 189 } 190 191 @Override 192 public void fieldPortValueUpdated(String name, FilterContext context) { 193 if (mLogVerbose) Log.v(TAG, "Port " + name + " has been updated"); 194 if (name.equals("recording")) return; 195 if (name.equals("inputRegion")) { 196 if (isOpen()) updateSourceRegion(); 197 return; 198 } 199 // TODO: Not sure if it is possible to update the maxFileSize 200 // when the recording is going on. For now, not doing that. 201 if (isOpen() && mRecordingActive) { 202 throw new RuntimeException("Cannot change recording parameters" 203 + " when the filter is recording!"); 204 } 205 } 206 207 private void updateSourceRegion() { 208 // Flip source quad to map to OpenGL origin 209 Quad flippedRegion = new Quad(); 210 flippedRegion.p0 = mSourceRegion.p2; 211 flippedRegion.p1 = mSourceRegion.p3; 212 flippedRegion.p2 = mSourceRegion.p0; 213 flippedRegion.p3 = mSourceRegion.p1; 214 mProgram.setSourceRegion(flippedRegion); 215 } 216 217 // update the MediaRecorderParams based on the variables. 218 // These have to be in certain order as per the MediaRecorder 219 // documentation 220 private void updateMediaRecorderParams() { 221 final int GRALLOC_BUFFER = 2; 222 mMediaRecorder.setVideoSource(GRALLOC_BUFFER); 223 if (mAudioSource != NO_AUDIO_SOURCE) { 224 mMediaRecorder.setAudioSource(mAudioSource); 225 } 226 if (mProfile != null) { 227 mMediaRecorder.setProfile(mProfile); 228 } else { 229 mMediaRecorder.setOutputFormat(mOutputFormat); 230 mMediaRecorder.setVideoEncoder(mVideoEncoder); 231 mMediaRecorder.setVideoSize(mWidth, mHeight); 232 mMediaRecorder.setVideoFrameRate(mFps); 233 } 234 mMediaRecorder.setOrientationHint(mOrientationHint); 235 mMediaRecorder.setOnInfoListener(mInfoListener); 236 mMediaRecorder.setOnErrorListener(mErrorListener); 237 if (mFd != null) { 238 mMediaRecorder.setOutputFile(mFd); 239 } else { 240 mMediaRecorder.setOutputFile(mOutputFile); 241 } 242 try { 243 mMediaRecorder.setMaxFileSize(mMaxFileSize); 244 } catch (Exception e) { 245 // Following the logic in VideoCamera.java (in Camera app) 246 // We are going to ignore failure of setMaxFileSize here, as 247 // a) The composer selected may simply not support it, or 248 // b) The underlying media framework may not handle 64-bit range 249 // on the size restriction. 250 Log.w(TAG, "Setting maxFileSize on MediaRecorder unsuccessful! " 251 + e.getMessage()); 252 } 253 } 254 255 @Override 256 public void prepare(FilterContext context) { 257 if (mLogVerbose) Log.v(TAG, "Preparing"); 258 259 mProgram = ShaderProgram.createIdentity(context); 260 261 mRecordingActive = false; 262 } 263 264 @Override 265 public void open(FilterContext context) { 266 if (mLogVerbose) Log.v(TAG, "Opening"); 267 updateSourceRegion(); 268 if (mRecording) startRecording(context); 269 } 270 271 private void startRecording(FilterContext context) { 272 if (mLogVerbose) Log.v(TAG, "Starting recording"); 273 274 // Create a frame representing the screen 275 MutableFrameFormat screenFormat = new MutableFrameFormat( 276 FrameFormat.TYPE_BYTE, FrameFormat.TARGET_GPU); 277 screenFormat.setBytesPerSample(4); 278 279 int width, height; 280 if (mProfile != null) { 281 width = mProfile.videoFrameWidth; 282 height = mProfile.videoFrameHeight; 283 } else { 284 width = mWidth; 285 height = mHeight; 286 } 287 screenFormat.setDimensions(width, height); 288 mScreen = (GLFrame)context.getFrameManager().newBoundFrame( 289 screenFormat, GLFrame.EXISTING_FBO_BINDING, 0); 290 291 // Initialize the media recorder 292 293 mMediaRecorder = new MediaRecorder(); 294 updateMediaRecorderParams(); 295 296 try { 297 mMediaRecorder.prepare(); 298 } catch (IllegalStateException e) { 299 throw e; 300 } catch (IOException e) { 301 throw new RuntimeException("IOException in" 302 + "MediaRecorder.prepare()!", e); 303 } catch (Exception e) { 304 throw new RuntimeException("Unknown Exception in" 305 + "MediaRecorder.prepare()!", e); 306 } 307 // Make sure start() is called before trying to 308 // register the surface. The native window handle needed to create 309 // the surface is initiated in start() 310 mMediaRecorder.start(); 311 Log.v(TAG, "ME Filter: Open: registering surface from Mediarecorder"); 312 mSurfaceId = context.getGLEnvironment(). 313 registerSurfaceFromMediaRecorder(mMediaRecorder); 314 315 mRecordingActive = true; 316 } 317 318 @Override 319 public void process(FilterContext context) { 320 if (mLogVerbose) Log.v(TAG, "Starting frame processing"); 321 322 GLEnvironment glEnv = context.getGLEnvironment(); 323 // Get input frame 324 Frame input = pullInput("videoframe"); 325 326 // Check if recording needs to start 327 if (!mRecordingActive && mRecording) { 328 startRecording(context); 329 } 330 // Check if recording needs to stop 331 if (mRecordingActive && !mRecording) { 332 stopRecording(context); 333 } 334 335 if (!mRecordingActive) return; 336 337 // Activate our surface 338 glEnv.activateSurfaceWithId(mSurfaceId); 339 340 // Process 341 mProgram.process(input, mScreen); 342 343 // Set timestamp from input 344 glEnv.setSurfaceTimestamp(input.getTimestamp()); 345 // And swap buffers 346 glEnv.swapBuffers(); 347 } 348 349 private void stopRecording(FilterContext context) { 350 if (mLogVerbose) Log.v(TAG, "Stopping recording"); 351 352 mRecordingActive = false; 353 GLEnvironment glEnv = context.getGLEnvironment(); 354 // The following call will switch the surface_id to 0 355 // (thus, calling eglMakeCurrent on surface with id 0) and 356 // then call eglDestroy on the surface. Hence, this will 357 // call disconnect the SurfaceMediaSource, which is needed to 358 // be called before calling Stop on the mediarecorder 359 if (mLogVerbose) Log.v(TAG, String.format("Unregistering surface %d", mSurfaceId)); 360 glEnv.unregisterSurfaceId(mSurfaceId); 361 try { 362 mMediaRecorder.stop(); 363 } catch (RuntimeException e) { 364 throw new MediaRecorderStopException("MediaRecorder.stop() failed!", e); 365 } 366 mMediaRecorder.release(); 367 mMediaRecorder = null; 368 369 mScreen.release(); 370 mScreen = null; 371 372 // Use an EffectsRecorder callback to forward a media finalization 373 // call so that it creates the video thumbnail, and whatever else needs 374 // to be done to finalize media. 375 if (mRecordingDoneListener != null) { 376 mRecordingDoneListener.onRecordingDone(); 377 } 378 } 379 380 @Override 381 public void close(FilterContext context) { 382 if (mLogVerbose) Log.v(TAG, "Closing"); 383 if (mRecordingActive) stopRecording(context); 384 } 385 386 @Override 387 public void tearDown(FilterContext context) { 388 // Release all the resources associated with the MediaRecorder 389 // and GLFrame members 390 if (mMediaRecorder != null) { 391 mMediaRecorder.release(); 392 } 393 if (mScreen != null) { 394 mScreen.release(); 395 } 396 397 } 398 399} 400