📜 ⬆️ ⬇️

Shooting Time Lapse video for Android

image Let's write a program to create your own movies in the Time Lapse technique. A fascinating video shot in this technique from the ISS can be viewed here , a more affordable version, which can be repeated using the described program - here .

The program has a simple interface and simple principle of operation:
• the user sets the frequency of images with an integrated camera (for example, 10 s) and the desired frame rate of the generated video (for example, 25 frames per second);
• after pressing the “Start” button, the program takes a photo every 10 seconds and writes a jpg file to the SD card;
• the procedure is repeated until the “Stop” and “Create video” buttons are clicked, after which the sequence of photos is converted into a Motion JPEG video file, which shows the footage 250 times (25 * 10) faster than the actual speed of the events.

The program has two main classes - MainActivity , which deals with user interaction and the accumulation of images, and the MJPEGGenerator , which is responsible for turning a sequence of images into a video file.
')
The MJPEGGenerator class, taken from code.google.com , was slightly reworked due to the fact that Android Java is missing the java.awt package.

Procedures for working with the camera were mainly taken from the material Working with the camera in Android , where there is a good description of the applied solutions, the problem of “sticking” the camera after the Android device lock / unlock was eliminated thanks to stackoverflow .

The program was debugged on the Prestigio MultiPad 7.0 Prime tablet for Android 4.0.


image


Let us consider in more detail the work of the individual components of the program, not related to the camera itself.

In order to prevent the screen from turning off, the PowerManager.WakeLock mechanism was applied:

private PowerManager.WakeLock wl; public void onCreate(Bundle savedInstanceState) { ... PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); wl = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, "DoNotDimScreen"); } 


The frequency of starting the camera is controlled by the timer:

 Timer updateTimer = new Timer(); ... updateTimer = new Timer(); updateTimer.scheduleAtFixedRate(new TimerTask() { public void run() { if ((camera != null) && (workMode == 1)) { camera.takePicture(null, null, null, MainActivity.this); }} }, 0, capturePeriod * 1000); 


Before starting, check the availability of the SD card:

 if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) Toast.makeText(MainActivity.this, "Please mount SD card", Toast.LENGTH_LONG).show(); 


The data entered by the user is pre-processed: commas entered as a decimal separator are replaced by a period; checks for non-numeric values; values ​​of the entered period and frame rate are checked for entry into the allowable range:

 periodEditText.addTextChangedListener(new TextWatcher() { public void afterTextChanged(Editable s) { if (periodEditText.getText().toString().length() == 0) capturePeriod = 0; else { if (isNum(periodEditText.getText().toString().replace(',', '.'))) { float a = Float.valueOf(periodEditText.getText().toString().replace(',', '.')); capturePeriod = (int) a; } else Toast.makeText(MainActivity.this, periodEditText.getText().toString() + " - not a digit.", Toast.LENGTH_LONG).show(); }} ... if ((fps < FPSMIN) || (fps > FPSMAX)) Toast.makeText(MainActivity.this, "FPS should be " + FPSMIN + " to " + FPSMAX + " frames per second", Toast.LENGTH_LONG).show(); 


At the beginning of work, all the * .jpg files remaining from the previous session are deleted from the program folder:

 String sdPath = Environment.getExternalStorageDirectory().getPath() + "/TimeLapseFolder/"; File saveDir = new File(sdPath); if (saveDir.isDirectory()) { String[] children = saveDir.list(); for (int i = 0; i < children.length; i++) { if (children[i].endsWith(".jpg")) new File(saveDir, children[i]).delete(); }} saveDir.delete(); 


After shooting the next frame, the user is shown the remaining space on the map:

 modeText.setText("Work mode: capturing, " + String.valueOf(roundOneDecimal(megAvailable)) + " Mbyte available on SD card"); 


Actually video generation:

 generator = new MJPEGGenerator(videofile, aviWidth, aviHeight, fps, lastPicture); for (int addpic = 1; addpic <= lastPicture; addpic++) { String numWithZeroes = intToString(addpic, 7); String curjpg = sdPath + numWithZeroes + ".jpg"; publishProgress(numWithZeroes); if (DEBUG) Log.v(TAG, "Rendering jpg sdPath = " + curjpg); Bitmap bmp = BitmapFactory.decodeFile(curjpg); generator.addImage(bmp); } 


MJPEGGenerator starts with the following parameters:
videofile - the name of the video file is taken every time a new one, numbered by the mask TimeLapseMovieXXX.avi, in order to save the files previously shot;
aviWidth, aviHeight - taken from the properties of the camera;
fps - user defined;
lastPicture - the number of the last picture taken.

In order not to suspend the user interface, video generation is launched in a separate AsyncTask stream interacting with the GUI via onProgressUpdate .

The contents of the key files are shown below, the project archive can be downloaded from sourceforge.net .

MainActivity.java
 package com.sample.timelapse; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.text.DecimalFormat; import java.util.Arrays; import java.util.Timer; import java.util.TimerTask; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.graphics.Point; import android.hardware.Camera; import android.hardware.Camera.Size; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.PowerManager; import android.os.StatFs; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; import android.util.TypedValue; import android.view.Display; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.view.View.OnFocusChangeListener; import android.view.ViewGroup.LayoutParams; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; public class MainActivity extends Activity implements SurfaceHolder.Callback, View.OnClickListener, Camera.PictureCallback, Camera.PreviewCallback { private MJPEGGenerator generator; Timer updateTimer = new Timer(); // Main shoot timer private static final int PERIODMIN = 2; // Seconds private static final int PERIODMAX = 1000; // Seconds private static final int FPSMIN = 2; private static final int FPSMAX = 30; int aviHeight = 0; // Dimensions of final video int aviWidth = 0; // Work mode // 0: Ready to start // 1: Capturing photos // 2: Ready to create video // 3: Create video private int workMode = 0; private int capturePeriod = 0; private int fps = 0; private Camera camera; private SurfaceHolder surfaceHolder; private SurfaceView preview; private static int LOGLEVEL = 2; // Set logging level private static boolean DEBUG = LOGLEVEL > 1; @SuppressWarnings("unused") private static boolean WARNING = LOGLEVEL > 0; public static final String PREFS_NAME = "MyPrefsFile"; // For save and restore preferences private static final String TAG = "MainActivity"; // Set logging tag int lastPicture = 0; // Current picture counter int lastVideo = 0; // Current video file counter int sWidth = 0; // Screen width int sHeight = 0; // Screen height int prevsWidth = 1; // Previous screen width (after previous onWindowFocusChanged) int prevsHeight = 1; // Previous screen height (after previous onWindowFocusChanged) int commentTextBottom = 0; int oldLandCommentTextBottom = 0; private TextView periodText; private TextView framerateText; private TextView totalsnapshotsText; private Button startButton; // "Start capture" private Button createButton; // "Create video" int nativeButtonColor = 0; private EditText periodEditText; // Period private TextView secondsText; private EditText fpsEditText; // Frame rate private TextView fpsText; private TextView modeText; // Show comments float roundOneDecimal(float toround) { DecimalFormat twoDForm = new DecimalFormat("#.#"); return Float.valueOf(twoDForm.format(toround)); } static String intToString(int num, int digits) { assert digits > 0 : "Invalid number of digits"; char[] zeros = new char[digits]; // Create variable length array of zeros Arrays.fill(zeros, '0'); DecimalFormat df = new DecimalFormat(String.valueOf(zeros)); // Format number as String return df.format(num); } public boolean isNum(String s) { try { Double.parseDouble(s); } catch (NumberFormatException e) { return false; } return true; } private PowerManager.WakeLock wl; // Stop screen from dimming by enforcing wake lock @Override protected void onPause() { super.onPause(); // onPause method in the parent class if (DEBUG) Log.v(TAG, "onPause"); surfaceHolder.removeCallback(this); if (camera != null) { camera.setPreviewCallback(null); camera.stopPreview(); camera.release(); camera = null; } preview.setVisibility(View.GONE); wl.release(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // onCreate method in the parent class if (DEBUG) Log.v(TAG, "onCreate"); requestWindowFeature(Window.FEATURE_NO_TITLE); // App without a title getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); // App without a status bar setContentView(R.layout.activity_main); // Set user interface PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); wl = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, "DoNotDimScreen"); periodText = (TextView) findViewById(R.id.periodText); // Text fields framerateText = (TextView) findViewById(R.id.framerateText); totalsnapshotsText = (TextView) findViewById(R.id.totalsnapshotsText); startButton = (Button) findViewById(R.id.startButton); // Start capture button startButton.setOnClickListener(this); createButton = (Button) findViewById(R.id.createButton); // Create video button createButton.setOnClickListener(this); nativeButtonColor = createButton.getCurrentTextColor(); createButton.setTextColor(Color.GRAY); periodEditText = (EditText) findViewById(R.id.periodEditText); // Period secondsText = (TextView) findViewById(R.id.secondsText); periodEditText.addTextChangedListener(new TextWatcher() { public void afterTextChanged(Editable s) { if (periodEditText.getText().toString().length() == 0) capturePeriod = 0; else { if (isNum(periodEditText.getText().toString().replace(',', '.'))) { float a = Float.valueOf(periodEditText.getText().toString().replace(',', '.')); capturePeriod = (int) a; } else Toast.makeText(MainActivity.this, periodEditText.getText().toString() + " - not a digit.", Toast.LENGTH_LONG).show(); } } public void beforeTextChanged(CharSequence s, int start, int count, int after) { } public void onTextChanged(CharSequence s, int start, int before, int count) { } }); fpsEditText = (EditText) findViewById(R.id.fpsEditText); // fps EditText fpsText = (TextView) findViewById(R.id.fpsText); fpsEditText.setOnFocusChangeListener(new OnFocusChangeListener() { public void onFocusChange(View v, boolean hasFocus) { if (!hasFocus) { // Hide soft keyboard after input InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(fpsEditText.getWindowToken(), 0); } } }); fpsEditText.addTextChangedListener(new TextWatcher() { public void afterTextChanged(Editable s) { if (fpsEditText.getText().toString().length() == 0) fps = 0; else { if (isNum(fpsEditText.getText().toString().replace(',', '.'))) { float a = Float.valueOf(fpsEditText.getText().toString().replace(',', '.')); fps = (int) a; } else Toast.makeText(MainActivity.this, fpsEditText.getText().toString() + " - not a digit.", Toast.LENGTH_LONG).show(); } } public void beforeTextChanged(CharSequence s, int start, int count, int after) { } public void onTextChanged(CharSequence s, int start, int before, int count) { } }); modeText = (TextView) findViewById(R.id.modeText); // Show comments SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0); // Restore preferences oldLandCommentTextBottom = settings.getInt("oldLandCommentTextBottom", 0); } @Override protected void onResume() { super.onResume(); // onResume method in the parent class if (DEBUG) Log.v(TAG, "onResume"); preview = (SurfaceView) findViewById(R.id.mSurfaceView); if (camera == null) { camera = Camera.open(); camera.startPreview(); } surfaceHolder = preview.getHolder(); surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); surfaceHolder.setSizeFromLayout(); surfaceHolder.addCallback(this); preview.setVisibility(View.VISIBLE); wl.acquire(); Size previewSize = camera.getParameters().getPreviewSize(); aviHeight = previewSize.height; aviWidth = previewSize.width; modeText.setFocusableInTouchMode(true); // Set focus (and hide soft keyboard) modeText.requestFocus(); } public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { if (DEBUG) Log.v(TAG, "surfaceChanged"); try { camera.setPreviewDisplay(surfaceHolder); } catch (IOException e) { Toast.makeText(MainActivity.this, "Error 1: " + e.toString(), Toast.LENGTH_LONG).show(); } camera.startPreview(); } public void surfaceCreated(SurfaceHolder holder) { if (DEBUG) Log.v(TAG, "surfaceCreated"); try { camera.setPreviewDisplay(holder); camera.setPreviewCallback(this); } catch (IOException e) { Toast.makeText(MainActivity.this, "Error 2: " + e.toString(), Toast.LENGTH_LONG).show(); camera.release(); camera = null; } Size previewSize = camera.getParameters().getPreviewSize(); float aspect = (float) previewSize.width / previewSize.height; int previewSurfaceWidth = preview.getWidth(); LayoutParams lp = preview.getLayoutParams(); //     preview   ,     // camera.setDisplayOrientation(0); lp.width = previewSurfaceWidth; lp.height = (int) (previewSurfaceWidth / aspect); preview.setLayoutParams(lp); camera.startPreview(); } public void surfaceDestroyed(SurfaceHolder holder) { if (DEBUG) Log.v(TAG, "surfaceDestroyed"); } @SuppressLint("NewApi") @SuppressWarnings("deprecation") void getDisplaySize() { try { if (Build.VERSION.SDK_INT >= 13) { Display display = getWindowManager().getDefaultDisplay(); Point size = new Point(); display.getSize(size); sWidth = size.x; sHeight = size.y; } else { Display display = getWindowManager().getDefaultDisplay(); sWidth = display.getWidth(); sHeight = display.getHeight(); } } catch (Exception e) { Toast.makeText(MainActivity.this, "Error 3: " + e.toString(), Toast.LENGTH_LONG).show(); } } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { getDisplaySize(); if ((prevsWidth != sWidth) || (prevsHeight != sHeight)) { // If orientation changed commentTextBottom = modeText.getTop() + modeText.getHeight(); // Calculate magnification factor float heightRatio = 0; // Landscape heightRatio = (float) sHeight / (float) commentTextBottom; oldLandCommentTextBottom = commentTextBottom; if (heightRatio > 1) heightRatio = 0.7f * heightRatio; else heightRatio = heightRatio / 0.7f; // Adjust fonts periodText.setTextSize(TypedValue.COMPLEX_UNIT_PX, heightRatio * periodText.getTextSize()); periodEditText.setTextSize(TypedValue.COMPLEX_UNIT_PX, heightRatio * periodEditText.getTextSize()); secondsText.setTextSize(TypedValue.COMPLEX_UNIT_PX, heightRatio * secondsText.getTextSize()); framerateText.setTextSize(TypedValue.COMPLEX_UNIT_PX, heightRatio * framerateText.getTextSize()); fpsEditText.setTextSize(TypedValue.COMPLEX_UNIT_PX, heightRatio * fpsEditText.getTextSize()); fpsText.setTextSize(TypedValue.COMPLEX_UNIT_PX, heightRatio * fpsText.getTextSize()); totalsnapshotsText.setTextSize(TypedValue.COMPLEX_UNIT_PX, heightRatio * totalsnapshotsText.getTextSize()); modeText.setTextSize(TypedValue.COMPLEX_UNIT_PX, heightRatio * modeText.getTextSize()); // Some components have text size a little less startButton.setTextSize(TypedValue.COMPLEX_UNIT_PX, 0.8f * heightRatio * startButton.getTextSize()); createButton.setTextSize(TypedValue.COMPLEX_UNIT_PX, 0.8f * heightRatio * createButton.getTextSize()); // If user comment string not formed if (modeText.getText().equals(getResources().getString(R.string.longestComment))) modeText.setText(getString(R.string.modeText)); } prevsWidth = sWidth; prevsHeight = sHeight; } } public void onClick(View v) { if (v == startButton) { if (workMode == 0) { if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) Toast.makeText(MainActivity.this, "Please mount SD card", Toast.LENGTH_LONG).show(); else if ((capturePeriod < PERIODMIN) || (capturePeriod > PERIODMAX)) Toast.makeText(MainActivity.this, "Snapshots period should be " + PERIODMIN + " to " + PERIODMAX + " seconds", Toast.LENGTH_LONG) .show(); else if ((fps < FPSMIN) || (fps > FPSMAX)) Toast.makeText(MainActivity.this, "FPS should be " + FPSMIN + " to " + FPSMAX + " frames per second", Toast.LENGTH_LONG).show(); else { if (updateTimer != null) updateTimer.cancel(); try { updateTimer = new Timer(); updateTimer.scheduleAtFixedRate(new TimerTask() { public void run() { if ((camera != null) && (workMode == 1)) { camera.takePicture(null, null, null, MainActivity.this); } } }, 0, capturePeriod * 1000); } catch (Exception e) { Toast.makeText(MainActivity.this, "Error 4: " + e.toString(), Toast.LENGTH_LONG).show(); } // Delete all jpg's try { String sdPath = Environment.getExternalStorageDirectory().getPath() + "/TimeLapseFolder/"; if (DEBUG) Log.v(TAG, "Delete jpg's sdPath = " + sdPath); File saveDir = new File(sdPath); if (saveDir.isDirectory()) { String[] children = saveDir.list(); for (int i = 0; i < children.length; i++) { if (children[i].endsWith(".jpg")) new File(saveDir, children[i]).delete(); } } saveDir.delete(); } catch (Exception e) { Toast.makeText(MainActivity.this, "Error 5: " + e.toString(), Toast.LENGTH_LONG).show(); } lastPicture = 0; workMode = 1; startButton.setText("Stop capture"); modeText.setText("Work mode: capturing"); totalsnapshotsText.setText("Total snapshots: " + String.valueOf(lastPicture)); } } else if (workMode == 1) { workMode = 2; createButton.setTextColor(nativeButtonColor); startButton.setText("Start capture"); startButton.setTextColor(Color.GRAY); modeText.setText("Work mode: ready to start"); } } if (v == createButton) { if (workMode == 2) { if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) Toast.makeText(MainActivity.this, "Please mount SD card", Toast.LENGTH_LONG).show(); else if ((capturePeriod < PERIODMIN) || (capturePeriod > PERIODMAX)) Toast.makeText(MainActivity.this, "Snapshots period should be " + PERIODMIN + " to " + PERIODMAX + " seconds", Toast.LENGTH_LONG) .show(); else if ((fps < FPSMIN) || (fps > FPSMAX)) Toast.makeText(MainActivity.this, "FPS should be " + FPSMIN + " to " + FPSMAX + " frames per second", Toast.LENGTH_LONG).show(); else { workMode = 3; createButton.setTextColor(Color.GRAY); startButton.setTextColor(Color.GRAY); modeText.setText("Work mode: create video file, please wait"); new CreateMovieInBackground().execute(); } } } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); // onSaveInstanceState method in the parent class if (DEBUG) Log.v(TAG, "onSaveInstanceState"); SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0); SharedPreferences.Editor editor = settings.edit(); editor.putInt("oldLandCommentTextBottom", oldLandCommentTextBottom); editor.commit(); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); // onRestoreInstanceState method in the parent class if (DEBUG) Log.v(TAG, "onRestoreInstanceState"); } public void onPictureTaken(byte[] paramArrayOfByte, Camera paramCamera) { new SaveInBackground().execute(paramArrayOfByte); if (DEBUG) Log.v(TAG, "onPictureTaken"); //  ,   ,   .    paramCamera.startPreview(); totalsnapshotsText.setText("Total snapshots: " + String.valueOf(lastPicture)); StatFs stat = new StatFs(Environment.getExternalStorageDirectory().getPath()); long bytesAvailable = (long) stat.getBlockSize() * (long) stat.getAvailableBlocks(); float megAvailable = bytesAvailable / (1024.f * 1024.f); modeText.setText("Work mode: capturing, " + String.valueOf(roundOneDecimal(megAvailable)) + " Mbyte available on SD card"); } class SaveInBackground extends AsyncTask<byte[], String, String> { @Override protected String doInBackground(byte[]... arrayOfByte) { try { String sdPath = Environment.getExternalStorageDirectory().getPath() + "/TimeLapseFolder/"; File saveDir = new File(sdPath); if (!saveDir.exists()) saveDir.mkdirs(); lastPicture++; String numWithZeroes = intToString(lastPicture, 7); String curjpg = sdPath + numWithZeroes + ".jpg"; if (DEBUG) Log.v(TAG, "Save jpg sdPath = " + curjpg); FileOutputStream os = new FileOutputStream(curjpg); os.write(arrayOfByte[0]); os.close(); } catch (Exception e) { Toast.makeText(MainActivity.this, "Error 6: " + e.toString(), Toast.LENGTH_LONG).show(); } return (null); } } class CreateMovieInBackground extends AsyncTask<byte[], String, String> { protected void onProgressUpdate(String... values) { modeText.setText("Work mode: rendering " + values[0] + ".jpg"); } protected void onPostExecute(String result) { workMode = 0; totalsnapshotsText.setText("Total snapshots: 0"); lastPicture = 0; String sdPath = Environment.getExternalStorageDirectory().getPath() + "/TimeLapseFolder/"; modeText.setText("Work mode:" + sdPath + "TimeLapseMovie" + intToString(lastVideo, 3) + ".avi is rendered"); Handler handler = new Handler(); handler.postDelayed(new Runnable() { public void run() { modeText.setText("Work mode: ready to start"); startButton.setTextColor(nativeButtonColor); } }, 5000); } @Override protected String doInBackground(byte[]... arrayOfByte) { try { File videofile = null; String sdPath = Environment.getExternalStorageDirectory().getPath() + "/TimeLapseFolder/"; // Choosing a name for the file do { lastVideo++; String curavi = sdPath + "TimeLapseMovie" + intToString(lastVideo, 3) + ".avi"; if (DEBUG) Log.v(TAG, "AVI name = " + curavi); videofile = new File(curavi); } while (videofile.exists()); generator = new MJPEGGenerator(videofile, aviWidth, aviHeight, fps, lastPicture); for (int addpic = 1; addpic <= lastPicture; addpic++) { String numWithZeroes = intToString(addpic, 7); String curjpg = sdPath + numWithZeroes + ".jpg"; publishProgress(numWithZeroes); if (DEBUG) Log.v(TAG, "Rendering jpg sdPath = " + curjpg); Bitmap bmp = BitmapFactory.decodeFile(curjpg); generator.addImage(bmp); } // Delete all jpg's try { if (DEBUG) Log.v(TAG, "Delete jpg's sdPath = " + sdPath); File saveDir = new File(sdPath); if (saveDir.isDirectory()) { String[] children = saveDir.list(); for (int i = 0; i < children.length; i++) { if (children[i].endsWith(".jpg")) new File(saveDir, children[i]).delete(); } } saveDir.delete(); } catch (Exception e) { Toast.makeText(MainActivity.this, "Error 7: " + e.toString(), Toast.LENGTH_LONG).show(); } generator.finishAVI(); } catch (Exception e) { Toast.makeText(MainActivity.this, "Error 8: " + e.toString(), Toast.LENGTH_LONG).show(); } return "OK"; } } public void onPreviewFrame(byte[] paramArrayOfByte, Camera paramCamera) { } } 



MJPEGGenerator.java
 package com.sample.timelapse; // // MJPEGGenerator.java // // Created on April 17, 2006, 11:48 PM // // To change this template, choose Tools | Options and locate the template under // the Source Creation and Management node. Right-click the template and choose // Open. You can then make changes to the template in the Source Editor. // import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.RandomAccessFile; import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import android.graphics.Bitmap; // // // @author monceaux // public class MJPEGGenerator { // // Info needed for MJPEG AVI // // - size of file minus "RIFF & 4 byte file size" // int width = 0; int height = 0; double framerate = 0; int numFrames = 0; File aviFile = null; FileOutputStream aviOutput = null; FileChannel aviChannel = null; long riffOffset = 0; long aviMovieOffset = 0; AVIIndexList indexlist = null; // Creates a new instance of MJPEGGenerator public MJPEGGenerator(File aviFile, int width, int height, double framerate, int numFrames) throws Exception { this.aviFile = aviFile; this.width = width; this.height = height; this.framerate = framerate; this.numFrames = numFrames; aviOutput = new FileOutputStream(aviFile); aviChannel = aviOutput.getChannel(); RIFFHeader rh = new RIFFHeader(); aviOutput.write(rh.toBytes()); aviOutput.write(new AVIMainHeader().toBytes()); aviOutput.write(new AVIStreamList().toBytes()); aviOutput.write(new AVIStreamHeader().toBytes()); aviOutput.write(new AVIStreamFormat().toBytes()); aviOutput.write(new AVIJunk().toBytes()); aviMovieOffset = aviChannel.position(); aviOutput.write(new AVIMovieList().toBytes()); indexlist = new AVIIndexList(); } public void addImage(Bitmap image) throws Exception { byte[] fcc = new byte[] { '0', '0', 'd', 'b' }; byte[] imagedata = writeImageToBytes(image); int useLength = imagedata.length; long position = aviChannel.position(); int extra = (useLength + (int) position) % 4; if (extra > 0) useLength = useLength + extra; indexlist.addAVIIndex((int) position, useLength); aviOutput.write(fcc); aviOutput.write(intBytes(swapInt(useLength))); aviOutput.write(imagedata); if (extra > 0) { for (int i = 0; i < extra; i++) aviOutput.write(0); } imagedata = null; } public void finishAVI() throws Exception { byte[] indexlistBytes = indexlist.toBytes(); aviOutput.write(indexlistBytes); aviOutput.close(); long size = aviFile.length(); RandomAccessFile raf = new RandomAccessFile(aviFile, "rw"); raf.seek(4); raf.write(intBytes(swapInt((int) size - 8))); raf.seek(aviMovieOffset + 4); raf.write(intBytes(swapInt((int) (size - 8 - aviMovieOffset - indexlistBytes.length)))); raf.close(); } public static int swapInt(int v) { return (v >>> 24) | (v << 24) | ((v << 8) & 0x00FF0000) | ((v >> 8) & 0x0000FF00); } public static short swapShort(short v) { return (short) ((v >>> 8) | (v << 8)); } public static byte[] intBytes(int i) { byte[] b = new byte[4]; b[0] = (byte) (i >>> 24); b[1] = (byte) ((i >>> 16) & 0x000000FF); b[2] = (byte) ((i >>> 8) & 0x000000FF); b[3] = (byte) (i & 0x000000FF); return b; } public static byte[] shortBytes(short i) { byte[] b = new byte[2]; b[0] = (byte) (i >>> 8); b[1] = (byte) (i & 0x000000FF); return b; } private class RIFFHeader { public byte[] fcc = new byte[] { 'R', 'I', 'F', 'F' }; public int fileSize = 0; public byte[] fcc2 = new byte[] { 'A', 'V', 'I', ' ' }; public byte[] fcc3 = new byte[] { 'L', 'I', 'S', 'T' }; public int listSize = 200; public byte[] fcc4 = new byte[] { 'h', 'd', 'r', 'l' }; public RIFFHeader() { } public byte[] toBytes() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(fcc); baos.write(intBytes(swapInt(fileSize))); baos.write(fcc2); baos.write(fcc3); baos.write(intBytes(swapInt(listSize))); baos.write(fcc4); baos.close(); return baos.toByteArray(); } } private class AVIMainHeader { // // // FOURCC fcc; DWORD cb; DWORD dwMicroSecPerFrame; DWORD dwMaxBytesPerSec; DWORD dwPaddingGranularity; DWORD // dwFlags; DWORD dwTotalFrames; DWORD dwInitialFrames; DWORD dwStreams; DWORD dwSuggestedBufferSize; DWORD // dwWidth; DWORD dwHeight; DWORD dwReserved[4]; // public byte[] fcc = new byte[] { 'a', 'v', 'i', 'h' }; public int cb = 56; public int dwMicroSecPerFrame = 0; // (1 // / // frames // per // sec) // * // 1,000,000 public int dwMaxBytesPerSec = 10000000; public int dwPaddingGranularity = 0; public int dwFlags = 65552; public int dwTotalFrames = 0; // replace // with // correct // value public int dwInitialFrames = 0; public int dwStreams = 1; public int dwSuggestedBufferSize = 0; public int dwWidth = 0; // replace // with // correct // value public int dwHeight = 0; // replace // with // correct // value public int[] dwReserved = new int[4]; public AVIMainHeader() { dwMicroSecPerFrame = (int) ((1.0 / framerate) * 1000000.0); dwWidth = width; dwHeight = height; dwTotalFrames = numFrames; } public byte[] toBytes() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(fcc); baos.write(intBytes(swapInt(cb))); baos.write(intBytes(swapInt(dwMicroSecPerFrame))); baos.write(intBytes(swapInt(dwMaxBytesPerSec))); baos.write(intBytes(swapInt(dwPaddingGranularity))); baos.write(intBytes(swapInt(dwFlags))); baos.write(intBytes(swapInt(dwTotalFrames))); baos.write(intBytes(swapInt(dwInitialFrames))); baos.write(intBytes(swapInt(dwStreams))); baos.write(intBytes(swapInt(dwSuggestedBufferSize))); baos.write(intBytes(swapInt(dwWidth))); baos.write(intBytes(swapInt(dwHeight))); baos.write(intBytes(swapInt(dwReserved[0]))); baos.write(intBytes(swapInt(dwReserved[1]))); baos.write(intBytes(swapInt(dwReserved[2]))); baos.write(intBytes(swapInt(dwReserved[3]))); baos.close(); return baos.toByteArray(); } } private class AVIStreamList { public byte[] fcc = new byte[] { 'L', 'I', 'S', 'T' }; public int size = 124; public byte[] fcc2 = new byte[] { 's', 't', 'r', 'l' }; public AVIStreamList() { } public byte[] toBytes() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(fcc); baos.write(intBytes(swapInt(size))); baos.write(fcc2); baos.close(); return baos.toByteArray(); } } private class AVIStreamHeader { // // FOURCC fcc; DWORD cb; FOURCC fccType; FOURCC fccHandler; DWORD dwFlags; WORD wPriority; WORD wLanguage; DWORD // dwInitialFrames; DWORD dwScale; DWORD dwRate; DWORD dwStart; DWORD dwLength; DWORD dwSuggestedBufferSize; // DWORD dwQuality; DWORD dwSampleSize; struct { short int left; short int top; short int right; short int // bottom; } rcFrame; // public byte[] fcc = new byte[] { 's', 't', 'r', 'h' }; public int cb = 64; public byte[] fccType = new byte[] { 'v', 'i', 'd', 's' }; public byte[] fccHandler = new byte[] { 'M', 'J', 'P', 'G' }; public int dwFlags = 0; public short wPriority = 0; public short wLanguage = 0; public int dwInitialFrames = 0; public int dwScale = 0; // microseconds // per // frame public int dwRate = 1000000; // dwRate // / // dwScale // = // frame // rate public int dwStart = 0; public int dwLength = 0; // num // frames public int dwSuggestedBufferSize = 0; public int dwQuality = -1; public int dwSampleSize = 0; public int left = 0; public int top = 0; public int right = 0; public int bottom = 0; public AVIStreamHeader() { dwScale = (int) ((1.0 / framerate) * 1000000.0); dwLength = numFrames; } public byte[] toBytes() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(fcc); baos.write(intBytes(swapInt(cb))); baos.write(fccType); baos.write(fccHandler); baos.write(intBytes(swapInt(dwFlags))); baos.write(shortBytes(swapShort(wPriority))); baos.write(shortBytes(swapShort(wLanguage))); baos.write(intBytes(swapInt(dwInitialFrames))); baos.write(intBytes(swapInt(dwScale))); baos.write(intBytes(swapInt(dwRate))); baos.write(intBytes(swapInt(dwStart))); baos.write(intBytes(swapInt(dwLength))); baos.write(intBytes(swapInt(dwSuggestedBufferSize))); baos.write(intBytes(swapInt(dwQuality))); baos.write(intBytes(swapInt(dwSampleSize))); baos.write(intBytes(swapInt(left))); baos.write(intBytes(swapInt(top))); baos.write(intBytes(swapInt(right))); baos.write(intBytes(swapInt(bottom))); baos.close(); return baos.toByteArray(); } } private class AVIStreamFormat { // // FOURCC fcc; DWORD cb; DWORD biSize; LONG biWidth; LONG biHeight; WORD biPlanes; WORD biBitCount; DWORD // biCompression; DWORD biSizeImage; LONG biXPelsPerMeter; LONG biYPelsPerMeter; DWORD biClrUsed; DWORD // biClrImportant; // public byte[] fcc = new byte[] { 's', 't', 'r', 'f' }; public int cb = 40; public int biSize = 40; // same // as // cb public int biWidth = 0; public int biHeight = 0; public short biPlanes = 1; public short biBitCount = 24; public byte[] biCompression = new byte[] { 'M', 'J', 'P', 'G' }; public int biSizeImage = 0; // width // x // height // in // pixels public int biXPelsPerMeter = 0; public int biYPelsPerMeter = 0; public int biClrUsed = 0; public int biClrImportant = 0; public AVIStreamFormat() { biWidth = width; biHeight = height; biSizeImage = width * height; } public byte[] toBytes() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(fcc); baos.write(intBytes(swapInt(cb))); baos.write(intBytes(swapInt(biSize))); baos.write(intBytes(swapInt(biWidth))); baos.write(intBytes(swapInt(biHeight))); baos.write(shortBytes(swapShort(biPlanes))); baos.write(shortBytes(swapShort(biBitCount))); baos.write(biCompression); baos.write(intBytes(swapInt(biSizeImage))); baos.write(intBytes(swapInt(biXPelsPerMeter))); baos.write(intBytes(swapInt(biYPelsPerMeter))); baos.write(intBytes(swapInt(biClrUsed))); baos.write(intBytes(swapInt(biClrImportant))); baos.close(); return baos.toByteArray(); } } private class AVIMovieList { public byte[] fcc = new byte[] { 'L', 'I', 'S', 'T' }; public int listSize = 0; public byte[] fcc2 = new byte[] { 'm', 'o', 'v', 'i' }; // 00db size jpg image data ... public AVIMovieList() { } public byte[] toBytes() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(fcc); baos.write(intBytes(swapInt(listSize))); baos.write(fcc2); baos.close(); return baos.toByteArray(); } } private class AVIIndexList { public byte[] fcc = new byte[] { 'i', 'd', 'x', '1' }; public int cb = 0; public List ind = new ArrayList(); public AVIIndexList() { } @SuppressWarnings("unused") public void addAVIIndex(AVIIndex ai) { ind.add(ai); } public void addAVIIndex(int dwOffset, int dwSize) { ind.add(new AVIIndex(dwOffset, dwSize)); } public byte[] toBytes() throws Exception { cb = 16 * ind.size(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(fcc); baos.write(intBytes(swapInt(cb))); for (int i = 0; i < ind.size(); i++) { AVIIndex in = (AVIIndex) ind.get(i); baos.write(in.toBytes()); } baos.close(); return baos.toByteArray(); } } private class AVIIndex { public byte[] fcc = new byte[] { '0', '0', 'd', 'b' }; public int dwFlags = 16; public int dwOffset = 0; public int dwSize = 0; public AVIIndex(int dwOffset, int dwSize) { this.dwOffset = dwOffset; this.dwSize = dwSize; } public byte[] toBytes() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(fcc); baos.write(intBytes(swapInt(dwFlags))); baos.write(intBytes(swapInt(dwOffset))); baos.write(intBytes(swapInt(dwSize))); baos.close(); return baos.toByteArray(); } } private class AVIJunk { public byte[] fcc = new byte[] { 'J', 'U', 'N', 'K' }; public int size = 1808; public byte[] data = new byte[size]; public AVIJunk() { Arrays.fill(data, (byte) 0); } public byte[] toBytes() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(fcc); baos.write(intBytes(swapInt(size))); baos.write(data); baos.close(); return baos.toByteArray(); } } private byte[] writeImageToBytes(Bitmap image) throws Exception { ByteArrayOutputStream stream = new ByteArrayOutputStream(); image.compress(Bitmap.CompressFormat.JPEG, 100, stream); stream.close(); return stream.toByteArray(); } } 



activity_main.xml
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:ads="http://schemas.android.com/apk/lib/com.google.ads" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/appBackgroundColor" > <TextView android:id="@+id/centerEmptyText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" /> <SurfaceView android:id="@+id/mSurfaceView" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_toRightOf="@+id/centerEmptyText" > </SurfaceView> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_marginRight="@dimen/baseui_horizontal_margin" android:layout_toLeftOf="@+id/centerEmptyText" android:orientation="vertical" > <TableLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" > <TableRow android:id="@+id/periodRow" android:layout_width="wrap_content" android:layout_height="wrap_content" > <TextView android:id="@+id/periodText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/baseui_horizontal_margin" android:focusable="true" android:focusableInTouchMode="true" android:text="@string/periodText" > <requestFocus /> </TextView> <EditText android:id="@+id/periodEditText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/baseui_horizontal_margin" android:ems="2" android:inputType="numberDecimal" android:singleLine="true" /> <TextView android:id="@+id/secondsText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/baseui_horizontal_margin" android:text="@string/seconds" /> </TableRow> <TableRow android:id="@+id/fpsRow" android:layout_width="wrap_content" android:layout_height="wrap_content" > <TextView android:id="@+id/framerateText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/baseui_horizontal_margin" android:focusable="true" android:focusableInTouchMode="true" android:text="@string/framerateText" /> <EditText android:id="@+id/fpsEditText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/baseui_horizontal_margin" android:ems="2" android:inputType="numberDecimal" android:singleLine="true" /> <TextView android:id="@+id/fpsText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/baseui_horizontal_margin" android:text="@string/fps" /> </TableRow> </TableLayout> <TableLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" > <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="horizontal" > <Button android:id="@+id/startButton" style="?android:attr/buttonStyleSmall" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginLeft="@dimen/baseui_horizontal_margin" android:layout_weight="1" android:text="@string/startButtonText" /> <Button android:id="@+id/createButton" style="?android:attr/buttonStyleSmall" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginLeft="@dimen/baseui_horizontal_margin" android:layout_weight="1" android:text="@string/createButtonText" /> </LinearLayout> <TableRow android:id="@+id/totalsnapshotsRow" android:layout_width="wrap_content" android:layout_height="wrap_content" > <TextView android:id="@+id/totalsnapshotsText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/baseui_horizontal_margin" android:text="@string/totalsnapshotsText" /> </TableRow> </TableLayout> <TextView android:id="@+id/modeText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/baseui_horizontal_margin" android:text="@string/longestComment" /> </LinearLayout> </RelativeLayout> 



AndroidManifest.xml
 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.sample.timelapse" android:installLocation="preferExternal" android:versionCode="8" android:versionName="0.8" > <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="15" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.camera.autofocus" android:required="false" /> <uses-feature android:name="there.isnt.a.vibrate.feature" android:required="false" /> <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" > <activity android:name=".MainActivity" android:label="@string/title_activity_main" android:screenOrientation="landscape" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> 



UPD : DevAndrew noticed that the ij.jar library is needed to compile the project ( ImageJ is needed for the MJPEGGenerator to work). The library is added to the sourceforge project files.

Source: https://habr.com/ru/post/172469/


All Articles