/*
* Copyright 2015 Constant Innovations Inc
*
* 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 com.constantinnovationsinc.livemultimedia.callbacks;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.hardware.Camera;
import android.os.Environment;
import android.util.Log;
import com.constantinnovationsinc.livemultimedia.utilities.ColorSpaceManager;
import com.constantinnovationsinc.livemultimedia.utilities.SharedVideoMemory;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Random;
/**************************************************
* FrameCatcher works for pre-Lollipop API
* This class wrap the Camera Preview Window CallBack
* so that every image is captured in this class.
*****************************************************/
@SuppressWarnings("deprecation")
public class FrameCatcher implements Camera.PreviewCallback {
private static final String TAG = FrameCatcher.class.getCanonicalName();
private final long mExpectedSize;
public Boolean mRecording = false;
public Boolean mStoringVideoFrames = false;
public SharedVideoMemory mVideoBuffer = null;
public int mNumFramesToBuffer = 210;
public int mNumFramesBeforeStartingEncoders = 210;
public int mWidth = -1;
public int mHeight = -1;
public FramesReadyCallback callback;
public String mImageFormat;
public FrameCatcher(long width, long height, String imageFormat, FramesReadyCallback receiver) {
Log.d(TAG, "Passing width,height to FrameCacther: " + String.valueOf(width) + "," + String.valueOf(height));
mExpectedSize = (width * height * 3 / 2);
mWidth = (int) width;
mHeight = (int) height;
mImageFormat = imageFormat;
callback = receiver;
}
/**********************************************************************
* onPreviewFrame receives each frame video frame.
* The frame will be color corrected and then stored in shared memory.
* Both image formats of NV21 or YU12 is supported
*******************************************************************/
@SuppressWarnings("deprecation")
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
if (data == null) {
Log.e(TAG, "Received null video frame data!");
return;
}
if (mExpectedSize != data.length) {
Log.e(TAG, "Bad frame size, got " + data.length + " expected " + mExpectedSize);
return;
}
byte[] value = new byte[data.length];
System.arraycopy(data, 0, value, 0, data.length);
camera.addCallbackBuffer(data);
if (mRecording) {
try {
if (mVideoBuffer == null) {
if (callback != null) {
callback.playLazerSound();
}
mVideoBuffer = new SharedVideoMemory("video", data.length, data.length * mNumFramesToBuffer);
mVideoBuffer.lockMemory();
Log.w(TAG, "Write out first video frame to shared memory!");
if (mImageFormat != null && !mImageFormat.equalsIgnoreCase("UNKNOWN")) {
if ( mImageFormat.equalsIgnoreCase("NV21")) {
ColorSpaceManager.NV21toYUV420SemiPlanar(data, value, mWidth, mHeight);
} else if ( mImageFormat.equalsIgnoreCase("YV12")) {
ColorSpaceManager.YV12toYUV420PackedSemiPlanar(data, value, mWidth, mHeight);
}
}
mVideoBuffer.writeBytes(value, 0, 0, value.length);
mStoringVideoFrames = true;
} else {
if (mVideoBuffer.getFrameCount() == mNumFramesBeforeStartingEncoders) {
startEncodingVideo();
} else if (mVideoBuffer.getFrameCount() <= mNumFramesToBuffer) {
try {
ColorSpaceManager.YV12toYUV420PackedSemiPlanar(data, value, mWidth, mHeight);
mVideoBuffer.writeBytes(value, 0, ((mVideoBuffer.getFrameCount() - 1) * value.length), value.length);
Log.w(TAG, " Frame " + mVideoBuffer.getFrameCount() + "color corrected!");
Log.w(TAG, "Written " + mVideoBuffer.getFrameCount() + " frames written out to shared memory file!");
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, e.toString());
Log.e(TAG, "Error writing out frame number " + mVideoBuffer.getFrameCount());
}
} else {
Log.w(TAG, "Finished writing out video frames!");
mRecording = false;
mStoringVideoFrames = false;
}
}
} catch (IOException e) {
Log.e(TAG, e.getMessage());
}
}
}
public synchronized SharedVideoMemory getSharedMemFile() {
return mVideoBuffer;
}
public synchronized void setRecordingState(Boolean state) {
mRecording = state;
if (mRecording) {
Log.e(TAG, "Recording set to true in FrameCatcher, Runthe encoders!");
}
}
public synchronized Boolean getRecordingState() {
return mRecording;
}
public synchronized Boolean isSavingVideoFrames() {
return mStoringVideoFrames;
}
/**********************************************************************
* getBitmapImageFromYUV returns a bitmap from an image captured in
* the camera in YUV12 format. Image formats and video formats are not
* the same thing.
*******************************************************************/
public static Bitmap getBitmapImageFromYUV(byte[] data, int width, int height) {
YuvImage yuvimage = new YuvImage(data, ImageFormat.NV21, width, height, null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
yuvimage.compressToJpeg(new Rect(0, 0, width, height), 80, baos);
byte[] jdata = baos.toByteArray();
BitmapFactory.Options bitmapFatoryOptions = new BitmapFactory.Options();
bitmapFatoryOptions.inPreferredConfig = Bitmap.Config.RGB_565;
return BitmapFactory.decodeByteArray(jdata, 0, jdata.length, bitmapFatoryOptions);
}
/**********************************************************************
* saveBitmap() was used to debug the code and to save each captured
* video frame into a JPEG bitmap on your mobile device
* use with caution as it will flood your mobile device /Picture dir
*******************************************************************/
public void saveBitmap(Bitmap finalBitmap) throws IllegalStateException {
String root = Environment.getExternalStorageDirectory().toString() + "/Pictures";
Random generator = new Random();
int n = 10000;
n = generator.nextInt(n);
String fname = "Image-" + n + ".jpg";
File file = new File(root, fname);
if (file.exists()) {
boolean status = file.delete();
if (!status) throw new IllegalStateException("Unable to delete File");
}
try {
FileOutputStream out = new FileOutputStream(file);
finalBitmap.compress(Bitmap.CompressFormat.JPEG, 90, out);
out.flush();
out.close();
} catch (IOException e) {
Log.e(TAG, e.getMessage());
}
}
/**********************************************************************
* startEncodingVideo - start the encoding by wraps its classes from
* the catcher as its an implementation detail
*******************************************************************/
public synchronized void startEncodingVideo() {
mRecording = false;
mStoringVideoFrames = false;
if (callback != null) {
callback.startVideoEncoder();
}
}
}