/**
*
* This file contains code from the IOCipher Camera Library "CipherCam".
*
* For more information about IOCipher, see https://guardianproject.info/code/iocipher
* and this sample library: https://github.com/n8fr8/IOCipherCameraExample
*
* IOCipher Camera Sample is distributed under this license (aka the 3-clause BSD license)
*
* @author n8fr8
*
*/
package info.guardianproject.iocipher.camera;
import info.guardianproject.iocipher.File;
import info.guardianproject.iocipher.FileInputStream;
import info.guardianproject.iocipher.FileOutputStream;
import info.guardianproject.iocipher.camera.encoders.AACHelper;
import info.guardianproject.iocipher.camera.encoders.ImageToMJPEGMOVMuxer;
import info.guardianproject.iocipher.camera.io.IOCipherFileChannelWrapper;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import org.jcodec.common.ArrayUtil;
import org.jcodec.common.SeekableByteChannel;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.provider.MediaStore;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast;
public class VideoCameraActivity extends CameraBaseActivity {
private final static String LOG = "VideoJPEGRecorder";
private String mFileBasePath = null;
private boolean mIsRecording = false;
private ArrayDeque<VideoFrame> mFrameQ = null;
private int mLastWidth = -1;
private int mLastHeight = -1;
private int mPreviewFormat = -1;
private ImageToMJPEGMOVMuxer muxer;
private AACHelper aac;
private boolean useAAC = false;
private byte[] audioData;
private AudioRecord audioRecord;
private boolean mPreCompressFrames = true;
private OutputStream outputStreamAudio;
private info.guardianproject.iocipher.File fileAudio;
private int mFpsCounter = 0;
private long start = 0;
private long lastTime = 0;
private int mFramesTotal = 0;
private int mFPS = 15; //default is 15fps
private boolean isRequest = false;
private ArrayList<String> mResultList = null;
private boolean mInTopHalf = false;
private info.guardianproject.iocipher.File fileOut;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mFileBasePath = getIntent().getStringExtra("basepath");
isRequest = getIntent().getAction() != null && getIntent().getAction().equals(MediaStore.ACTION_VIDEO_CAPTURE);
mResultList = new ArrayList<String>();
}
@Override
public void onPause() {
super.onPause();
if (mIsRecording)
stopRecording();
}
@Override
protected int getLayout()
{
return R.layout.base_camera;
}
private float mDownX = -1,mLastX = -1;
private float mDownY = -1,mLastY=-1;
private boolean mIsOnClick = false;
private final float SCROLL_THRESHOLD = 10;
@Override
public boolean onTouch(View v, MotionEvent ev) {
//if short tap then take a picture
//if long and hold then start video, then end on release
//if location is on top half then front camera, on bottom have then back camera
switch (ev.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
mDownX = ev.getX();
mDownY = ev.getY();
mIsOnClick = true;
if (!mIsRecording)
handler.postDelayed(mLongPressed, 1000);
mInTopHalf = mDownY < (mLastHeight/2);
toggleCamera(mInTopHalf);
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
handler.removeCallbacks(mLongPressed);
if (!mIsRecording) {
try {
//take a picture
mPreviewing = false;
camera.takePicture(null, null, this);
}
catch (RuntimeException re)
{
//hardware failure of take picture
}
}
else
{
stopRecording();
}
break;
case MotionEvent.ACTION_MOVE:
mLastX = ev.getX();
mLastY = ev.getY();
mInTopHalf = mLastY < (mDownY-100);
toggleCamera(mInTopHalf);
if (mIsOnClick && (Math.abs(mDownX - ev.getX()) > SCROLL_THRESHOLD || Math.abs(mDownY - ev.getY()) > SCROLL_THRESHOLD)) {
mIsOnClick = false;
}
break;
default:
break;
}
return true;
}
final Handler handler = new Handler();
Runnable mLongPressed = new Runnable() {
public void run() {
Log.i("", "Long press!");
startRecording();
}
};
@Override
public void onClick(View view) {
if (view == button)
{
if (!mIsRecording)
{
startRecording();
}
else
{
stopRecording ();
}
}
}
private void startRecording ()
{
mFrameQ = new ArrayDeque<VideoFrame>();
mFramesTotal = 0;
mFpsCounter = 0;
lastTime = System.currentTimeMillis();
String fileName = "camerav_video_" + new java.util.Date().getTime() + ".mp4";
fileOut = new info.guardianproject.iocipher.File(mFileBasePath,fileName);
mResultList.add(fileOut.getAbsolutePath());
Intent intentResult = new Intent().putExtra(MediaStore.EXTRA_OUTPUT, mResultList.toArray(new String[mResultList.size()]));
setResult(Activity.RESULT_OK, intentResult);
try {
mIsRecording = true;
if (useAAC)
initAudio(fileOut.getAbsolutePath()+".aac");
else
initAudio(fileOut.getAbsolutePath()+".pcm");
boolean withEmbeddedAudio = true;
Encoder encoder = new Encoder(fileOut,mFPS,withEmbeddedAudio);
encoder.start();
//start capture
startAudioRecording();
progress.setText("[REC]");
} catch (Exception e) {
Log.d("Video","error starting video",e);
Toast.makeText(this, "Error init'ing video: " + e.getLocalizedMessage(), Toast.LENGTH_LONG).show();
finish();
}
}
private void stopRecording ()
{
progress.setText("[SAVING]");
h.sendEmptyMessageDelayed(1, 2000);
progress.setText("");
}
private void toggleCamera (boolean isSelfie)
{
if (isSelfie != mIsSelfie)
{
mIsSelfie = isSelfie;
releaseCamera();
initCamera();
}
}
//support still pictures if you tap on the screen
@Override
public void onPictureTaken(final byte[] data, Camera camera) {
File fileSecurePicture;
try {
overlayView.setBackgroundResource(R.color.flash);
long mTime = System.currentTimeMillis();
fileSecurePicture = new File(mFileBasePath,"camerav_image_" + mTime + ".jpg");
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(fileSecurePicture));
out.write(data);
out.flush();
out.close();
mResultList.add(fileSecurePicture.getAbsolutePath());
Intent intentResult = new Intent().putExtra(MediaStore.EXTRA_OUTPUT, mResultList.toArray(new String[mResultList.size()]));
setResult(Activity.RESULT_OK, intentResult);
view.postDelayed(new Runnable()
{
@Override
public void run() {
overlayView.setBackgroundColor(Color.TRANSPARENT);
resumePreview();
}
},100);
} catch (Exception e) {
e.printStackTrace();
setResult(Activity.RESULT_CANCELED);
}
}
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
//even when not recording, we'll compress frames in order to estimate our FPS
Camera.Parameters parameters = camera.getParameters();
mLastWidth = parameters.getPreviewSize().width;
mLastHeight = parameters.getPreviewSize().height;
if (mRotation > 0) //flip height and width
{
mLastWidth =parameters.getPreviewSize().height;
mLastHeight =parameters.getPreviewSize().width;
}
mPreviewFormat = parameters.getPreviewFormat();
byte[] dataResult = data;
if (mPreCompressFrames)
{
if (mRotation > 0)
{
dataResult = rotateYUV420Degree90(data,mLastHeight,mLastWidth);
if (getCameraDirection() == CameraInfo.CAMERA_FACING_FRONT)
{
dataResult = rotateYUV420Degree90(dataResult,mLastWidth,mLastHeight);
dataResult = rotateYUV420Degree90(dataResult,mLastHeight,mLastWidth);
}
}
YuvImage yuv = new YuvImage(dataResult, mPreviewFormat, mLastWidth, mLastHeight, null);
ByteArrayOutputStream out = new ByteArrayOutputStream();
yuv.compressToJpeg(new Rect(0, 0, mLastWidth, mLastHeight), MediaConstants.sJpegQuality, out);
dataResult = out.toByteArray();
}
if (mFramesTotal == 0 && fileOut != null)
{
try {
info.guardianproject.iocipher.FileOutputStream fosThumb = new info.guardianproject.iocipher.FileOutputStream(new info.guardianproject.iocipher.File(fileOut.getAbsolutePath() + ".thumb.jpg"));
fosThumb.write(dataResult);
fosThumb.flush();
fosThumb.close();
} catch (Exception e) {
Log.e("VideoCam","can't save thumb",e);
}
}
if (mIsRecording && mFrameQ != null)
if (data != null)
{
VideoFrame vf = new VideoFrame();
vf.image = dataResult;
vf.duration = 1;//this is frame duration, not time //System.currentTimeMillis() - lastTime;
vf.fps = mFPS;
mFrameQ.add(vf);
mFramesTotal++;
}
mFpsCounter++;
if((System.currentTimeMillis() - start) >= 1000) {
mFPS = mFpsCounter;
mFpsCounter = 0;
start = System.currentTimeMillis();
}
}
private byte[] rotateYUV420Degree90(byte[] data, int imageWidth, int imageHeight)
{
byte [] yuv = new byte[imageWidth*imageHeight*3/2];
// Rotate the Y luma
int i = 0;
for(int x = 0;x < imageWidth;x++)
{
for(int y = imageHeight-1;y >= 0;y--)
{
yuv[i] = data[y*imageWidth+x];
i++;
}
}
// Rotate the U and V color components
i = imageWidth*imageHeight*3/2-1;
for(int x = imageWidth-1;x > 0;x=x-2)
{
for(int y = 0;y < imageHeight/2;y++)
{
yuv[i] = data[(imageWidth*imageHeight)+(y*imageWidth)+x];
i--;
yuv[i] = data[(imageWidth*imageHeight)+(y*imageWidth)+(x-1)];
i--;
}
}
return yuv;
}
private class VideoFrame
{
byte[] image;
long fps;
long duration;
}
private class Encoder extends Thread {
private static final String TAG = "ENCODER";
private File fileOut;
private FileOutputStream fos;
public Encoder (File fileOut, int baseFramesPerSecond, boolean withEmbeddedAudio) throws IOException
{
this.fileOut = fileOut;
fos = new info.guardianproject.iocipher.FileOutputStream(fileOut);
SeekableByteChannel sbc = new IOCipherFileChannelWrapper(fos.getChannel());
org.jcodec.common.AudioFormat af = null;
if (withEmbeddedAudio)
af = new org.jcodec.common.AudioFormat(org.jcodec.common.AudioFormat.MONO_S16_LE(MediaConstants.sAudioSampleRate));
muxer = new ImageToMJPEGMOVMuxer(sbc,af,baseFramesPerSecond);
}
public void run ()
{
try {
while (mIsRecording || (!mFrameQ.isEmpty()))
{
if (mFrameQ.peek() != null)
{
VideoFrame vf = mFrameQ.pop();
muxer.addFrame(mLastWidth, mLastHeight, ByteBuffer.wrap(vf.image),vf.fps,vf.duration);
}
}
//now write audio
FileInputStream fis = new FileInputStream(fileAudio);
byte[] audioBuffer = new byte[1024*32];
int bytesRead = -1;
while ((bytesRead = fis.read(audioBuffer))!=-1)
{
muxer.addAudio(ByteBuffer.wrap(audioBuffer, 0, bytesRead));
}
muxer.finish();
fis.close();
// fos.close();
} catch (Exception e) {
Log.e(TAG, "IO", e);
}
}
}
Handler h = new Handler ()
{
@Override
public void handleMessage(Message msg) {
// TODO Auto-generated method stub
super.handleMessage(msg);
if (msg.what == 0)
{
int frames = msg.getData().getInt("frames");
if (!mIsRecording)
if (frames == 0)
progress.setText("");
else
progress.setText("Processing: " + (mFramesTotal-frames) + '/' + mFramesTotal);
else
progress.setText("Recording: " + mFramesTotal);
}
else if (msg.what == 1)
{
mIsRecording = false; //stop recording
if (aac != null)
aac.stopRecording();
}
}
};
private void initAudio(final String audioPath) throws Exception {
fileAudio = new File(audioPath);
outputStreamAudio = new BufferedOutputStream(new info.guardianproject.iocipher.FileOutputStream(fileAudio),8192*8);
if (useAAC)
{
aac = new AACHelper();
aac.setEncoder(MediaConstants.sAudioSampleRate, MediaConstants.sAudioChannels, MediaConstants.sAudioBitRate);
}
else
{
int minBufferSize = AudioRecord.getMinBufferSize(MediaConstants.sAudioSampleRate,
MediaConstants.sChannelConfigIn,
AudioFormat.ENCODING_PCM_16BIT)*8;
audioData = new byte[minBufferSize];
int audioSource = MediaRecorder.AudioSource.CAMCORDER;
if (this.getCameraDirection() == CameraInfo.CAMERA_FACING_FRONT)
{
audioSource = MediaRecorder.AudioSource.MIC;
}
audioRecord = new AudioRecord(audioSource,
MediaConstants.sAudioSampleRate,
MediaConstants.sChannelConfigIn,
AudioFormat.ENCODING_PCM_16BIT,
minBufferSize);
}
}
private void startAudioRecording ()
{
Thread thread = new Thread ()
{
public void run ()
{
if (useAAC)
{
try {
aac.startRecording(outputStreamAudio);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
else
{
audioRecord.startRecording();
while(mIsRecording){
int audioDataBytes = audioRecord.read(audioData, 0, audioData.length);
if (AudioRecord.ERROR_INVALID_OPERATION != audioDataBytes
&& outputStreamAudio != null) {
try {
outputStreamAudio.write(audioData,0,audioDataBytes);
} catch (IOException e) {
e.printStackTrace();
}
}
}
audioRecord.stop();
try {
outputStreamAudio.flush();
outputStreamAudio.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
};
thread.start();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
mIsRecording = false;
super.onConfigurationChanged(newConfig);
}
}