Давайте напишем программу для создания своих собственных фильмов в технике Time Lapse. Завораживающее видео, снятое в этой технике с борта МКС, можно посмотреть здесь, более доступный вариант, который можно повторить с помощью описываемой программы — здесь.
Программа имеет простой интерфейс и несложный принцип работы:
• пользователь задает периодичность снимков встроенной камерой (например, 10 с) и желаемую частоту кадров генерируемого видео (например, 25 кадров в секунду);
• после нажатия кнопки «Старт» программа каждые 10 секунд делает фотографию и записывает jpg-файл на SD-карту;
• процедура повторяется до нажатия кнопки «Стоп» и «Создать видео», после чего последовательность фотографий превращается в видео файл формата Motion JPEG, который показывает отснятый материал в 250 раз (25 * 10) быстрее реальной скорости происходивших событий.
В программе два основных класса — MainActivity, занимающийся взаимодействием с пользователем и накоплением снимков и MJPEGGenerator, ответственный за превращение последовательности изображений в видео файл.
Класс MJPEGGenerator, взятый с code.google.com, был слегка переделан в связи с тем, что в Android Java отсутствует пакет java.awt.
Процедуры работы с камерой были преимущественно взяты из материала Работа с камерой в Android, где есть хорошее описание примененных решений, проблема «залипаний» камеры после лока/анлока Android-устройства была устранена благодаря stackoverflow.
Программа была отлажена на планшете Prestigio MultiPad 7.0 Prime под Android 4.0.
Рассмотрим более подробно работу отдельных компонентов программы, не касающихся собственно камеры.
Для того, чтобы экран во время съемки не выключался, был применен механизм PowerManager.WakeLock:
private PowerManager.WakeLock wl;
public void onCreate(Bundle savedInstanceState) {
...
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
wl = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, "DoNotDimScreen");
}
Периодичность запуска камеры регулируется таймером:
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);
Перед началом работы проверяется наличие SD-карты:
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
Toast.makeText(MainActivity.this, "Please mount SD card", Toast.LENGTH_LONG).show();
Данные, введенные пользователем, проходят предварительную обработку: запятые, введенные в качестве разделителя десятичных разрядов, заменяются на точку; проверяется ввод не цифровых значений; значения введенного периода и частоты кадров проверяются на вхождение в допустимый диапазон:
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();
В начале работы из папки программы удаляются все файла *.jpg, оставшиеся после предыдущего сеанса:
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();
После съемки очередного кадра пользователю показывается оставшееся на карте место:
modeText.setText("Work mode: capturing, " + String.valueOf(roundOneDecimal(megAvailable)) + " Mbyte available on SD card");
Собственно генерация видео:
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 запускается со следующими параметрами:
videofile — название файла видео каждый раз берется новое, нумеруемое по маске TimeLapseMovieXXX.avi, чтобы сохранить файлы, отснятые ранее;
aviWidth, aviHeight — берется из свойств камеры;
fps — задается пользователем;
lastPicture — номер последней снятой фотографии.
Чтобы не подвешивать пользовательский интерфейс, генерация видео запускается в отдельном потоке AsyncTask, взаимодействующим с GUI через onProgressUpdate.
Содержимое ключевых файлов показано ниже, архив проекта можно скачать с sourceforge.net.
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) {
}
}
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();
}
}
<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>
<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>
Автор: sepulkary