blob: 01729c92680066e4522b9e98d7f6f7fc7c4f3fe2 [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.videoeditor;
import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import java.text.SimpleDateFormat;
import android.app.ActionBar;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.Bitmap.Config;
import android.media.videoeditor.MediaItem;
import android.media.videoeditor.MediaProperties;
import android.media.videoeditor.VideoEditor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
import android.provider.MediaStore;
import android.text.InputType;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.GestureDetector;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.SurfaceHolder;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.android.videoeditor.service.ApiService;
import com.android.videoeditor.service.MovieMediaItem;
import com.android.videoeditor.service.VideoEditorProject;
import com.android.videoeditor.util.FileUtils;
import com.android.videoeditor.util.MediaItemUtils;
import com.android.videoeditor.util.StringUtils;
import com.android.videoeditor.widgets.AudioTrackLinearLayout;
import com.android.videoeditor.widgets.MediaLinearLayout;
import com.android.videoeditor.widgets.MediaLinearLayoutListener;
import com.android.videoeditor.widgets.OverlayLinearLayout;
import com.android.videoeditor.widgets.PlayheadView;
import com.android.videoeditor.widgets.PreviewSurfaceView;
import com.android.videoeditor.widgets.ScrollViewListener;
import com.android.videoeditor.widgets.TimelineHorizontalScrollView;
import com.android.videoeditor.widgets.TimelineRelativeLayout;
import com.android.videoeditor.widgets.ZoomControl;
/**
* Main activity of the video editor. It handles video editing of
* a project.
*/
public class VideoEditorActivity extends VideoEditorBaseActivity
implements SurfaceHolder.Callback {
private static final String TAG = "VideoEditorActivity";
// State keys
private static final String STATE_INSERT_AFTER_MEDIA_ITEM_ID = "insert_after_media_item_id";
private static final String STATE_PLAYING = "playing";
private static final String STATE_CAPTURE_URI = "capture_uri";
private static final String STATE_SELECTED_POS_ID = "selected_pos_id";
private static final String DCIM =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).toString();
private static final String DIRECTORY = DCIM + "/Camera";
// Dialog ids
private static final int DIALOG_DELETE_PROJECT_ID = 1;
private static final int DIALOG_EDIT_PROJECT_NAME_ID = 2;
private static final int DIALOG_CHOOSE_ASPECT_RATIO_ID = 3;
private static final int DIALOG_EXPORT_OPTIONS_ID = 4;
public static final int DIALOG_REMOVE_MEDIA_ITEM_ID = 10;
public static final int DIALOG_REMOVE_TRANSITION_ID = 11;
public static final int DIALOG_CHANGE_RENDERING_MODE_ID = 12;
public static final int DIALOG_REMOVE_OVERLAY_ID = 13;
public static final int DIALOG_REMOVE_EFFECT_ID = 14;
public static final int DIALOG_REMOVE_AUDIO_TRACK_ID = 15;
// Dialog parameters
private static final String PARAM_ASPECT_RATIOS_LIST = "aspect_ratios";
private static final String PARAM_CURRENT_ASPECT_RATIO_INDEX = "current_aspect_ratio";
// Request codes
private static final int REQUEST_CODE_IMPORT_VIDEO = 1;
private static final int REQUEST_CODE_IMPORT_IMAGE = 2;
private static final int REQUEST_CODE_IMPORT_MUSIC = 3;
private static final int REQUEST_CODE_CAPTURE_VIDEO = 4;
private static final int REQUEST_CODE_CAPTURE_IMAGE = 5;
public static final int REQUEST_CODE_EDIT_TRANSITION = 10;
public static final int REQUEST_CODE_PICK_TRANSITION = 11;
public static final int REQUEST_CODE_PICK_OVERLAY = 12;
public static final int REQUEST_CODE_KEN_BURNS = 13;
// The maximum zoom level
private static final int MAX_ZOOM_LEVEL = 120;
private static final int ZOOM_STEP = 2;
// Threshold in width dip for showing title in action bar.
private static final int SHOW_TITLE_THRESHOLD_WIDTH_DIP = 1000;
private final TimelineRelativeLayout.LayoutCallback mLayoutCallback =
new TimelineRelativeLayout.LayoutCallback() {
@Override
public void onLayoutComplete() {
// Scroll the timeline such that the specified position
// is in the center of the screen.
movePlayhead(mProject.getPlayheadPos(), false);
}
};
// Instance variables
private PreviewSurfaceView mSurfaceView;
private SurfaceHolder mSurfaceHolder;
private boolean mHaveSurface;
// The width and height of the preview surface. They are defined only if
// mHaveSurface is true. If the values are still unknown (before
// surfaceChanged() is called), mSurfaceWidth is set to -1.
private int mSurfaceWidth, mSurfaceHeight;
private boolean mResumed;
private ImageView mOverlayView;
private PreviewThread mPreviewThread;
private View mEditorProjectView;
private View mEditorEmptyView;
private TimelineHorizontalScrollView mTimelineScroller;
private TimelineRelativeLayout mTimelineLayout;
private OverlayLinearLayout mOverlayLayout;
private AudioTrackLinearLayout mAudioTrackLayout;
private MediaLinearLayout mMediaLayout;
private int mMediaLayoutSelectedPos;
private PlayheadView mPlayheadView;
private TextView mTimeView;
private ImageButton mPreviewPlayButton;
private ImageButton mPreviewRewindButton, mPreviewNextButton, mPreviewPrevButton;
private int mActivityWidth;
private String mInsertMediaItemAfterMediaItemId;
private long mCurrentPlayheadPosMs;
private ProgressDialog mExportProgressDialog;
private ZoomControl mZoomControl;
private PowerManager.WakeLock mCpuWakeLock;
// Variables used in onActivityResult
private Uri mAddMediaItemVideoUri;
private Uri mAddMediaItemImageUri;
private Uri mAddAudioTrackUri;
private String mAddTransitionAfterMediaId;
private int mAddTransitionType;
private long mAddTransitionDurationMs;
private String mEditTransitionAfterMediaId, mEditTransitionId;
private int mEditTransitionType;
private long mEditTransitionDurationMs;
private String mAddOverlayMediaItemId;
private Bundle mAddOverlayUserAttributes;
private String mEditOverlayMediaItemId;
private String mEditOverlayId;
private Bundle mEditOverlayUserAttributes;
private String mAddEffectMediaItemId;
private int mAddEffectType;
private Rect mAddKenBurnsStartRect;
private Rect mAddKenBurnsEndRect;
private boolean mRestartPreview;
private Uri mCaptureMediaUri;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final ActionBar actionBar = getActionBar();
DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
// Only show title on large screens (width >= 1000 dip).
int widthDip = (int) (displayMetrics.widthPixels / displayMetrics.scaledDensity);
if (widthDip >= SHOW_TITLE_THRESHOLD_WIDTH_DIP) {
actionBar.setDisplayOptions(actionBar.getDisplayOptions() | ActionBar.DISPLAY_SHOW_TITLE);
actionBar.setTitle(R.string.full_app_name);
}
// Prepare the surface holder
mSurfaceView = (PreviewSurfaceView) findViewById(R.id.video_view);
mSurfaceHolder = mSurfaceView.getHolder();
mSurfaceHolder.addCallback(this);
mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
mOverlayView = (ImageView)findViewById(R.id.overlay_layer);
mEditorProjectView = findViewById(R.id.editor_project_view);
mEditorEmptyView = findViewById(R.id.empty_project_view);
mTimelineScroller = (TimelineHorizontalScrollView)findViewById(R.id.timeline_scroller);
mTimelineLayout = (TimelineRelativeLayout)findViewById(R.id.timeline);
mMediaLayout = (MediaLinearLayout)findViewById(R.id.timeline_media);
mOverlayLayout = (OverlayLinearLayout)findViewById(R.id.timeline_overlays);
mAudioTrackLayout = (AudioTrackLinearLayout)findViewById(R.id.timeline_audio_tracks);
mPlayheadView = (PlayheadView)findViewById(R.id.timeline_playhead);
mPreviewPlayButton = (ImageButton)findViewById(R.id.editor_play);
mPreviewRewindButton = (ImageButton)findViewById(R.id.editor_rewind);
mPreviewNextButton = (ImageButton)findViewById(R.id.editor_next);
mPreviewPrevButton = (ImageButton)findViewById(R.id.editor_prev);
mTimeView = (TextView)findViewById(R.id.editor_time);
actionBar.setDisplayHomeAsUpEnabled(true);
mMediaLayout.setListener(new MediaLinearLayoutListener() {
@Override
public void onRequestScrollBy(int scrollBy, boolean smooth) {
mTimelineScroller.appScrollBy(scrollBy, smooth);
}
@Override
public void onRequestMovePlayhead(long scrollToTime, boolean smooth) {
movePlayhead(scrollToTime);
}
@Override
public void onAddMediaItem(String afterMediaItemId) {
mInsertMediaItemAfterMediaItemId = afterMediaItemId;
final Intent intent = new Intent(Intent.ACTION_PICK);
intent.setData(MediaStore.Video.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(intent, REQUEST_CODE_IMPORT_VIDEO);
}
@Override
public void onTrimMediaItemBegin(MovieMediaItem mediaItem) {
onProjectEditStateChange(true);
}
@Override
public void onTrimMediaItem(MovieMediaItem mediaItem, long timeMs) {
updateTimelineDuration();
if (mProject != null && isPreviewPlaying()) {
if (mediaItem.isVideoClip()) {
if (timeMs >= 0) {
mPreviewThread.renderMediaItemFrame(mediaItem, timeMs);
}
} else {
mPreviewThread.previewFrame(mProject,
mProject.getMediaItemBeginTime(mediaItem.getId()) + timeMs,
mProject.getMediaItemCount() == 0);
}
}
}
@Override
public void onTrimMediaItemEnd(MovieMediaItem mediaItem, long timeMs) {
onProjectEditStateChange(false);
// We need to repaint the timeline layout to clear the old
// playhead position (the one drawn during trimming).
mTimelineLayout.invalidate();
showPreviewFrame();
}
});
mAudioTrackLayout.setListener(new AudioTrackLinearLayout.AudioTracksLayoutListener() {
@Override
public void onAddAudioTrack() {
final Intent intent = new Intent(Intent.ACTION_PICK);
intent.setData(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(intent, REQUEST_CODE_IMPORT_MUSIC);
}
});
mTimelineScroller.addScrollListener(new ScrollViewListener() {
// Instance variables
private int mActiveWidth;
private long mDurationMs;
@Override
public void onScrollBegin(View view, int scrollX, int scrollY, boolean appScroll) {
if (!appScroll && mProject != null) {
mActiveWidth = mMediaLayout.getWidth() - mActivityWidth;
mDurationMs = mProject.computeDuration();
} else {
mActiveWidth = 0;
}
}
@Override
public void onScrollProgress(View view, int scrollX, int scrollY, boolean appScroll) {
}
@Override
public void onScrollEnd(View view, int scrollX, int scrollY, boolean appScroll) {
// We check if the project is valid since the project may
// close while scrolling
if (!appScroll && mActiveWidth > 0 && mProject != null) {
final long timeMs = (scrollX * mDurationMs) / mActiveWidth;
if (setPlayhead(timeMs < 0 ? 0 : timeMs)) {
showPreviewFrame();
}
}
}
});
mTimelineScroller.setScaleListener(new ScaleGestureDetector.SimpleOnScaleGestureListener() {
// Guard against this many scale events in the opposite direction
private static final int SCALE_TOLERANCE = 3;
private int mLastScaleFactorSign;
private float mLastScaleFactor;
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
mLastScaleFactorSign = 0;
return true;
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
if (mProject == null) {
return false;
}
final float scaleFactor = detector.getScaleFactor();
final float deltaScaleFactor = scaleFactor - mLastScaleFactor;
if (deltaScaleFactor > 0.01f || deltaScaleFactor < -0.01f) {
if (scaleFactor < 1.0f) {
if (mLastScaleFactorSign <= 0) {
zoomTimeline(mProject.getZoomLevel() - ZOOM_STEP, true);
}
if (mLastScaleFactorSign > -SCALE_TOLERANCE) {
mLastScaleFactorSign--;
}
} else if (scaleFactor > 1.0f) {
if (mLastScaleFactorSign >= 0) {
zoomTimeline(mProject.getZoomLevel() + ZOOM_STEP, true);
}
if (mLastScaleFactorSign < SCALE_TOLERANCE) {
mLastScaleFactorSign++;
}
}
}
mLastScaleFactor = scaleFactor;
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
}
});
if (savedInstanceState != null) {
mInsertMediaItemAfterMediaItemId = savedInstanceState.getString(
STATE_INSERT_AFTER_MEDIA_ITEM_ID);
mRestartPreview = savedInstanceState.getBoolean(STATE_PLAYING);
mCaptureMediaUri = savedInstanceState.getParcelable(STATE_CAPTURE_URI);
mMediaLayoutSelectedPos = savedInstanceState.getInt(STATE_SELECTED_POS_ID, -1);
} else {
mRestartPreview = false;
mMediaLayoutSelectedPos = -1;
}
// Compute the activity width
final Display display = getWindowManager().getDefaultDisplay();
mActivityWidth = display.getWidth();
mSurfaceView.setGestureListener(new GestureDetector(this,
new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
if (isPreviewPlaying()) {
return false;
}
mTimelineScroller.fling(-(int)velocityX);
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
if (isPreviewPlaying()) {
return false;
}
mTimelineScroller.scrollBy((int)distanceX, 0);
return true;
}
}));
mZoomControl = ((ZoomControl)findViewById(R.id.editor_zoom));
mZoomControl.setMax(MAX_ZOOM_LEVEL);
mZoomControl.setOnZoomChangeListener(new ZoomControl.OnZoomChangeListener() {
@Override
public void onProgressChanged(int progress, boolean fromUser) {
if (mProject != null) {
zoomTimeline(progress, false);
}
}
});
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
mCpuWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Video Editor Activity CPU Wake Lock");
}
@Override
public void onPause() {
super.onPause();
mResumed = false;
// Stop the preview now (we will stop it in surfaceDestroyed(), but
// that may be too late for releasing resources to other activities)
stopPreviewThread();
// Dismiss the export progress dialog. If the export will still be pending
// when we return to this activity, we will display this dialog again.
if (mExportProgressDialog != null) {
mExportProgressDialog.dismiss();
mExportProgressDialog = null;
}
}
@Override
public void onResume() {
super.onResume();
mResumed = true;
if (mProject != null) {
mMediaLayout.onResume();
mAudioTrackLayout.onResume();
}
createPreviewThreadIfNeeded();
}
private void createPreviewThreadIfNeeded() {
// We want to have the preview thread if and only if (1) we have a
// surface, and (2) we are resumed.
if (mHaveSurface && mResumed && mPreviewThread == null) {
mPreviewThread = new PreviewThread(mSurfaceHolder);
if (mSurfaceWidth != -1) {
mPreviewThread.onSurfaceChanged(mSurfaceWidth, mSurfaceHeight);
}
restartPreview();
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(STATE_INSERT_AFTER_MEDIA_ITEM_ID, mInsertMediaItemAfterMediaItemId);
outState.putBoolean(STATE_PLAYING, isPreviewPlaying() || mRestartPreview);
outState.putParcelable(STATE_CAPTURE_URI, mCaptureMediaUri);
outState.putInt(STATE_SELECTED_POS_ID, mMediaLayout.getSelectedViewPos());
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.action_bar_menu, menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
final boolean haveProject = (mProject != null);
final boolean haveMediaItems = haveProject && mProject.getMediaItemCount() > 0;
menu.findItem(R.id.menu_item_capture_video).setVisible(haveProject);
menu.findItem(R.id.menu_item_capture_image).setVisible(haveProject);
menu.findItem(R.id.menu_item_import_video).setVisible(haveProject);
menu.findItem(R.id.menu_item_import_image).setVisible(haveProject);
menu.findItem(R.id.menu_item_import_audio).setVisible(haveProject &&
mProject.getAudioTracks().size() == 0 && haveMediaItems);
menu.findItem(R.id.menu_item_change_aspect_ratio).setVisible(haveProject &&
mProject.hasMultipleAspectRatios());
menu.findItem(R.id.menu_item_edit_project_name).setVisible(haveProject);
// Check if there is an operation pending or preview is on.
boolean enableMenu = haveProject;
if (enableMenu && mPreviewThread != null) {
// Preview is in progress
enableMenu = mPreviewThread.isStopped();
if (enableMenu && mProjectPath != null) {
enableMenu = !ApiService.isProjectBeingEdited(mProjectPath);
}
}
menu.findItem(R.id.menu_item_export_movie).setVisible(enableMenu && haveMediaItems);
menu.findItem(R.id.menu_item_delete_project).setVisible(enableMenu);
menu.findItem(R.id.menu_item_play_exported_movie).setVisible(enableMenu &&
mProject.getExportedMovieUri() != null);
menu.findItem(R.id.menu_item_share_movie).setVisible(enableMenu &&
mProject.getExportedMovieUri() != null);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home: {
// Returns to project picker if user clicks on the app icon in the action bar.
final Intent intent = new Intent(this, ProjectsActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
finish();
return true;
}
case R.id.menu_item_capture_video: {
mInsertMediaItemAfterMediaItemId = mProject.getLastMediaItemId();
// Create parameters for Intent with filename
final ContentValues values = new ContentValues();
String videoFilename = DIRECTORY + '/' + getVideoOutputMediaFileTitle() + ".mp4";
values.put(MediaStore.Video.Media.DATA, videoFilename);
mCaptureMediaUri = getContentResolver().insert(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
final Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, mCaptureMediaUri);
startActivityForResult(intent, REQUEST_CODE_CAPTURE_VIDEO);
return true;
}
case R.id.menu_item_capture_image: {
mInsertMediaItemAfterMediaItemId = mProject.getLastMediaItemId();
// Create parameters for Intent with filename
final ContentValues values = new ContentValues();
mCaptureMediaUri = getContentResolver().insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
final Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, mCaptureMediaUri);
startActivityForResult(intent, REQUEST_CODE_CAPTURE_IMAGE);
return true;
}
case R.id.menu_item_import_video: {
mInsertMediaItemAfterMediaItemId = mProject.getLastMediaItemId();
final Intent intent = new Intent(Intent.ACTION_PICK);
intent.setData(MediaStore.Video.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(intent, REQUEST_CODE_IMPORT_VIDEO);
return true;
}
case R.id.menu_item_import_image: {
mInsertMediaItemAfterMediaItemId = mProject.getLastMediaItemId();
final Intent intent = new Intent(Intent.ACTION_PICK);
intent.setData(MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(intent, REQUEST_CODE_IMPORT_IMAGE);
return true;
}
case R.id.menu_item_import_audio: {
final Intent intent = new Intent(Intent.ACTION_PICK);
intent.setData(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(intent, REQUEST_CODE_IMPORT_MUSIC);
return true;
}
case R.id.menu_item_change_aspect_ratio: {
final ArrayList<Integer> aspectRatiosList = mProject.getUniqueAspectRatiosList();
final int size = aspectRatiosList.size();
if (size > 1) {
final Bundle bundle = new Bundle();
bundle.putIntegerArrayList(PARAM_ASPECT_RATIOS_LIST, aspectRatiosList);
// Get the current aspect ratio index
final int currentAspectRatio = mProject.getAspectRatio();
int currentAspectRatioIndex = 0;
for (int i = 0; i < size; i++) {
final int aspectRatio = aspectRatiosList.get(i);
if (aspectRatio == currentAspectRatio) {
currentAspectRatioIndex = i;
break;
}
}
bundle.putInt(PARAM_CURRENT_ASPECT_RATIO_INDEX, currentAspectRatioIndex);
showDialog(DIALOG_CHOOSE_ASPECT_RATIO_ID, bundle);
}
return true;
}
case R.id.menu_item_edit_project_name: {
showDialog(DIALOG_EDIT_PROJECT_NAME_ID);
return true;
}
case R.id.menu_item_delete_project: {
// Confirm project delete
showDialog(DIALOG_DELETE_PROJECT_ID);
return true;
}
case R.id.menu_item_export_movie: {
// Present the user with a dialog to choose export options
showDialog(DIALOG_EXPORT_OPTIONS_ID);
return true;
}
case R.id.menu_item_play_exported_movie: {
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(mProject.getExportedMovieUri(), "video/*");
intent.putExtra(MediaStore.EXTRA_FINISH_ON_COMPLETION, false);
startActivity(intent);
return true;
}
case R.id.menu_item_share_movie: {
final Intent intent = new Intent(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_STREAM, mProject.getExportedMovieUri());
intent.setType("video/*");
startActivity(intent);
return true;
}
default: {
return false;
}
}
}
private String getVideoOutputMediaFileTitle() {
long dateTaken = System.currentTimeMillis();
Date date = new Date(dateTaken);
SimpleDateFormat dateFormat = new SimpleDateFormat("'VID'_yyyyMMdd_HHmmss", Locale.US);
return dateFormat.format(date);
}
@Override
public Dialog onCreateDialog(int id, final Bundle bundle) {
switch (id) {
case DIALOG_CHOOSE_ASPECT_RATIO_ID: {
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.editor_change_aspect_ratio));
final ArrayList<Integer> aspectRatios =
bundle.getIntegerArrayList(PARAM_ASPECT_RATIOS_LIST);
final int count = aspectRatios.size();
final CharSequence[] aspectRatioStrings = new CharSequence[count];
for (int i = 0; i < count; i++) {
int aspectRatio = aspectRatios.get(i);
switch (aspectRatio) {
case MediaProperties.ASPECT_RATIO_11_9: {
aspectRatioStrings[i] = getString(R.string.aspect_ratio_11_9);
break;
}
case MediaProperties.ASPECT_RATIO_16_9: {
aspectRatioStrings[i] = getString(R.string.aspect_ratio_16_9);
break;
}
case MediaProperties.ASPECT_RATIO_3_2: {
aspectRatioStrings[i] = getString(R.string.aspect_ratio_3_2);
break;
}
case MediaProperties.ASPECT_RATIO_4_3: {
aspectRatioStrings[i] = getString(R.string.aspect_ratio_4_3);
break;
}
case MediaProperties.ASPECT_RATIO_5_3: {
aspectRatioStrings[i] = getString(R.string.aspect_ratio_5_3);
break;
}
default: {
break;
}
}
}
builder.setSingleChoiceItems(aspectRatioStrings,
bundle.getInt(PARAM_CURRENT_ASPECT_RATIO_INDEX),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
final int aspectRatio = aspectRatios.get(which);
ApiService.setAspectRatio(VideoEditorActivity.this, mProjectPath,
aspectRatio);
removeDialog(DIALOG_CHOOSE_ASPECT_RATIO_ID);
}
});
builder.setCancelable(true);
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
removeDialog(DIALOG_CHOOSE_ASPECT_RATIO_ID);
}
});
return builder.create();
}
case DIALOG_DELETE_PROJECT_ID: {
return AlertDialogs.createAlert(this, getString(R.string.editor_delete_project), 0,
getString(R.string.editor_delete_project_question),
getString(R.string.yes),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ApiService.deleteProject(VideoEditorActivity.this, mProjectPath);
mProjectPath = null;
mProject = null;
enterDisabledState(R.string.editor_no_project);
removeDialog(DIALOG_DELETE_PROJECT_ID);
finish();
}
}, getString(R.string.no), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
removeDialog(DIALOG_DELETE_PROJECT_ID);
}
}, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
removeDialog(DIALOG_DELETE_PROJECT_ID);
}
}, true);
}
case DIALOG_DELETE_BAD_PROJECT_ID: {
return AlertDialogs.createAlert(this, getString(R.string.editor_delete_project), 0,
getString(R.string.editor_load_error),
getString(R.string.yes),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ApiService.deleteProject(VideoEditorActivity.this,
bundle.getString(PARAM_PROJECT_PATH));
removeDialog(DIALOG_DELETE_BAD_PROJECT_ID);
finish();
}
}, getString(R.string.no), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
removeDialog(DIALOG_DELETE_BAD_PROJECT_ID);
}
}, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
removeDialog(DIALOG_DELETE_BAD_PROJECT_ID);
}
}, true);
}
case DIALOG_EDIT_PROJECT_NAME_ID: {
if (mProject == null) {
return null;
}
return AlertDialogs.createEditDialog(this,
getString(R.string.editor_edit_project_name),
mProject.getName(),
getString(android.R.string.ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
final TextView tv =
(TextView)((AlertDialog)dialog).findViewById(R.id.text_1);
mProject.setProjectName(tv.getText().toString());
getActionBar().setTitle(tv.getText());
removeDialog(DIALOG_EDIT_PROJECT_NAME_ID);
}
},
getString(android.R.string.cancel),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
removeDialog(DIALOG_EDIT_PROJECT_NAME_ID);
}
},
new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
removeDialog(DIALOG_EDIT_PROJECT_NAME_ID);
}
},
InputType.TYPE_NULL,
32,
null);
}
case DIALOG_EXPORT_OPTIONS_ID: {
if (mProject == null) {
return null;
}
return ExportOptionsDialog.create(this,
new ExportOptionsDialog.ExportOptionsListener() {
@Override
public void onExportOptions(int movieHeight, int movieBitrate) {
mPendingExportFilename = FileUtils.createMovieName(
MediaProperties.FILE_MP4);
ApiService.exportVideoEditor(VideoEditorActivity.this, mProjectPath,
mPendingExportFilename, movieHeight, movieBitrate);
removeDialog(DIALOG_EXPORT_OPTIONS_ID);
showExportProgress();
}
}, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
removeDialog(DIALOG_EXPORT_OPTIONS_ID);
}
}, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
removeDialog(DIALOG_EXPORT_OPTIONS_ID);
}
}, mProject.getAspectRatio());
}
case DIALOG_REMOVE_MEDIA_ITEM_ID: {
return mMediaLayout.onCreateDialog(id, bundle);
}
case DIALOG_CHANGE_RENDERING_MODE_ID: {
return mMediaLayout.onCreateDialog(id, bundle);
}
case DIALOG_REMOVE_TRANSITION_ID: {
return mMediaLayout.onCreateDialog(id, bundle);
}
case DIALOG_REMOVE_OVERLAY_ID: {
return mOverlayLayout.onCreateDialog(id, bundle);
}
case DIALOG_REMOVE_EFFECT_ID: {
return mMediaLayout.onCreateDialog(id, bundle);
}
case DIALOG_REMOVE_AUDIO_TRACK_ID: {
return mAudioTrackLayout.onCreateDialog(id, bundle);
}
default: {
return null;
}
}
}
/**
* Called when user clicks on the button in the control panel.
* @param target one of the "play", "rewind", "next",
* and "prev" buttons in the control panel
*/
public void onClickHandler(View target) {
final long playheadPosMs = mProject.getPlayheadPos();
switch (target.getId()) {
case R.id.editor_play: {
if (mProject != null && mPreviewThread != null) {
if (mPreviewThread.isPlaying()) {
mPreviewThread.stopPreviewPlayback();
} else if (mProject.getMediaItemCount() > 0) {
mPreviewThread.startPreviewPlayback(mProject, playheadPosMs);
}
}
break;
}
case R.id.editor_rewind: {
if (mProject != null && mPreviewThread != null) {
if (mPreviewThread.isPlaying()) {
mPreviewThread.stopPreviewPlayback();
movePlayhead(0);
mPreviewThread.startPreviewPlayback(mProject, 0);
} else {
movePlayhead(0);
showPreviewFrame();
}
}
break;
}
case R.id.editor_next: {
if (mProject != null && mPreviewThread != null) {
final boolean restartPreview;
if (mPreviewThread.isPlaying()) {
mPreviewThread.stopPreviewPlayback();
restartPreview = true;
} else {
restartPreview = false;
}
final MovieMediaItem mediaItem = mProject.getNextMediaItem(playheadPosMs);
if (mediaItem != null) {
movePlayhead(mProject.getMediaItemBeginTime(mediaItem.getId()));
if (restartPreview) {
mPreviewThread.startPreviewPlayback(mProject,
mProject.getPlayheadPos());
} else {
showPreviewFrame();
}
} else { // Move to the end of the timeline
movePlayhead(mProject.computeDuration());
showPreviewFrame();
}
}
break;
}
case R.id.editor_prev: {
if (mProject != null && mPreviewThread != null) {
final boolean restartPreview;
if (mPreviewThread.isPlaying()) {
mPreviewThread.stopPreviewPlayback();
restartPreview = true;
} else {
restartPreview = false;
}
final MovieMediaItem mediaItem = mProject.getPreviousMediaItem(playheadPosMs);
if (mediaItem != null) {
movePlayhead(mProject.getMediaItemBeginTime(mediaItem.getId()));
} else { // Move to the beginning of the timeline
movePlayhead(0);
}
if (restartPreview) {
mPreviewThread.startPreviewPlayback(mProject, mProject.getPlayheadPos());
} else {
showPreviewFrame();
}
}
break;
}
default: {
break;
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent extras) {
super.onActivityResult(requestCode, resultCode, extras);
if (resultCode == RESULT_CANCELED) {
switch (requestCode) {
case REQUEST_CODE_CAPTURE_VIDEO:
case REQUEST_CODE_CAPTURE_IMAGE: {
if (mCaptureMediaUri != null) {
getContentResolver().delete(mCaptureMediaUri, null, null);
mCaptureMediaUri = null;
}
break;
}
default: {
break;
}
}
return;
}
switch (requestCode) {
case REQUEST_CODE_CAPTURE_VIDEO: {
if (mProject != null) {
ApiService.addMediaItemVideoUri(this, mProjectPath,
ApiService.generateId(), mInsertMediaItemAfterMediaItemId,
mCaptureMediaUri, MediaItem.RENDERING_MODE_BLACK_BORDER,
mProject.getTheme());
mInsertMediaItemAfterMediaItemId = null;
} else {
// Add this video after the project loads
mAddMediaItemVideoUri = mCaptureMediaUri;
}
mCaptureMediaUri = null;
break;
}
case REQUEST_CODE_CAPTURE_IMAGE: {
if (mProject != null) {
ApiService.addMediaItemImageUri(this, mProjectPath,
ApiService.generateId(), mInsertMediaItemAfterMediaItemId,
mCaptureMediaUri, MediaItem.RENDERING_MODE_BLACK_BORDER,
MediaItemUtils.getDefaultImageDuration(),
mProject.getTheme());
mInsertMediaItemAfterMediaItemId = null;
} else {
// Add this image after the project loads
mAddMediaItemImageUri = mCaptureMediaUri;
}
mCaptureMediaUri = null;
break;
}
case REQUEST_CODE_IMPORT_VIDEO: {
final Uri mediaUri = extras.getData();
if (mProject != null) {
if ("media".equals(mediaUri.getAuthority())) {
ApiService.addMediaItemVideoUri(this, mProjectPath,
ApiService.generateId(), mInsertMediaItemAfterMediaItemId,
mediaUri, MediaItem.RENDERING_MODE_BLACK_BORDER,
mProject.getTheme());
} else {
// Notify the user that this item needs to be downloaded.
Toast.makeText(this, getString(R.string.editor_video_load),
Toast.LENGTH_LONG).show();
// When the download is complete insert it into the project.
ApiService.loadMediaItem(this, mProjectPath, mediaUri, "video/*");
}
mInsertMediaItemAfterMediaItemId = null;
} else {
// Add this video after the project loads
mAddMediaItemVideoUri = mediaUri;
}
break;
}
case REQUEST_CODE_IMPORT_IMAGE: {
final Uri mediaUri = extras.getData();
if (mProject != null) {
if ("media".equals(mediaUri.getAuthority())) {
ApiService.addMediaItemImageUri(this, mProjectPath,
ApiService.generateId(), mInsertMediaItemAfterMediaItemId,
mediaUri, MediaItem.RENDERING_MODE_BLACK_BORDER,
MediaItemUtils.getDefaultImageDuration(), mProject.getTheme());
} else {
// Notify the user that this item needs to be downloaded.
Toast.makeText(this, getString(R.string.editor_image_load),
Toast.LENGTH_LONG).show();
// When the download is complete insert it into the project.
ApiService.loadMediaItem(this, mProjectPath, mediaUri, "image/*");
}
mInsertMediaItemAfterMediaItemId = null;
} else {
// Add this image after the project loads
mAddMediaItemImageUri = mediaUri;
}
break;
}
case REQUEST_CODE_IMPORT_MUSIC: {
final Uri data = extras.getData();
if (mProject != null) {
ApiService.addAudioTrack(this, mProjectPath, ApiService.generateId(), data,
true);
} else {
mAddAudioTrackUri = data;
}
break;
}
case REQUEST_CODE_EDIT_TRANSITION: {
final int type = extras.getIntExtra(TransitionsActivity.PARAM_TRANSITION_TYPE, -1);
final String afterMediaId = extras.getStringExtra(
TransitionsActivity.PARAM_AFTER_MEDIA_ITEM_ID);
final String transitionId = extras.getStringExtra(
TransitionsActivity.PARAM_TRANSITION_ID);
final long transitionDurationMs = extras.getLongExtra(
TransitionsActivity.PARAM_TRANSITION_DURATION, 500);
if (mProject != null) {
mMediaLayout.editTransition(afterMediaId, transitionId, type,
transitionDurationMs);
} else {
// Add this transition after you load the project
mEditTransitionAfterMediaId = afterMediaId;
mEditTransitionId = transitionId;
mEditTransitionType = type;
mEditTransitionDurationMs = transitionDurationMs;
}
break;
}
case REQUEST_CODE_PICK_TRANSITION: {
final int type = extras.getIntExtra(TransitionsActivity.PARAM_TRANSITION_TYPE, -1);
final String afterMediaId = extras.getStringExtra(
TransitionsActivity.PARAM_AFTER_MEDIA_ITEM_ID);
final long transitionDurationMs = extras.getLongExtra(
TransitionsActivity.PARAM_TRANSITION_DURATION, 500);
if (mProject != null) {
mMediaLayout.addTransition(afterMediaId, type, transitionDurationMs);
} else {
// Add this transition after you load the project
mAddTransitionAfterMediaId = afterMediaId;
mAddTransitionType = type;
mAddTransitionDurationMs = transitionDurationMs;
}
break;
}
case REQUEST_CODE_PICK_OVERLAY: {
// If there is no overlay id, it means we are adding a new overlay.
// Otherwise we generate a unique new id for the new overlay.
final String mediaItemId =
extras.getStringExtra(OverlayTitleEditor.PARAM_MEDIA_ITEM_ID);
final String overlayId =
extras.getStringExtra(OverlayTitleEditor.PARAM_OVERLAY_ID);
final Bundle bundle =
extras.getBundleExtra(OverlayTitleEditor.PARAM_OVERLAY_ATTRIBUTES);
if (mProject != null) {
final MovieMediaItem mediaItem = mProject.getMediaItem(mediaItemId);
if (mediaItem != null) {
if (overlayId == null) {
ApiService.addOverlay(this, mProject.getPath(), mediaItemId,
ApiService.generateId(), bundle,
mediaItem.getAppBoundaryBeginTime(),
OverlayLinearLayout.DEFAULT_TITLE_DURATION);
} else {
ApiService.setOverlayUserAttributes(this, mProject.getPath(),
mediaItemId, overlayId, bundle);
}
mOverlayLayout.invalidateCAB();
}
} else {
// Add this overlay after you load the project.
mAddOverlayMediaItemId = mediaItemId;
mAddOverlayUserAttributes = bundle;
mEditOverlayId = overlayId;
}
break;
}
case REQUEST_CODE_KEN_BURNS: {
final String mediaItemId = extras.getStringExtra(
KenBurnsActivity.PARAM_MEDIA_ITEM_ID);
final Rect startRect = extras.getParcelableExtra(
KenBurnsActivity.PARAM_START_RECT);
final Rect endRect = extras.getParcelableExtra(
KenBurnsActivity.PARAM_END_RECT);
if (mProject != null) {
mMediaLayout.addEffect(EffectType.EFFECT_KEN_BURNS, mediaItemId,
startRect, endRect);
mMediaLayout.invalidateActionBar();
} else {
// Add this effect after you load the project.
mAddEffectMediaItemId = mediaItemId;
mAddEffectType = EffectType.EFFECT_KEN_BURNS;
mAddKenBurnsStartRect = startRect;
mAddKenBurnsEndRect = endRect;
}
break;
}
default: {
break;
}
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
logd("surfaceCreated");
mHaveSurface = true;
mSurfaceWidth = -1;
createPreviewThreadIfNeeded();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
logd("surfaceChanged: " + width + "x" + height);
mSurfaceWidth = width;
mSurfaceHeight = height;
if (mPreviewThread != null) {
mPreviewThread.onSurfaceChanged(width, height);
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
logd("surfaceDestroyed");
mHaveSurface = false;
stopPreviewThread();
}
// Stop the preview playback if pending and quit the preview thread
private void stopPreviewThread() {
if (mPreviewThread != null) {
mPreviewThread.stopPreviewPlayback();
mPreviewThread.quit();
mPreviewThread = null;
}
}
@Override
protected void enterTransitionalState(int statusStringId) {
mEditorProjectView.setVisibility(View.GONE);
mEditorEmptyView.setVisibility(View.VISIBLE);
((TextView)findViewById(R.id.empty_project_text)).setText(statusStringId);
findViewById(R.id.empty_project_progress).setVisibility(View.VISIBLE);
}
@Override
protected void enterDisabledState(int statusStringId) {
mEditorProjectView.setVisibility(View.GONE);
mEditorEmptyView.setVisibility(View.VISIBLE);
getActionBar().setTitle(R.string.full_app_name);
((TextView)findViewById(R.id.empty_project_text)).setText(statusStringId);
findViewById(R.id.empty_project_progress).setVisibility(View.GONE);
}
@Override
protected void enterReadyState() {
mEditorProjectView.setVisibility(View.VISIBLE);
mEditorEmptyView.setVisibility(View.GONE);
}
@Override
protected boolean showPreviewFrame() {
if (mPreviewThread == null) { // The surface is not ready yet.
return false;
}
// Regenerate the preview frame
if (mProject != null && !mPreviewThread.isPlaying() && mPendingExportFilename == null) {
// Display the preview frame
mPreviewThread.previewFrame(mProject, mProject.getPlayheadPos(),
mProject.getMediaItemCount() == 0);
}
return true;
}
@Override
protected void updateTimelineDuration() {
if (mProject == null) {
return;
}
final long durationMs = mProject.computeDuration();
// Resize the timeline according to the new timeline duration
final int zoomWidth = mActivityWidth + timeToDimension(durationMs);
final int childrenCount = mTimelineLayout.getChildCount();
for (int i = 0; i < childrenCount; i++) {
final View child = mTimelineLayout.getChildAt(i);
final ViewGroup.LayoutParams lp = child.getLayoutParams();
lp.width = zoomWidth;
child.setLayoutParams(lp);
}
mTimelineLayout.requestLayout(mLayoutCallback);
// Since the duration has changed make sure that the playhead
// position is valid.
if (mProject.getPlayheadPos() > durationMs) {
movePlayhead(durationMs);
}
mAudioTrackLayout.updateTimelineDuration();
}
/**
* Convert the time to dimension
* At zoom level 1: one activity width = 1200 seconds
* At zoom level 2: one activity width = 600 seconds
* ...
* At zoom level 100: one activity width = 12 seconds
*
* At zoom level 1000: one activity width = 1.2 seconds
*
* @param durationMs The time
*
* @return The dimension
*/
private int timeToDimension(long durationMs) {
return (int)((mProject.getZoomLevel() * mActivityWidth * durationMs) / 1200000);
}
/**
* Zoom the timeline
*
* @param level The zoom level
* @param updateControl true to set the control position to match the
* zoom level
*/
private int zoomTimeline(int level, boolean updateControl) {
if (level < 1 || level > MAX_ZOOM_LEVEL) {
return mProject.getZoomLevel();
}
mProject.setZoomLevel(level);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "zoomTimeline level: " + level + " -> " + timeToDimension(1000) + " pix/s");
}
updateTimelineDuration();
if (updateControl) {
mZoomControl.setProgress(level);
}
return level;
}
@Override
protected void movePlayhead(long timeMs) {
movePlayhead(timeMs, true);
}
private void movePlayhead(long timeMs, boolean smooth) {
if (mProject == null) {
return;
}
if (setPlayhead(timeMs)) {
// Scroll the timeline such that the specified position
// is in the center of the screen
mTimelineScroller.appScrollTo(timeToDimension(timeMs), smooth);
}
}
/**
* Set the playhead at the specified time position
*
* @param timeMs The time position
*
* @return true if the playhead was set at the specified time position
*/
private boolean setPlayhead(long timeMs) {
// Check if the position would change
if (mCurrentPlayheadPosMs == timeMs) {
return false;
}
// Check if the time is valid. Note that invalid values are common due
// to overscrolling the timeline
if (timeMs < 0) {
return false;
} else if (timeMs > mProject.computeDuration()) {
return false;
}
mCurrentPlayheadPosMs = timeMs;
mTimeView.setText(StringUtils.getTimestampAsString(this, timeMs));
mProject.setPlayheadPos(timeMs);
return true;
}
@Override
protected void setAspectRatio(final int aspectRatio) {
final FrameLayout.LayoutParams lp =
(FrameLayout.LayoutParams)mSurfaceView.getLayoutParams();
switch (aspectRatio) {
case MediaProperties.ASPECT_RATIO_5_3: {
lp.width = (lp.height * 5) / 3;
break;
}
case MediaProperties.ASPECT_RATIO_4_3: {
lp.width = (lp.height * 4) / 3;
break;
}
case MediaProperties.ASPECT_RATIO_3_2: {
lp.width = (lp.height * 3) / 2;
break;
}
case MediaProperties.ASPECT_RATIO_11_9: {
lp.width = (lp.height * 11) / 9;
break;
}
case MediaProperties.ASPECT_RATIO_16_9: {
lp.width = (lp.height * 16) / 9;
break;
}
default: {
break;
}
}
logd("setAspectRatio: " + aspectRatio + ", size: " + lp.width + "x" + lp.height);
mSurfaceView.setLayoutParams(lp);
mOverlayView.setLayoutParams(lp);
}
@Override
protected MediaLinearLayout getMediaLayout() {
return mMediaLayout;
}
@Override
protected OverlayLinearLayout getOverlayLayout() {
return mOverlayLayout;
}
@Override
protected AudioTrackLinearLayout getAudioTrackLayout() {
return mAudioTrackLayout;
}
@Override
protected void onExportProgress(int progress) {
if (mExportProgressDialog != null) {
mExportProgressDialog.setProgress(progress);
}
}
@Override
protected void onExportComplete() {
if (mExportProgressDialog != null) {
mExportProgressDialog.dismiss();
mExportProgressDialog = null;
}
}
@Override
protected void onProjectEditStateChange(boolean projectEdited) {
logd("onProjectEditStateChange: " + projectEdited);
mPreviewPlayButton.setAlpha(projectEdited ? 100 : 255);
mPreviewPlayButton.setEnabled(!projectEdited);
mPreviewRewindButton.setEnabled(!projectEdited);
mPreviewNextButton.setEnabled(!projectEdited);
mPreviewPrevButton.setEnabled(!projectEdited);
mMediaLayout.invalidateActionBar();
mOverlayLayout.invalidateCAB();
invalidateOptionsMenu();
}
@Override
protected void initializeFromProject(boolean updateUI) {
logd("Project was clean: " + mProject.isClean());
if (updateUI || !mProject.isClean()) {
getActionBar().setTitle(mProject.getName());
// Clear the media related to the previous project and
// add the media for the current project.
mMediaLayout.setParentTimelineScrollView(mTimelineScroller);
mMediaLayout.setProject(mProject);
mOverlayLayout.setProject(mProject);
mAudioTrackLayout.setProject(mProject);
mPlayheadView.setProject(mProject);
// Add the media items to the media item layout
mMediaLayout.addMediaItems(mProject.getMediaItems());
mMediaLayout.setSelectedView(mMediaLayoutSelectedPos);
// Add the media items to the overlay layout
mOverlayLayout.addMediaItems(mProject.getMediaItems());
// Add the audio tracks to the audio tracks layout
mAudioTrackLayout.addAudioTracks(mProject.getAudioTracks());
setAspectRatio(mProject.getAspectRatio());
}
updateTimelineDuration();
zoomTimeline(mProject.getZoomLevel(), true);
// Set the playhead position. We need to wait for the layout to
// complete before we can scroll to the playhead position.
final Handler handler = new Handler();
handler.post(new Runnable() {
private final long DELAY = 100;
private final int ATTEMPTS = 20;
private int mAttempts = ATTEMPTS;
@Override
public void run() {
// If the surface is not yet created (showPreviewFrame()
// returns false) wait for a while (DELAY * ATTEMPTS).
if (showPreviewFrame() == false && mAttempts >= 0) {
mAttempts--;
if (mAttempts >= 0) {
handler.postDelayed(this, DELAY);
}
}
}
});
if (mAddMediaItemVideoUri != null) {
ApiService.addMediaItemVideoUri(this, mProjectPath, ApiService.generateId(),
mInsertMediaItemAfterMediaItemId,
mAddMediaItemVideoUri, MediaItem.RENDERING_MODE_BLACK_BORDER,
mProject.getTheme());
mAddMediaItemVideoUri = null;
mInsertMediaItemAfterMediaItemId = null;
}
if (mAddMediaItemImageUri != null) {
ApiService.addMediaItemImageUri(this, mProjectPath, ApiService.generateId(),
mInsertMediaItemAfterMediaItemId,
mAddMediaItemImageUri, MediaItem.RENDERING_MODE_BLACK_BORDER,
MediaItemUtils.getDefaultImageDuration(), mProject.getTheme());
mAddMediaItemImageUri = null;
mInsertMediaItemAfterMediaItemId = null;
}
if (mAddAudioTrackUri != null) {
ApiService.addAudioTrack(this, mProject.getPath(), ApiService.generateId(),
mAddAudioTrackUri, true);
mAddAudioTrackUri = null;
}
if (mAddTransitionAfterMediaId != null) {
mMediaLayout.addTransition(mAddTransitionAfterMediaId, mAddTransitionType,
mAddTransitionDurationMs);
mAddTransitionAfterMediaId = null;
}
if (mEditTransitionId != null) {
mMediaLayout.editTransition(mEditTransitionAfterMediaId, mEditTransitionId,
mEditTransitionType, mEditTransitionDurationMs);
mEditTransitionId = null;
mEditTransitionAfterMediaId = null;
}
if (mAddOverlayMediaItemId != null) {
ApiService.addOverlay(this, mProject.getPath(), mAddOverlayMediaItemId,
ApiService.generateId(), mAddOverlayUserAttributes, 0,
OverlayLinearLayout.DEFAULT_TITLE_DURATION);
mAddOverlayMediaItemId = null;
mAddOverlayUserAttributes = null;
}
if (mEditOverlayMediaItemId != null) {
ApiService.setOverlayUserAttributes(this, mProject.getPath(), mEditOverlayMediaItemId,
mEditOverlayId, mEditOverlayUserAttributes);
mEditOverlayMediaItemId = null;
mEditOverlayId = null;
mEditOverlayUserAttributes = null;
}
if (mAddEffectMediaItemId != null) {
mMediaLayout.addEffect(mAddEffectType, mAddEffectMediaItemId,
mAddKenBurnsStartRect, mAddKenBurnsEndRect);
mAddEffectMediaItemId = null;
}
enterReadyState();
if (mPendingExportFilename != null) {
if (ApiService.isVideoEditorExportPending(mProjectPath, mPendingExportFilename)) {
// The export is still pending
// Display the export project dialog
showExportProgress();
} else {
// The export completed while the Activity was paused
mPendingExportFilename = null;
}
}
invalidateOptionsMenu();
restartPreview();
}
/**
* Restarts preview.
*/
private void restartPreview() {
if (mRestartPreview == false) {
return;
}
if (mProject == null) {
return;
}
if (mPreviewThread != null) {
mRestartPreview = false;
mPreviewThread.startPreviewPlayback(mProject, mProject.getPlayheadPos());
}
}
/**
* Shows progress dialog during export operation.
*/
private void showExportProgress() {
// Keep the CPU on throughout the export operation.
mExportProgressDialog = new ProgressDialog(this) {
@Override
public void onStart() {
super.onStart();
mCpuWakeLock.acquire();
}
@Override
public void onStop() {
super.onStop();
mCpuWakeLock.release();
}
};
mExportProgressDialog.setTitle(getString(R.string.export_dialog_export));
mExportProgressDialog.setMessage(null);
mExportProgressDialog.setIndeterminate(false);
// Allow cancellation with BACK button.
mExportProgressDialog.setCancelable(true);
mExportProgressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
cancelExport();
}
});
mExportProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
mExportProgressDialog.setMax(100);
mExportProgressDialog.setCanceledOnTouchOutside(false);
mExportProgressDialog.setButton(getString(android.R.string.cancel),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
cancelExport();
}
}
);
mExportProgressDialog.setCanceledOnTouchOutside(false);
mExportProgressDialog.show();
mExportProgressDialog.setProgressNumberFormat("");
}
private void cancelExport() {
ApiService.cancelExportVideoEditor(VideoEditorActivity.this, mProjectPath,
mPendingExportFilename);
mPendingExportFilename = null;
mExportProgressDialog = null;
}
private boolean isPreviewPlaying() {
if (mPreviewThread == null)
return false;
return mPreviewThread.isPlaying();
}
/**
* The preview thread
*/
private class PreviewThread extends Thread {
// Preview states
private final int PREVIEW_STATE_STOPPED = 0;
private final int PREVIEW_STATE_STARTING = 1;
private final int PREVIEW_STATE_STARTED = 2;
private final int PREVIEW_STATE_STOPPING = 3;
private final int OVERLAY_DATA_COUNT = 16;
private final Handler mMainHandler;
private final Queue<Runnable> mQueue;
private final SurfaceHolder mSurfaceHolder;
private final Queue<VideoEditor.OverlayData> mOverlayDataQueue;
private Handler mThreadHandler;
private int mPreviewState;
private Bitmap mOverlayBitmap;
private final Runnable mProcessQueueRunnable = new Runnable() {
@Override
public void run() {
// Process whatever accumulated in the queue
Runnable runnable;
while ((runnable = mQueue.poll()) != null) {
runnable.run();
}
}
};
/**
* Constructor
*
* @param surfaceHolder The surface holder
*/
public PreviewThread(SurfaceHolder surfaceHolder) {
mMainHandler = new Handler(Looper.getMainLooper());
mQueue = new LinkedBlockingQueue<Runnable>();
mSurfaceHolder = surfaceHolder;
mPreviewState = PREVIEW_STATE_STOPPED;
mOverlayDataQueue = new LinkedBlockingQueue<VideoEditor.OverlayData>();
for (int i = 0; i < OVERLAY_DATA_COUNT; i++) {
mOverlayDataQueue.add(new VideoEditor.OverlayData());
}
start();
}
/**
* Preview the specified frame
*
* @param project The video editor project
* @param timeMs The frame time
* @param clear true to clear the output
*/
public void previewFrame(final VideoEditorProject project, final long timeMs,
final boolean clear) {
if (mPreviewState == PREVIEW_STATE_STARTING || mPreviewState == PREVIEW_STATE_STARTED) {
stopPreviewPlayback();
}
logd("Preview frame at: " + timeMs + " " + clear);
// We only need to see the last frame
mQueue.clear();
mQueue.add(new Runnable() {
@Override
public void run() {
if (clear) {
try {
project.clearSurface(mSurfaceHolder);
} catch (Exception ex) {
Log.w(TAG, "Surface cannot be cleared");
}
mMainHandler.post(new Runnable() {
@Override
public void run() {
if (mOverlayBitmap != null) {
mOverlayBitmap.eraseColor(Color.TRANSPARENT);
mOverlayView.invalidate();
}
}
});
} else {
final VideoEditor.OverlayData overlayData;
try {
overlayData = mOverlayDataQueue.remove();
} catch (NoSuchElementException ex) {
Log.e(TAG, "Out of OverlayData elements");
return;
}
try {
if (project.renderPreviewFrame(mSurfaceHolder, timeMs, overlayData)
< 0) {
logd("Cannot render preview frame at: " + timeMs +
" of " + mProject.computeDuration());
mOverlayDataQueue.add(overlayData);
} else {
if (overlayData.needsRendering()) {
mMainHandler.post(new Runnable() {
/*
* {@inheritDoc}
*/
@Override
public void run() {
if (mOverlayBitmap != null) {
overlayData.renderOverlay(mOverlayBitmap);
mOverlayView.invalidate();
} else {
overlayData.release();
}
mOverlayDataQueue.add(overlayData);
}
});
} else {
mOverlayDataQueue.add(overlayData);
}
}
} catch (Exception ex) {
logd("renderPreviewFrame failed at timeMs: " + timeMs + "\n" + ex);
mOverlayDataQueue.add(overlayData);
}
}
}
});
if (mThreadHandler != null) {
mThreadHandler.post(mProcessQueueRunnable);
}
}
/**
* Display the frame at the specified time position
*
* @param mediaItem The media item
* @param timeMs The frame time
*/
public void renderMediaItemFrame(final MovieMediaItem mediaItem, final long timeMs) {
if (mPreviewState == PREVIEW_STATE_STARTING || mPreviewState == PREVIEW_STATE_STARTED) {
stopPreviewPlayback();
}
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Render media item frame at: " + timeMs);
}
// We only need to see the last frame
mQueue.clear();
mQueue.add(new Runnable() {
@Override
public void run() {
try {
if (mProject.renderMediaItemFrame(mSurfaceHolder, mediaItem.getId(),
timeMs) < 0) {
logd("Cannot render media item frame at: " + timeMs +
" of " + mediaItem.getDuration());
}
} catch (Exception ex) {
logd("Cannot render preview frame at: " + timeMs + "\n" + ex);
}
}
});
if (mThreadHandler != null) {
mThreadHandler.post(mProcessQueueRunnable);
}
}
/**
* Starts the preview playback.
*
* @param project The video editor project
* @param fromMs Start playing from the specified position
*/
private void startPreviewPlayback(final VideoEditorProject project, final long fromMs) {
if (mPreviewState != PREVIEW_STATE_STOPPED) {
logd("Preview did not start: " + mPreviewState);
return;
}
previewStarted(project);
logd("Start preview at: " + fromMs);
// Clear any pending preview frames
mQueue.clear();
mQueue.add(new Runnable() {
@Override
public void run() {
try {
project.startPreview(mSurfaceHolder, fromMs, -1, false, 3,
new VideoEditor.PreviewProgressListener() {
@Override
public void onStart(VideoEditor videoEditor) {
}
@Override
public void onProgress(VideoEditor videoEditor, final long timeMs,
final VideoEditor.OverlayData overlayData) {
mMainHandler.post(new Runnable() {
@Override
public void run() {
if (overlayData != null && overlayData.needsRendering()) {
if (mOverlayBitmap != null) {
overlayData.renderOverlay(mOverlayBitmap);
mOverlayView.invalidate();
} else {
overlayData.release();
}
}
if (mPreviewState == PREVIEW_STATE_STARTED ||
mPreviewState == PREVIEW_STATE_STOPPING) {
movePlayhead(timeMs);
}
}
});
}
@Override
public void onStop(VideoEditor videoEditor) {
mMainHandler.post(new Runnable() {
@Override
public void run() {
if (mPreviewState == PREVIEW_STATE_STARTED ||
mPreviewState == PREVIEW_STATE_STOPPING) {
previewStopped(false);
}
}
});
}
public void onError(VideoEditor videoEditor, int error) {
Log.w(TAG, "PreviewProgressListener onError:" + error);
// Notify the user that some error happened.
mMainHandler.post(new Runnable() {
@Override
public void run() {
String msg = getString(R.string.editor_preview_error);
Toast.makeText(VideoEditorActivity.this, msg,
Toast.LENGTH_LONG).show();
}
});
onStop(videoEditor);
}
});
mMainHandler.post(new Runnable() {
@Override
public void run() {
mPreviewState = PREVIEW_STATE_STARTED;
}
});
} catch (Exception ex) {
// This exception may occur when trying to play frames
// at the end of the timeline
// (e.g. when fromMs == clip duration)
Log.w(TAG, "Cannot start preview at: " + fromMs + "\n" + ex);
mMainHandler.post(new Runnable() {
@Override
public void run() {
mPreviewState = PREVIEW_STATE_STARTED;
previewStopped(true);
}
});
}
}
});
if (mThreadHandler != null) {
mThreadHandler.post(mProcessQueueRunnable);
}
}
/**
* The preview started.
* This method is always invoked from the UI thread.
*
* @param project The project
*/
private void previewStarted(VideoEditorProject project) {
// Change the button image back to a pause icon
mPreviewPlayButton.setImageResource(R.drawable.btn_playback_ic_pause);
mTimelineScroller.enableUserScrolling(false);
mMediaLayout.setPlaybackInProgress(true);
mOverlayLayout.setPlaybackInProgress(true);
mAudioTrackLayout.setPlaybackInProgress(true);
mPreviewState = PREVIEW_STATE_STARTING;
// Keep the screen on during the preview.
VideoEditorActivity.this.getWindow().addFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
/**
* Stops the preview.
*/
private void stopPreviewPlayback() {
switch (mPreviewState) {
case PREVIEW_STATE_STOPPED: {
logd("stopPreviewPlayback: State was PREVIEW_STATE_STOPPED");
return;
}
case PREVIEW_STATE_STOPPING: {
logd("stopPreviewPlayback: State was PREVIEW_STATE_STOPPING");
return;
}
case PREVIEW_STATE_STARTING: {
logd("stopPreviewPlayback: State was PREVIEW_STATE_STARTING " +
"now PREVIEW_STATE_STOPPING");
mPreviewState = PREVIEW_STATE_STOPPING;
// We need to wait until the preview starts
mMainHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (mPreviewState == PREVIEW_STATE_STARTED) {
logd("stopPreviewPlayback: Now PREVIEW_STATE_STARTED");
previewStopped(false);
} else if (mPreviewState == PREVIEW_STATE_STOPPING) {
// Keep waiting
mMainHandler.postDelayed(this, 100);
logd("stopPreviewPlayback: Waiting for PREVIEW_STATE_STARTED");
} else {
logd("stopPreviewPlayback: PREVIEW_STATE_STOPPED while waiting");
}
}
}, 50);
break;
}
case PREVIEW_STATE_STARTED: {
logd("stopPreviewPlayback: State was PREVIEW_STATE_STARTED");
// We need to stop
previewStopped(false);
return;
}
default: {
throw new IllegalArgumentException("stopPreviewPlayback state: " +
mPreviewState);
}
}
}
/**
* The surface size has changed
*
* @param width The new surface width
* @param height The new surface height
*/
private void onSurfaceChanged(int width, int height) {
if (mOverlayBitmap != null) {
if (mOverlayBitmap.getWidth() == width && mOverlayBitmap.getHeight() == height) {
// The size has not changed
return;
}
mOverlayView.setImageBitmap(null);
mOverlayBitmap.recycle();
mOverlayBitmap = null;
}
// Create the overlay bitmap
logd("Overlay size: " + width + " x " + height);
mOverlayBitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888);
mOverlayView.setImageBitmap(mOverlayBitmap);
}
/**
* Preview stopped. This method is always invoked from the UI thread.
*
* @param error true if the preview stopped due to an error
*/
private void previewStopped(boolean error) {
if (mProject == null) {
Log.w(TAG, "previewStopped: project was deleted.");
return;
}
if (mPreviewState != PREVIEW_STATE_STARTED) {
throw new IllegalStateException("previewStopped in state: " + mPreviewState);
}
// Change the button image back to a play icon
mPreviewPlayButton.setImageResource(R.drawable.btn_playback_ic_play);
if (error == false) {
// Set the playhead position at the position where the playback stopped
final long stopTimeMs = mProject.stopPreview();
movePlayhead(stopTimeMs);
logd("PREVIEW_STATE_STOPPED: " + stopTimeMs);
} else {
logd("PREVIEW_STATE_STOPPED due to error");
}
mPreviewState = PREVIEW_STATE_STOPPED;
// The playback has stopped
mTimelineScroller.enableUserScrolling(true);
mMediaLayout.setPlaybackInProgress(false);
mAudioTrackLayout.setPlaybackInProgress(false);
mOverlayLayout.setPlaybackInProgress(false);
// Do not keep the screen on if there is no preview in progress.
VideoEditorActivity.this.getWindow().clearFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
/**
* @return true if preview playback is in progress
*/
private boolean isPlaying() {
return mPreviewState == PREVIEW_STATE_STARTING ||
mPreviewState == PREVIEW_STATE_STARTED;
}
/**
* @return true if the preview is stopped
*/
private boolean isStopped() {
return mPreviewState == PREVIEW_STATE_STOPPED;
}
@Override
public void run() {
setPriority(MAX_PRIORITY);
Looper.prepare();
mThreadHandler = new Handler();
// Ensure that the queued items are processed
mThreadHandler.post(mProcessQueueRunnable);
// Run the loop
Looper.loop();
}
/**
* Quits the thread
*/
public void quit() {
// Release the overlay bitmap
if (mOverlayBitmap != null) {
mOverlayView.setImageBitmap(null);
mOverlayBitmap.recycle();
mOverlayBitmap = null;
}
if (mThreadHandler != null) {
mThreadHandler.getLooper().quit();
try {
// Wait for the thread to quit. An ANR waiting to happen.
mThreadHandler.getLooper().getThread().join();
} catch (InterruptedException ex) {
}
}
mQueue.clear();
}
}
private static void logd(String message) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, message);
}
}
}