VideoEditorActivity.java revision ca7783e6051a8fc35434ba514af2558205acc902
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.videoeditor; 18 19import java.util.ArrayList; 20import java.util.NoSuchElementException; 21import java.util.Queue; 22import java.util.concurrent.LinkedBlockingQueue; 23 24import android.app.ActionBar; 25import android.app.AlertDialog; 26import android.app.Dialog; 27import android.app.ProgressDialog; 28import android.content.ContentValues; 29import android.content.DialogInterface; 30import android.content.Intent; 31import android.graphics.Bitmap; 32import android.graphics.Color; 33import android.graphics.Rect; 34import android.graphics.Bitmap.Config; 35import android.media.videoeditor.MediaItem; 36import android.media.videoeditor.MediaProperties; 37import android.media.videoeditor.VideoEditor; 38import android.net.Uri; 39import android.os.Bundle; 40import android.os.Handler; 41import android.os.Looper; 42import android.provider.MediaStore; 43import android.text.InputType; 44import android.util.Log; 45import android.view.Display; 46import android.view.GestureDetector; 47import android.view.Menu; 48import android.view.MenuItem; 49import android.view.MotionEvent; 50import android.view.ScaleGestureDetector; 51import android.view.SurfaceHolder; 52import android.view.View; 53import android.view.ViewGroup; 54import android.widget.FrameLayout; 55import android.widget.ImageButton; 56import android.widget.ImageView; 57import android.widget.TextView; 58import android.widget.Toast; 59 60import com.android.videoeditor.service.ApiService; 61import com.android.videoeditor.service.MovieMediaItem; 62import com.android.videoeditor.service.VideoEditorProject; 63import com.android.videoeditor.util.FileUtils; 64import com.android.videoeditor.util.MediaItemUtils; 65import com.android.videoeditor.util.StringUtils; 66import com.android.videoeditor.widgets.AudioTrackLinearLayout; 67import com.android.videoeditor.widgets.MediaLinearLayout; 68import com.android.videoeditor.widgets.OverlayLinearLayout; 69import com.android.videoeditor.widgets.PlayheadView; 70import com.android.videoeditor.widgets.PreviewSurfaceView; 71import com.android.videoeditor.widgets.ScrollViewListener; 72import com.android.videoeditor.widgets.TimelineHorizontalScrollView; 73import com.android.videoeditor.widgets.TimelineRelativeLayout; 74import com.android.videoeditor.widgets.ZoomControl; 75 76/** 77 * This is the main activity of the video editor. It handles video editing of 78 * a project. 79 */ 80public class VideoEditorActivity extends VideoEditorBaseActivity 81 implements SurfaceHolder.Callback { 82 // Logging 83 private static final String TAG = "VideoEditorActivity"; 84 85 // State keys 86 private static final String STATE_INSERT_AFTER_MEDIA_ITEM_ID = "insert_after_media_item_id"; 87 private static final String STATE_PLAYING = "playing"; 88 private static final String STATE_CAPTURE_URI = "capture_uri"; 89 90 // Menu ids 91 private static final int MENU_IMPORT_IMAGE_ID = 2; 92 private static final int MENU_IMPORT_VIDEO_ID = 3; 93 private static final int MENU_IMPORT_AUDIO_ID = 4; 94 private static final int MENU_CHANGE_ASPECT_RATIO_ID = 5; 95 private static final int MENU_EDIT_PROJECT_NAME_ID = 6; 96 private static final int MENU_DELETE_PROJECT_ID = 7; 97 private static final int MENU_EXPORT_MOVIE_ID = 8; 98 private static final int MENU_PLAY_EXPORTED_MOVIE = 9; 99 private static final int MENU_SHARE_VIDEO = 10; 100 private static final int MENU_CAPTURE_VIDEO_ID = 11; 101 private static final int MENU_CAPTURE_IMAGE_ID = 12; 102 103 // Dialog ids 104 private static final int DIALOG_DELETE_PROJECT_ID = 1; 105 private static final int DIALOG_EDIT_PROJECT_NAME_ID = 2; 106 private static final int DIALOG_CHOOSE_ASPECT_RATIO_ID = 3; 107 private static final int DIALOG_EXPORT_OPTIONS_ID = 4; 108 109 public static final int DIALOG_REMOVE_MEDIA_ITEM_ID = 10; 110 public static final int DIALOG_REMOVE_TRANSITION_ID = 11; 111 public static final int DIALOG_CHANGE_RENDERING_MODE_ID = 12; 112 public static final int DIALOG_REMOVE_OVERLAY_ID = 13; 113 public static final int DIALOG_REMOVE_EFFECT_ID = 14; 114 public static final int DIALOG_REMOVE_AUDIO_TRACK_ID = 15; 115 116 // Dialog parameters 117 private static final String PARAM_ASPECT_RATIOS_LIST = "aspect_ratios"; 118 private static final String PARAM_CURRENT_ASPECT_RATIO_INDEX = "current_aspect_ratio"; 119 120 // Request codes 121 private static final int REQUEST_CODE_IMPORT_VIDEO = 1; 122 private static final int REQUEST_CODE_IMPORT_IMAGE = 2; 123 private static final int REQUEST_CODE_IMPORT_MUSIC = 3; 124 private static final int REQUEST_CODE_CAPTURE_VIDEO = 4; 125 private static final int REQUEST_CODE_CAPTURE_IMAGE = 5; 126 127 public static final int REQUEST_CODE_EDIT_TRANSITION = 10; 128 public static final int REQUEST_CODE_PICK_TRANSITION = 11; 129 public static final int REQUEST_CODE_PICK_OVERLAY = 12; 130 public static final int REQUEST_CODE_EDIT_OVERLAY = 13; 131 public static final int REQUEST_CODE_PICK_EFFECT = 14; 132 public static final int REQUEST_CODE_EDIT_EFFECT = 15; 133 134 // The maximum zoom level 135 private static final int MAX_ZOOM_LEVEL = 120; 136 private static final int ZOOM_STEP = 2; 137 138 private final TimelineRelativeLayout.LayoutCallback mLayoutCallback = 139 new TimelineRelativeLayout.LayoutCallback() { 140 /* 141 * {@inheritDoc} 142 */ 143 public void onLayoutComplete() { 144 // Scroll the timeline such that the specified position 145 // is in the center of the screen 146 mTimelineScroller.appScrollTo(timeToDimension(mProject.getPlayheadPos()), false); 147 } 148 }; 149 150 // Instance variables 151 private PreviewSurfaceView mSurfaceView; 152 private SurfaceHolder mSurfaceHolder; 153 private ImageView mOverlayView; 154 private PreviewThread mPreviewThread; 155 private View mEditorProjectView; 156 private View mEditorEmptyView; 157 private TimelineHorizontalScrollView mTimelineScroller; 158 private TimelineRelativeLayout mTimelineLayout; 159 private OverlayLinearLayout mOverlayLayout; 160 private AudioTrackLinearLayout mAudioTrackLayout; 161 private MediaLinearLayout mMediaLayout; 162 private PlayheadView mPlayheadView; 163 private TextView mTimeView; 164 private ImageButton mPreviewPlayButton; 165 private ImageButton mPreviewRewindButton, mPreviewNextButton, mPreviewPrevButton; 166 private int mActivityWidth; 167 private String mInsertMediaItemAfterMediaItemId; 168 private long mCurrentPlayheadPosMs; 169 private ProgressDialog mExportProgressDialog; 170 private ZoomControl mZoomControl; 171 172 // Variables used in onActivityResult 173 private Uri mAddMediaItemVideoUri; 174 private Uri mAddMediaItemImageUri; 175 private Uri mAddAudioTrackUri; 176 private String mAddTransitionAfterMediaId; 177 private int mAddTransitionType; 178 private long mAddTransitionDurationMs; 179 private String mEditTransitionAfterMediaId, mEditTransitionId; 180 private int mEditTransitionType; 181 private long mEditTransitionDurationMs; 182 private String mAddOverlayMediaItemId; 183 private Bundle mAddOverlayUserAttributes; 184 private String mEditOverlayMediaItemId; 185 private String mEditOverlayId; 186 private Bundle mEditOverlayUserAttributes; 187 private String mAddEffectMediaItemId; 188 private int mAddEffectType; 189 private Rect mAddKenBurnsStartRect; 190 private Rect mAddKenBurnsEndRect; 191 private String mEditEffectMediaItemId; 192 private int mEditEffectType; 193 private Rect mEditKenBurnsStartRect; 194 private Rect mEditKenBurnsEndRect; 195 private boolean mRestartPreview; 196 private Uri mCaptureMediaUri; 197 198 /* 199 * {@inheritDoc} 200 */ 201 @Override 202 public void onCreate(Bundle savedInstanceState) { 203 super.onCreate(savedInstanceState); 204 205 // Turn on the title display. 206 // It is turned off in the style used for the activity. 207 final ActionBar actionBar = getActionBar(); 208 actionBar.setDisplayOptions(actionBar.getDisplayOptions() | ActionBar.DISPLAY_SHOW_TITLE); 209 actionBar.setTitle(R.string.full_app_name); 210 211 // Prepare the surface holder 212 mSurfaceView = (PreviewSurfaceView)findViewById(R.id.video_view); 213 mSurfaceHolder = mSurfaceView.getHolder(); 214 mSurfaceHolder.addCallback(this); 215 mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); 216 217 mOverlayView = (ImageView)findViewById(R.id.overlay_layer); 218 219 mEditorProjectView = findViewById(R.id.editor_project_view); 220 mEditorEmptyView = findViewById(R.id.empty_project_view); 221 222 mTimelineScroller = (TimelineHorizontalScrollView)findViewById(R.id.timeline_scroller); 223 mTimelineLayout = (TimelineRelativeLayout)findViewById(R.id.timeline); 224 mMediaLayout = (MediaLinearLayout)findViewById(R.id.timeline_media); 225 mOverlayLayout = (OverlayLinearLayout)findViewById(R.id.timeline_overlays); 226 mAudioTrackLayout = (AudioTrackLinearLayout)findViewById(R.id.timeline_audio_tracks); 227 mPlayheadView = (PlayheadView)findViewById(R.id.timeline_playhead); 228 mPreviewPlayButton = (ImageButton)findViewById(R.id.editor_play); 229 mPreviewRewindButton = (ImageButton)findViewById(R.id.editor_rewind); 230 mPreviewNextButton = (ImageButton)findViewById(R.id.editor_next); 231 mPreviewPrevButton = (ImageButton)findViewById(R.id.editor_prev); 232 233 mTimeView = (TextView)findViewById(R.id.editor_time); 234 235 actionBar.setDisplayHomeAsUpEnabled(true); 236 237 mMediaLayout.setListener(new MediaLinearLayout.MediaLayoutListener() { 238 /* 239 * {@inheritDoc} 240 */ 241 public void onRequestScrollBy(int scrollBy, boolean smooth) { 242 mTimelineScroller.appScrollBy(scrollBy, smooth); 243 } 244 245 /* 246 * {@inheritDoc} 247 */ 248 public void onRequestMovePlayhead(long scrollToTime, boolean smooth) { 249 movePlayhead(scrollToTime); 250 } 251 252 /* 253 * {@inheritDoc} 254 */ 255 public void onAddMediaItem(String afterMediaItemId) { 256 mInsertMediaItemAfterMediaItemId = afterMediaItemId; 257 258 final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 259 intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); 260 intent.setType("video/*"); 261 startActivityForResult(intent, REQUEST_CODE_IMPORT_VIDEO); 262 } 263 264 /* 265 * {@inheritDoc} 266 */ 267 public void onTrimMediaItemBegin(MovieMediaItem mediaItem) { 268 onProjectEditStateChange(true); 269 } 270 271 /* 272 * {@inheritDoc} 273 */ 274 public void onTrimMediaItem(MovieMediaItem mediaItem, long timeMs) { 275 updateTimelineDuration(); 276 if (mProject != null && mPreviewThread != null && !mPreviewThread.isPlaying()) { 277 if (mediaItem.isVideoClip()) { 278 if (timeMs >= 0) { 279 mPreviewThread.renderMediaItemFrame(mediaItem, timeMs); 280 } 281 } else { 282 mPreviewThread.previewFrame(mProject, 283 mProject.getMediaItemBeginTime(mediaItem.getId()) + timeMs, 284 mProject.getMediaItemCount() == 0); 285 } 286 } 287 } 288 289 /* 290 * {@inheritDoc} 291 */ 292 public void onTrimMediaItemEnd(MovieMediaItem mediaItem, long timeMs) { 293 onProjectEditStateChange(false); 294 // We need to repaint the timeline layout to clear the old 295 // playhead position (the one drawn during trimming) 296 mTimelineLayout.invalidate(); 297 showPreviewFrame(); 298 } 299 }); 300 301 mAudioTrackLayout.setListener(new AudioTrackLinearLayout.AudioTracksLayoutListener() { 302 /* 303 * {@inheritDoc} 304 */ 305 public void onAddAudioTrack() { 306 final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 307 intent.setType("audio/*"); 308 startActivityForResult(intent, REQUEST_CODE_IMPORT_MUSIC); 309 } 310 }); 311 312 mTimelineScroller.addScrollListener(new ScrollViewListener() { 313 // Instance variables 314 private int mActiveWidth; 315 private long mDurationMs; 316 private int mLastScrollX; 317 318 /* 319 * {@inheritDoc} 320 */ 321 public void onScrollBegin(View view, int scrollX, int scrollY, boolean appScroll) { 322 if (!appScroll && mProject != null) { 323 mActiveWidth = mMediaLayout.getWidth() - mActivityWidth; 324 mDurationMs = mProject.computeDuration(); 325 } else { 326 mActiveWidth = 0; 327 } 328 329 mLastScrollX = scrollX; 330 } 331 332 /* 333 * {@inheritDoc} 334 */ 335 public void onScrollProgress(View view, int scrollX, int scrollY, boolean appScroll) { 336 // We check if the project is valid since the project may 337 // close while scrolling 338 if (!appScroll && mActiveWidth > 0 && mProject != null) { 339 final int deltaScrollX = Math.abs(mLastScrollX - scrollX); 340 341 if (deltaScrollX < 100) { 342 mLastScrollX = scrollX; 343 // When scrolling at high speed do not display the 344 // preview frame 345 final long timeMs = (scrollX * mDurationMs) / mActiveWidth; 346 if (setPlayhead(timeMs < 0 ? 0 : timeMs)) { 347 showPreviewFrame(); 348 } 349 } 350 } 351 } 352 353 /* 354 * {@inheritDoc} 355 */ 356 public void onScrollEnd(View view, int scrollX, int scrollY, boolean appScroll) { 357 // We check if the project is valid since the project may 358 // close while scrolling 359 if (!appScroll && mActiveWidth > 0 && mProject != null && scrollX != mLastScrollX) { 360 final long timeMs = (scrollX * mDurationMs) / mActiveWidth; 361 if (setPlayhead(timeMs < 0 ? 0 : timeMs)) { 362 showPreviewFrame(); 363 } 364 } 365 } 366 }); 367 368 mTimelineScroller.setScaleListener(new ScaleGestureDetector.SimpleOnScaleGestureListener() { 369 // Guard against this many scale events in the opposite direction 370 private static final int SCALE_TOLERANCE = 3; 371 372 private int mLastScaleFactorSign; 373 private float mLastScaleFactor; 374 375 /* 376 * {@inheritDoc} 377 */ 378 @Override 379 public boolean onScaleBegin(ScaleGestureDetector detector) { 380 mLastScaleFactorSign = 0; 381 return true; 382 } 383 384 /* 385 * {@inheritDoc} 386 */ 387 @Override 388 public boolean onScale(ScaleGestureDetector detector) { 389 if (mProject == null) { 390 return false; 391 } 392 393 final float scaleFactor = detector.getScaleFactor(); 394 final float deltaScaleFactor = scaleFactor - mLastScaleFactor; 395 if (deltaScaleFactor > 0.01f || deltaScaleFactor < -0.01f) { 396 if (scaleFactor < 1.0f) { 397 if (mLastScaleFactorSign <= 0) { 398 zoomTimeline(mProject.getZoomLevel() - ZOOM_STEP, true); 399 } 400 401 if (mLastScaleFactorSign > -SCALE_TOLERANCE) { 402 mLastScaleFactorSign--; 403 } 404 } else if (scaleFactor > 1.0f) { 405 if (mLastScaleFactorSign >= 0) { 406 zoomTimeline(mProject.getZoomLevel() + ZOOM_STEP, true); 407 } 408 409 if (mLastScaleFactorSign < SCALE_TOLERANCE) { 410 mLastScaleFactorSign++; 411 } 412 } 413 } 414 415 mLastScaleFactor = scaleFactor; 416 return true; 417 } 418 419 /* 420 * {@inheritDoc} 421 */ 422 @Override 423 public void onScaleEnd(ScaleGestureDetector detector) { 424 } 425 }); 426 427 if (savedInstanceState != null) { 428 mInsertMediaItemAfterMediaItemId = savedInstanceState.getString( 429 STATE_INSERT_AFTER_MEDIA_ITEM_ID); 430 mRestartPreview = savedInstanceState.getBoolean(STATE_PLAYING); 431 mCaptureMediaUri = savedInstanceState.getParcelable(STATE_CAPTURE_URI); 432 } else { 433 mRestartPreview = false; 434 } 435 436 // Compute the activity width 437 final Display display = getWindowManager().getDefaultDisplay(); 438 mActivityWidth = display.getWidth(); 439 440 mSurfaceView.setGestureListener(new GestureDetector(this, 441 new GestureDetector.SimpleOnGestureListener() { 442 /* 443 * {@inheritDoc} 444 */ 445 @Override 446 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, 447 float velocityY) { 448 if (mPreviewThread != null && mPreviewThread.isPlaying()) { 449 return false; 450 } 451 452 mTimelineScroller.fling(-(int)velocityX); 453 return true; 454 } 455 456 /* 457 * {@inheritDoc} 458 */ 459 @Override 460 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, 461 float distanceY) { 462 if (mPreviewThread != null && mPreviewThread.isPlaying()) { 463 return false; 464 } 465 466 mTimelineScroller.scrollBy((int)distanceX, 0); 467 return true; 468 } 469 })); 470 471 mZoomControl = ((ZoomControl)findViewById(R.id.editor_zoom)); 472 mZoomControl.setMax(MAX_ZOOM_LEVEL); 473 mZoomControl.setOnZoomChangeListener(new ZoomControl.OnZoomChangeListener() { 474 /* 475 * {@inheritDoc} 476 */ 477 public void onProgressChanged(int progress, boolean fromUser) { 478 if (mProject != null) { 479 zoomTimeline(progress, false); 480 } 481 } 482 }); 483 } 484 485 /* 486 * {@inheritDoc} 487 */ 488 @Override 489 public void onPause() { 490 super.onPause(); 491 492 // Dismiss the export progress dialog. If the export will still be pending 493 // when we return to this activity, we will display this dialog again. 494 if (mExportProgressDialog != null) { 495 mExportProgressDialog.dismiss(); 496 mExportProgressDialog = null; 497 } 498 } 499 500 /* 501 * {@inheritDoc} 502 */ 503 @Override 504 public void onResume() { 505 super.onResume(); 506 507 if (mProject != null) { 508 mMediaLayout.onResume(); 509 mAudioTrackLayout.onResume(); 510 } 511 } 512 513 /* 514 * {@inheritDoc} 515 */ 516 @Override 517 public void onSaveInstanceState(Bundle outState) { 518 super.onSaveInstanceState(outState); 519 520 outState.putString(STATE_INSERT_AFTER_MEDIA_ITEM_ID, mInsertMediaItemAfterMediaItemId); 521 outState.putBoolean(STATE_PLAYING, 522 mPreviewThread != null ? mPreviewThread.isPlaying() : false); 523 outState.putParcelable(STATE_CAPTURE_URI, mCaptureMediaUri); 524 } 525 526 /* 527 * {@inheritDoc} 528 */ 529 @Override 530 public boolean onCreateOptionsMenu(Menu menu) { 531 menu.add(Menu.NONE, MENU_CAPTURE_VIDEO_ID, Menu.NONE, 532 R.string.editor_capture_video).setIcon( 533 R.drawable.ic_menu_video_camera).setShowAsAction( 534 MenuItem.SHOW_AS_ACTION_ALWAYS); 535 menu.add(Menu.NONE, MENU_CAPTURE_IMAGE_ID, Menu.NONE, 536 R.string.editor_capture_image).setIcon( 537 R.drawable.ic_menu_camera).setShowAsAction( 538 MenuItem.SHOW_AS_ACTION_ALWAYS); 539 menu.add(Menu.NONE, MENU_IMPORT_VIDEO_ID, Menu.NONE, 540 R.string.editor_import_video).setIcon( 541 R.drawable.ic_menu_add_video).setShowAsAction( 542 MenuItem.SHOW_AS_ACTION_ALWAYS); 543 menu.add(Menu.NONE, MENU_IMPORT_IMAGE_ID, Menu.NONE, 544 R.string.editor_import_image).setIcon( 545 R.drawable.ic_menu_add_image).setShowAsAction( 546 MenuItem.SHOW_AS_ACTION_ALWAYS); 547 menu.add(Menu.NONE, MENU_IMPORT_AUDIO_ID, Menu.NONE, R.string.editor_import_audio).setIcon( 548 R.drawable.ic_menu_add_audio_clip); 549 menu.add(Menu.NONE, MENU_CHANGE_ASPECT_RATIO_ID, Menu.NONE, 550 R.string.editor_change_aspect_ratio); 551 menu.add(Menu.NONE, MENU_EDIT_PROJECT_NAME_ID, Menu.NONE, 552 R.string.editor_edit_project_name); 553 menu.add(Menu.NONE, MENU_EXPORT_MOVIE_ID, Menu.NONE, R.string.editor_export_movie); 554 menu.add(Menu.NONE, MENU_PLAY_EXPORTED_MOVIE, Menu.NONE, 555 R.string.editor_play_exported_movie); 556 menu.add(Menu.NONE, MENU_SHARE_VIDEO, Menu.NONE, R.string.editor_share_movie); 557 menu.add(Menu.NONE, MENU_DELETE_PROJECT_ID, Menu.NONE, R.string.editor_delete_project); 558 return true; 559 } 560 561 /* 562 * {@inheritDoc} 563 */ 564 @Override 565 public boolean onPrepareOptionsMenu(Menu menu) { 566 final boolean haveProject = mProject != null; 567 final boolean haveMediaItems = haveProject && mProject.getMediaItemCount() > 0; 568 menu.findItem(MENU_CAPTURE_VIDEO_ID).setVisible(haveProject); 569 menu.findItem(MENU_CAPTURE_IMAGE_ID).setVisible(haveProject); 570 menu.findItem(MENU_IMPORT_VIDEO_ID).setVisible(haveProject); 571 menu.findItem(MENU_IMPORT_IMAGE_ID).setVisible(haveProject); 572 menu.findItem(MENU_IMPORT_AUDIO_ID).setVisible(haveProject && 573 mProject.getAudioTracks().size() == 0 && haveMediaItems); 574 menu.findItem(MENU_CHANGE_ASPECT_RATIO_ID).setVisible(haveProject && 575 mProject.hasMultipleAspectRatios()); 576 menu.findItem(MENU_EDIT_PROJECT_NAME_ID).setVisible(haveProject); 577 578 // Check if there is an operation pending or preview is on 579 boolean enableMenu = haveProject; 580 if (enableMenu && mPreviewThread != null) { 581 // Preview is in progress 582 enableMenu = mPreviewThread.isStopped(); 583 if (enableMenu && mProjectPath != null) { 584 enableMenu = !ApiService.isProjectEdited(mProjectPath); 585 } 586 } 587 588 menu.findItem(MENU_EXPORT_MOVIE_ID).setVisible(enableMenu && haveMediaItems); 589 menu.findItem(MENU_PLAY_EXPORTED_MOVIE).setVisible(enableMenu && 590 mProject.getExportedMovieUri() != null); 591 menu.findItem(MENU_SHARE_VIDEO).setVisible(enableMenu && 592 mProject.getExportedMovieUri() != null); 593 menu.findItem(MENU_DELETE_PROJECT_ID).setVisible(enableMenu); 594 return true; 595 } 596 597 /* 598 * {@inheritDoc} 599 */ 600 @Override 601 public boolean onOptionsItemSelected(MenuItem item) { 602 switch (item.getItemId()) { 603 case android.R.id.home: { 604 final Intent intent = new Intent(this, ProjectsActivity.class); 605 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 606 startActivity(intent); 607 608 finish(); 609 return true; 610 } 611 612 case MENU_CAPTURE_VIDEO_ID: { 613 final MovieMediaItem mediaItem = mProject.getInsertAfterMediaItem( 614 mProject.getPlayheadPos()); 615 if (mediaItem != null) { 616 mInsertMediaItemAfterMediaItemId = mediaItem.getId(); 617 } else { 618 mInsertMediaItemAfterMediaItemId = null; 619 } 620 621 // Create parameters for Intent with filename 622 final ContentValues values = new ContentValues(); 623 mCaptureMediaUri = getContentResolver().insert( 624 MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values); 625 final Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); 626 intent.putExtra(MediaStore.EXTRA_OUTPUT, mCaptureMediaUri); 627 intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1); 628 startActivityForResult(intent, REQUEST_CODE_CAPTURE_VIDEO); 629 return true; 630 } 631 632 case MENU_CAPTURE_IMAGE_ID: { 633 final MovieMediaItem mediaItem = mProject.getInsertAfterMediaItem( 634 mProject.getPlayheadPos()); 635 if (mediaItem != null) { 636 mInsertMediaItemAfterMediaItemId = mediaItem.getId(); 637 } else { 638 mInsertMediaItemAfterMediaItemId = null; 639 } 640 641 // Create parameters for Intent with filename 642 final ContentValues values = new ContentValues(); 643 mCaptureMediaUri = getContentResolver().insert( 644 MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); 645 final Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 646 intent.putExtra(MediaStore.EXTRA_OUTPUT, mCaptureMediaUri); 647 intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1); 648 startActivityForResult(intent, REQUEST_CODE_CAPTURE_IMAGE); 649 return true; 650 } 651 652 case MENU_IMPORT_VIDEO_ID: { 653 final MovieMediaItem mediaItem = mProject.getInsertAfterMediaItem( 654 mProject.getPlayheadPos()); 655 if (mediaItem != null) { 656 mInsertMediaItemAfterMediaItemId = mediaItem.getId(); 657 } else { 658 mInsertMediaItemAfterMediaItemId = null; 659 } 660 661 final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 662 intent.setType("video/*"); 663 startActivityForResult(intent, REQUEST_CODE_IMPORT_VIDEO); 664 return true; 665 } 666 667 case MENU_IMPORT_IMAGE_ID: { 668 final MovieMediaItem mediaItem = mProject.getInsertAfterMediaItem( 669 mProject.getPlayheadPos()); 670 if (mediaItem != null) { 671 mInsertMediaItemAfterMediaItemId = mediaItem.getId(); 672 } else { 673 mInsertMediaItemAfterMediaItemId = null; 674 } 675 676 final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 677 intent.setType("image/*"); 678 startActivityForResult(intent, REQUEST_CODE_IMPORT_IMAGE); 679 return true; 680 } 681 682 case MENU_IMPORT_AUDIO_ID: { 683 final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 684 intent.setType("audio/*"); 685 startActivityForResult(intent, REQUEST_CODE_IMPORT_MUSIC); 686 return true; 687 } 688 689 case MENU_CHANGE_ASPECT_RATIO_ID: { 690 final ArrayList<Integer> aspectRatiosList = mProject.getUniqueAspectRatiosList(); 691 final int size = aspectRatiosList.size(); 692 if (size > 1) { 693 final Bundle bundle = new Bundle(); 694 bundle.putIntegerArrayList(PARAM_ASPECT_RATIOS_LIST, aspectRatiosList); 695 696 // Get the current aspect ratio index 697 final int currentAspectRatio = mProject.getAspectRatio(); 698 int currentAspectRatioIndex = 0; 699 for (int i = 0; i < size; i++) { 700 final int aspectRatio = aspectRatiosList.get(i); 701 if (aspectRatio == currentAspectRatio) { 702 currentAspectRatioIndex = i; 703 break; 704 } 705 } 706 bundle.putInt(PARAM_CURRENT_ASPECT_RATIO_INDEX, currentAspectRatioIndex); 707 showDialog(DIALOG_CHOOSE_ASPECT_RATIO_ID, bundle); 708 } 709 return true; 710 } 711 712 case MENU_EDIT_PROJECT_NAME_ID: { 713 showDialog(DIALOG_EDIT_PROJECT_NAME_ID); 714 return true; 715 } 716 717 case MENU_DELETE_PROJECT_ID: { 718 // Confirm project delete 719 showDialog(DIALOG_DELETE_PROJECT_ID); 720 return true; 721 } 722 723 case MENU_EXPORT_MOVIE_ID: { 724 // Present the user with a dialog to choose export options 725 showDialog(DIALOG_EXPORT_OPTIONS_ID); 726 return true; 727 } 728 729 case MENU_PLAY_EXPORTED_MOVIE: { 730 final Intent intent = new Intent(Intent.ACTION_VIEW); 731 intent.setDataAndType(mProject.getExportedMovieUri(), "video/*"); 732 intent.putExtra(MediaStore.EXTRA_FINISH_ON_COMPLETION, false); 733 startActivity(intent); 734 return true; 735 } 736 737 case MENU_SHARE_VIDEO: { 738 final Intent intent = new Intent(Intent.ACTION_SEND); 739 intent.putExtra(Intent.EXTRA_STREAM, mProject.getExportedMovieUri()); 740 intent.setType("video/*"); 741 startActivity(intent); 742 return true; 743 } 744 745 default: { 746 return false; 747 } 748 } 749 } 750 751 /* 752 * {@inheritDoc} 753 */ 754 @Override 755 public Dialog onCreateDialog(int id, final Bundle bundle) { 756 switch (id) { 757 case DIALOG_CHOOSE_ASPECT_RATIO_ID: { 758 final AlertDialog.Builder builder = new AlertDialog.Builder(this); 759 builder.setTitle(getString(R.string.editor_change_aspect_ratio)); 760 final ArrayList<Integer> aspectRatios = 761 bundle.getIntegerArrayList(PARAM_ASPECT_RATIOS_LIST); 762 final int count = aspectRatios.size(); 763 final CharSequence[] aspectRatioStrings = new CharSequence[count]; 764 for (int i = 0; i < count; i++) { 765 int aspectRatio = aspectRatios.get(i); 766 switch (aspectRatio) { 767 case MediaProperties.ASPECT_RATIO_11_9: { 768 aspectRatioStrings[i] = getString(R.string.aspect_ratio_11_9); 769 break; 770 } 771 772 case MediaProperties.ASPECT_RATIO_16_9: { 773 aspectRatioStrings[i] = getString(R.string.aspect_ratio_16_9); 774 break; 775 } 776 777 case MediaProperties.ASPECT_RATIO_3_2: { 778 aspectRatioStrings[i] = getString(R.string.aspect_ratio_3_2); 779 break; 780 } 781 782 case MediaProperties.ASPECT_RATIO_4_3: { 783 aspectRatioStrings[i] = getString(R.string.aspect_ratio_4_3); 784 break; 785 } 786 787 case MediaProperties.ASPECT_RATIO_5_3: { 788 aspectRatioStrings[i] = getString(R.string.aspect_ratio_5_3); 789 break; 790 } 791 792 default: { 793 break; 794 } 795 } 796 } 797 798 builder.setSingleChoiceItems(aspectRatioStrings, 799 bundle.getInt(PARAM_CURRENT_ASPECT_RATIO_INDEX), 800 new DialogInterface.OnClickListener() { 801 /* 802 * {@inheritDoc} 803 */ 804 public void onClick(DialogInterface dialog, int which) { 805 final int aspectRatio = aspectRatios.get(which); 806 ApiService.setAspectRatio(VideoEditorActivity.this, mProjectPath, 807 aspectRatio); 808 809 removeDialog(DIALOG_CHOOSE_ASPECT_RATIO_ID); 810 } 811 }); 812 builder.setCancelable(true); 813 builder.setOnCancelListener(new DialogInterface.OnCancelListener() { 814 /* 815 * {@inheritDoc} 816 */ 817 public void onCancel(DialogInterface dialog) { 818 removeDialog(DIALOG_CHOOSE_ASPECT_RATIO_ID); 819 } 820 }); 821 return builder.create(); 822 } 823 824 case DIALOG_DELETE_PROJECT_ID: { 825 return AlertDialogs.createAlert(this, getString(R.string.editor_delete_project), 0, 826 getString(R.string.editor_delete_project_question), 827 getString(R.string.yes), 828 new DialogInterface.OnClickListener() { 829 /* 830 * {@inheritDoc} 831 */ 832 public void onClick(DialogInterface dialog, int which) { 833 ApiService.deleteProject(VideoEditorActivity.this, mProjectPath); 834 mProjectPath = null; 835 mProject = null; 836 enterDisabledState(R.string.editor_no_project); 837 838 removeDialog(DIALOG_DELETE_PROJECT_ID); 839 finish(); 840 } 841 }, getString(R.string.no), new DialogInterface.OnClickListener() { 842 /* 843 * {@inheritDoc} 844 */ 845 public void onClick(DialogInterface dialog, int which) { 846 removeDialog(DIALOG_DELETE_PROJECT_ID); 847 } 848 }, new DialogInterface.OnCancelListener() { 849 /* 850 * {@inheritDoc} 851 */ 852 public void onCancel(DialogInterface dialog) { 853 removeDialog(DIALOG_DELETE_PROJECT_ID); 854 } 855 }, true); 856 } 857 858 case DIALOG_DELETE_BAD_PROJECT_ID: { 859 return AlertDialogs.createAlert(this, getString(R.string.editor_delete_project), 0, 860 getString(R.string.editor_load_error), 861 getString(R.string.yes), 862 new DialogInterface.OnClickListener() { 863 /* 864 * {@inheritDoc} 865 */ 866 public void onClick(DialogInterface dialog, int which) { 867 ApiService.deleteProject(VideoEditorActivity.this, 868 bundle.getString(PARAM_PROJECT_PATH)); 869 870 removeDialog(DIALOG_DELETE_BAD_PROJECT_ID); 871 finish(); 872 } 873 }, getString(R.string.no), new DialogInterface.OnClickListener() { 874 /* 875 * {@inheritDoc} 876 */ 877 public void onClick(DialogInterface dialog, int which) { 878 removeDialog(DIALOG_DELETE_BAD_PROJECT_ID); 879 } 880 }, new DialogInterface.OnCancelListener() { 881 /* 882 * {@inheritDoc} 883 */ 884 public void onCancel(DialogInterface dialog) { 885 removeDialog(DIALOG_DELETE_BAD_PROJECT_ID); 886 } 887 }, true); 888 } 889 890 case DIALOG_EDIT_PROJECT_NAME_ID: { 891 if (mProject == null) { 892 return null; 893 } 894 895 return AlertDialogs.createEditDialog(this, 896 getString(R.string.editor_edit_project_name), 897 mProject.getName(), getString(android.R.string.ok), 898 new DialogInterface.OnClickListener() { 899 /* 900 * {@inheritDoc} 901 */ 902 public void onClick(DialogInterface dialog, int which) { 903 final TextView tv = 904 (TextView)((AlertDialog)dialog).findViewById(R.id.text_1); 905 mProject.setProjectName(tv.getText().toString()); 906 getActionBar().setTitle(tv.getText()); 907 removeDialog(DIALOG_EDIT_PROJECT_NAME_ID); 908 } 909 }, getString(android.R.string.cancel), 910 new DialogInterface.OnClickListener() { 911 /* 912 * {@inheritDoc} 913 */ 914 public void onClick(DialogInterface dialog, int which) { 915 removeDialog(DIALOG_EDIT_PROJECT_NAME_ID); 916 } 917 }, new DialogInterface.OnCancelListener() { 918 /* 919 * {@inheritDoc} 920 */ 921 public void onCancel(DialogInterface dialog) { 922 removeDialog(DIALOG_EDIT_PROJECT_NAME_ID); 923 } 924 }, InputType.TYPE_NULL, 32); 925 } 926 927 case DIALOG_EXPORT_OPTIONS_ID: { 928 if (mProject == null) { 929 return null; 930 } 931 932 return ExportOptionsDialog.create(this, 933 new ExportOptionsDialog.ExportOptionsListener() { 934 /* 935 * {@inheritDoc} 936 */ 937 public void onExportOptions(int movieHeight, int movieBitrate) { 938 mPendingExportFilename = FileUtils.createMovieName( 939 MediaProperties.FILE_MP4); 940 ApiService.exportVideoEditor(VideoEditorActivity.this, mProjectPath, 941 mPendingExportFilename, movieHeight, movieBitrate); 942 943 removeDialog(DIALOG_EXPORT_OPTIONS_ID); 944 945 showExportProgress(); 946 } 947 }, new DialogInterface.OnClickListener() { 948 /* 949 * {@inheritDoc} 950 */ 951 public void onClick(DialogInterface dialog, int which) { 952 removeDialog(DIALOG_EXPORT_OPTIONS_ID); 953 } 954 }, new DialogInterface.OnCancelListener() { 955 /* 956 * {@inheritDoc} 957 */ 958 public void onCancel(DialogInterface dialog) { 959 removeDialog(DIALOG_EXPORT_OPTIONS_ID); 960 } 961 }, mProject.getAspectRatio()); 962 } 963 964 case DIALOG_REMOVE_MEDIA_ITEM_ID: { 965 return mMediaLayout.onCreateDialog(id, bundle); 966 } 967 968 case DIALOG_CHANGE_RENDERING_MODE_ID: { 969 return mMediaLayout.onCreateDialog(id, bundle); 970 } 971 972 case DIALOG_REMOVE_TRANSITION_ID: { 973 return mMediaLayout.onCreateDialog(id, bundle); 974 } 975 976 case DIALOG_REMOVE_OVERLAY_ID: { 977 return mOverlayLayout.onCreateDialog(id, bundle); 978 } 979 980 case DIALOG_REMOVE_EFFECT_ID: { 981 return mMediaLayout.onCreateDialog(id, bundle); 982 } 983 984 case DIALOG_REMOVE_AUDIO_TRACK_ID: { 985 return mAudioTrackLayout.onCreateDialog(id, bundle); 986 } 987 988 default: { 989 return null; 990 } 991 } 992 } 993 994 /* 995 * {@inheritDoc} 996 */ 997 public void onClickHandler(View target) { 998 final long playheadPosMs = mProject.getPlayheadPos(); 999 1000 switch (target.getId()) { 1001 case R.id.editor_play: { 1002 if (mProject != null && mPreviewThread != null) { 1003 if (mPreviewThread.isPlaying()) { 1004 mPreviewThread.stopPreviewPlayback(); 1005 } else if (mProject.getMediaItemCount() > 0){ 1006 mPreviewThread.startPreviewPlayback(mProject, playheadPosMs); 1007 } 1008 } 1009 break; 1010 } 1011 1012 case R.id.editor_rewind: { 1013 if (mProject != null && mPreviewThread != null) { 1014 if (mPreviewThread.isPlaying()) { 1015 mPreviewThread.stopPreviewPlayback(); 1016 movePlayhead(0); 1017 mPreviewThread.startPreviewPlayback(mProject, 0); 1018 } else { 1019 movePlayhead(0); 1020 showPreviewFrame(); 1021 } 1022 } 1023 break; 1024 } 1025 1026 case R.id.editor_next: { 1027 if (mProject != null && mPreviewThread != null) { 1028 final boolean restartPreview; 1029 if (mPreviewThread.isPlaying()) { 1030 mPreviewThread.stopPreviewPlayback(); 1031 restartPreview = true; 1032 } else { 1033 restartPreview = false; 1034 } 1035 1036 final MovieMediaItem mediaItem = mProject.getNextMediaItem(playheadPosMs); 1037 if (mediaItem != null) { 1038 movePlayhead(mProject.getMediaItemBeginTime(mediaItem.getId())); 1039 if (restartPreview) { 1040 mPreviewThread.startPreviewPlayback(mProject, 1041 mProject.getPlayheadPos()); 1042 } else { 1043 showPreviewFrame(); 1044 } 1045 } else { // Move to the end of the timeline 1046 movePlayhead(mProject.computeDuration()); 1047 showPreviewFrame(); 1048 } 1049 } 1050 break; 1051 } 1052 1053 case R.id.editor_prev: { 1054 if (mProject != null && mPreviewThread != null) { 1055 final boolean restartPreview; 1056 if (mPreviewThread.isPlaying()) { 1057 mPreviewThread.stopPreviewPlayback(); 1058 restartPreview = true; 1059 } else { 1060 restartPreview = false; 1061 } 1062 1063 final MovieMediaItem mediaItem = mProject.getPreviousMediaItem(playheadPosMs); 1064 if (mediaItem != null) { 1065 movePlayhead(mProject.getMediaItemBeginTime(mediaItem.getId())); 1066 } else { // Move to the beginning of the timeline 1067 movePlayhead(0); 1068 } 1069 1070 if (restartPreview) { 1071 mPreviewThread.startPreviewPlayback(mProject, mProject.getPlayheadPos()); 1072 } else { 1073 showPreviewFrame(); 1074 } 1075 } 1076 break; 1077 } 1078 1079 default: { 1080 break; 1081 } 1082 } 1083 } 1084 1085 /* 1086 * {@inheritDoc} 1087 */ 1088 @Override 1089 protected void onActivityResult(int requestCode, int resultCode, Intent extras) { 1090 super.onActivityResult(requestCode, resultCode, extras); 1091 if (resultCode == RESULT_CANCELED) { 1092 switch (requestCode) { 1093 case REQUEST_CODE_CAPTURE_VIDEO: 1094 case REQUEST_CODE_CAPTURE_IMAGE: { 1095 if (mCaptureMediaUri != null) { 1096 getContentResolver().delete(mCaptureMediaUri, null, null); 1097 mCaptureMediaUri = null; 1098 } 1099 break; 1100 } 1101 1102 default: { 1103 break; 1104 } 1105 } 1106 return; 1107 } 1108 1109 switch (requestCode) { 1110 case REQUEST_CODE_CAPTURE_VIDEO: { 1111 if (mProject != null) { 1112 ApiService.addMediaItemVideoUri(this, mProjectPath, 1113 ApiService.generateId(), mInsertMediaItemAfterMediaItemId, 1114 mCaptureMediaUri, MediaItem.RENDERING_MODE_BLACK_BORDER, 1115 mProject.getTheme()); 1116 mInsertMediaItemAfterMediaItemId = null; 1117 } else { 1118 // Add this video after the project loads 1119 mAddMediaItemVideoUri = mCaptureMediaUri; 1120 } 1121 mCaptureMediaUri = null; 1122 break; 1123 } 1124 1125 case REQUEST_CODE_CAPTURE_IMAGE: { 1126 if (mProject != null) { 1127 ApiService.addMediaItemImageUri(this, mProjectPath, 1128 ApiService.generateId(), mInsertMediaItemAfterMediaItemId, 1129 mCaptureMediaUri, MediaItem.RENDERING_MODE_BLACK_BORDER, 1130 MediaItemUtils.getDefaultImageDuration(), 1131 mProject.getTheme()); 1132 mInsertMediaItemAfterMediaItemId = null; 1133 } else { 1134 // Add this image after the project loads 1135 mAddMediaItemImageUri = mCaptureMediaUri; 1136 } 1137 mCaptureMediaUri = null; 1138 break; 1139 } 1140 1141 case REQUEST_CODE_IMPORT_VIDEO: { 1142 final Uri mediaUri = extras.getData(); 1143 if (mProject != null) { 1144 if ("media".equals(mediaUri.getAuthority())) { 1145 ApiService.addMediaItemVideoUri(this, mProjectPath, 1146 ApiService.generateId(), mInsertMediaItemAfterMediaItemId, 1147 mediaUri, MediaItem.RENDERING_MODE_BLACK_BORDER, 1148 mProject.getTheme()); 1149 } else { 1150 // Notify the user that this item needs to be downloaded. 1151 Toast.makeText(this, getString(R.string.editor_video_load), 1152 Toast.LENGTH_LONG).show(); 1153 // When the download is complete insert it into the project. 1154 ApiService.loadMediaItem(this, mProjectPath, mediaUri, "video/*"); 1155 } 1156 mInsertMediaItemAfterMediaItemId = null; 1157 } else { 1158 // Add this video after the project loads 1159 mAddMediaItemVideoUri = mediaUri; 1160 } 1161 break; 1162 } 1163 1164 case REQUEST_CODE_IMPORT_IMAGE: { 1165 final Uri mediaUri = extras.getData(); 1166 if (mProject != null) { 1167 if ("media".equals(mediaUri.getAuthority())) { 1168 ApiService.addMediaItemImageUri(this, mProjectPath, 1169 ApiService.generateId(), mInsertMediaItemAfterMediaItemId, 1170 mediaUri, MediaItem.RENDERING_MODE_BLACK_BORDER, 1171 MediaItemUtils.getDefaultImageDuration(), mProject.getTheme()); 1172 } else { 1173 // Notify the user that this item needs to be downloaded. 1174 Toast.makeText(this, getString(R.string.editor_image_load), 1175 Toast.LENGTH_LONG).show(); 1176 // When the download is complete insert it into the project. 1177 ApiService.loadMediaItem(this, mProjectPath, mediaUri, "image/*"); 1178 } 1179 mInsertMediaItemAfterMediaItemId = null; 1180 } else { 1181 // Add this image after the project loads 1182 mAddMediaItemImageUri = mediaUri; 1183 } 1184 break; 1185 } 1186 1187 case REQUEST_CODE_IMPORT_MUSIC: { 1188 final Uri data = extras.getData(); 1189 if (mProject != null) { 1190 ApiService.addAudioTrack(this, mProjectPath, ApiService.generateId(), data, 1191 true); 1192 } else { 1193 mAddAudioTrackUri = data; 1194 } 1195 break; 1196 } 1197 1198 case REQUEST_CODE_EDIT_TRANSITION: { 1199 final int type = extras.getIntExtra(TransitionsActivity.PARAM_TRANSITION_TYPE, -1); 1200 final String afterMediaId = extras.getStringExtra( 1201 TransitionsActivity.PARAM_AFTER_MEDIA_ITEM_ID); 1202 final String transitionId = extras.getStringExtra( 1203 TransitionsActivity.PARAM_TRANSITION_ID); 1204 final long transitionDurationMs = extras.getLongExtra( 1205 TransitionsActivity.PARAM_TRANSITION_DURATION, 500); 1206 if (mProject != null) { 1207 mMediaLayout.editTransition(afterMediaId, transitionId, type, 1208 transitionDurationMs); 1209 } else { 1210 // Add this transition after you load the project 1211 mEditTransitionAfterMediaId = afterMediaId; 1212 mEditTransitionId = transitionId; 1213 mEditTransitionType = type; 1214 mEditTransitionDurationMs = transitionDurationMs; 1215 } 1216 break; 1217 } 1218 1219 case REQUEST_CODE_PICK_TRANSITION: { 1220 final int type = extras.getIntExtra(TransitionsActivity.PARAM_TRANSITION_TYPE, -1); 1221 final String afterMediaId = extras.getStringExtra( 1222 TransitionsActivity.PARAM_AFTER_MEDIA_ITEM_ID); 1223 final long transitionDurationMs = extras.getLongExtra( 1224 TransitionsActivity.PARAM_TRANSITION_DURATION, 500); 1225 if (mProject != null) { 1226 mMediaLayout.addTransition(afterMediaId, type, transitionDurationMs); 1227 } else { 1228 // Add this transition after you load the project 1229 mAddTransitionAfterMediaId = afterMediaId; 1230 mAddTransitionType = type; 1231 mAddTransitionDurationMs = transitionDurationMs; 1232 } 1233 break; 1234 } 1235 1236 case REQUEST_CODE_PICK_OVERLAY: { 1237 final String mediaItemId = 1238 extras.getStringExtra(OverlayTitleActivity.PARAM_MEDIA_ITEM_ID); 1239 final Bundle bundle = 1240 extras.getBundleExtra(OverlayTitleActivity.PARAM_OVERLAY_ATTRIBUTES); 1241 if (mProject != null) { 1242 final MovieMediaItem mediaItem = mProject.getMediaItem(mediaItemId); 1243 if (mediaItem != null) { 1244 ApiService.addOverlay(this, mProject.getPath(), mediaItemId, 1245 ApiService.generateId(), bundle, 1246 mediaItem.getAppBoundaryBeginTime(), 1247 OverlayLinearLayout.DEFAULT_TITLE_DURATION); 1248 mOverlayLayout.invalidateCAB(); 1249 } 1250 } else { 1251 // Add this overlay after you load the project 1252 mAddOverlayMediaItemId = mediaItemId; 1253 mAddOverlayUserAttributes = bundle; 1254 } 1255 break; 1256 } 1257 1258 case REQUEST_CODE_EDIT_OVERLAY: { 1259 final Bundle bundle = 1260 extras.getBundleExtra(OverlayTitleActivity.PARAM_OVERLAY_ATTRIBUTES); 1261 final String mediaItemId = 1262 extras.getStringExtra(OverlayTitleActivity.PARAM_MEDIA_ITEM_ID); 1263 final String overlayId = 1264 extras.getStringExtra(OverlayTitleActivity.PARAM_OVERLAY_ID); 1265 if (mProject != null) { 1266 ApiService.setOverlayUserAttributes(this, mProject.getPath(), mediaItemId, 1267 overlayId, bundle); 1268 mOverlayLayout.invalidateCAB(); 1269 } else { 1270 // Edit this overlay after you load the project 1271 mEditOverlayMediaItemId = mediaItemId; 1272 mEditOverlayId = overlayId; 1273 mEditOverlayUserAttributes = bundle; 1274 } 1275 break; 1276 } 1277 1278 case REQUEST_CODE_PICK_EFFECT: { 1279 final String mediaItemId = 1280 extras.getStringExtra(EffectsActivity.PARAM_MEDIA_ITEM_ID); 1281 final int type = extras.getIntExtra(EffectsActivity.PARAM_EFFECT_TYPE, 1282 EffectType.EFFECT_COLOR_GRADIENT); 1283 final Rect startRect = extras.getParcelableExtra(EffectsActivity.PARAM_START_RECT); 1284 final Rect endRect = extras.getParcelableExtra(EffectsActivity.PARAM_END_RECT); 1285 if (mProject != null) { 1286 mMediaLayout.addEffect(type, mediaItemId, startRect, endRect); 1287 } else { 1288 // Add this effect after you load the project 1289 mAddEffectMediaItemId = mediaItemId; 1290 mAddEffectType = type; 1291 mAddKenBurnsStartRect = startRect; 1292 mAddKenBurnsEndRect = endRect; 1293 } 1294 break; 1295 } 1296 1297 case REQUEST_CODE_EDIT_EFFECT: { 1298 final String mediaItemId = 1299 extras.getStringExtra(EffectsActivity.PARAM_MEDIA_ITEM_ID); 1300 final int type = extras.getIntExtra(EffectsActivity.PARAM_EFFECT_TYPE, 1301 EffectType.EFFECT_COLOR_GRADIENT); 1302 final Rect startRect = extras.getParcelableExtra(EffectsActivity.PARAM_START_RECT); 1303 final Rect endRect = extras.getParcelableExtra(EffectsActivity.PARAM_END_RECT); 1304 if (mProject != null) { 1305 mMediaLayout.editEffect(type, mediaItemId, startRect, endRect); 1306 } else { 1307 // Add this effect after you load the project 1308 mEditEffectMediaItemId = mediaItemId; 1309 mEditEffectType = type; 1310 mEditKenBurnsStartRect = startRect; 1311 mEditKenBurnsEndRect = endRect; 1312 } 1313 break; 1314 } 1315 1316 default: { 1317 break; 1318 } 1319 } 1320 } 1321 1322 /* 1323 * {@inheritDoc} 1324 */ 1325 public void surfaceCreated(SurfaceHolder holder) { 1326 if (Log.isLoggable(TAG, Log.DEBUG)) { 1327 Log.d(TAG, "surfaceCreated"); 1328 } 1329 1330 mPreviewThread = new PreviewThread(mSurfaceHolder); 1331 1332 restartPreview(); 1333 } 1334 1335 /* 1336 * {@inheritDoc} 1337 */ 1338 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 1339 if (Log.isLoggable(TAG, Log.DEBUG)) { 1340 Log.d(TAG, "surfaceChanged: " + width + "x" + height); 1341 } 1342 1343 if (mPreviewThread != null) { 1344 mPreviewThread.onSurfaceChanged(width, height); 1345 } 1346 } 1347 1348 /* 1349 * {@inheritDoc} 1350 */ 1351 public void surfaceDestroyed(SurfaceHolder holder) { 1352 if (Log.isLoggable(TAG, Log.DEBUG)) { 1353 Log.d(TAG, "surfaceDestroyed"); 1354 } 1355 1356 // Stop the preview playback if pending and quit the preview thread 1357 if (mPreviewThread != null) { 1358 mPreviewThread.stopPreviewPlayback(); 1359 mPreviewThread.quit(); 1360 mPreviewThread = null; 1361 } 1362 } 1363 1364 /* 1365 * {@inheritDoc} 1366 */ 1367 @Override 1368 protected void enterTransitionalState(int statusStringId) { 1369 mEditorProjectView.setVisibility(View.GONE); 1370 mEditorEmptyView.setVisibility(View.VISIBLE); 1371 1372 ((TextView)findViewById(R.id.empty_project_text)).setText(statusStringId); 1373 findViewById(R.id.empty_project_progress).setVisibility(View.VISIBLE); 1374 } 1375 1376 /* 1377 * {@inheritDoc} 1378 */ 1379 @Override 1380 protected void enterDisabledState(int statusStringId) { 1381 mEditorProjectView.setVisibility(View.GONE); 1382 mEditorEmptyView.setVisibility(View.VISIBLE); 1383 1384 getActionBar().setTitle(R.string.full_app_name); 1385 1386 ((TextView)findViewById(R.id.empty_project_text)).setText(statusStringId); 1387 findViewById(R.id.empty_project_progress).setVisibility(View.GONE); 1388 } 1389 1390 /* 1391 * {@inheritDoc} 1392 */ 1393 @Override 1394 protected void enterReadyState() { 1395 mEditorProjectView.setVisibility(View.VISIBLE); 1396 mEditorEmptyView.setVisibility(View.GONE); 1397 } 1398 1399 /* 1400 * {@inheritDoc} 1401 */ 1402 @Override 1403 protected boolean showPreviewFrame() { 1404 if (mPreviewThread == null) { // The surface is not ready 1405 return false; 1406 } 1407 1408 // Regenerate the preview frame 1409 if (mProject != null && !mPreviewThread.isPlaying() && mPendingExportFilename == null) { 1410 // Display the preview frame 1411 mPreviewThread.previewFrame(mProject, mProject.getPlayheadPos(), 1412 mProject.getMediaItemCount() == 0); 1413 } 1414 1415 return true; 1416 } 1417 1418 /* 1419 * {@inheritDoc} 1420 */ 1421 @Override 1422 protected void updateTimelineDuration() { 1423 if (mProject == null) { 1424 return; 1425 } 1426 1427 final long durationMs = mProject.computeDuration(); 1428 1429 // Resize the timeline according to the new timeline duration 1430 final int zoomWidth = mActivityWidth + timeToDimension(durationMs); 1431 final int childrenCount = mTimelineLayout.getChildCount(); 1432 for (int i = 0; i < childrenCount; i++) { 1433 final View child = mTimelineLayout.getChildAt(i); 1434 final ViewGroup.LayoutParams lp = child.getLayoutParams(); 1435 lp.width = zoomWidth; 1436 child.setLayoutParams(lp); 1437 } 1438 1439 mTimelineLayout.requestLayout(mLayoutCallback); 1440 1441 // Since the duration has changed make sure that the playhead 1442 // position is valid. 1443 if (mProject.getPlayheadPos() > durationMs) { 1444 movePlayhead(durationMs); 1445 } 1446 1447 mAudioTrackLayout.updateTimelineDuration(); 1448 } 1449 1450 /** 1451 * Convert the time to dimension 1452 * At zoom level 1: one activity width = 1200 seconds 1453 * At zoom level 2: one activity width = 600 seconds 1454 * ... 1455 * At zoom level 100: one activity width = 12 seconds 1456 * 1457 * At zoom level 1000: one activity width = 1.2 seconds 1458 * 1459 * @param durationMs The time 1460 * 1461 * @return The dimension 1462 */ 1463 private int timeToDimension(long durationMs) { 1464 return (int)((mProject.getZoomLevel() * mActivityWidth * durationMs) / 1200000); 1465 } 1466 1467 /** 1468 * Zoom the timeline 1469 * 1470 * @param level The zoom level 1471 * @param updateControl true to set the control position to match the 1472 * zoom level 1473 */ 1474 private int zoomTimeline(int level, boolean updateControl) { 1475 if (level < 1 || level > MAX_ZOOM_LEVEL) { 1476 return mProject.getZoomLevel(); 1477 } 1478 1479 mProject.setZoomLevel(level); 1480 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1481 Log.v(TAG, "zoomTimeline level: " + level + " -> " + timeToDimension(1000) + " pix/s"); 1482 } 1483 1484 updateTimelineDuration(); 1485 1486 if (updateControl) { 1487 mZoomControl.setProgress(level); 1488 } 1489 return level; 1490 } 1491 1492 /* 1493 * {@inheritDoc} 1494 */ 1495 @Override 1496 protected void movePlayhead(long timeMs) { 1497 if (mProject == null) { 1498 return; 1499 } 1500 1501 if (setPlayhead(timeMs)) { 1502 // Scroll the timeline such that the specified position 1503 // is in the center of the screen 1504 mTimelineScroller.appScrollTo(timeToDimension(timeMs), true); 1505 } 1506 } 1507 1508 /** 1509 * Set the playhead at the specified time position 1510 * 1511 * @param timeMs The time position 1512 * 1513 * @return true if the playhead was set at the specified time position 1514 */ 1515 private boolean setPlayhead(long timeMs) { 1516 // Check if the position would change 1517 if (mCurrentPlayheadPosMs == timeMs) { 1518 return false; 1519 } 1520 1521 // Check if the time is valid. Note that invalid values are common due 1522 // to overscrolling the timeline 1523 if (timeMs < 0) { 1524 return false; 1525 } else if (timeMs > mProject.computeDuration()) { 1526 return false; 1527 } 1528 1529 mCurrentPlayheadPosMs = timeMs; 1530 1531 mTimeView.setText(StringUtils.getTimestampAsString(this, timeMs)); 1532 mProject.setPlayheadPos(timeMs); 1533 return true; 1534 } 1535 1536 /* 1537 * {@inheritDoc} 1538 */ 1539 @Override 1540 protected void setAspectRatio(final int aspectRatio) { 1541 final FrameLayout.LayoutParams lp = 1542 (FrameLayout.LayoutParams)mSurfaceView.getLayoutParams(); 1543 1544 switch (aspectRatio) { 1545 case MediaProperties.ASPECT_RATIO_5_3: { 1546 lp.width = (lp.height * 5) / 3; 1547 break; 1548 } 1549 1550 case MediaProperties.ASPECT_RATIO_4_3: { 1551 lp.width = (lp.height * 4) / 3; 1552 break; 1553 } 1554 1555 case MediaProperties.ASPECT_RATIO_3_2: { 1556 lp.width = (lp.height * 3) / 2; 1557 break; 1558 } 1559 1560 case MediaProperties.ASPECT_RATIO_11_9: { 1561 lp.width = (lp.height * 11) / 9; 1562 break; 1563 } 1564 1565 case MediaProperties.ASPECT_RATIO_16_9: { 1566 lp.width = (lp.height * 16) / 9; 1567 break; 1568 } 1569 1570 default: { 1571 break; 1572 } 1573 } 1574 1575 if (Log.isLoggable(TAG, Log.DEBUG)) { 1576 Log.d(TAG, "setAspectRatio: " + aspectRatio + ", size: " + lp.width + "x" + lp.height); 1577 } 1578 mSurfaceView.setLayoutParams(lp); 1579 mOverlayView.setLayoutParams(lp); 1580 } 1581 1582 /* 1583 * {@inheritDoc} 1584 */ 1585 @Override 1586 protected MediaLinearLayout getMediaLayout() { 1587 return mMediaLayout; 1588 } 1589 1590 /* 1591 * {@inheritDoc} 1592 */ 1593 @Override 1594 protected OverlayLinearLayout getOverlayLayout() { 1595 return mOverlayLayout; 1596 } 1597 1598 /* 1599 * {@inheritDoc} 1600 */ 1601 @Override 1602 protected AudioTrackLinearLayout getAudioTrackLayout() { 1603 return mAudioTrackLayout; 1604 } 1605 1606 /* 1607 * {@inheritDoc} 1608 */ 1609 @Override 1610 protected void onExportProgress(int progress) { 1611 if (mExportProgressDialog != null) { 1612 mExportProgressDialog.setProgress(progress); 1613 } 1614 } 1615 1616 /* 1617 * {@inheritDoc} 1618 */ 1619 @Override 1620 protected void onExportComplete() { 1621 if (mExportProgressDialog != null) { 1622 mExportProgressDialog.dismiss(); 1623 mExportProgressDialog = null; 1624 } 1625 } 1626 1627 /* 1628 * {@inheritDoc} 1629 */ 1630 @Override 1631 protected void onProjectEditStateChange(boolean projectEdited) { 1632 if (Log.isLoggable(TAG, Log.DEBUG)) { 1633 Log.d(TAG, "onProjectEditStateChange: " + projectEdited); 1634 } 1635 1636 mPreviewPlayButton.setAlpha(projectEdited ? 100 : 255); 1637 mPreviewPlayButton.setEnabled(!projectEdited); 1638 mPreviewRewindButton.setEnabled(!projectEdited); 1639 mPreviewNextButton.setEnabled(!projectEdited); 1640 mPreviewPrevButton.setEnabled(!projectEdited); 1641 1642 mMediaLayout.invalidateCAB(); 1643 mOverlayLayout.invalidateCAB(); 1644 } 1645 1646 /* 1647 * {@inheritDoc} 1648 */ 1649 @Override 1650 protected void initializeFromProject(boolean updateUI) { 1651 if (Log.isLoggable(TAG, Log.DEBUG)) { 1652 Log.d(TAG, "Project was clean: " + mProject.isClean()); 1653 } 1654 1655 if (updateUI || !mProject.isClean()) { 1656 getActionBar().setTitle(mProject.getName()); 1657 1658 // Clear the media related to the previous project and 1659 // add the media for the current project. 1660 mMediaLayout.setProject(mProject); 1661 mOverlayLayout.setProject(mProject); 1662 mAudioTrackLayout.setProject(mProject); 1663 mPlayheadView.setProject(mProject); 1664 1665 // Add the media items to the media item layout 1666 mMediaLayout.addMediaItems(mProject.getMediaItems()); 1667 1668 // Add the media items to the overlay layout 1669 mOverlayLayout.addMediaItems(mProject.getMediaItems()); 1670 1671 // Add the audio tracks to the audio tracks layout 1672 mAudioTrackLayout.addAudioTracks(mProject.getAudioTracks()); 1673 1674 setAspectRatio(mProject.getAspectRatio()); 1675 } 1676 1677 updateTimelineDuration(); 1678 zoomTimeline(mProject.getZoomLevel(), true); 1679 1680 // Set the playhead position. We need to wait for the layout to 1681 // complete before we can scroll to the playhead position. 1682 final Handler handler = new Handler(); 1683 handler.post(new Runnable() { 1684 private final long DELAY = 100; 1685 private final int ATTEMPTS = 20; 1686 private int mAttempts = ATTEMPTS; 1687 1688 /* 1689 * {@inheritDoc} 1690 */ 1691 public void run() { 1692 if (mAttempts == ATTEMPTS) { // Only scroll once 1693 movePlayhead(mProject.getPlayheadPos()); 1694 } 1695 1696 // If the surface is not yet created (showPreviewFrame() 1697 // returns false) wait for a while (DELAY * ATTEMPTS). 1698 if (showPreviewFrame() == false && mAttempts >= 0) { 1699 mAttempts--; 1700 if (mAttempts >= 0) { 1701 handler.postDelayed(this, DELAY); 1702 } 1703 } 1704 } 1705 }); 1706 1707 if (mAddMediaItemVideoUri != null) { 1708 ApiService.addMediaItemVideoUri(this, mProjectPath, ApiService.generateId(), 1709 mInsertMediaItemAfterMediaItemId, 1710 mAddMediaItemVideoUri, MediaItem.RENDERING_MODE_BLACK_BORDER, 1711 mProject.getTheme()); 1712 mAddMediaItemVideoUri = null; 1713 mInsertMediaItemAfterMediaItemId = null; 1714 } 1715 1716 if (mAddMediaItemImageUri != null) { 1717 ApiService.addMediaItemImageUri(this, mProjectPath, ApiService.generateId(), 1718 mInsertMediaItemAfterMediaItemId, 1719 mAddMediaItemImageUri, MediaItem.RENDERING_MODE_BLACK_BORDER, 1720 MediaItemUtils.getDefaultImageDuration(), mProject.getTheme()); 1721 mAddMediaItemImageUri = null; 1722 mInsertMediaItemAfterMediaItemId = null; 1723 } 1724 1725 if (mAddAudioTrackUri != null) { 1726 ApiService.addAudioTrack(this, mProject.getPath(), ApiService.generateId(), 1727 mAddAudioTrackUri, true); 1728 mAddAudioTrackUri = null; 1729 } 1730 1731 if (mAddTransitionAfterMediaId != null) { 1732 mMediaLayout.addTransition(mAddTransitionAfterMediaId, mAddTransitionType, 1733 mAddTransitionDurationMs); 1734 mAddTransitionAfterMediaId = null; 1735 } 1736 1737 if (mEditTransitionId != null) { 1738 mMediaLayout.editTransition(mEditTransitionAfterMediaId, mEditTransitionId, 1739 mEditTransitionType, mEditTransitionDurationMs); 1740 mEditTransitionId = null; 1741 mEditTransitionAfterMediaId = null; 1742 } 1743 1744 if (mAddOverlayMediaItemId != null) { 1745 ApiService.addOverlay(this, mProject.getPath(), mAddOverlayMediaItemId, 1746 ApiService.generateId(), mAddOverlayUserAttributes, 0, 1747 OverlayLinearLayout.DEFAULT_TITLE_DURATION); 1748 mAddOverlayMediaItemId = null; 1749 mAddOverlayUserAttributes = null; 1750 } 1751 1752 if (mEditOverlayMediaItemId != null) { 1753 ApiService.setOverlayUserAttributes(this, mProject.getPath(), mEditOverlayMediaItemId, 1754 mEditOverlayId, mEditOverlayUserAttributes); 1755 mEditOverlayMediaItemId = null; 1756 mEditOverlayId = null; 1757 mEditOverlayUserAttributes = null; 1758 } 1759 1760 if (mAddEffectMediaItemId != null) { 1761 mMediaLayout.addEffect(mAddEffectType, mAddEffectMediaItemId, 1762 mAddKenBurnsStartRect, mAddKenBurnsEndRect); 1763 mAddEffectMediaItemId = null; 1764 } 1765 1766 if (mEditEffectMediaItemId != null) { 1767 mMediaLayout.editEffect(mEditEffectType, mEditEffectMediaItemId, 1768 mEditKenBurnsStartRect, mEditKenBurnsEndRect); 1769 mEditEffectMediaItemId = null; 1770 } 1771 1772 enterReadyState(); 1773 1774 if (mPendingExportFilename != null) { 1775 if (ApiService.isVideoEditorExportPending(mProjectPath, mPendingExportFilename)) { 1776 // The export is still pending 1777 // Display the export project dialog 1778 showExportProgress(); 1779 } else { 1780 // The export completed while the Activity was paused 1781 mPendingExportFilename = null; 1782 } 1783 } 1784 1785 invalidateOptionsMenu(); 1786 1787 restartPreview(); 1788 } 1789 1790 /** 1791 * Restart preview 1792 */ 1793 private void restartPreview() { 1794 if (mRestartPreview == false) { 1795 return; 1796 } 1797 1798 if (mProject == null) { 1799 return; 1800 } 1801 1802 if (mPreviewThread != null) { 1803 mRestartPreview = false; 1804 mPreviewThread.startPreviewPlayback(mProject, mProject.getPlayheadPos()); 1805 } 1806 } 1807 1808 /** 1809 * Show progress during export operation 1810 */ 1811 private void showExportProgress() { 1812 mExportProgressDialog = new ProgressDialog(this); 1813 mExportProgressDialog.setTitle(getString(R.string.export_dialog_export)); 1814 mExportProgressDialog.setMessage(null); 1815 mExportProgressDialog.setIndeterminate(false); 1816 mExportProgressDialog.setCancelable(true); 1817 mExportProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); 1818 mExportProgressDialog.setMax(100); 1819 mExportProgressDialog.setCanceledOnTouchOutside(false); 1820 mExportProgressDialog.setButton(getString(android.R.string.cancel), 1821 new DialogInterface.OnClickListener() { 1822 /* 1823 * {@inheritDoc} 1824 */ 1825 public void onClick(DialogInterface dialog, int which) { 1826 ApiService.cancelExportVideoEditor(VideoEditorActivity.this, 1827 mProjectPath, mPendingExportFilename); 1828 mPendingExportFilename = null; 1829 mExportProgressDialog = null; 1830 } 1831 }); 1832 mExportProgressDialog.setCanceledOnTouchOutside(true); 1833 mExportProgressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { 1834 /* 1835 * {@inheritDoc} 1836 */ 1837 public void onCancel(DialogInterface dialog) { 1838 ApiService.cancelExportVideoEditor(VideoEditorActivity.this, 1839 mProjectPath, mPendingExportFilename); 1840 mPendingExportFilename = null; 1841 mExportProgressDialog = null; 1842 } 1843 }); 1844 mExportProgressDialog.show(); 1845 mExportProgressDialog.setProgressNumberFormat(""); 1846 } 1847 1848 /** 1849 * The preview thread 1850 */ 1851 private class PreviewThread extends Thread { 1852 // Preview states 1853 private final int PREVIEW_STATE_STOPPED = 0; 1854 private final int PREVIEW_STATE_STARTING = 1; 1855 private final int PREVIEW_STATE_STARTED = 2; 1856 private final int PREVIEW_STATE_STOPPING = 3; 1857 1858 private final int OVERLAY_DATA_COUNT = 16; 1859 1860 private final Handler mMainHandler; 1861 private final Queue<Runnable> mQueue; 1862 private final SurfaceHolder mSurfaceHolder; 1863 private final Queue<VideoEditor.OverlayData> mOverlayDataQueue; 1864 private Handler mThreadHandler; 1865 private int mPreviewState; 1866 private Bitmap mOverlayBitmap; 1867 1868 private final Runnable mProcessQueueRunnable = new Runnable() { 1869 /* 1870 * {@inheritDoc} 1871 */ 1872 public void run() { 1873 // Process whatever accumulated in the queue 1874 Runnable runnable; 1875 while ((runnable = mQueue.poll()) != null) { 1876 runnable.run(); 1877 } 1878 } 1879 }; 1880 1881 /** 1882 * Constructor 1883 * 1884 * @param surfaceHolder The surface holder 1885 */ 1886 public PreviewThread(SurfaceHolder surfaceHolder) { 1887 mMainHandler = new Handler(Looper.getMainLooper()); 1888 mQueue = new LinkedBlockingQueue<Runnable>(); 1889 mSurfaceHolder = surfaceHolder; 1890 mPreviewState = PREVIEW_STATE_STOPPED; 1891 1892 mOverlayDataQueue = new LinkedBlockingQueue<VideoEditor.OverlayData>(); 1893 for (int i = 0; i < OVERLAY_DATA_COUNT; i++) { 1894 mOverlayDataQueue.add(new VideoEditor.OverlayData()); 1895 } 1896 1897 start(); 1898 } 1899 1900 /** 1901 * Preview the specified frame 1902 * 1903 * @param project The video editor project 1904 * @param timeMs The frame time 1905 * @param clear true to clear the output 1906 */ 1907 public void previewFrame(final VideoEditorProject project, final long timeMs, 1908 final boolean clear) { 1909 if (mPreviewState == PREVIEW_STATE_STARTING || mPreviewState == PREVIEW_STATE_STARTED) { 1910 stopPreviewPlayback(); 1911 } 1912 1913 if (Log.isLoggable(TAG, Log.DEBUG)) { 1914 Log.d(TAG, "Preview frame at: " + timeMs + " " + clear); 1915 } 1916 1917 // We only need to see the last frame 1918 mQueue.clear(); 1919 1920 mQueue.add(new Runnable() { 1921 /* 1922 * {@inheritDoc} 1923 */ 1924 public void run() { 1925 if (clear) { 1926 project.clearSurface(mSurfaceHolder); 1927 mMainHandler.post(new Runnable() { 1928 /* 1929 * {@inheritDoc} 1930 */ 1931 public void run() { 1932 if (mOverlayBitmap != null) { 1933 mOverlayBitmap.eraseColor(Color.TRANSPARENT); 1934 mOverlayView.invalidate(); 1935 } 1936 } 1937 }); 1938 } else { 1939 final VideoEditor.OverlayData overlayData; 1940 try { 1941 overlayData = mOverlayDataQueue.remove(); 1942 } catch (NoSuchElementException ex) { 1943 Log.e(TAG, "Out of OverlayData elements"); 1944 return; 1945 } 1946 1947 try { 1948 if (project.renderPreviewFrame(mSurfaceHolder, timeMs, overlayData) 1949 < 0) { 1950 if (Log.isLoggable(TAG, Log.DEBUG)) { 1951 Log.d(TAG, "Cannot render preview frame at: " + timeMs + 1952 " of " + mProject.computeDuration()); 1953 } 1954 1955 mOverlayDataQueue.add(overlayData); 1956 } else { 1957 if (overlayData.needsRendering()) { 1958 mMainHandler.post(new Runnable() { 1959 /* 1960 * {@inheritDoc} 1961 */ 1962 public void run() { 1963 if (mOverlayBitmap != null) { 1964 overlayData.renderOverlay(mOverlayBitmap); 1965 mOverlayView.invalidate(); 1966 } else { 1967 overlayData.release(); 1968 } 1969 1970 mOverlayDataQueue.add(overlayData); 1971 } 1972 }); 1973 } else { 1974 mOverlayDataQueue.add(overlayData); 1975 } 1976 } 1977 } catch (Exception ex) { 1978 if (Log.isLoggable(TAG, Log.DEBUG)) { 1979 Log.d(TAG, "renderPreviewFrame failed at timeMs: " + timeMs, ex); 1980 } 1981 mOverlayDataQueue.add(overlayData); 1982 } 1983 } 1984 } 1985 }); 1986 1987 if (mThreadHandler != null) { 1988 mThreadHandler.post(mProcessQueueRunnable); 1989 } 1990 } 1991 1992 /** 1993 * Display the frame at the specified time position 1994 * 1995 * @param mediaItem The media item 1996 * @param timeMs The frame time 1997 */ 1998 public void renderMediaItemFrame(final MovieMediaItem mediaItem, final long timeMs) { 1999 if (mPreviewState == PREVIEW_STATE_STARTING || mPreviewState == PREVIEW_STATE_STARTED) { 2000 stopPreviewPlayback(); 2001 } 2002 2003 if (Log.isLoggable(TAG, Log.VERBOSE)) { 2004 Log.v(TAG, "Render media item frame at: " + timeMs); 2005 } 2006 2007 // We only need to see the last frame 2008 mQueue.clear(); 2009 2010 mQueue.add(new Runnable() { 2011 /* 2012 * {@inheritDoc} 2013 */ 2014 public void run() { 2015 try { 2016 if (mProject.renderMediaItemFrame(mSurfaceHolder, mediaItem.getId(), 2017 timeMs) < 0) { 2018 if (Log.isLoggable(TAG, Log.DEBUG)) { 2019 Log.d(TAG, "Cannot render media item frame at: " + timeMs + 2020 " of " + mediaItem.getDuration()); 2021 } 2022 } 2023 } catch (Exception ex) { 2024 if (Log.isLoggable(TAG, Log.DEBUG)) { 2025 Log.d(TAG, "Cannot render preview frame at: " + timeMs, ex); 2026 } 2027 } 2028 } 2029 }); 2030 2031 if (mThreadHandler != null) { 2032 mThreadHandler.post(mProcessQueueRunnable); 2033 } 2034 } 2035 2036 /** 2037 * Start the preview playback 2038 * 2039 * @param project The video editor project 2040 * @param fromMs Start playing from the specified position 2041 */ 2042 private void startPreviewPlayback(final VideoEditorProject project, final long fromMs) { 2043 if (mPreviewState != PREVIEW_STATE_STOPPED) { 2044 if (Log.isLoggable(TAG, Log.DEBUG)) { 2045 Log.d(TAG, "Preview did not start: " + mPreviewState); 2046 } 2047 return; 2048 } 2049 2050 previewStarted(project); 2051 if (Log.isLoggable(TAG, Log.DEBUG)) { 2052 Log.d(TAG, "Start preview at: " + fromMs); 2053 } 2054 2055 // Clear any pending preview frames 2056 mQueue.clear(); 2057 mQueue.add(new Runnable() { 2058 /* 2059 * {@inheritDoc} 2060 */ 2061 public void run() { 2062 try { 2063 project.startPreview(mSurfaceHolder, fromMs, -1, false, 3, 2064 new VideoEditor.PreviewProgressListener() { 2065 /* 2066 * {@inheritDoc} 2067 */ 2068 public void onStart(VideoEditor videoEditor) { 2069 } 2070 2071 /* 2072 * {@inheritDoc} 2073 */ 2074 public void onProgress(VideoEditor videoEditor, final long timeMs, 2075 final VideoEditor.OverlayData overlayData) { 2076 mMainHandler.post(new Runnable() { 2077 /* 2078 * {@inheritDoc} 2079 */ 2080 public void run() { 2081 if (overlayData != null && overlayData.needsRendering()) { 2082 if (mOverlayBitmap != null) { 2083 overlayData.renderOverlay(mOverlayBitmap); 2084 mOverlayView.invalidate(); 2085 } else { 2086 overlayData.release(); 2087 } 2088 } 2089 2090 if (mPreviewState == PREVIEW_STATE_STARTED || 2091 mPreviewState == PREVIEW_STATE_STOPPING) { 2092 movePlayhead(timeMs); 2093 } 2094 } 2095 }); 2096 } 2097 2098 /* 2099 * {@inheritDoc} 2100 */ 2101 public void onStop(VideoEditor videoEditor) { 2102 mMainHandler.post(new Runnable() { 2103 /* 2104 * {@inheritDoc} 2105 */ 2106 public void run() { 2107 if (mPreviewState == PREVIEW_STATE_STARTED || 2108 mPreviewState == PREVIEW_STATE_STOPPING) { 2109 previewStopped(false); 2110 } 2111 } 2112 }); 2113 } 2114 }); 2115 2116 mMainHandler.post(new Runnable() { 2117 /* 2118 * {@inheritDoc} 2119 */ 2120 public void run() { 2121 mPreviewState = PREVIEW_STATE_STARTED; 2122 } 2123 }); 2124 } catch (Exception ex) { 2125 // This exception may occur when trying to play frames 2126 // at the end of the timeline 2127 // (e.g. when fromMs == clip duration) 2128 if (Log.isLoggable(TAG, Log.DEBUG)) { 2129 Log.d(TAG, "Cannot start preview at: " + fromMs, ex); 2130 } 2131 2132 mMainHandler.post(new Runnable() { 2133 /* 2134 * {@inheritDoc} 2135 */ 2136 public void run() { 2137 mPreviewState = PREVIEW_STATE_STARTED; 2138 previewStopped(true); 2139 } 2140 }); 2141 } 2142 } 2143 }); 2144 2145 if (mThreadHandler != null) { 2146 mThreadHandler.post(mProcessQueueRunnable); 2147 } 2148 } 2149 2150 /** 2151 * The preview started. 2152 * This method is always invoked from the UI thread. 2153 * 2154 * @param project The project 2155 */ 2156 private void previewStarted(VideoEditorProject project) { 2157 // Change the button image back to a play icon 2158 mPreviewPlayButton.setImageResource(R.drawable.btn_playback_pause_selector); 2159 2160 mTimelineScroller.enableUserScrolling(false); 2161 mMediaLayout.setPlaybackInProgress(true); 2162 mOverlayLayout.setPlaybackInProgress(true); 2163 mAudioTrackLayout.setPlaybackInProgress(true); 2164 2165 mPreviewState = PREVIEW_STATE_STARTING; 2166 } 2167 2168 /** 2169 * Stop previewing 2170 */ 2171 private void stopPreviewPlayback() { 2172 switch (mPreviewState) { 2173 case PREVIEW_STATE_STOPPED: { 2174 if (Log.isLoggable(TAG, Log.DEBUG)) { 2175 Log.d(TAG, "stopPreviewPlayback: State was PREVIEW_STATE_STOPPED"); 2176 } 2177 return; 2178 } 2179 2180 case PREVIEW_STATE_STOPPING: { 2181 if (Log.isLoggable(TAG, Log.DEBUG)) { 2182 Log.d(TAG, "stopPreviewPlayback: State was PREVIEW_STATE_STOPPING"); 2183 } 2184 return; 2185 } 2186 2187 case PREVIEW_STATE_STARTING: { 2188 if (Log.isLoggable(TAG, Log.DEBUG)) { 2189 Log.d(TAG, "stopPreviewPlayback: State was PREVIEW_STATE_STARTING " + 2190 "now PREVIEW_STATE_STOPPING"); 2191 } 2192 2193 mPreviewState = PREVIEW_STATE_STOPPING; 2194 2195 // We need to wait until the preview starts 2196 mMainHandler.postDelayed(new Runnable() { 2197 /* 2198 * {@inheritDoc} 2199 */ 2200 public void run() { 2201 if (isFinishing() || isChangingConfigurations()) { 2202 // The activity is shutting down. Force stopping now. 2203 if (Log.isLoggable(TAG, Log.DEBUG)) { 2204 Log.d(TAG, "stopPreviewPlayback: Activity is shutting down"); 2205 } 2206 2207 mPreviewState = PREVIEW_STATE_STARTED; 2208 previewStopped(true); 2209 } else if (mPreviewState == PREVIEW_STATE_STARTED) { 2210 if (Log.isLoggable(TAG, Log.DEBUG)) { 2211 Log.d(TAG, "stopPreviewPlayback: Now PREVIEW_STATE_STARTED"); 2212 } 2213 2214 previewStopped(false); 2215 } else if (mPreviewState == PREVIEW_STATE_STOPPING) { 2216 // Keep waiting 2217 mMainHandler.postDelayed(this, 100); 2218 2219 if (Log.isLoggable(TAG, Log.DEBUG)) { 2220 Log.d(TAG, "stopPreviewPlayback: Waiting for PREVIEW_STATE_STARTED"); 2221 } 2222 } else { 2223 if (Log.isLoggable(TAG, Log.DEBUG)) { 2224 Log.d(TAG, "stopPreviewPlayback: PREVIEW_STATE_STOPPED while waiting"); 2225 } 2226 } 2227 } 2228 }, 50); 2229 2230 break; 2231 } 2232 2233 case PREVIEW_STATE_STARTED: { 2234 if (Log.isLoggable(TAG, Log.DEBUG)) { 2235 Log.d(TAG, "stopPreviewPlayback: State was PREVIEW_STATE_STARTED"); 2236 } 2237 2238 // We need to stop 2239 previewStopped(false); 2240 return; 2241 } 2242 2243 default: { 2244 throw new IllegalArgumentException("stopPreviewPlayback state: " + 2245 mPreviewState); 2246 } 2247 } 2248 } 2249 2250 /** 2251 * The surface size has changed 2252 * 2253 * @param width The new surface width 2254 * @param heightThe new surface height 2255 */ 2256 private void onSurfaceChanged(int width, int height) { 2257 if (mOverlayBitmap != null) { 2258 if (mOverlayBitmap.getWidth() == width && mOverlayBitmap.getHeight() == height) { 2259 // The size has not changed 2260 return; 2261 } 2262 2263 mOverlayView.setImageBitmap(null); 2264 mOverlayBitmap.recycle(); 2265 mOverlayBitmap = null; 2266 } 2267 2268 // Create the overlay bitmap 2269 if (Log.isLoggable(TAG, Log.DEBUG)) { 2270 Log.d(TAG, "Overlay size: " + width + " x " + height); 2271 } 2272 2273 mOverlayBitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888); 2274 mOverlayView.setImageBitmap(mOverlayBitmap); 2275 } 2276 2277 /** 2278 * Preview stopped. This method is always invoked from the UI thread. 2279 * 2280 * @param error true if the preview stopped due to an error 2281 * 2282 * @return The stop position 2283 */ 2284 private void previewStopped(boolean error) { 2285 if (mProject == null) { 2286 Log.w(TAG, "previewStopped: project was deleted."); 2287 return; 2288 } 2289 2290 if (mPreviewState != PREVIEW_STATE_STARTED) { 2291 throw new IllegalStateException("previewStopped in state: " + mPreviewState); 2292 } 2293 2294 // Change the button image back to a play icon 2295 mPreviewPlayButton.setImageResource(R.drawable.btn_playback_play_selector); 2296 2297 if (error == false) { 2298 // Set the playhead position at the position where the playback stopped 2299 final long stopTimeMs = mProject.stopPreview(); 2300 movePlayhead(stopTimeMs); 2301 if (Log.isLoggable(TAG, Log.DEBUG)) { 2302 Log.d(TAG, "PREVIEW_STATE_STOPPED: " + stopTimeMs); 2303 } 2304 } else { 2305 if (Log.isLoggable(TAG, Log.DEBUG)) { 2306 Log.d(TAG, "PREVIEW_STATE_STOPPED due to error"); 2307 } 2308 } 2309 2310 mPreviewState = PREVIEW_STATE_STOPPED; 2311 2312 // The playback has stopped 2313 mTimelineScroller.enableUserScrolling(true); 2314 mMediaLayout.setPlaybackInProgress(false); 2315 mAudioTrackLayout.setPlaybackInProgress(false); 2316 mOverlayLayout.setPlaybackInProgress(false); 2317 } 2318 2319 /** 2320 * @return true if preview playback is in progress 2321 */ 2322 private boolean isPlaying() { 2323 return mPreviewState == PREVIEW_STATE_STARTING || 2324 mPreviewState == PREVIEW_STATE_STARTED; 2325 } 2326 2327 /** 2328 * @return true if the preview is stopped 2329 */ 2330 private boolean isStopped() { 2331 return mPreviewState == PREVIEW_STATE_STOPPED; 2332 } 2333 2334 /* 2335 * {@inheritDoc} 2336 */ 2337 @Override 2338 public void run() { 2339 setPriority(MAX_PRIORITY); 2340 Looper.prepare(); 2341 mThreadHandler = new Handler(); 2342 2343 // Ensure that the queued items are processed 2344 mMainHandler.post(new Runnable() { 2345 /* 2346 * {@inheritDoc} 2347 */ 2348 public void run() { 2349 // Start processing the queue of runnables 2350 mThreadHandler.post(mProcessQueueRunnable); 2351 } 2352 }); 2353 2354 // Run the loop 2355 Looper.loop(); 2356 } 2357 2358 /** 2359 * Quit the thread 2360 */ 2361 public void quit() { 2362 // Release the overlay bitmap 2363 if (mOverlayBitmap != null) { 2364 mOverlayView.setImageBitmap(null); 2365 mOverlayBitmap.recycle(); 2366 mOverlayBitmap = null; 2367 } 2368 2369 if (mThreadHandler != null) { 2370 mThreadHandler.getLooper().quit(); 2371 try { 2372 // Wait for the thread to quit. An ANR waiting to happen. 2373 mThreadHandler.getLooper().getThread().join(); 2374 } catch (InterruptedException ex) { 2375 } 2376 } 2377 2378 mQueue.clear(); 2379 } 2380 } 2381} 2382