/* * 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.example.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 roughcut.media.videoeditor.MediaItem; import roughcut.media.videoeditor.MediaProperties; import roughcut.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.example.android.videoeditor.service.ApiService; import com.example.android.videoeditor.service.MovieMediaItem; import com.example.android.videoeditor.service.VideoEditorProject; import com.example.android.videoeditor.util.FileUtils; import com.example.android.videoeditor.util.MediaItemUtils; import com.example.android.videoeditor.util.StringUtils; import com.example.android.videoeditor.widgets.AudioTrackLinearLayout; import com.example.android.videoeditor.widgets.MediaLinearLayout; import com.example.android.videoeditor.widgets.MediaLinearLayoutListener; import com.example.android.videoeditor.widgets.OverlayLinearLayout; import com.example.android.videoeditor.widgets.PlayheadView; import com.example.android.videoeditor.widgets.PreviewSurfaceView; import com.example.android.videoeditor.widgets.ScrollViewListener; import com.example.android.videoeditor.widgets.TimelineHorizontalScrollView; import com.example.android.videoeditor.widgets.TimelineRelativeLayout; import com.example.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; // To store the export progress when the activity is destroyed private static final String EXPORT_PROGRESS = "export_progress"; private int mExportProgress; 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); mExportProgress = savedInstanceState.getInt(EXPORT_PROGRESS); } else { mRestartPreview = false; mMediaLayoutSelectedPos = -1; mExportProgress = 0; } // 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()); outState.putInt(EXPORT_PROGRESS,mExportProgress); } @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 mExportProgress = 0; 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) { mExportProgress = progress; mExportProgressDialog.setProgress(progress); } } @Override protected void onExportComplete() { if (mExportProgressDialog != null) { mExportProgressDialog.dismiss(); mExportProgressDialog = null; mExportProgress = 0; } } @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(""); if (mExportProgress >= 0 && mExportProgress <= 100) { mExportProgressDialog.setProgress(mExportProgress); } } private void cancelExport() { ApiService.cancelExportVideoEditor(VideoEditorActivity.this, mProjectPath, mPendingExportFilename); mExportProgress = 0; 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); } } }