| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| |
| package android.filterpacks.videosink; |
| |
| import android.filterfw.core.Filter; |
| import android.filterfw.core.FilterContext; |
| import android.filterfw.core.Frame; |
| import android.filterfw.core.FrameFormat; |
| import android.filterfw.core.GenerateFieldPort; |
| import android.filterfw.core.GLFrame; |
| import android.filterfw.core.MutableFrameFormat; |
| import android.filterfw.core.ShaderProgram; |
| import android.filterfw.format.ImageFormat; |
| import android.filterfw.geometry.Point; |
| import android.filterfw.geometry.Quad; |
| import android.media.MediaRecorder; |
| import android.media.CamcorderProfile; |
| import android.filterfw.core.GLEnvironment; |
| |
| import java.io.IOException; |
| import java.io.FileDescriptor; |
| |
| import android.util.Log; |
| |
| /** @hide */ |
| public class MediaEncoderFilter extends Filter { |
| |
| /** User-visible parameters */ |
| |
| /** Recording state. When set to false, recording will stop, or will not |
| * start if not yet running the graph. Instead, frames are simply ignored. |
| * When switched back to true, recording will restart. This allows a single |
| * graph to both provide preview and to record video. If this is false, |
| * recording settings can be updated while the graph is running. |
| */ |
| @GenerateFieldPort(name = "recording", hasDefault = true) |
| private boolean mRecording = true; |
| |
| /** Filename to save the output. */ |
| @GenerateFieldPort(name = "outputFile", hasDefault = true) |
| private String mOutputFile = new String("/sdcard/MediaEncoderOut.mp4"); |
| |
| /** File Descriptor to save the output. */ |
| @GenerateFieldPort(name = "outputFileDescriptor", hasDefault = true) |
| private FileDescriptor mFd = null; |
| |
| /** Input audio source. If not set, no audio will be recorded. |
| * Select from the values in MediaRecorder.AudioSource |
| */ |
| @GenerateFieldPort(name = "audioSource", hasDefault = true) |
| private int mAudioSource = NO_AUDIO_SOURCE; |
| |
| /** Media recorder info listener, which needs to implement |
| * MediaRecorder.OnInfoListener. Set this to receive notifications about |
| * recording events. |
| */ |
| @GenerateFieldPort(name = "infoListener", hasDefault = true) |
| private MediaRecorder.OnInfoListener mInfoListener = null; |
| |
| /** Media recorder error listener, which needs to implement |
| * MediaRecorder.OnErrorListener. Set this to receive notifications about |
| * recording errors. |
| */ |
| @GenerateFieldPort(name = "errorListener", hasDefault = true) |
| private MediaRecorder.OnErrorListener mErrorListener = null; |
| |
| /** Media recording done callback, which needs to implement OnRecordingDoneListener. |
| * Set this to finalize media upon completion of media recording. |
| */ |
| @GenerateFieldPort(name = "recordingDoneListener", hasDefault = true) |
| private OnRecordingDoneListener mRecordingDoneListener = null; |
| |
| /** Orientation hint. Used for indicating proper video playback orientation. |
| * Units are in degrees of clockwise rotation, valid values are (0, 90, 180, |
| * 270). |
| */ |
| @GenerateFieldPort(name = "orientationHint", hasDefault = true) |
| private int mOrientationHint = 0; |
| |
| /** Camcorder profile to use. Select from the profiles available in |
| * android.media.CamcorderProfile. If this field is set, it overrides |
| * settings to width, height, framerate, outputFormat, and videoEncoder. |
| */ |
| @GenerateFieldPort(name = "recordingProfile", hasDefault = true) |
| private CamcorderProfile mProfile = null; |
| |
| /** Frame width to be encoded, defaults to 320. |
| * Actual received frame size has to match this */ |
| @GenerateFieldPort(name = "width", hasDefault = true) |
| private int mWidth = 0; |
| |
| /** Frame height to to be encoded, defaults to 240. |
| * Actual received frame size has to match */ |
| @GenerateFieldPort(name = "height", hasDefault = true) |
| private int mHeight = 0; |
| |
| /** Stream framerate to encode the frames at. |
| * By default, frames are encoded at 30 FPS*/ |
| @GenerateFieldPort(name = "framerate", hasDefault = true) |
| private int mFps = 30; |
| |
| /** The output format to encode the frames in. |
| * Choose an output format from the options in |
| * android.media.MediaRecorder.OutputFormat */ |
| @GenerateFieldPort(name = "outputFormat", hasDefault = true) |
| private int mOutputFormat = MediaRecorder.OutputFormat.MPEG_4; |
| |
| /** The videoencoder to encode the frames with. |
| * Choose a videoencoder from the options in |
| * android.media.MediaRecorder.VideoEncoder */ |
| @GenerateFieldPort(name = "videoEncoder", hasDefault = true) |
| private int mVideoEncoder = MediaRecorder.VideoEncoder.H264; |
| |
| /** The input region to read from the frame. The corners of this quad are |
| * mapped to the output rectangle. The input frame ranges from (0,0)-(1,1), |
| * top-left to bottom-right. The corners of the quad are specified in the |
| * order bottom-left, bottom-right, top-left, top-right. |
| */ |
| @GenerateFieldPort(name = "inputRegion", hasDefault = true) |
| private Quad mSourceRegion; |
| |
| /** The maximum filesize (in bytes) of the recording session. |
| * By default, it will be 0 and will be passed on to the MediaRecorder. |
| * If the limit is zero or negative, MediaRecorder will disable the limit*/ |
| @GenerateFieldPort(name = "maxFileSize", hasDefault = true) |
| private long mMaxFileSize = 0; |
| |
| /** The maximum duration (in milliseconds) of the recording session. |
| * By default, it will be 0 and will be passed on to the MediaRecorder. |
| * If the limit is zero or negative, MediaRecorder will record indefinitely*/ |
| @GenerateFieldPort(name = "maxDurationMs", hasDefault = true) |
| private int mMaxDurationMs = 0; |
| |
| /** TimeLapse Interval between frames. |
| * By default, it will be 0. Whether the recording is timelapsed |
| * is inferred based on its value being greater than 0 */ |
| @GenerateFieldPort(name = "timelapseRecordingIntervalUs", hasDefault = true) |
| private long mTimeBetweenTimeLapseFrameCaptureUs = 0; |
| |
| // End of user visible parameters |
| |
| private static final int NO_AUDIO_SOURCE = -1; |
| |
| private int mSurfaceId; |
| private ShaderProgram mProgram; |
| private GLFrame mScreen; |
| |
| private boolean mRecordingActive = false; |
| private long mTimestampNs = 0; |
| private long mLastTimeLapseFrameRealTimestampNs = 0; |
| private int mNumFramesEncoded = 0; |
| // Used to indicate whether recording is timelapsed. |
| // Inferred based on (mTimeBetweenTimeLapseFrameCaptureUs > 0) |
| private boolean mCaptureTimeLapse = false; |
| |
| private boolean mLogVerbose; |
| private static final String TAG = "MediaEncoderFilter"; |
| |
| // Our hook to the encoder |
| private MediaRecorder mMediaRecorder; |
| |
| /** Callback to be called when media recording completes. */ |
| |
| public interface OnRecordingDoneListener { |
| public void onRecordingDone(); |
| } |
| |
| public MediaEncoderFilter(String name) { |
| super(name); |
| Point bl = new Point(0, 0); |
| Point br = new Point(1, 0); |
| Point tl = new Point(0, 1); |
| Point tr = new Point(1, 1); |
| mSourceRegion = new Quad(bl, br, tl, tr); |
| mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE); |
| } |
| |
| @Override |
| public void setupPorts() { |
| // Add input port- will accept RGBA GLFrames |
| addMaskedInputPort("videoframe", ImageFormat.create(ImageFormat.COLORSPACE_RGBA, |
| FrameFormat.TARGET_GPU)); |
| } |
| |
| @Override |
| public void fieldPortValueUpdated(String name, FilterContext context) { |
| if (mLogVerbose) Log.v(TAG, "Port " + name + " has been updated"); |
| if (name.equals("recording")) return; |
| if (name.equals("inputRegion")) { |
| if (isOpen()) updateSourceRegion(); |
| return; |
| } |
| // TODO: Not sure if it is possible to update the maxFileSize |
| // when the recording is going on. For now, not doing that. |
| if (isOpen() && mRecordingActive) { |
| throw new RuntimeException("Cannot change recording parameters" |
| + " when the filter is recording!"); |
| } |
| } |
| |
| private void updateSourceRegion() { |
| // Flip source quad to map to OpenGL origin |
| Quad flippedRegion = new Quad(); |
| flippedRegion.p0 = mSourceRegion.p2; |
| flippedRegion.p1 = mSourceRegion.p3; |
| flippedRegion.p2 = mSourceRegion.p0; |
| flippedRegion.p3 = mSourceRegion.p1; |
| mProgram.setSourceRegion(flippedRegion); |
| } |
| |
| // update the MediaRecorderParams based on the variables. |
| // These have to be in certain order as per the MediaRecorder |
| // documentation |
| private void updateMediaRecorderParams() { |
| mCaptureTimeLapse = mTimeBetweenTimeLapseFrameCaptureUs > 0; |
| final int GRALLOC_BUFFER = 2; |
| mMediaRecorder.setVideoSource(GRALLOC_BUFFER); |
| if (!mCaptureTimeLapse && (mAudioSource != NO_AUDIO_SOURCE)) { |
| mMediaRecorder.setAudioSource(mAudioSource); |
| } |
| if (mProfile != null) { |
| mMediaRecorder.setProfile(mProfile); |
| mFps = mProfile.videoFrameRate; |
| // If width and height are set larger than 0, then those |
| // overwrite the ones in the profile. |
| if (mWidth > 0 && mHeight > 0) { |
| mMediaRecorder.setVideoSize(mWidth, mHeight); |
| } |
| } else { |
| mMediaRecorder.setOutputFormat(mOutputFormat); |
| mMediaRecorder.setVideoEncoder(mVideoEncoder); |
| mMediaRecorder.setVideoSize(mWidth, mHeight); |
| mMediaRecorder.setVideoFrameRate(mFps); |
| } |
| mMediaRecorder.setOrientationHint(mOrientationHint); |
| mMediaRecorder.setOnInfoListener(mInfoListener); |
| mMediaRecorder.setOnErrorListener(mErrorListener); |
| if (mFd != null) { |
| mMediaRecorder.setOutputFile(mFd); |
| } else { |
| mMediaRecorder.setOutputFile(mOutputFile); |
| } |
| try { |
| mMediaRecorder.setMaxFileSize(mMaxFileSize); |
| } catch (Exception e) { |
| // Following the logic in VideoCamera.java (in Camera app) |
| // We are going to ignore failure of setMaxFileSize here, as |
| // a) The composer selected may simply not support it, or |
| // b) The underlying media framework may not handle 64-bit range |
| // on the size restriction. |
| Log.w(TAG, "Setting maxFileSize on MediaRecorder unsuccessful! " |
| + e.getMessage()); |
| } |
| mMediaRecorder.setMaxDuration(mMaxDurationMs); |
| } |
| |
| @Override |
| public void prepare(FilterContext context) { |
| if (mLogVerbose) Log.v(TAG, "Preparing"); |
| |
| mProgram = ShaderProgram.createIdentity(context); |
| |
| mRecordingActive = false; |
| } |
| |
| @Override |
| public void open(FilterContext context) { |
| if (mLogVerbose) Log.v(TAG, "Opening"); |
| updateSourceRegion(); |
| if (mRecording) startRecording(context); |
| } |
| |
| private void startRecording(FilterContext context) { |
| if (mLogVerbose) Log.v(TAG, "Starting recording"); |
| |
| // Create a frame representing the screen |
| MutableFrameFormat screenFormat = new MutableFrameFormat( |
| FrameFormat.TYPE_BYTE, FrameFormat.TARGET_GPU); |
| screenFormat.setBytesPerSample(4); |
| |
| int width, height; |
| boolean widthHeightSpecified = mWidth > 0 && mHeight > 0; |
| // If width and height are specified, then use those instead |
| // of that in the profile. |
| if (mProfile != null && !widthHeightSpecified) { |
| width = mProfile.videoFrameWidth; |
| height = mProfile.videoFrameHeight; |
| } else { |
| width = mWidth; |
| height = mHeight; |
| } |
| screenFormat.setDimensions(width, height); |
| mScreen = (GLFrame)context.getFrameManager().newBoundFrame( |
| screenFormat, GLFrame.EXISTING_FBO_BINDING, 0); |
| |
| // Initialize the media recorder |
| |
| mMediaRecorder = new MediaRecorder(); |
| updateMediaRecorderParams(); |
| |
| try { |
| mMediaRecorder.prepare(); |
| } catch (IllegalStateException e) { |
| throw e; |
| } catch (IOException e) { |
| throw new RuntimeException("IOException in" |
| + "MediaRecorder.prepare()!", e); |
| } catch (Exception e) { |
| throw new RuntimeException("Unknown Exception in" |
| + "MediaRecorder.prepare()!", e); |
| } |
| // Make sure start() is called before trying to |
| // register the surface. The native window handle needed to create |
| // the surface is initiated in start() |
| mMediaRecorder.start(); |
| if (mLogVerbose) Log.v(TAG, "Open: registering surface from Mediarecorder"); |
| mSurfaceId = context.getGLEnvironment(). |
| registerSurfaceFromMediaRecorder(mMediaRecorder); |
| mNumFramesEncoded = 0; |
| mRecordingActive = true; |
| } |
| |
| public boolean skipFrameAndModifyTimestamp(long timestampNs) { |
| // first frame- encode. Don't skip |
| if (mNumFramesEncoded == 0) { |
| mLastTimeLapseFrameRealTimestampNs = timestampNs; |
| mTimestampNs = timestampNs; |
| if (mLogVerbose) Log.v(TAG, "timelapse: FIRST frame, last real t= " |
| + mLastTimeLapseFrameRealTimestampNs + |
| ", setting t = " + mTimestampNs ); |
| return false; |
| } |
| |
| // Workaround to bypass the first 2 input frames for skipping. |
| // The first 2 output frames from the encoder are: decoder specific info and |
| // the compressed video frame data for the first input video frame. |
| if (mNumFramesEncoded >= 2 && timestampNs < |
| (mLastTimeLapseFrameRealTimestampNs + 1000L * mTimeBetweenTimeLapseFrameCaptureUs)) { |
| // If 2 frames have been already encoded, |
| // Skip all frames from last encoded frame until |
| // sufficient time (mTimeBetweenTimeLapseFrameCaptureUs) has passed. |
| if (mLogVerbose) Log.v(TAG, "timelapse: skipping intermediate frame"); |
| return true; |
| } else { |
| // Desired frame has arrived after mTimeBetweenTimeLapseFrameCaptureUs time: |
| // - Reset mLastTimeLapseFrameRealTimestampNs to current time. |
| // - Artificially modify timestampNs to be one frame time (1/framerate) ahead |
| // of the last encoded frame's time stamp. |
| if (mLogVerbose) Log.v(TAG, "timelapse: encoding frame, Timestamp t = " + timestampNs + |
| ", last real t= " + mLastTimeLapseFrameRealTimestampNs + |
| ", interval = " + mTimeBetweenTimeLapseFrameCaptureUs); |
| mLastTimeLapseFrameRealTimestampNs = timestampNs; |
| mTimestampNs = mTimestampNs + (1000000000L / (long)mFps); |
| if (mLogVerbose) Log.v(TAG, "timelapse: encoding frame, setting t = " |
| + mTimestampNs + ", delta t = " + (1000000000L / (long)mFps) + |
| ", fps = " + mFps ); |
| return false; |
| } |
| } |
| |
| @Override |
| public void process(FilterContext context) { |
| GLEnvironment glEnv = context.getGLEnvironment(); |
| // Get input frame |
| Frame input = pullInput("videoframe"); |
| |
| // Check if recording needs to start |
| if (!mRecordingActive && mRecording) { |
| startRecording(context); |
| } |
| // Check if recording needs to stop |
| if (mRecordingActive && !mRecording) { |
| stopRecording(context); |
| } |
| |
| if (!mRecordingActive) return; |
| |
| if (mCaptureTimeLapse) { |
| if (skipFrameAndModifyTimestamp(input.getTimestamp())) { |
| return; |
| } |
| } else { |
| mTimestampNs = input.getTimestamp(); |
| } |
| |
| // Activate our surface |
| glEnv.activateSurfaceWithId(mSurfaceId); |
| |
| // Process |
| mProgram.process(input, mScreen); |
| |
| // Set timestamp from input |
| glEnv.setSurfaceTimestamp(mTimestampNs); |
| // And swap buffers |
| glEnv.swapBuffers(); |
| mNumFramesEncoded++; |
| } |
| |
| private void stopRecording(FilterContext context) { |
| if (mLogVerbose) Log.v(TAG, "Stopping recording"); |
| |
| mRecordingActive = false; |
| mNumFramesEncoded = 0; |
| GLEnvironment glEnv = context.getGLEnvironment(); |
| // The following call will switch the surface_id to 0 |
| // (thus, calling eglMakeCurrent on surface with id 0) and |
| // then call eglDestroy on the surface. Hence, this will |
| // call disconnect the SurfaceMediaSource, which is needed to |
| // be called before calling Stop on the mediarecorder |
| if (mLogVerbose) Log.v(TAG, String.format("Unregistering surface %d", mSurfaceId)); |
| glEnv.unregisterSurfaceId(mSurfaceId); |
| try { |
| mMediaRecorder.stop(); |
| } catch (RuntimeException e) { |
| throw new MediaRecorderStopException("MediaRecorder.stop() failed!", e); |
| } |
| mMediaRecorder.release(); |
| mMediaRecorder = null; |
| |
| mScreen.release(); |
| mScreen = null; |
| |
| // Use an EffectsRecorder callback to forward a media finalization |
| // call so that it creates the video thumbnail, and whatever else needs |
| // to be done to finalize media. |
| if (mRecordingDoneListener != null) { |
| mRecordingDoneListener.onRecordingDone(); |
| } |
| } |
| |
| @Override |
| public void close(FilterContext context) { |
| if (mLogVerbose) Log.v(TAG, "Closing"); |
| if (mRecordingActive) stopRecording(context); |
| } |
| |
| @Override |
| public void tearDown(FilterContext context) { |
| // Release all the resources associated with the MediaRecorder |
| // and GLFrame members |
| if (mMediaRecorder != null) { |
| mMediaRecorder.release(); |
| } |
| if (mScreen != null) { |
| mScreen.release(); |
| } |
| |
| } |
| |
| } |