VideoEditorActivity.java revision a34fab86f98fe477173f1d64b54de760476ec662
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 intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); 664 startActivityForResult(intent, REQUEST_CODE_IMPORT_VIDEO); 665 return true; 666 } 667 668 case MENU_IMPORT_IMAGE_ID: { 669 final MovieMediaItem mediaItem = mProject.getInsertAfterMediaItem( 670 mProject.getPlayheadPos()); 671 if (mediaItem != null) { 672 mInsertMediaItemAfterMediaItemId = mediaItem.getId(); 673 } else { 674 mInsertMediaItemAfterMediaItemId = null; 675 } 676 677 final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 678 intent.setType("image/*"); 679 intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); 680 startActivityForResult(intent, REQUEST_CODE_IMPORT_IMAGE); 681 return true; 682 } 683 684 case MENU_IMPORT_AUDIO_ID: { 685 final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 686 intent.setType("audio/*"); 687 startActivityForResult(intent, REQUEST_CODE_IMPORT_MUSIC); 688 return true; 689 } 690 691 case MENU_CHANGE_ASPECT_RATIO_ID: { 692 final ArrayList<Integer> aspectRatiosList = mProject.getUniqueAspectRatiosList(); 693 final int size = aspectRatiosList.size(); 694 if (size > 1) { 695 final Bundle bundle = new Bundle(); 696 bundle.putIntegerArrayList(PARAM_ASPECT_RATIOS_LIST, aspectRatiosList); 697 698 // Get the current aspect ratio index 699 final int currentAspectRatio = mProject.getAspectRatio(); 700 int currentAspectRatioIndex = 0; 701 for (int i = 0; i < size; i++) { 702 final int aspectRatio = aspectRatiosList.get(i); 703 if (aspectRatio == currentAspectRatio) { 704 currentAspectRatioIndex = i; 705 break; 706 } 707 } 708 bundle.putInt(PARAM_CURRENT_ASPECT_RATIO_INDEX, currentAspectRatioIndex); 709 showDialog(DIALOG_CHOOSE_ASPECT_RATIO_ID, bundle); 710 } 711 return true; 712 } 713 714 case MENU_EDIT_PROJECT_NAME_ID: { 715 showDialog(DIALOG_EDIT_PROJECT_NAME_ID); 716 return true; 717 } 718 719 case MENU_DELETE_PROJECT_ID: { 720 // Confirm project delete 721 showDialog(DIALOG_DELETE_PROJECT_ID); 722 return true; 723 } 724 725 case MENU_EXPORT_MOVIE_ID: { 726 // Present the user with a dialog to choose export options 727 showDialog(DIALOG_EXPORT_OPTIONS_ID); 728 return true; 729 } 730 731 case MENU_PLAY_EXPORTED_MOVIE: { 732 final Intent intent = new Intent(Intent.ACTION_VIEW); 733 intent.setDataAndType(mProject.getExportedMovieUri(), "video/*"); 734 intent.putExtra(MediaStore.EXTRA_FINISH_ON_COMPLETION, false); 735 startActivity(intent); 736 return true; 737 } 738 739 case MENU_SHARE_VIDEO: { 740 final Intent intent = new Intent(Intent.ACTION_SEND); 741 intent.putExtra(Intent.EXTRA_STREAM, mProject.getExportedMovieUri()); 742 intent.setType("video/*"); 743 startActivity(intent); 744 return true; 745 } 746 747 default: { 748 return false; 749 } 750 } 751 } 752 753 /* 754 * {@inheritDoc} 755 */ 756 @Override 757 public Dialog onCreateDialog(int id, final Bundle bundle) { 758 switch (id) { 759 case DIALOG_CHOOSE_ASPECT_RATIO_ID: { 760 final AlertDialog.Builder builder = new AlertDialog.Builder(this); 761 builder.setTitle(getString(R.string.editor_change_aspect_ratio)); 762 final ArrayList<Integer> aspectRatios = 763 bundle.getIntegerArrayList(PARAM_ASPECT_RATIOS_LIST); 764 final int count = aspectRatios.size(); 765 final CharSequence[] aspectRatioStrings = new CharSequence[count]; 766 for (int i = 0; i < count; i++) { 767 int aspectRatio = aspectRatios.get(i); 768 switch (aspectRatio) { 769 case MediaProperties.ASPECT_RATIO_11_9: { 770 aspectRatioStrings[i] = getString(R.string.aspect_ratio_11_9); 771 break; 772 } 773 774 case MediaProperties.ASPECT_RATIO_16_9: { 775 aspectRatioStrings[i] = getString(R.string.aspect_ratio_16_9); 776 break; 777 } 778 779 case MediaProperties.ASPECT_RATIO_3_2: { 780 aspectRatioStrings[i] = getString(R.string.aspect_ratio_3_2); 781 break; 782 } 783 784 case MediaProperties.ASPECT_RATIO_4_3: { 785 aspectRatioStrings[i] = getString(R.string.aspect_ratio_4_3); 786 break; 787 } 788 789 case MediaProperties.ASPECT_RATIO_5_3: { 790 aspectRatioStrings[i] = getString(R.string.aspect_ratio_5_3); 791 break; 792 } 793 794 default: { 795 break; 796 } 797 } 798 } 799 800 builder.setSingleChoiceItems(aspectRatioStrings, 801 bundle.getInt(PARAM_CURRENT_ASPECT_RATIO_INDEX), 802 new DialogInterface.OnClickListener() { 803 /* 804 * {@inheritDoc} 805 */ 806 public void onClick(DialogInterface dialog, int which) { 807 final int aspectRatio = aspectRatios.get(which); 808 ApiService.setAspectRatio(VideoEditorActivity.this, mProjectPath, 809 aspectRatio); 810 811 removeDialog(DIALOG_CHOOSE_ASPECT_RATIO_ID); 812 } 813 }); 814 builder.setCancelable(true); 815 builder.setOnCancelListener(new DialogInterface.OnCancelListener() { 816 /* 817 * {@inheritDoc} 818 */ 819 public void onCancel(DialogInterface dialog) { 820 removeDialog(DIALOG_CHOOSE_ASPECT_RATIO_ID); 821 } 822 }); 823 return builder.create(); 824 } 825 826 case DIALOG_DELETE_PROJECT_ID: { 827 return AlertDialogs.createAlert(this, getString(R.string.editor_delete_project), 0, 828 getString(R.string.editor_delete_project_question), 829 getString(R.string.yes), 830 new DialogInterface.OnClickListener() { 831 /* 832 * {@inheritDoc} 833 */ 834 public void onClick(DialogInterface dialog, int which) { 835 ApiService.deleteProject(VideoEditorActivity.this, mProjectPath); 836 mProjectPath = null; 837 mProject = null; 838 enterDisabledState(R.string.editor_no_project); 839 840 removeDialog(DIALOG_DELETE_PROJECT_ID); 841 finish(); 842 } 843 }, getString(R.string.no), new DialogInterface.OnClickListener() { 844 /* 845 * {@inheritDoc} 846 */ 847 public void onClick(DialogInterface dialog, int which) { 848 removeDialog(DIALOG_DELETE_PROJECT_ID); 849 } 850 }, new DialogInterface.OnCancelListener() { 851 /* 852 * {@inheritDoc} 853 */ 854 public void onCancel(DialogInterface dialog) { 855 removeDialog(DIALOG_DELETE_PROJECT_ID); 856 } 857 }, true); 858 } 859 860 case DIALOG_DELETE_BAD_PROJECT_ID: { 861 return AlertDialogs.createAlert(this, getString(R.string.editor_delete_project), 0, 862 getString(R.string.editor_load_error), 863 getString(R.string.yes), 864 new DialogInterface.OnClickListener() { 865 /* 866 * {@inheritDoc} 867 */ 868 public void onClick(DialogInterface dialog, int which) { 869 ApiService.deleteProject(VideoEditorActivity.this, 870 bundle.getString(PARAM_PROJECT_PATH)); 871 872 removeDialog(DIALOG_DELETE_BAD_PROJECT_ID); 873 finish(); 874 } 875 }, getString(R.string.no), new DialogInterface.OnClickListener() { 876 /* 877 * {@inheritDoc} 878 */ 879 public void onClick(DialogInterface dialog, int which) { 880 removeDialog(DIALOG_DELETE_BAD_PROJECT_ID); 881 } 882 }, new DialogInterface.OnCancelListener() { 883 /* 884 * {@inheritDoc} 885 */ 886 public void onCancel(DialogInterface dialog) { 887 removeDialog(DIALOG_DELETE_BAD_PROJECT_ID); 888 } 889 }, true); 890 } 891 892 case DIALOG_EDIT_PROJECT_NAME_ID: { 893 if (mProject == null) { 894 return null; 895 } 896 897 return AlertDialogs.createEditDialog(this, 898 getString(R.string.editor_edit_project_name), 899 mProject.getName(), getString(android.R.string.ok), 900 new DialogInterface.OnClickListener() { 901 /* 902 * {@inheritDoc} 903 */ 904 public void onClick(DialogInterface dialog, int which) { 905 final TextView tv = 906 (TextView)((AlertDialog)dialog).findViewById(R.id.text_1); 907 mProject.setProjectName(tv.getText().toString()); 908 getActionBar().setTitle(tv.getText()); 909 removeDialog(DIALOG_EDIT_PROJECT_NAME_ID); 910 } 911 }, getString(android.R.string.cancel), 912 new DialogInterface.OnClickListener() { 913 /* 914 * {@inheritDoc} 915 */ 916 public void onClick(DialogInterface dialog, int which) { 917 removeDialog(DIALOG_EDIT_PROJECT_NAME_ID); 918 } 919 }, new DialogInterface.OnCancelListener() { 920 /* 921 * {@inheritDoc} 922 */ 923 public void onCancel(DialogInterface dialog) { 924 removeDialog(DIALOG_EDIT_PROJECT_NAME_ID); 925 } 926 }, InputType.TYPE_NULL, 32); 927 } 928 929 case DIALOG_EXPORT_OPTIONS_ID: { 930 if (mProject == null) { 931 return null; 932 } 933 934 return ExportOptionsDialog.create(this, 935 new ExportOptionsDialog.ExportOptionsListener() { 936 /* 937 * {@inheritDoc} 938 */ 939 public void onExportOptions(int movieHeight, int movieBitrate) { 940 mPendingExportFilename = FileUtils.createMovieName( 941 MediaProperties.FILE_MP4); 942 ApiService.exportVideoEditor(VideoEditorActivity.this, mProjectPath, 943 mPendingExportFilename, movieHeight, movieBitrate); 944 945 removeDialog(DIALOG_EXPORT_OPTIONS_ID); 946 947 showExportProgress(); 948 } 949 }, new DialogInterface.OnClickListener() { 950 /* 951 * {@inheritDoc} 952 */ 953 public void onClick(DialogInterface dialog, int which) { 954 removeDialog(DIALOG_EXPORT_OPTIONS_ID); 955 } 956 }, new DialogInterface.OnCancelListener() { 957 /* 958 * {@inheritDoc} 959 */ 960 public void onCancel(DialogInterface dialog) { 961 removeDialog(DIALOG_EXPORT_OPTIONS_ID); 962 } 963 }, mProject.getAspectRatio()); 964 } 965 966 case DIALOG_REMOVE_MEDIA_ITEM_ID: { 967 return mMediaLayout.onCreateDialog(id, bundle); 968 } 969 970 case DIALOG_CHANGE_RENDERING_MODE_ID: { 971 return mMediaLayout.onCreateDialog(id, bundle); 972 } 973 974 case DIALOG_REMOVE_TRANSITION_ID: { 975 return mMediaLayout.onCreateDialog(id, bundle); 976 } 977 978 case DIALOG_REMOVE_OVERLAY_ID: { 979 return mOverlayLayout.onCreateDialog(id, bundle); 980 } 981 982 case DIALOG_REMOVE_EFFECT_ID: { 983 return mMediaLayout.onCreateDialog(id, bundle); 984 } 985 986 case DIALOG_REMOVE_AUDIO_TRACK_ID: { 987 return mAudioTrackLayout.onCreateDialog(id, bundle); 988 } 989 990 default: { 991 return null; 992 } 993 } 994 } 995 996 /* 997 * {@inheritDoc} 998 */ 999 public void onClickHandler(View target) { 1000 final long playheadPosMs = mProject.getPlayheadPos(); 1001 1002 switch (target.getId()) { 1003 case R.id.editor_play: { 1004 if (mProject != null && mPreviewThread != null) { 1005 if (mPreviewThread.isPlaying()) { 1006 mPreviewThread.stopPreviewPlayback(); 1007 } else if (mProject.getMediaItemCount() > 0){ 1008 mPreviewThread.startPreviewPlayback(mProject, playheadPosMs); 1009 } 1010 } 1011 break; 1012 } 1013 1014 case R.id.editor_rewind: { 1015 if (mProject != null && mPreviewThread != null) { 1016 if (mPreviewThread.isPlaying()) { 1017 mPreviewThread.stopPreviewPlayback(); 1018 movePlayhead(0); 1019 mPreviewThread.startPreviewPlayback(mProject, 0); 1020 } else { 1021 movePlayhead(0); 1022 showPreviewFrame(); 1023 } 1024 } 1025 break; 1026 } 1027 1028 case R.id.editor_next: { 1029 if (mProject != null && mPreviewThread != null) { 1030 final boolean restartPreview; 1031 if (mPreviewThread.isPlaying()) { 1032 mPreviewThread.stopPreviewPlayback(); 1033 restartPreview = true; 1034 } else { 1035 restartPreview = false; 1036 } 1037 1038 final MovieMediaItem mediaItem = mProject.getNextMediaItem(playheadPosMs); 1039 if (mediaItem != null) { 1040 movePlayhead(mProject.getMediaItemBeginTime(mediaItem.getId())); 1041 if (restartPreview) { 1042 mPreviewThread.startPreviewPlayback(mProject, 1043 mProject.getPlayheadPos()); 1044 } else { 1045 showPreviewFrame(); 1046 } 1047 } else { // Move to the end of the timeline 1048 movePlayhead(mProject.computeDuration()); 1049 showPreviewFrame(); 1050 } 1051 } 1052 break; 1053 } 1054 1055 case R.id.editor_prev: { 1056 if (mProject != null && mPreviewThread != null) { 1057 final boolean restartPreview; 1058 if (mPreviewThread.isPlaying()) { 1059 mPreviewThread.stopPreviewPlayback(); 1060 restartPreview = true; 1061 } else { 1062 restartPreview = false; 1063 } 1064 1065 final MovieMediaItem mediaItem = mProject.getPreviousMediaItem(playheadPosMs); 1066 if (mediaItem != null) { 1067 movePlayhead(mProject.getMediaItemBeginTime(mediaItem.getId())); 1068 } else { // Move to the beginning of the timeline 1069 movePlayhead(0); 1070 } 1071 1072 if (restartPreview) { 1073 mPreviewThread.startPreviewPlayback(mProject, mProject.getPlayheadPos()); 1074 } else { 1075 showPreviewFrame(); 1076 } 1077 } 1078 break; 1079 } 1080 1081 default: { 1082 break; 1083 } 1084 } 1085 } 1086 1087 /* 1088 * {@inheritDoc} 1089 */ 1090 @Override 1091 protected void onActivityResult(int requestCode, int resultCode, Intent extras) { 1092 super.onActivityResult(requestCode, resultCode, extras); 1093 if (resultCode == RESULT_CANCELED) { 1094 switch (requestCode) { 1095 case REQUEST_CODE_CAPTURE_VIDEO: 1096 case REQUEST_CODE_CAPTURE_IMAGE: { 1097 if (mCaptureMediaUri != null) { 1098 getContentResolver().delete(mCaptureMediaUri, null, null); 1099 mCaptureMediaUri = null; 1100 } 1101 break; 1102 } 1103 1104 default: { 1105 break; 1106 } 1107 } 1108 return; 1109 } 1110 1111 switch (requestCode) { 1112 case REQUEST_CODE_CAPTURE_VIDEO: { 1113 if (mProject != null) { 1114 ApiService.addMediaItemVideoUri(this, mProjectPath, 1115 ApiService.generateId(), mInsertMediaItemAfterMediaItemId, 1116 mCaptureMediaUri, MediaItem.RENDERING_MODE_BLACK_BORDER, 1117 mProject.getTheme()); 1118 mInsertMediaItemAfterMediaItemId = null; 1119 } else { 1120 // Add this video after the project loads 1121 mAddMediaItemVideoUri = mCaptureMediaUri; 1122 } 1123 mCaptureMediaUri = null; 1124 break; 1125 } 1126 1127 case REQUEST_CODE_CAPTURE_IMAGE: { 1128 if (mProject != null) { 1129 ApiService.addMediaItemImageUri(this, mProjectPath, 1130 ApiService.generateId(), mInsertMediaItemAfterMediaItemId, 1131 mCaptureMediaUri, MediaItem.RENDERING_MODE_BLACK_BORDER, 1132 MediaItemUtils.getDefaultImageDuration(), 1133 mProject.getTheme()); 1134 mInsertMediaItemAfterMediaItemId = null; 1135 } else { 1136 // Add this image after the project loads 1137 mAddMediaItemImageUri = mCaptureMediaUri; 1138 } 1139 mCaptureMediaUri = null; 1140 break; 1141 } 1142 1143 case REQUEST_CODE_IMPORT_VIDEO: { 1144 final Uri mediaUri = extras.getData(); 1145 if (mProject != null) { 1146 if ("media".equals(mediaUri.getAuthority())) { 1147 ApiService.addMediaItemVideoUri(this, mProjectPath, 1148 ApiService.generateId(), mInsertMediaItemAfterMediaItemId, 1149 mediaUri, MediaItem.RENDERING_MODE_BLACK_BORDER, 1150 mProject.getTheme()); 1151 } else { 1152 // Notify the user that this item needs to be downloaded. 1153 Toast.makeText(this, getString(R.string.editor_video_load), 1154 Toast.LENGTH_LONG).show(); 1155 // When the download is complete insert it into the project. 1156 ApiService.loadMediaItem(this, mProjectPath, mediaUri, "video/*"); 1157 } 1158 mInsertMediaItemAfterMediaItemId = null; 1159 } else { 1160 // Add this video after the project loads 1161 mAddMediaItemVideoUri = mediaUri; 1162 } 1163 break; 1164 } 1165 1166 case REQUEST_CODE_IMPORT_IMAGE: { 1167 final Uri mediaUri = extras.getData(); 1168 if (mProject != null) { 1169 if ("media".equals(mediaUri.getAuthority())) { 1170 ApiService.addMediaItemImageUri(this, mProjectPath, 1171 ApiService.generateId(), mInsertMediaItemAfterMediaItemId, 1172 mediaUri, MediaItem.RENDERING_MODE_BLACK_BORDER, 1173 MediaItemUtils.getDefaultImageDuration(), mProject.getTheme()); 1174 } else { 1175 // Notify the user that this item needs to be downloaded. 1176 Toast.makeText(this, getString(R.string.editor_image_load), 1177 Toast.LENGTH_LONG).show(); 1178 // When the download is complete insert it into the project. 1179 ApiService.loadMediaItem(this, mProjectPath, mediaUri, "image/*"); 1180 } 1181 mInsertMediaItemAfterMediaItemId = null; 1182 } else { 1183 // Add this image after the project loads 1184 mAddMediaItemImageUri = mediaUri; 1185 } 1186 break; 1187 } 1188 1189 case REQUEST_CODE_IMPORT_MUSIC: { 1190 final Uri data = extras.getData(); 1191 if (mProject != null) { 1192 ApiService.addAudioTrack(this, mProjectPath, ApiService.generateId(), data, 1193 true); 1194 } else { 1195 mAddAudioTrackUri = data; 1196 } 1197 break; 1198 } 1199 1200 case REQUEST_CODE_EDIT_TRANSITION: { 1201 final int type = extras.getIntExtra(TransitionsActivity.PARAM_TRANSITION_TYPE, -1); 1202 final String afterMediaId = extras.getStringExtra( 1203 TransitionsActivity.PARAM_AFTER_MEDIA_ITEM_ID); 1204 final String transitionId = extras.getStringExtra( 1205 TransitionsActivity.PARAM_TRANSITION_ID); 1206 final long transitionDurationMs = extras.getLongExtra( 1207 TransitionsActivity.PARAM_TRANSITION_DURATION, 500); 1208 if (mProject != null) { 1209 mMediaLayout.editTransition(afterMediaId, transitionId, type, 1210 transitionDurationMs); 1211 } else { 1212 // Add this transition after you load the project 1213 mEditTransitionAfterMediaId = afterMediaId; 1214 mEditTransitionId = transitionId; 1215 mEditTransitionType = type; 1216 mEditTransitionDurationMs = transitionDurationMs; 1217 } 1218 break; 1219 } 1220 1221 case REQUEST_CODE_PICK_TRANSITION: { 1222 final int type = extras.getIntExtra(TransitionsActivity.PARAM_TRANSITION_TYPE, -1); 1223 final String afterMediaId = extras.getStringExtra( 1224 TransitionsActivity.PARAM_AFTER_MEDIA_ITEM_ID); 1225 final long transitionDurationMs = extras.getLongExtra( 1226 TransitionsActivity.PARAM_TRANSITION_DURATION, 500); 1227 if (mProject != null) { 1228 mMediaLayout.addTransition(afterMediaId, type, transitionDurationMs); 1229 } else { 1230 // Add this transition after you load the project 1231 mAddTransitionAfterMediaId = afterMediaId; 1232 mAddTransitionType = type; 1233 mAddTransitionDurationMs = transitionDurationMs; 1234 } 1235 break; 1236 } 1237 1238 case REQUEST_CODE_PICK_OVERLAY: { 1239 final String mediaItemId = 1240 extras.getStringExtra(OverlayTitleActivity.PARAM_MEDIA_ITEM_ID); 1241 final Bundle bundle = 1242 extras.getBundleExtra(OverlayTitleActivity.PARAM_OVERLAY_ATTRIBUTES); 1243 if (mProject != null) { 1244 final MovieMediaItem mediaItem = mProject.getMediaItem(mediaItemId); 1245 if (mediaItem != null) { 1246 ApiService.addOverlay(this, mProject.getPath(), mediaItemId, 1247 ApiService.generateId(), bundle, 1248 mediaItem.getAppBoundaryBeginTime(), 1249 OverlayLinearLayout.DEFAULT_TITLE_DURATION); 1250 mOverlayLayout.invalidateCAB(); 1251 } 1252 } else { 1253 // Add this overlay after you load the project 1254 mAddOverlayMediaItemId = mediaItemId; 1255 mAddOverlayUserAttributes = bundle; 1256 } 1257 break; 1258 } 1259 1260 case REQUEST_CODE_EDIT_OVERLAY: { 1261 final Bundle bundle = 1262 extras.getBundleExtra(OverlayTitleActivity.PARAM_OVERLAY_ATTRIBUTES); 1263 final String mediaItemId = 1264 extras.getStringExtra(OverlayTitleActivity.PARAM_MEDIA_ITEM_ID); 1265 final String overlayId = 1266 extras.getStringExtra(OverlayTitleActivity.PARAM_OVERLAY_ID); 1267 if (mProject != null) { 1268 ApiService.setOverlayUserAttributes(this, mProject.getPath(), mediaItemId, 1269 overlayId, bundle); 1270 mOverlayLayout.invalidateCAB(); 1271 } else { 1272 // Edit this overlay after you load the project 1273 mEditOverlayMediaItemId = mediaItemId; 1274 mEditOverlayId = overlayId; 1275 mEditOverlayUserAttributes = bundle; 1276 } 1277 break; 1278 } 1279 1280 case REQUEST_CODE_PICK_EFFECT: { 1281 final String mediaItemId = 1282 extras.getStringExtra(EffectsActivity.PARAM_MEDIA_ITEM_ID); 1283 final int type = extras.getIntExtra(EffectsActivity.PARAM_EFFECT_TYPE, 1284 EffectType.EFFECT_COLOR_GRADIENT); 1285 final Rect startRect = extras.getParcelableExtra(EffectsActivity.PARAM_START_RECT); 1286 final Rect endRect = extras.getParcelableExtra(EffectsActivity.PARAM_END_RECT); 1287 if (mProject != null) { 1288 mMediaLayout.addEffect(type, mediaItemId, startRect, endRect); 1289 } else { 1290 // Add this effect after you load the project 1291 mAddEffectMediaItemId = mediaItemId; 1292 mAddEffectType = type; 1293 mAddKenBurnsStartRect = startRect; 1294 mAddKenBurnsEndRect = endRect; 1295 } 1296 break; 1297 } 1298 1299 case REQUEST_CODE_EDIT_EFFECT: { 1300 final String mediaItemId = 1301 extras.getStringExtra(EffectsActivity.PARAM_MEDIA_ITEM_ID); 1302 final int type = extras.getIntExtra(EffectsActivity.PARAM_EFFECT_TYPE, 1303 EffectType.EFFECT_COLOR_GRADIENT); 1304 final Rect startRect = extras.getParcelableExtra(EffectsActivity.PARAM_START_RECT); 1305 final Rect endRect = extras.getParcelableExtra(EffectsActivity.PARAM_END_RECT); 1306 if (mProject != null) { 1307 mMediaLayout.editEffect(type, mediaItemId, startRect, endRect); 1308 } else { 1309 // Add this effect after you load the project 1310 mEditEffectMediaItemId = mediaItemId; 1311 mEditEffectType = type; 1312 mEditKenBurnsStartRect = startRect; 1313 mEditKenBurnsEndRect = endRect; 1314 } 1315 break; 1316 } 1317 1318 default: { 1319 break; 1320 } 1321 } 1322 } 1323 1324 /* 1325 * {@inheritDoc} 1326 */ 1327 public void surfaceCreated(SurfaceHolder holder) { 1328 if (Log.isLoggable(TAG, Log.DEBUG)) { 1329 Log.d(TAG, "surfaceCreated"); 1330 } 1331 1332 mPreviewThread = new PreviewThread(mSurfaceHolder); 1333 1334 restartPreview(); 1335 } 1336 1337 /* 1338 * {@inheritDoc} 1339 */ 1340 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 1341 if (Log.isLoggable(TAG, Log.DEBUG)) { 1342 Log.d(TAG, "surfaceChanged: " + width + "x" + height); 1343 } 1344 1345 if (mPreviewThread != null) { 1346 mPreviewThread.onSurfaceChanged(width, height); 1347 } 1348 } 1349 1350 /* 1351 * {@inheritDoc} 1352 */ 1353 public void surfaceDestroyed(SurfaceHolder holder) { 1354 if (Log.isLoggable(TAG, Log.DEBUG)) { 1355 Log.d(TAG, "surfaceDestroyed"); 1356 } 1357 1358 // Stop the preview playback if pending and quit the preview thread 1359 if (mPreviewThread != null) { 1360 mPreviewThread.stopPreviewPlayback(); 1361 mPreviewThread.quit(); 1362 mPreviewThread = null; 1363 } 1364 } 1365 1366 /* 1367 * {@inheritDoc} 1368 */ 1369 @Override 1370 protected void enterTransitionalState(int statusStringId) { 1371 mEditorProjectView.setVisibility(View.GONE); 1372 mEditorEmptyView.setVisibility(View.VISIBLE); 1373 1374 ((TextView)findViewById(R.id.empty_project_text)).setText(statusStringId); 1375 findViewById(R.id.empty_project_progress).setVisibility(View.VISIBLE); 1376 } 1377 1378 /* 1379 * {@inheritDoc} 1380 */ 1381 @Override 1382 protected void enterDisabledState(int statusStringId) { 1383 mEditorProjectView.setVisibility(View.GONE); 1384 mEditorEmptyView.setVisibility(View.VISIBLE); 1385 1386 getActionBar().setTitle(R.string.full_app_name); 1387 1388 ((TextView)findViewById(R.id.empty_project_text)).setText(statusStringId); 1389 findViewById(R.id.empty_project_progress).setVisibility(View.GONE); 1390 } 1391 1392 /* 1393 * {@inheritDoc} 1394 */ 1395 @Override 1396 protected void enterReadyState() { 1397 mEditorProjectView.setVisibility(View.VISIBLE); 1398 mEditorEmptyView.setVisibility(View.GONE); 1399 } 1400 1401 /* 1402 * {@inheritDoc} 1403 */ 1404 @Override 1405 protected boolean showPreviewFrame() { 1406 if (mPreviewThread == null) { // The surface is not ready 1407 return false; 1408 } 1409 1410 // Regenerate the preview frame 1411 if (mProject != null && !mPreviewThread.isPlaying() && mPendingExportFilename == null) { 1412 // Display the preview frame 1413 mPreviewThread.previewFrame(mProject, mProject.getPlayheadPos(), 1414 mProject.getMediaItemCount() == 0); 1415 } 1416 1417 return true; 1418 } 1419 1420 /* 1421 * {@inheritDoc} 1422 */ 1423 @Override 1424 protected void updateTimelineDuration() { 1425 if (mProject == null) { 1426 return; 1427 } 1428 1429 final long durationMs = mProject.computeDuration(); 1430 1431 // Resize the timeline according to the new timeline duration 1432 final int zoomWidth = mActivityWidth + timeToDimension(durationMs); 1433 final int childrenCount = mTimelineLayout.getChildCount(); 1434 for (int i = 0; i < childrenCount; i++) { 1435 final View child = mTimelineLayout.getChildAt(i); 1436 final ViewGroup.LayoutParams lp = child.getLayoutParams(); 1437 lp.width = zoomWidth; 1438 child.setLayoutParams(lp); 1439 } 1440 1441 mTimelineLayout.requestLayout(mLayoutCallback); 1442 1443 // Since the duration has changed make sure that the playhead 1444 // position is valid. 1445 if (mProject.getPlayheadPos() > durationMs) { 1446 movePlayhead(durationMs); 1447 } 1448 1449 mAudioTrackLayout.updateTimelineDuration(); 1450 } 1451 1452 /** 1453 * Convert the time to dimension 1454 * At zoom level 1: one activity width = 1200 seconds 1455 * At zoom level 2: one activity width = 600 seconds 1456 * ... 1457 * At zoom level 100: one activity width = 12 seconds 1458 * 1459 * At zoom level 1000: one activity width = 1.2 seconds 1460 * 1461 * @param durationMs The time 1462 * 1463 * @return The dimension 1464 */ 1465 private int timeToDimension(long durationMs) { 1466 return (int)((mProject.getZoomLevel() * mActivityWidth * durationMs) / 1200000); 1467 } 1468 1469 /** 1470 * Zoom the timeline 1471 * 1472 * @param level The zoom level 1473 * @param updateControl true to set the control position to match the 1474 * zoom level 1475 */ 1476 private int zoomTimeline(int level, boolean updateControl) { 1477 if (level < 1 || level > MAX_ZOOM_LEVEL) { 1478 return mProject.getZoomLevel(); 1479 } 1480 1481 mProject.setZoomLevel(level); 1482 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1483 Log.v(TAG, "zoomTimeline level: " + level + " -> " + timeToDimension(1000) + " pix/s"); 1484 } 1485 1486 updateTimelineDuration(); 1487 1488 if (updateControl) { 1489 mZoomControl.setProgress(level); 1490 } 1491 return level; 1492 } 1493 1494 /* 1495 * {@inheritDoc} 1496 */ 1497 @Override 1498 protected void movePlayhead(long timeMs) { 1499 if (mProject == null) { 1500 return; 1501 } 1502 1503 if (setPlayhead(timeMs)) { 1504 // Scroll the timeline such that the specified position 1505 // is in the center of the screen 1506 mTimelineScroller.appScrollTo(timeToDimension(timeMs), true); 1507 } 1508 } 1509 1510 /** 1511 * Set the playhead at the specified time position 1512 * 1513 * @param timeMs The time position 1514 * 1515 * @return true if the playhead was set at the specified time position 1516 */ 1517 private boolean setPlayhead(long timeMs) { 1518 // Check if the position would change 1519 if (mCurrentPlayheadPosMs == timeMs) { 1520 return false; 1521 } 1522 1523 // Check if the time is valid. Note that invalid values are common due 1524 // to overscrolling the timeline 1525 if (timeMs < 0) { 1526 return false; 1527 } else if (timeMs > mProject.computeDuration()) { 1528 return false; 1529 } 1530 1531 mCurrentPlayheadPosMs = timeMs; 1532 1533 mTimeView.setText(StringUtils.getTimestampAsString(this, timeMs)); 1534 mProject.setPlayheadPos(timeMs); 1535 return true; 1536 } 1537 1538 /* 1539 * {@inheritDoc} 1540 */ 1541 @Override 1542 protected void setAspectRatio(final int aspectRatio) { 1543 final FrameLayout.LayoutParams lp = 1544 (FrameLayout.LayoutParams)mSurfaceView.getLayoutParams(); 1545 1546 switch (aspectRatio) { 1547 case MediaProperties.ASPECT_RATIO_5_3: { 1548 lp.width = (lp.height * 5) / 3; 1549 break; 1550 } 1551 1552 case MediaProperties.ASPECT_RATIO_4_3: { 1553 lp.width = (lp.height * 4) / 3; 1554 break; 1555 } 1556 1557 case MediaProperties.ASPECT_RATIO_3_2: { 1558 lp.width = (lp.height * 3) / 2; 1559 break; 1560 } 1561 1562 case MediaProperties.ASPECT_RATIO_11_9: { 1563 lp.width = (lp.height * 11) / 9; 1564 break; 1565 } 1566 1567 case MediaProperties.ASPECT_RATIO_16_9: { 1568 lp.width = (lp.height * 16) / 9; 1569 break; 1570 } 1571 1572 default: { 1573 break; 1574 } 1575 } 1576 1577 if (Log.isLoggable(TAG, Log.DEBUG)) { 1578 Log.d(TAG, "setAspectRatio: " + aspectRatio + ", size: " + lp.width + "x" + lp.height); 1579 } 1580 mSurfaceView.setLayoutParams(lp); 1581 mOverlayView.setLayoutParams(lp); 1582 } 1583 1584 /* 1585 * {@inheritDoc} 1586 */ 1587 @Override 1588 protected MediaLinearLayout getMediaLayout() { 1589 return mMediaLayout; 1590 } 1591 1592 /* 1593 * {@inheritDoc} 1594 */ 1595 @Override 1596 protected OverlayLinearLayout getOverlayLayout() { 1597 return mOverlayLayout; 1598 } 1599 1600 /* 1601 * {@inheritDoc} 1602 */ 1603 @Override 1604 protected AudioTrackLinearLayout getAudioTrackLayout() { 1605 return mAudioTrackLayout; 1606 } 1607 1608 /* 1609 * {@inheritDoc} 1610 */ 1611 @Override 1612 protected void onExportProgress(int progress) { 1613 if (mExportProgressDialog != null) { 1614 mExportProgressDialog.setProgress(progress); 1615 } 1616 } 1617 1618 /* 1619 * {@inheritDoc} 1620 */ 1621 @Override 1622 protected void onExportComplete() { 1623 if (mExportProgressDialog != null) { 1624 mExportProgressDialog.dismiss(); 1625 mExportProgressDialog = null; 1626 } 1627 } 1628 1629 /* 1630 * {@inheritDoc} 1631 */ 1632 @Override 1633 protected void onProjectEditStateChange(boolean projectEdited) { 1634 if (Log.isLoggable(TAG, Log.DEBUG)) { 1635 Log.d(TAG, "onProjectEditStateChange: " + projectEdited); 1636 } 1637 1638 mPreviewPlayButton.setAlpha(projectEdited ? 100 : 255); 1639 mPreviewPlayButton.setEnabled(!projectEdited); 1640 mPreviewRewindButton.setEnabled(!projectEdited); 1641 mPreviewNextButton.setEnabled(!projectEdited); 1642 mPreviewPrevButton.setEnabled(!projectEdited); 1643 1644 mMediaLayout.invalidateCAB(); 1645 mOverlayLayout.invalidateCAB(); 1646 } 1647 1648 /* 1649 * {@inheritDoc} 1650 */ 1651 @Override 1652 protected void initializeFromProject(boolean updateUI) { 1653 if (Log.isLoggable(TAG, Log.DEBUG)) { 1654 Log.d(TAG, "Project was clean: " + mProject.isClean()); 1655 } 1656 1657 if (updateUI || !mProject.isClean()) { 1658 getActionBar().setTitle(mProject.getName()); 1659 1660 // Clear the media related to the previous project and 1661 // add the media for the current project. 1662 mMediaLayout.setProject(mProject); 1663 mOverlayLayout.setProject(mProject); 1664 mAudioTrackLayout.setProject(mProject); 1665 mPlayheadView.setProject(mProject); 1666 1667 // Add the media items to the media item layout 1668 mMediaLayout.addMediaItems(mProject.getMediaItems()); 1669 1670 // Add the media items to the overlay layout 1671 mOverlayLayout.addMediaItems(mProject.getMediaItems()); 1672 1673 // Add the audio tracks to the audio tracks layout 1674 mAudioTrackLayout.addAudioTracks(mProject.getAudioTracks()); 1675 1676 setAspectRatio(mProject.getAspectRatio()); 1677 } 1678 1679 updateTimelineDuration(); 1680 zoomTimeline(mProject.getZoomLevel(), true); 1681 1682 // Set the playhead position. We need to wait for the layout to 1683 // complete before we can scroll to the playhead position. 1684 final Handler handler = new Handler(); 1685 handler.post(new Runnable() { 1686 private final long DELAY = 100; 1687 private final int ATTEMPTS = 20; 1688 private int mAttempts = ATTEMPTS; 1689 1690 /* 1691 * {@inheritDoc} 1692 */ 1693 public void run() { 1694 if (mAttempts == ATTEMPTS) { // Only scroll once 1695 movePlayhead(mProject.getPlayheadPos()); 1696 } 1697 1698 // If the surface is not yet created (showPreviewFrame() 1699 // returns false) wait for a while (DELAY * ATTEMPTS). 1700 if (showPreviewFrame() == false && mAttempts >= 0) { 1701 mAttempts--; 1702 if (mAttempts >= 0) { 1703 handler.postDelayed(this, DELAY); 1704 } 1705 } 1706 } 1707 }); 1708 1709 if (mAddMediaItemVideoUri != null) { 1710 ApiService.addMediaItemVideoUri(this, mProjectPath, ApiService.generateId(), 1711 mInsertMediaItemAfterMediaItemId, 1712 mAddMediaItemVideoUri, MediaItem.RENDERING_MODE_BLACK_BORDER, 1713 mProject.getTheme()); 1714 mAddMediaItemVideoUri = null; 1715 mInsertMediaItemAfterMediaItemId = null; 1716 } 1717 1718 if (mAddMediaItemImageUri != null) { 1719 ApiService.addMediaItemImageUri(this, mProjectPath, ApiService.generateId(), 1720 mInsertMediaItemAfterMediaItemId, 1721 mAddMediaItemImageUri, MediaItem.RENDERING_MODE_BLACK_BORDER, 1722 MediaItemUtils.getDefaultImageDuration(), mProject.getTheme()); 1723 mAddMediaItemImageUri = null; 1724 mInsertMediaItemAfterMediaItemId = null; 1725 } 1726 1727 if (mAddAudioTrackUri != null) { 1728 ApiService.addAudioTrack(this, mProject.getPath(), ApiService.generateId(), 1729 mAddAudioTrackUri, true); 1730 mAddAudioTrackUri = null; 1731 } 1732 1733 if (mAddTransitionAfterMediaId != null) { 1734 mMediaLayout.addTransition(mAddTransitionAfterMediaId, mAddTransitionType, 1735 mAddTransitionDurationMs); 1736 mAddTransitionAfterMediaId = null; 1737 } 1738 1739 if (mEditTransitionId != null) { 1740 mMediaLayout.editTransition(mEditTransitionAfterMediaId, mEditTransitionId, 1741 mEditTransitionType, mEditTransitionDurationMs); 1742 mEditTransitionId = null; 1743 mEditTransitionAfterMediaId = null; 1744 } 1745 1746 if (mAddOverlayMediaItemId != null) { 1747 ApiService.addOverlay(this, mProject.getPath(), mAddOverlayMediaItemId, 1748 ApiService.generateId(), mAddOverlayUserAttributes, 0, 1749 OverlayLinearLayout.DEFAULT_TITLE_DURATION); 1750 mAddOverlayMediaItemId = null; 1751 mAddOverlayUserAttributes = null; 1752 } 1753 1754 if (mEditOverlayMediaItemId != null) { 1755 ApiService.setOverlayUserAttributes(this, mProject.getPath(), mEditOverlayMediaItemId, 1756 mEditOverlayId, mEditOverlayUserAttributes); 1757 mEditOverlayMediaItemId = null; 1758 mEditOverlayId = null; 1759 mEditOverlayUserAttributes = null; 1760 } 1761 1762 if (mAddEffectMediaItemId != null) { 1763 mMediaLayout.addEffect(mAddEffectType, mAddEffectMediaItemId, 1764 mAddKenBurnsStartRect, mAddKenBurnsEndRect); 1765 mAddEffectMediaItemId = null; 1766 } 1767 1768 if (mEditEffectMediaItemId != null) { 1769 mMediaLayout.editEffect(mEditEffectType, mEditEffectMediaItemId, 1770 mEditKenBurnsStartRect, mEditKenBurnsEndRect); 1771 mEditEffectMediaItemId = null; 1772 } 1773 1774 enterReadyState(); 1775 1776 if (mPendingExportFilename != null) { 1777 if (ApiService.isVideoEditorExportPending(mProjectPath, mPendingExportFilename)) { 1778 // The export is still pending 1779 // Display the export project dialog 1780 showExportProgress(); 1781 } else { 1782 // The export completed while the Activity was paused 1783 mPendingExportFilename = null; 1784 } 1785 } 1786 1787 invalidateOptionsMenu(); 1788 1789 restartPreview(); 1790 } 1791 1792 /** 1793 * Restart preview 1794 */ 1795 private void restartPreview() { 1796 if (mRestartPreview == false) { 1797 return; 1798 } 1799 1800 if (mProject == null) { 1801 return; 1802 } 1803 1804 if (mPreviewThread != null) { 1805 mRestartPreview = false; 1806 mPreviewThread.startPreviewPlayback(mProject, mProject.getPlayheadPos()); 1807 } 1808 } 1809 1810 /** 1811 * Show progress during export operation 1812 */ 1813 private void showExportProgress() { 1814 mExportProgressDialog = new ProgressDialog(this); 1815 mExportProgressDialog.setTitle(getString(R.string.export_dialog_export)); 1816 mExportProgressDialog.setMessage(null); 1817 mExportProgressDialog.setIndeterminate(false); 1818 mExportProgressDialog.setCancelable(true); 1819 mExportProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); 1820 mExportProgressDialog.setMax(100); 1821 mExportProgressDialog.setCanceledOnTouchOutside(false); 1822 mExportProgressDialog.setButton(getString(android.R.string.cancel), 1823 new DialogInterface.OnClickListener() { 1824 /* 1825 * {@inheritDoc} 1826 */ 1827 public void onClick(DialogInterface dialog, int which) { 1828 ApiService.cancelExportVideoEditor(VideoEditorActivity.this, 1829 mProjectPath, mPendingExportFilename); 1830 mPendingExportFilename = null; 1831 mExportProgressDialog = null; 1832 } 1833 }); 1834 mExportProgressDialog.setCanceledOnTouchOutside(true); 1835 mExportProgressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { 1836 /* 1837 * {@inheritDoc} 1838 */ 1839 public void onCancel(DialogInterface dialog) { 1840 ApiService.cancelExportVideoEditor(VideoEditorActivity.this, 1841 mProjectPath, mPendingExportFilename); 1842 mPendingExportFilename = null; 1843 mExportProgressDialog = null; 1844 } 1845 }); 1846 mExportProgressDialog.show(); 1847 mExportProgressDialog.setProgressNumberFormat(""); 1848 } 1849 1850 /** 1851 * The preview thread 1852 */ 1853 private class PreviewThread extends Thread { 1854 // Preview states 1855 private final int PREVIEW_STATE_STOPPED = 0; 1856 private final int PREVIEW_STATE_STARTING = 1; 1857 private final int PREVIEW_STATE_STARTED = 2; 1858 private final int PREVIEW_STATE_STOPPING = 3; 1859 1860 private final int OVERLAY_DATA_COUNT = 16; 1861 1862 private final Handler mMainHandler; 1863 private final Queue<Runnable> mQueue; 1864 private final SurfaceHolder mSurfaceHolder; 1865 private final Queue<VideoEditor.OverlayData> mOverlayDataQueue; 1866 private Handler mThreadHandler; 1867 private int mPreviewState; 1868 private Bitmap mOverlayBitmap; 1869 1870 private final Runnable mProcessQueueRunnable = new Runnable() { 1871 /* 1872 * {@inheritDoc} 1873 */ 1874 public void run() { 1875 // Process whatever accumulated in the queue 1876 Runnable runnable; 1877 while ((runnable = mQueue.poll()) != null) { 1878 runnable.run(); 1879 } 1880 } 1881 }; 1882 1883 /** 1884 * Constructor 1885 * 1886 * @param surfaceHolder The surface holder 1887 */ 1888 public PreviewThread(SurfaceHolder surfaceHolder) { 1889 mMainHandler = new Handler(Looper.getMainLooper()); 1890 mQueue = new LinkedBlockingQueue<Runnable>(); 1891 mSurfaceHolder = surfaceHolder; 1892 mPreviewState = PREVIEW_STATE_STOPPED; 1893 1894 mOverlayDataQueue = new LinkedBlockingQueue<VideoEditor.OverlayData>(); 1895 for (int i = 0; i < OVERLAY_DATA_COUNT; i++) { 1896 mOverlayDataQueue.add(new VideoEditor.OverlayData()); 1897 } 1898 1899 start(); 1900 } 1901 1902 /** 1903 * Preview the specified frame 1904 * 1905 * @param project The video editor project 1906 * @param timeMs The frame time 1907 * @param clear true to clear the output 1908 */ 1909 public void previewFrame(final VideoEditorProject project, final long timeMs, 1910 final boolean clear) { 1911 if (mPreviewState == PREVIEW_STATE_STARTING || mPreviewState == PREVIEW_STATE_STARTED) { 1912 stopPreviewPlayback(); 1913 } 1914 1915 if (Log.isLoggable(TAG, Log.DEBUG)) { 1916 Log.d(TAG, "Preview frame at: " + timeMs + " " + clear); 1917 } 1918 1919 // We only need to see the last frame 1920 mQueue.clear(); 1921 1922 mQueue.add(new Runnable() { 1923 /* 1924 * {@inheritDoc} 1925 */ 1926 public void run() { 1927 if (clear) { 1928 project.clearSurface(mSurfaceHolder); 1929 mMainHandler.post(new Runnable() { 1930 /* 1931 * {@inheritDoc} 1932 */ 1933 public void run() { 1934 if (mOverlayBitmap != null) { 1935 mOverlayBitmap.eraseColor(Color.TRANSPARENT); 1936 mOverlayView.invalidate(); 1937 } 1938 } 1939 }); 1940 } else { 1941 final VideoEditor.OverlayData overlayData; 1942 try { 1943 overlayData = mOverlayDataQueue.remove(); 1944 } catch (NoSuchElementException ex) { 1945 Log.e(TAG, "Out of OverlayData elements"); 1946 return; 1947 } 1948 1949 try { 1950 if (project.renderPreviewFrame(mSurfaceHolder, timeMs, overlayData) 1951 < 0) { 1952 if (Log.isLoggable(TAG, Log.DEBUG)) { 1953 Log.d(TAG, "Cannot render preview frame at: " + timeMs + 1954 " of " + mProject.computeDuration()); 1955 } 1956 1957 mOverlayDataQueue.add(overlayData); 1958 } else { 1959 if (overlayData.needsRendering()) { 1960 mMainHandler.post(new Runnable() { 1961 /* 1962 * {@inheritDoc} 1963 */ 1964 public void run() { 1965 if (mOverlayBitmap != null) { 1966 overlayData.renderOverlay(mOverlayBitmap); 1967 mOverlayView.invalidate(); 1968 } else { 1969 overlayData.release(); 1970 } 1971 1972 mOverlayDataQueue.add(overlayData); 1973 } 1974 }); 1975 } else { 1976 mOverlayDataQueue.add(overlayData); 1977 } 1978 } 1979 } catch (Exception ex) { 1980 if (Log.isLoggable(TAG, Log.DEBUG)) { 1981 Log.d(TAG, "renderPreviewFrame failed at timeMs: " + timeMs, ex); 1982 } 1983 mOverlayDataQueue.add(overlayData); 1984 } 1985 } 1986 } 1987 }); 1988 1989 if (mThreadHandler != null) { 1990 mThreadHandler.post(mProcessQueueRunnable); 1991 } 1992 } 1993 1994 /** 1995 * Display the frame at the specified time position 1996 * 1997 * @param mediaItem The media item 1998 * @param timeMs The frame time 1999 */ 2000 public void renderMediaItemFrame(final MovieMediaItem mediaItem, final long timeMs) { 2001 if (mPreviewState == PREVIEW_STATE_STARTING || mPreviewState == PREVIEW_STATE_STARTED) { 2002 stopPreviewPlayback(); 2003 } 2004 2005 if (Log.isLoggable(TAG, Log.VERBOSE)) { 2006 Log.v(TAG, "Render media item frame at: " + timeMs); 2007 } 2008 2009 // We only need to see the last frame 2010 mQueue.clear(); 2011 2012 mQueue.add(new Runnable() { 2013 /* 2014 * {@inheritDoc} 2015 */ 2016 public void run() { 2017 try { 2018 if (mProject.renderMediaItemFrame(mSurfaceHolder, mediaItem.getId(), 2019 timeMs) < 0) { 2020 if (Log.isLoggable(TAG, Log.DEBUG)) { 2021 Log.d(TAG, "Cannot render media item frame at: " + timeMs + 2022 " of " + mediaItem.getDuration()); 2023 } 2024 } 2025 } catch (Exception ex) { 2026 if (Log.isLoggable(TAG, Log.DEBUG)) { 2027 Log.d(TAG, "Cannot render preview frame at: " + timeMs, ex); 2028 } 2029 } 2030 } 2031 }); 2032 2033 if (mThreadHandler != null) { 2034 mThreadHandler.post(mProcessQueueRunnable); 2035 } 2036 } 2037 2038 /** 2039 * Start the preview playback 2040 * 2041 * @param project The video editor project 2042 * @param fromMs Start playing from the specified position 2043 */ 2044 private void startPreviewPlayback(final VideoEditorProject project, final long fromMs) { 2045 if (mPreviewState != PREVIEW_STATE_STOPPED) { 2046 if (Log.isLoggable(TAG, Log.DEBUG)) { 2047 Log.d(TAG, "Preview did not start: " + mPreviewState); 2048 } 2049 return; 2050 } 2051 2052 previewStarted(project); 2053 if (Log.isLoggable(TAG, Log.DEBUG)) { 2054 Log.d(TAG, "Start preview at: " + fromMs); 2055 } 2056 2057 // Clear any pending preview frames 2058 mQueue.clear(); 2059 mQueue.add(new Runnable() { 2060 /* 2061 * {@inheritDoc} 2062 */ 2063 public void run() { 2064 try { 2065 project.startPreview(mSurfaceHolder, fromMs, -1, false, 3, 2066 new VideoEditor.PreviewProgressListener() { 2067 /* 2068 * {@inheritDoc} 2069 */ 2070 public void onStart(VideoEditor videoEditor) { 2071 } 2072 2073 /* 2074 * {@inheritDoc} 2075 */ 2076 public void onProgress(VideoEditor videoEditor, final long timeMs, 2077 final VideoEditor.OverlayData overlayData) { 2078 mMainHandler.post(new Runnable() { 2079 /* 2080 * {@inheritDoc} 2081 */ 2082 public void run() { 2083 if (overlayData != null && overlayData.needsRendering()) { 2084 if (mOverlayBitmap != null) { 2085 overlayData.renderOverlay(mOverlayBitmap); 2086 mOverlayView.invalidate(); 2087 } else { 2088 overlayData.release(); 2089 } 2090 } 2091 2092 if (mPreviewState == PREVIEW_STATE_STARTED || 2093 mPreviewState == PREVIEW_STATE_STOPPING) { 2094 movePlayhead(timeMs); 2095 } 2096 } 2097 }); 2098 } 2099 2100 /* 2101 * {@inheritDoc} 2102 */ 2103 public void onStop(VideoEditor videoEditor) { 2104 mMainHandler.post(new Runnable() { 2105 /* 2106 * {@inheritDoc} 2107 */ 2108 public void run() { 2109 if (mPreviewState == PREVIEW_STATE_STARTED || 2110 mPreviewState == PREVIEW_STATE_STOPPING) { 2111 previewStopped(false); 2112 } 2113 } 2114 }); 2115 } 2116 }); 2117 2118 mMainHandler.post(new Runnable() { 2119 /* 2120 * {@inheritDoc} 2121 */ 2122 public void run() { 2123 mPreviewState = PREVIEW_STATE_STARTED; 2124 } 2125 }); 2126 } catch (Exception ex) { 2127 // This exception may occur when trying to play frames 2128 // at the end of the timeline 2129 // (e.g. when fromMs == clip duration) 2130 if (Log.isLoggable(TAG, Log.DEBUG)) { 2131 Log.d(TAG, "Cannot start preview at: " + fromMs, ex); 2132 } 2133 2134 mMainHandler.post(new Runnable() { 2135 /* 2136 * {@inheritDoc} 2137 */ 2138 public void run() { 2139 mPreviewState = PREVIEW_STATE_STARTED; 2140 previewStopped(true); 2141 } 2142 }); 2143 } 2144 } 2145 }); 2146 2147 if (mThreadHandler != null) { 2148 mThreadHandler.post(mProcessQueueRunnable); 2149 } 2150 } 2151 2152 /** 2153 * The preview started. 2154 * This method is always invoked from the UI thread. 2155 * 2156 * @param project The project 2157 */ 2158 private void previewStarted(VideoEditorProject project) { 2159 // Change the button image back to a play icon 2160 mPreviewPlayButton.setImageResource(R.drawable.btn_playback_pause_selector); 2161 2162 mTimelineScroller.enableUserScrolling(false); 2163 mMediaLayout.setPlaybackInProgress(true); 2164 mOverlayLayout.setPlaybackInProgress(true); 2165 mAudioTrackLayout.setPlaybackInProgress(true); 2166 2167 mPreviewState = PREVIEW_STATE_STARTING; 2168 } 2169 2170 /** 2171 * Stop previewing 2172 */ 2173 private void stopPreviewPlayback() { 2174 switch (mPreviewState) { 2175 case PREVIEW_STATE_STOPPED: { 2176 if (Log.isLoggable(TAG, Log.DEBUG)) { 2177 Log.d(TAG, "stopPreviewPlayback: State was PREVIEW_STATE_STOPPED"); 2178 } 2179 return; 2180 } 2181 2182 case PREVIEW_STATE_STOPPING: { 2183 if (Log.isLoggable(TAG, Log.DEBUG)) { 2184 Log.d(TAG, "stopPreviewPlayback: State was PREVIEW_STATE_STOPPING"); 2185 } 2186 return; 2187 } 2188 2189 case PREVIEW_STATE_STARTING: { 2190 if (Log.isLoggable(TAG, Log.DEBUG)) { 2191 Log.d(TAG, "stopPreviewPlayback: State was PREVIEW_STATE_STARTING " + 2192 "now PREVIEW_STATE_STOPPING"); 2193 } 2194 2195 mPreviewState = PREVIEW_STATE_STOPPING; 2196 2197 // We need to wait until the preview starts 2198 mMainHandler.postDelayed(new Runnable() { 2199 /* 2200 * {@inheritDoc} 2201 */ 2202 public void run() { 2203 if (isFinishing() || isChangingConfigurations()) { 2204 // The activity is shutting down. Force stopping now. 2205 if (Log.isLoggable(TAG, Log.DEBUG)) { 2206 Log.d(TAG, "stopPreviewPlayback: Activity is shutting down"); 2207 } 2208 2209 mPreviewState = PREVIEW_STATE_STARTED; 2210 previewStopped(true); 2211 } else if (mPreviewState == PREVIEW_STATE_STARTED) { 2212 if (Log.isLoggable(TAG, Log.DEBUG)) { 2213 Log.d(TAG, "stopPreviewPlayback: Now PREVIEW_STATE_STARTED"); 2214 } 2215 2216 previewStopped(false); 2217 } else if (mPreviewState == PREVIEW_STATE_STOPPING) { 2218 // Keep waiting 2219 mMainHandler.postDelayed(this, 100); 2220 2221 if (Log.isLoggable(TAG, Log.DEBUG)) { 2222 Log.d(TAG, "stopPreviewPlayback: Waiting for PREVIEW_STATE_STARTED"); 2223 } 2224 } else { 2225 if (Log.isLoggable(TAG, Log.DEBUG)) { 2226 Log.d(TAG, "stopPreviewPlayback: PREVIEW_STATE_STOPPED while waiting"); 2227 } 2228 } 2229 } 2230 }, 50); 2231 2232 break; 2233 } 2234 2235 case PREVIEW_STATE_STARTED: { 2236 if (Log.isLoggable(TAG, Log.DEBUG)) { 2237 Log.d(TAG, "stopPreviewPlayback: State was PREVIEW_STATE_STARTED"); 2238 } 2239 2240 // We need to stop 2241 previewStopped(false); 2242 return; 2243 } 2244 2245 default: { 2246 throw new IllegalArgumentException("stopPreviewPlayback state: " + 2247 mPreviewState); 2248 } 2249 } 2250 } 2251 2252 /** 2253 * The surface size has changed 2254 * 2255 * @param width The new surface width 2256 * @param heightThe new surface height 2257 */ 2258 private void onSurfaceChanged(int width, int height) { 2259 if (mOverlayBitmap != null) { 2260 if (mOverlayBitmap.getWidth() == width && mOverlayBitmap.getHeight() == height) { 2261 // The size has not changed 2262 return; 2263 } 2264 2265 mOverlayView.setImageBitmap(null); 2266 mOverlayBitmap.recycle(); 2267 mOverlayBitmap = null; 2268 } 2269 2270 // Create the overlay bitmap 2271 if (Log.isLoggable(TAG, Log.DEBUG)) { 2272 Log.d(TAG, "Overlay size: " + width + " x " + height); 2273 } 2274 2275 mOverlayBitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888); 2276 mOverlayView.setImageBitmap(mOverlayBitmap); 2277 } 2278 2279 /** 2280 * Preview stopped. This method is always invoked from the UI thread. 2281 * 2282 * @param error true if the preview stopped due to an error 2283 * 2284 * @return The stop position 2285 */ 2286 private void previewStopped(boolean error) { 2287 if (mProject == null) { 2288 Log.w(TAG, "previewStopped: project was deleted."); 2289 return; 2290 } 2291 2292 if (mPreviewState != PREVIEW_STATE_STARTED) { 2293 throw new IllegalStateException("previewStopped in state: " + mPreviewState); 2294 } 2295 2296 // Change the button image back to a play icon 2297 mPreviewPlayButton.setImageResource(R.drawable.btn_playback_play_selector); 2298 2299 if (error == false) { 2300 // Set the playhead position at the position where the playback stopped 2301 final long stopTimeMs = mProject.stopPreview(); 2302 movePlayhead(stopTimeMs); 2303 if (Log.isLoggable(TAG, Log.DEBUG)) { 2304 Log.d(TAG, "PREVIEW_STATE_STOPPED: " + stopTimeMs); 2305 } 2306 } else { 2307 if (Log.isLoggable(TAG, Log.DEBUG)) { 2308 Log.d(TAG, "PREVIEW_STATE_STOPPED due to error"); 2309 } 2310 } 2311 2312 mPreviewState = PREVIEW_STATE_STOPPED; 2313 2314 // The playback has stopped 2315 mTimelineScroller.enableUserScrolling(true); 2316 mMediaLayout.setPlaybackInProgress(false); 2317 mAudioTrackLayout.setPlaybackInProgress(false); 2318 mOverlayLayout.setPlaybackInProgress(false); 2319 } 2320 2321 /** 2322 * @return true if preview playback is in progress 2323 */ 2324 private boolean isPlaying() { 2325 return mPreviewState == PREVIEW_STATE_STARTING || 2326 mPreviewState == PREVIEW_STATE_STARTED; 2327 } 2328 2329 /** 2330 * @return true if the preview is stopped 2331 */ 2332 private boolean isStopped() { 2333 return mPreviewState == PREVIEW_STATE_STOPPED; 2334 } 2335 2336 /* 2337 * {@inheritDoc} 2338 */ 2339 @Override 2340 public void run() { 2341 setPriority(MAX_PRIORITY); 2342 Looper.prepare(); 2343 mThreadHandler = new Handler(); 2344 2345 // Ensure that the queued items are processed 2346 mMainHandler.post(new Runnable() { 2347 /* 2348 * {@inheritDoc} 2349 */ 2350 public void run() { 2351 // Start processing the queue of runnables 2352 mThreadHandler.post(mProcessQueueRunnable); 2353 } 2354 }); 2355 2356 // Run the loop 2357 Looper.loop(); 2358 } 2359 2360 /** 2361 * Quit the thread 2362 */ 2363 public void quit() { 2364 // Release the overlay bitmap 2365 if (mOverlayBitmap != null) { 2366 mOverlayView.setImageBitmap(null); 2367 mOverlayBitmap.recycle(); 2368 mOverlayBitmap = null; 2369 } 2370 2371 if (mThreadHandler != null) { 2372 mThreadHandler.getLooper().quit(); 2373 try { 2374 // Wait for the thread to quit. An ANR waiting to happen. 2375 mThreadHandler.getLooper().getThread().join(); 2376 } catch (InterruptedException ex) { 2377 } 2378 } 2379 2380 mQueue.clear(); 2381 } 2382 } 2383} 2384