VideoEditorImpl.java revision 731e46575aeffa26b41d7590a0a4de637d792258
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 android.media.videoeditor; 18 19import java.io.File; 20import java.io.FileInputStream; 21import java.io.FileNotFoundException; 22import java.io.FileOutputStream; 23import java.io.IOException; 24import java.io.StringWriter; 25import java.util.ArrayList; 26import java.util.Iterator; 27import java.util.List; 28import java.util.Map; 29 30import org.xmlpull.v1.XmlPullParser; 31import org.xmlpull.v1.XmlPullParserException; 32import org.xmlpull.v1.XmlSerializer; 33 34import android.graphics.Rect; 35import android.util.Log; 36import android.util.Xml; 37import android.view.SurfaceHolder; 38 39/** 40 * The VideoEditor implementation {@hide} 41 */ 42public class VideoEditorImpl implements VideoEditor { 43 // Logging 44 private static final String TAG = "VideoEditorImpl"; 45 46 // The project filename 47 private static final String PROJECT_FILENAME = "videoeditor.xml"; 48 49 // XML tags 50 private static final String TAG_PROJECT = "project"; 51 private static final String TAG_MEDIA_ITEMS = "media_items"; 52 private static final String TAG_MEDIA_ITEM = "media_item"; 53 private static final String TAG_TRANSITIONS = "transitions"; 54 private static final String TAG_TRANSITION = "transition"; 55 private static final String TAG_OVERLAYS = "overlays"; 56 private static final String TAG_OVERLAY = "overlay"; 57 private static final String TAG_OVERLAY_USER_ATTRIBUTES = "overlay_user_attributes"; 58 private static final String TAG_EFFECTS = "effects"; 59 private static final String TAG_EFFECT = "effect"; 60 private static final String TAG_AUDIO_TRACKS = "audio_tracks"; 61 private static final String TAG_AUDIO_TRACK = "audio_track"; 62 63 private static final String ATTR_ID = "id"; 64 private static final String ATTR_FILENAME = "filename"; 65 private static final String ATTR_AUDIO_WAVEFORM_FILENAME = "wavefoem"; 66 private static final String ATTR_RENDERING_MODE = "rendering_mode"; 67 private static final String ATTR_ASPECT_RATIO = "aspect_ratio"; 68 private static final String ATTR_TYPE = "type"; 69 private static final String ATTR_DURATION = "duration"; 70 private static final String ATTR_START_TIME = "start_time"; 71 private static final String ATTR_BEGIN_TIME = "begin_time"; 72 private static final String ATTR_END_TIME = "end_time"; 73 private static final String ATTR_VOLUME = "volume"; 74 private static final String ATTR_BEHAVIOR = "behavior"; 75 private static final String ATTR_DIRECTION = "direction"; 76 private static final String ATTR_BLENDING = "blending"; 77 private static final String ATTR_INVERT = "invert"; 78 private static final String ATTR_MASK = "mask"; 79 private static final String ATTR_BEFORE_MEDIA_ITEM_ID = "before_media_item"; 80 private static final String ATTR_AFTER_MEDIA_ITEM_ID = "after_media_item"; 81 private static final String ATTR_COLOR_EFFECT_TYPE = "color_type"; 82 private static final String ATTR_COLOR_EFFECT_VALUE = "color_value"; 83 private static final String ATTR_START_RECT_L = "start_l"; 84 private static final String ATTR_START_RECT_T = "start_t"; 85 private static final String ATTR_START_RECT_R = "start_r"; 86 private static final String ATTR_START_RECT_B = "start_b"; 87 private static final String ATTR_END_RECT_L = "end_l"; 88 private static final String ATTR_END_RECT_T = "end_t"; 89 private static final String ATTR_END_RECT_R = "end_r"; 90 private static final String ATTR_END_RECT_B = "end_b"; 91 private static final String ATTR_LOOP = "loop"; 92 private static final String ATTR_MUTED = "muted"; 93 private static final String ATTR_DUCK_ENABLED = "ducking_enabled"; 94 private static final String ATTR_DUCK_THRESHOLD = "ducking_threshold"; 95 private static final String ATTR_DUCKED_TRACK_VOLUME = "ducking_volume"; 96 97 // Instance variables 98 private long mDurationMs; 99 private final String mProjectPath; 100 private final List<MediaItem> mMediaItems = new ArrayList<MediaItem>(); 101 private final List<AudioTrack> mAudioTracks = new ArrayList<AudioTrack>(); 102 private final List<Transition> mTransitions = new ArrayList<Transition>(); 103 private PreviewThread mPreviewThread; 104 private int mAspectRatio; 105 106 /** 107 * The preview thread 108 */ 109 private class PreviewThread extends Thread { 110 // Instance variables 111 private final static long FRAME_DURATION = 33; 112 113 // Instance variables 114 private final PreviewProgressListener mListener; 115 private final int mCallbackAfterFrameCount; 116 private final long mFromMs, mToMs; 117 private boolean mRun, mLoop; 118 private long mPositionMs; 119 120 /** 121 * Constructor 122 * 123 * @param fromMs Start preview at this position 124 * @param toMs The time (relative to the timeline) at which the preview 125 * will stop. Use -1 to play to the end of the timeline 126 * @param callbackAfterFrameCount The listener interface should be 127 * invoked after the number of frames specified by this 128 * parameter. 129 * @param loop true if the preview should be looped once it reaches the 130 * end 131 * @param listener The listener 132 */ 133 public PreviewThread(long fromMs, long toMs, boolean loop, int callbackAfterFrameCount, 134 PreviewProgressListener listener) { 135 mPositionMs = mFromMs = fromMs; 136 if (toMs < 0) { 137 mToMs = mDurationMs; 138 } else { 139 mToMs = toMs; 140 } 141 mLoop = loop; 142 mCallbackAfterFrameCount = callbackAfterFrameCount; 143 mListener = listener; 144 mRun = true; 145 } 146 147 /* 148 * {@inheritDoc} 149 */ 150 @Override 151 public void run() { 152 if (Log.isLoggable(TAG, Log.DEBUG)) { 153 Log.d(TAG, "===> PreviewThread.run enter"); 154 } 155 int frameCount = 0; 156 while (mRun) { 157 try { 158 sleep(FRAME_DURATION); 159 } catch (InterruptedException ex) { 160 break; 161 } 162 frameCount++; 163 mPositionMs += FRAME_DURATION; 164 165 if (mPositionMs >= mToMs) { 166 if (!mLoop) { 167 if (mListener != null) { 168 mListener.onProgress(VideoEditorImpl.this, mPositionMs, true); 169 } 170 if (Log.isLoggable(TAG, Log.DEBUG)) { 171 Log.d(TAG, "PreviewThread.run playback complete"); 172 } 173 break; 174 } else { 175 // Fire a notification for the end of the clip 176 if (mListener != null) { 177 mListener.onProgress(VideoEditorImpl.this, mToMs, false); 178 } 179 180 // Rewind 181 mPositionMs = mFromMs; 182 if (mListener != null) { 183 mListener.onProgress(VideoEditorImpl.this, mPositionMs, false); 184 } 185 if (Log.isLoggable(TAG, Log.DEBUG)) { 186 Log.d(TAG, "PreviewThread.run playback complete"); 187 } 188 frameCount = 0; 189 } 190 } else { 191 if (frameCount == mCallbackAfterFrameCount) { 192 if (mListener != null) { 193 mListener.onProgress(VideoEditorImpl.this, mPositionMs, false); 194 } 195 frameCount = 0; 196 } 197 } 198 } 199 200 if (Log.isLoggable(TAG, Log.DEBUG)) { 201 Log.d(TAG, "===> PreviewThread.run exit"); 202 } 203 } 204 205 /** 206 * Stop the preview 207 * 208 * @return The stop position 209 */ 210 public long stopPreview() { 211 mRun = false; 212 try { 213 join(); 214 } catch (InterruptedException ex) { 215 } 216 return mPositionMs; 217 } 218 }; 219 220 /** 221 * Constructor 222 * 223 * @param projectPath 224 */ 225 public VideoEditorImpl(String projectPath) throws IOException { 226 mProjectPath = projectPath; 227 final File projectXml = new File(projectPath, PROJECT_FILENAME); 228 if (projectXml.exists()) { 229 try { 230 load(); 231 } catch (Exception ex) { 232 throw new IOException(ex); 233 } 234 } else { 235 mAspectRatio = MediaProperties.ASPECT_RATIO_16_9; 236 mDurationMs = 0; 237 } 238 } 239 240 /* 241 * {@inheritDoc} 242 */ 243 public String getPath() { 244 return mProjectPath; 245 } 246 247 /* 248 * {@inheritDoc} 249 */ 250 public synchronized void addMediaItem(MediaItem mediaItem) { 251 if (mPreviewThread != null) { 252 throw new IllegalStateException("Previewing is in progress"); 253 } 254 255 if (mMediaItems.contains(mediaItem)) { 256 throw new IllegalArgumentException("Media item already exists: " + mediaItem.getId()); 257 } 258 259 // Invalidate the end transition if necessary 260 final int mediaItemsCount = mMediaItems.size(); 261 if ( mediaItemsCount > 0) { 262 removeTransitionAfter(mediaItemsCount - 1); 263 } 264 265 // Add the new media item 266 mMediaItems.add(mediaItem); 267 268 computeTimelineDuration(); 269 } 270 271 /* 272 * {@inheritDoc} 273 */ 274 public synchronized void insertMediaItem(MediaItem mediaItem, String afterMediaItemId) { 275 if (mPreviewThread != null) { 276 throw new IllegalStateException("Previewing is in progress"); 277 } 278 279 if (mMediaItems.contains(mediaItem)) { 280 throw new IllegalArgumentException("Media item already exists: " + mediaItem.getId()); 281 } 282 283 if (afterMediaItemId == null) { 284 if (mMediaItems.size() > 0) { 285 // Invalidate the transition at the beginning of the timeline 286 removeTransitionBefore(0); 287 } 288 mMediaItems.add(0, mediaItem); 289 computeTimelineDuration(); 290 } else { 291 final int mediaItemCount = mMediaItems.size(); 292 for (int i = 0; i < mediaItemCount; i++) { 293 final MediaItem mi = mMediaItems.get(i); 294 if (mi.getId().equals(afterMediaItemId)) { 295 // Invalidate the transition at this position 296 removeTransitionAfter(i); 297 // Insert the new media item 298 mMediaItems.add(i + 1, mediaItem); 299 computeTimelineDuration(); 300 return; 301 } 302 } 303 throw new IllegalArgumentException("MediaItem not found: " + afterMediaItemId); 304 } 305 } 306 307 /* 308 * {@inheritDoc} 309 */ 310 public synchronized void moveMediaItem(String mediaItemId, String afterMediaItemId) { 311 if (mPreviewThread != null) { 312 throw new IllegalStateException("Previewing is in progress"); 313 } 314 315 final MediaItem moveMediaItem = removeMediaItem(mediaItemId); 316 if (moveMediaItem == null) { 317 throw new IllegalArgumentException("Target MediaItem not found: " + mediaItemId); 318 } 319 320 if (afterMediaItemId == null) { 321 if (mMediaItems.size() > 0) { 322 // Invalidate adjacent transitions at the insertion point 323 removeTransitionBefore(0); 324 325 // Insert the media item at the new position 326 mMediaItems.add(0, moveMediaItem); 327 computeTimelineDuration(); 328 } else { 329 throw new IllegalStateException("Cannot move media item (it is the only item)"); 330 } 331 } else { 332 final int mediaItemCount = mMediaItems.size(); 333 for (int i = 0; i < mediaItemCount; i++) { 334 final MediaItem mi = mMediaItems.get(i); 335 if (mi.getId().equals(afterMediaItemId)) { 336 // Invalidate adjacent transitions at the insertion point 337 removeTransitionAfter(i); 338 // Insert the media item at the new position 339 mMediaItems.add(i + 1, moveMediaItem); 340 computeTimelineDuration(); 341 return; 342 } 343 } 344 345 throw new IllegalArgumentException("MediaItem not found: " + afterMediaItemId); 346 } 347 } 348 349 /* 350 * {@inheritDoc} 351 */ 352 public synchronized MediaItem removeMediaItem(String mediaItemId) { 353 if (mPreviewThread != null) { 354 throw new IllegalStateException("Previewing is in progress"); 355 } 356 357 final MediaItem mediaItem = getMediaItem(mediaItemId); 358 if (mediaItem != null) { 359 // Remove the media item 360 mMediaItems.remove(mediaItem); 361 // Remove the adjacent transitions 362 removeAdjacentTransitions(mediaItem); 363 computeTimelineDuration(); 364 } 365 366 return mediaItem; 367 } 368 369 /* 370 * {@inheritDoc} 371 */ 372 public synchronized MediaItem getMediaItem(String mediaItemId) { 373 for (MediaItem mediaItem : mMediaItems) { 374 if (mediaItem.getId().equals(mediaItemId)) { 375 return mediaItem; 376 } 377 } 378 379 return null; 380 } 381 382 /* 383 * {@inheritDoc} 384 */ 385 public synchronized List<MediaItem> getAllMediaItems() { 386 return mMediaItems; 387 } 388 389 /* 390 * {@inheritDoc} 391 */ 392 public synchronized void removeAllMediaItems() { 393 mMediaItems.clear(); 394 395 // Invalidate all transitions 396 for (Transition transition : mTransitions) { 397 transition.invalidate(); 398 } 399 mTransitions.clear(); 400 401 mDurationMs = 0; 402 } 403 404 /* 405 * {@inheritDoc} 406 */ 407 public synchronized void addTransition(Transition transition) { 408 mTransitions.add(transition); 409 410 final MediaItem beforeMediaItem = transition.getBeforeMediaItem(); 411 final MediaItem afterMediaItem = transition.getAfterMediaItem(); 412 413 // Cross reference the transitions 414 if (afterMediaItem != null) { 415 // If a transition already exists at the specified position then 416 // invalidate it. 417 if (afterMediaItem.getEndTransition() != null) { 418 afterMediaItem.getEndTransition().invalidate(); 419 } 420 afterMediaItem.setEndTransition(transition); 421 } 422 423 if (beforeMediaItem != null) { 424 // If a transition already exists at the specified position then 425 // invalidate it. 426 if (beforeMediaItem.getBeginTransition() != null) { 427 beforeMediaItem.getBeginTransition().invalidate(); 428 } 429 beforeMediaItem.setBeginTransition(transition); 430 } 431 432 computeTimelineDuration(); 433 } 434 435 /* 436 * {@inheritDoc} 437 */ 438 public synchronized Transition removeTransition(String transitionId) { 439 if (mPreviewThread != null) { 440 throw new IllegalStateException("Previewing is in progress"); 441 } 442 443 final Transition transition = getTransition(transitionId); 444 if (transition == null) { 445 throw new IllegalStateException("Transition not found: " + transitionId); 446 } 447 448 // Remove the transition references 449 final MediaItem afterMediaItem = transition.getAfterMediaItem(); 450 if (afterMediaItem != null) { 451 afterMediaItem.setEndTransition(null); 452 } 453 454 final MediaItem beforeMediaItem = transition.getBeforeMediaItem(); 455 if (beforeMediaItem != null) { 456 beforeMediaItem.setBeginTransition(null); 457 } 458 459 mTransitions.remove(transition); 460 transition.invalidate(); 461 computeTimelineDuration(); 462 463 return transition; 464 } 465 466 /* 467 * {@inheritDoc} 468 */ 469 public List<Transition> getAllTransitions() { 470 return mTransitions; 471 } 472 473 /* 474 * {@inheritDoc} 475 */ 476 public Transition getTransition(String transitionId) { 477 for (Transition transition : mTransitions) { 478 if (transition.getId().equals(transitionId)) { 479 return transition; 480 } 481 } 482 483 return null; 484 } 485 486 /* 487 * {@inheritDoc} 488 */ 489 public synchronized void addAudioTrack(AudioTrack audioTrack) { 490 if (mPreviewThread != null) { 491 throw new IllegalStateException("Previewing is in progress"); 492 } 493 494 mAudioTracks.add(audioTrack); 495 } 496 497 /* 498 * {@inheritDoc} 499 */ 500 public synchronized void insertAudioTrack(AudioTrack audioTrack, String afterAudioTrackId) { 501 if (mPreviewThread != null) { 502 throw new IllegalStateException("Previewing is in progress"); 503 } 504 505 if (afterAudioTrackId == null) { 506 mAudioTracks.add(0, audioTrack); 507 } else { 508 final int audioTrackCount = mAudioTracks.size(); 509 for (int i = 0; i < audioTrackCount; i++) { 510 AudioTrack at = mAudioTracks.get(i); 511 if (at.getId().equals(afterAudioTrackId)) { 512 mAudioTracks.add(i + 1, audioTrack); 513 return; 514 } 515 } 516 517 throw new IllegalArgumentException("AudioTrack not found: " + afterAudioTrackId); 518 } 519 } 520 521 /* 522 * {@inheritDoc} 523 */ 524 public synchronized void moveAudioTrack(String audioTrackId, String afterAudioTrackId) { 525 throw new IllegalStateException("Not supported"); 526 } 527 528 /* 529 * {@inheritDoc} 530 */ 531 public synchronized AudioTrack removeAudioTrack(String audioTrackId) { 532 if (mPreviewThread != null) { 533 throw new IllegalStateException("Previewing is in progress"); 534 } 535 536 final AudioTrack audioTrack = getAudioTrack(audioTrackId); 537 if (audioTrack != null) { 538 mAudioTracks.remove(audioTrack); 539 } 540 541 return audioTrack; 542 } 543 544 /* 545 * {@inheritDoc} 546 */ 547 public AudioTrack getAudioTrack(String audioTrackId) { 548 for (AudioTrack at : mAudioTracks) { 549 if (at.getId().equals(audioTrackId)) { 550 return at; 551 } 552 } 553 554 return null; 555 } 556 557 /* 558 * {@inheritDoc} 559 */ 560 public List<AudioTrack> getAllAudioTracks() { 561 return mAudioTracks; 562 } 563 564 /* 565 * {@inheritDoc} 566 */ 567 public void save() throws IOException { 568 final XmlSerializer serializer = Xml.newSerializer(); 569 final StringWriter writer = new StringWriter(); 570 serializer.setOutput(writer); 571 serializer.startDocument("UTF-8", true); 572 serializer.startTag("", TAG_PROJECT); 573 serializer.attribute("", ATTR_ASPECT_RATIO, Integer.toString(mAspectRatio)); 574 575 serializer.startTag("", TAG_MEDIA_ITEMS); 576 for (MediaItem mediaItem : mMediaItems) { 577 serializer.startTag("", TAG_MEDIA_ITEM); 578 serializer.attribute("", ATTR_ID, mediaItem.getId()); 579 serializer.attribute("", ATTR_TYPE, mediaItem.getClass().getSimpleName()); 580 serializer.attribute("", ATTR_FILENAME, mediaItem.getFilename()); 581 serializer.attribute("", ATTR_RENDERING_MODE, Integer.toString( 582 mediaItem.getRenderingMode())); 583 if (mediaItem instanceof MediaVideoItem) { 584 final MediaVideoItem mvi = (MediaVideoItem)mediaItem; 585 serializer 586 .attribute("", ATTR_BEGIN_TIME, Long.toString(mvi.getBoundaryBeginTime())); 587 serializer.attribute("", ATTR_END_TIME, Long.toString(mvi.getBoundaryEndTime())); 588 serializer.attribute("", ATTR_VOLUME, Integer.toString(mvi.getVolume())); 589 serializer.attribute("", ATTR_MUTED, Boolean.toString(mvi.isMuted())); 590 if (mvi.getAudioWaveformFilename() != null) { 591 serializer.attribute("", ATTR_AUDIO_WAVEFORM_FILENAME, 592 mvi.getAudioWaveformFilename()); 593 } 594 } else if (mediaItem instanceof MediaImageItem) { 595 serializer.attribute("", ATTR_DURATION, 596 Long.toString(mediaItem.getTimelineDuration())); 597 } 598 599 final List<Overlay> overlays = mediaItem.getAllOverlays(); 600 if (overlays.size() > 0) { 601 serializer.startTag("", TAG_OVERLAYS); 602 for (Overlay overlay : overlays) { 603 serializer.startTag("", TAG_OVERLAY); 604 serializer.attribute("", ATTR_ID, overlay.getId()); 605 serializer.attribute("", ATTR_TYPE, overlay.getClass().getSimpleName()); 606 serializer.attribute("", ATTR_BEGIN_TIME, 607 Long.toString(overlay.getStartTime())); 608 serializer.attribute("", ATTR_DURATION, Long.toString(overlay.getDuration())); 609 if (overlay instanceof OverlayFrame) { 610 final OverlayFrame overlayFrame = (OverlayFrame)overlay; 611 overlayFrame.save(getPath()); 612 if (overlayFrame.getFilename() != null) { 613 serializer.attribute("", ATTR_FILENAME, overlayFrame.getFilename()); 614 } 615 } 616 617 // Save the user attributes 618 serializer.startTag("", TAG_OVERLAY_USER_ATTRIBUTES); 619 final Map<String, String> userAttributes = overlay.getUserAttributes(); 620 for (String name : userAttributes.keySet()) { 621 final String value = userAttributes.get(name); 622 if (value != null) { 623 serializer.attribute("", name, value); 624 } 625 } 626 serializer.endTag("", TAG_OVERLAY_USER_ATTRIBUTES); 627 628 serializer.endTag("", TAG_OVERLAY); 629 } 630 serializer.endTag("", TAG_OVERLAYS); 631 } 632 633 final List<Effect> effects = mediaItem.getAllEffects(); 634 if (effects.size() > 0) { 635 serializer.startTag("", TAG_EFFECTS); 636 for (Effect effect : effects) { 637 serializer.startTag("", TAG_EFFECT); 638 serializer.attribute("", ATTR_ID, effect.getId()); 639 serializer.attribute("", ATTR_TYPE, effect.getClass().getSimpleName()); 640 serializer.attribute("", ATTR_BEGIN_TIME, 641 Long.toString(effect.getStartTime())); 642 serializer.attribute("", ATTR_DURATION, Long.toString(effect.getDuration())); 643 if (effect instanceof EffectColor) { 644 final EffectColor colorEffect = (EffectColor)effect; 645 serializer.attribute("", ATTR_COLOR_EFFECT_TYPE, 646 Integer.toString(colorEffect.getType())); 647 if (colorEffect.getType() == EffectColor.TYPE_COLOR || 648 colorEffect.getType() == EffectColor.TYPE_GRADIENT) { 649 serializer.attribute("", ATTR_COLOR_EFFECT_VALUE, 650 Integer.toString(colorEffect.getColor())); 651 } 652 } else if (effect instanceof EffectKenBurns) { 653 final Rect startRect = ((EffectKenBurns)effect).getStartRect(); 654 serializer.attribute("", ATTR_START_RECT_L, 655 Integer.toString(startRect.left)); 656 serializer.attribute("", ATTR_START_RECT_T, 657 Integer.toString(startRect.top)); 658 serializer.attribute("", ATTR_START_RECT_R, 659 Integer.toString(startRect.right)); 660 serializer.attribute("", ATTR_START_RECT_B, 661 Integer.toString(startRect.bottom)); 662 663 final Rect endRect = ((EffectKenBurns)effect).getEndRect(); 664 serializer.attribute("", ATTR_END_RECT_L, Integer.toString(endRect.left)); 665 serializer.attribute("", ATTR_END_RECT_T, Integer.toString(endRect.top)); 666 serializer.attribute("", ATTR_END_RECT_R, Integer.toString(endRect.right)); 667 serializer.attribute("", ATTR_END_RECT_B, 668 Integer.toString(endRect.bottom)); 669 } 670 671 serializer.endTag("", TAG_EFFECT); 672 } 673 serializer.endTag("", TAG_EFFECTS); 674 } 675 676 serializer.endTag("", TAG_MEDIA_ITEM); 677 } 678 serializer.endTag("", TAG_MEDIA_ITEMS); 679 680 serializer.startTag("", TAG_TRANSITIONS); 681 682 for (Transition transition : mTransitions) { 683 serializer.startTag("", TAG_TRANSITION); 684 serializer.attribute("", ATTR_ID, transition.getId()); 685 serializer.attribute("", ATTR_TYPE, transition.getClass().getSimpleName()); 686 serializer.attribute("", ATTR_DURATION, Long.toString(transition.getDuration())); 687 serializer.attribute("", ATTR_BEHAVIOR, Integer.toString(transition.getBehavior())); 688 final MediaItem afterMediaItem = transition.getAfterMediaItem(); 689 if (afterMediaItem != null) { 690 serializer.attribute("", ATTR_AFTER_MEDIA_ITEM_ID, afterMediaItem.getId()); 691 } 692 693 final MediaItem beforeMediaItem = transition.getBeforeMediaItem(); 694 if (beforeMediaItem != null) { 695 serializer.attribute("", ATTR_BEFORE_MEDIA_ITEM_ID, beforeMediaItem.getId()); 696 } 697 698 if (transition instanceof TransitionSliding) { 699 serializer.attribute("", ATTR_DIRECTION, 700 Integer.toString(((TransitionSliding)transition).getDirection())); 701 } else if (transition instanceof TransitionAlpha) { 702 TransitionAlpha ta = (TransitionAlpha)transition; 703 serializer.attribute("", ATTR_BLENDING, Integer.toString(ta.getBlendingPercent())); 704 serializer.attribute("", ATTR_INVERT, Boolean.toString(ta.isInvert())); 705 if (ta.getMaskFilename() != null) { 706 serializer.attribute("", ATTR_MASK, ta.getMaskFilename()); 707 } 708 } 709 serializer.endTag("", TAG_TRANSITION); 710 } 711 serializer.endTag("", TAG_TRANSITIONS); 712 713 serializer.startTag("", TAG_AUDIO_TRACKS); 714 for (AudioTrack at : mAudioTracks) { 715 serializer.startTag("", TAG_AUDIO_TRACK); 716 serializer.attribute("", ATTR_ID, at.getId()); 717 serializer.attribute("", ATTR_FILENAME, at.getFilename()); 718 serializer.attribute("", ATTR_START_TIME, Long.toString(at.getStartTime())); 719 serializer.attribute("", ATTR_BEGIN_TIME, Long.toString(at.getBoundaryBeginTime())); 720 serializer.attribute("", ATTR_END_TIME, Long.toString(at.getBoundaryEndTime())); 721 serializer.attribute("", ATTR_VOLUME, Integer.toString(at.getVolume())); 722 serializer.attribute("", ATTR_DUCK_ENABLED, Boolean.toString(at.isDuckingEnabled())); 723 serializer.attribute("", ATTR_DUCKED_TRACK_VOLUME, Integer.toString(at.getDuckedTrackVolume())); 724 serializer.attribute("", ATTR_DUCK_THRESHOLD, Integer.toString(at.getDuckingThreshhold())); 725 serializer.attribute("", ATTR_MUTED, Boolean.toString(at.isMuted())); 726 serializer.attribute("", ATTR_LOOP, Boolean.toString(at.isLooping())); 727 if (at.getAudioWaveformFilename() != null) { 728 serializer.attribute("", ATTR_AUDIO_WAVEFORM_FILENAME, 729 at.getAudioWaveformFilename()); 730 } 731 732 serializer.endTag("", TAG_AUDIO_TRACK); 733 } 734 serializer.endTag("", TAG_AUDIO_TRACKS); 735 736 serializer.endTag("", TAG_PROJECT); 737 serializer.endDocument(); 738 739 // Save the metadata XML file 740 final FileOutputStream out = new FileOutputStream(new File(getPath(), PROJECT_FILENAME)); 741 out.write(writer.toString().getBytes()); 742 out.flush(); 743 out.close(); 744 } 745 746 /** 747 * Load the project form XML 748 */ 749 private void load() throws FileNotFoundException, XmlPullParserException, IOException { 750 final File file = new File(mProjectPath, PROJECT_FILENAME); 751 final FileInputStream fis = new FileInputStream(file); 752 753 try { 754 // Load the metadata 755 final XmlPullParser parser = Xml.newPullParser(); 756 parser.setInput(fis, "UTF-8"); 757 int eventType = parser.getEventType(); 758 String name; 759 MediaItem currentMediaItem = null; 760 Overlay currentOverlay = null; 761 while (eventType != XmlPullParser.END_DOCUMENT) { 762 switch (eventType) { 763 case XmlPullParser.START_TAG: { 764 name = parser.getName(); 765 if (TAG_PROJECT.equals(name)) { 766 mAspectRatio = Integer.parseInt(parser.getAttributeValue("", 767 ATTR_ASPECT_RATIO)); 768 } else if (TAG_MEDIA_ITEM.equals(name)) { 769 final String mediaItemId = parser.getAttributeValue("", ATTR_ID); 770 final String type = parser.getAttributeValue("", ATTR_TYPE); 771 final String filename = parser.getAttributeValue("", ATTR_FILENAME); 772 final int renderingMode = Integer.parseInt( 773 parser.getAttributeValue("", ATTR_RENDERING_MODE)); 774 775 if (MediaImageItem.class.getSimpleName().equals(type)) { 776 final long durationMs = Long.parseLong( 777 parser.getAttributeValue("", ATTR_DURATION)); 778 currentMediaItem = new MediaImageItem(this, mediaItemId, filename, 779 durationMs, renderingMode); 780 } else if (MediaVideoItem.class.getSimpleName().equals(type)) { 781 final long beginMs = Long.parseLong( 782 parser.getAttributeValue("", ATTR_BEGIN_TIME)); 783 final long endMs = Long.parseLong( 784 parser.getAttributeValue("", ATTR_END_TIME)); 785 final int volume = Integer.parseInt( 786 parser.getAttributeValue("", ATTR_VOLUME)); 787 final boolean muted = Boolean.parseBoolean( 788 parser.getAttributeValue("", ATTR_MUTED)); 789 final String audioWaveformFilename = 790 parser.getAttributeValue("", ATTR_AUDIO_WAVEFORM_FILENAME); 791 currentMediaItem = new MediaVideoItem(this, mediaItemId, filename, 792 renderingMode, beginMs, endMs, volume, muted, 793 audioWaveformFilename); 794 795 final long beginTimeMs = Long.parseLong( 796 parser.getAttributeValue("", ATTR_BEGIN_TIME)); 797 final long endTimeMs = Long.parseLong( 798 parser.getAttributeValue("", ATTR_END_TIME)); 799 ((MediaVideoItem)currentMediaItem).setExtractBoundaries( 800 beginTimeMs, endTimeMs); 801 802 final int volumePercent = Integer.parseInt( 803 parser.getAttributeValue("", ATTR_VOLUME)); 804 ((MediaVideoItem)currentMediaItem).setVolume(volumePercent); 805 } else { 806 Log.e(TAG, "Unknown media item type: " + type); 807 currentMediaItem = null; 808 } 809 810 if (currentMediaItem != null) { 811 mMediaItems.add(currentMediaItem); 812 } 813 } else if (TAG_TRANSITION.equals(name)) { 814 final Transition transition = parseTransition(parser); 815 if (transition != null) { 816 mTransitions.add(transition); 817 } 818 } else if (TAG_OVERLAY.equals(name)) { 819 if (currentMediaItem != null) { 820 currentOverlay = parseOverlay(parser, currentMediaItem); 821 if (currentOverlay != null) { 822 currentMediaItem.addOverlay(currentOverlay); 823 } 824 } 825 } else if (TAG_OVERLAY_USER_ATTRIBUTES.equals(name)) { 826 if (currentOverlay != null) { 827 final int attributesCount = parser.getAttributeCount(); 828 for (int i = 0; i < attributesCount; i++) { 829 currentOverlay.setUserAttribute(parser.getAttributeName(i), 830 parser.getAttributeValue(i)); 831 } 832 } 833 } else if (TAG_EFFECT.equals(name)) { 834 if (currentMediaItem != null) { 835 final Effect effect = parseEffect(parser, currentMediaItem); 836 if (effect != null) { 837 currentMediaItem.addEffect(effect); 838 } 839 } 840 } else if (TAG_AUDIO_TRACK.equals(name)) { 841 final AudioTrack audioTrack = parseAudioTrack(parser); 842 if (audioTrack != null) { 843 addAudioTrack(audioTrack); 844 } 845 } 846 break; 847 } 848 849 case XmlPullParser.END_TAG: { 850 name = parser.getName(); 851 if (TAG_MEDIA_ITEM.equals(name)) { 852 currentMediaItem = null; 853 } else if (TAG_OVERLAY.equals(name)) { 854 currentOverlay = null; 855 } 856 break; 857 } 858 859 default: { 860 break; 861 } 862 } 863 eventType = parser.next(); 864 } 865 computeTimelineDuration(); 866 } finally { 867 if (fis != null) { 868 fis.close(); 869 } 870 } 871 } 872 873 /** 874 * Parse the transition 875 * 876 * @param parser The parser 877 * @return The transition 878 */ 879 private Transition parseTransition(XmlPullParser parser) { 880 final String transitionId = parser.getAttributeValue("", ATTR_ID); 881 final String type = parser.getAttributeValue("", ATTR_TYPE); 882 final long durationMs = Long.parseLong(parser.getAttributeValue("", ATTR_DURATION)); 883 final int behavior = Integer.parseInt(parser.getAttributeValue("", ATTR_BEHAVIOR)); 884 885 final String beforeMediaItemId = parser.getAttributeValue("", ATTR_BEFORE_MEDIA_ITEM_ID); 886 final MediaItem beforeMediaItem; 887 if (beforeMediaItemId != null) { 888 beforeMediaItem = getMediaItem(beforeMediaItemId); 889 } else { 890 beforeMediaItem = null; 891 } 892 893 final String afterMediaItemId = parser.getAttributeValue("", ATTR_AFTER_MEDIA_ITEM_ID); 894 final MediaItem afterMediaItem; 895 if (afterMediaItemId != null) { 896 afterMediaItem = getMediaItem(afterMediaItemId); 897 } else { 898 afterMediaItem = null; 899 } 900 901 final Transition transition; 902 if (TransitionAlpha.class.getSimpleName().equals(type)) { 903 final int blending = Integer.parseInt(parser.getAttributeValue("", ATTR_BLENDING)); 904 final String maskFilename = parser.getAttributeValue("", ATTR_MASK); 905 final boolean invert = Boolean.getBoolean(parser.getAttributeValue("", ATTR_INVERT)); 906 transition = new TransitionAlpha(transitionId, afterMediaItem, beforeMediaItem, 907 durationMs, behavior, maskFilename, blending, invert); 908 } else if (TransitionCrossfade.class.getSimpleName().equals(type)) { 909 transition = new TransitionCrossfade(transitionId, afterMediaItem, beforeMediaItem, 910 durationMs, behavior); 911 } else if (TransitionSliding.class.getSimpleName().equals(type)) { 912 final int direction = Integer.parseInt(parser.getAttributeValue("", ATTR_DIRECTION)); 913 transition = new TransitionSliding(transitionId, afterMediaItem, beforeMediaItem, 914 durationMs, behavior, direction); 915 } else if (TransitionFadeBlack.class.getSimpleName().equals(type)) { 916 transition = new TransitionFadeBlack(transitionId, afterMediaItem, beforeMediaItem, 917 durationMs, behavior); 918 } else { 919 transition = null; 920 } 921 922 if (beforeMediaItem != null) { 923 beforeMediaItem.setBeginTransition(transition); 924 } 925 926 if (afterMediaItem != null) { 927 afterMediaItem.setEndTransition(transition); 928 } 929 930 return transition; 931 } 932 933 /** 934 * Parse the overlay 935 * 936 * @param parser The parser 937 * @param mediaItem The media item owner 938 * 939 * @return The overlay 940 */ 941 private Overlay parseOverlay(XmlPullParser parser, MediaItem mediaItem) { 942 final String overlayId = parser.getAttributeValue("", ATTR_ID); 943 final String type = parser.getAttributeValue("", ATTR_TYPE); 944 final long durationMs = Long.parseLong(parser.getAttributeValue("", ATTR_DURATION)); 945 final long startTimeMs = Long.parseLong(parser.getAttributeValue("", ATTR_BEGIN_TIME)); 946 947 final Overlay overlay; 948 if (OverlayFrame.class.getSimpleName().equals(type)) { 949 final String filename = parser.getAttributeValue("", ATTR_FILENAME); 950 overlay = new OverlayFrame(mediaItem, overlayId, filename, startTimeMs, durationMs); 951 } else { 952 overlay = null; 953 } 954 955 return overlay; 956 } 957 958 /** 959 * Parse the effect 960 * 961 * @param parser The parser 962 * @param mediaItem The media item owner 963 * 964 * @return The effect 965 */ 966 private Effect parseEffect(XmlPullParser parser, MediaItem mediaItem) { 967 final String effectId = parser.getAttributeValue("", ATTR_ID); 968 final String type = parser.getAttributeValue("", ATTR_TYPE); 969 final long durationMs = Long.parseLong(parser.getAttributeValue("", ATTR_DURATION)); 970 final long startTimeMs = Long.parseLong(parser.getAttributeValue("", ATTR_BEGIN_TIME)); 971 972 final Effect effect; 973 if (EffectColor.class.getSimpleName().equals(type)) { 974 final int colorEffectType = 975 Integer.parseInt(parser.getAttributeValue("", ATTR_COLOR_EFFECT_TYPE)); 976 final int color; 977 if (colorEffectType == EffectColor.TYPE_COLOR 978 || colorEffectType == EffectColor.TYPE_GRADIENT) { 979 color = Integer.parseInt(parser.getAttributeValue("", ATTR_COLOR_EFFECT_VALUE)); 980 } else { 981 color = 0; 982 } 983 effect = new EffectColor(mediaItem, effectId, startTimeMs, durationMs, 984 colorEffectType, color); 985 } else if (EffectKenBurns.class.getSimpleName().equals(type)) { 986 final Rect startRect = new Rect( 987 Integer.parseInt(parser.getAttributeValue("", ATTR_START_RECT_L)), 988 Integer.parseInt(parser.getAttributeValue("", ATTR_START_RECT_T)), 989 Integer.parseInt(parser.getAttributeValue("", ATTR_START_RECT_R)), 990 Integer.parseInt(parser.getAttributeValue("", ATTR_START_RECT_B))); 991 final Rect endRect = new Rect( 992 Integer.parseInt(parser.getAttributeValue("", ATTR_END_RECT_L)), 993 Integer.parseInt(parser.getAttributeValue("", ATTR_END_RECT_T)), 994 Integer.parseInt(parser.getAttributeValue("", ATTR_END_RECT_R)), 995 Integer.parseInt(parser.getAttributeValue("", ATTR_END_RECT_B))); 996 effect = new EffectKenBurns(mediaItem, effectId, startRect, endRect, startTimeMs, 997 durationMs); 998 } else { 999 effect = null; 1000 } 1001 1002 return effect; 1003 } 1004 1005 /** 1006 * Parse the audio track 1007 * 1008 * @param parser The parser 1009 * 1010 * @return The audio track 1011 */ 1012 private AudioTrack parseAudioTrack(XmlPullParser parser) { 1013 final String audioTrackId = parser.getAttributeValue("", ATTR_ID); 1014 final String filename = parser.getAttributeValue("", ATTR_FILENAME); 1015 final long startTimeMs = Long.parseLong(parser.getAttributeValue("", ATTR_START_TIME)); 1016 final long beginMs = Long.parseLong(parser.getAttributeValue("", ATTR_BEGIN_TIME)); 1017 final long endMs = Long.parseLong(parser.getAttributeValue("", ATTR_END_TIME)); 1018 final int volume = Integer.parseInt(parser.getAttributeValue("", ATTR_VOLUME)); 1019 final boolean muted = Boolean.parseBoolean(parser.getAttributeValue("", ATTR_MUTED)); 1020 final boolean loop = Boolean.parseBoolean(parser.getAttributeValue("", ATTR_LOOP)); 1021 final boolean duckingEnabled = Boolean.parseBoolean(parser.getAttributeValue("", ATTR_DUCK_ENABLED)); 1022 final int duckThreshold = Integer.parseInt(parser.getAttributeValue("", ATTR_DUCK_THRESHOLD)); 1023 final int duckedTrackVolume = Integer.parseInt(parser.getAttributeValue("", ATTR_DUCKED_TRACK_VOLUME)); 1024 final String waveformFilename = parser.getAttributeValue("", ATTR_AUDIO_WAVEFORM_FILENAME); 1025 try { 1026 final AudioTrack audioTrack = new AudioTrack(this, audioTrackId, filename, startTimeMs, 1027 beginMs, endMs, loop, volume, muted, duckingEnabled, duckThreshold, duckedTrackVolume, waveformFilename); 1028 1029 return audioTrack; 1030 } catch (IOException ex) { 1031 return null; 1032 } 1033 } 1034 1035 /* 1036 * {@inheritDoc} 1037 */ 1038 public void cancelExport(String filename) { 1039 } 1040 1041 /* 1042 * {@inheritDoc} 1043 */ 1044 public void export(String filename, int height, int bitrate, ExportProgressListener listener) 1045 throws IOException { 1046 } 1047 1048 /* 1049 * {@inheritDoc} 1050 */ 1051 public void export(String filename, int height, int bitrate, int audioCodec, int videoCodec, 1052 ExportProgressListener listener) throws IOException { 1053 } 1054 1055 /* 1056 * {@inheritDoc} 1057 */ 1058 public void generatePreview(MediaProcessingProgressListener listener) { 1059 // Generate all the needed transitions 1060 for (Transition transition : mTransitions) { 1061 if (!transition.isGenerated()) { 1062 transition.generate(); 1063 } 1064 } 1065 1066 // This is necessary because the user may had called setDuration on 1067 // MediaImageItems 1068 computeTimelineDuration(); 1069 } 1070 1071 /* 1072 * {@inheritDoc} 1073 */ 1074 public void release() { 1075 stopPreview(); 1076 } 1077 1078 /* 1079 * {@inheritDoc} 1080 */ 1081 public long getDuration() { 1082 // Since MediaImageItem can change duration we need to compute the 1083 // duration here 1084 computeTimelineDuration(); 1085 return mDurationMs; 1086 } 1087 1088 /* 1089 * {@inheritDoc} 1090 */ 1091 public int getAspectRatio() { 1092 return mAspectRatio; 1093 } 1094 1095 /* 1096 * {@inheritDoc} 1097 */ 1098 public void setAspectRatio(int aspectRatio) { 1099 mAspectRatio = aspectRatio; 1100 1101 for (Transition transition : mTransitions) { 1102 transition.invalidate(); 1103 } 1104 } 1105 1106 /* 1107 * {@inheritDoc} 1108 */ 1109 public long renderPreviewFrame(SurfaceHolder surfaceHolder, long timeMs) { 1110 if (mPreviewThread != null) { 1111 throw new IllegalStateException("Previewing is in progress"); 1112 } 1113 return timeMs; 1114 } 1115 1116 /* 1117 * {@inheritDoc} 1118 */ 1119 public synchronized void startPreview(SurfaceHolder surfaceHolder, long fromMs, long toMs, 1120 boolean loop, int callbackAfterFrameCount, PreviewProgressListener listener) { 1121 if (fromMs >= mDurationMs) { 1122 return; 1123 } 1124 mPreviewThread = new PreviewThread(fromMs, toMs, loop, callbackAfterFrameCount, listener); 1125 mPreviewThread.start(); 1126 } 1127 1128 /* 1129 * {@inheritDoc} 1130 */ 1131 public synchronized long stopPreview() { 1132 final long stopTimeMs; 1133 if (mPreviewThread != null) { 1134 stopTimeMs = mPreviewThread.stopPreview(); 1135 mPreviewThread = null; 1136 } else { 1137 stopTimeMs = 0; 1138 } 1139 return stopTimeMs; 1140 } 1141 1142 /** 1143 * Compute the duration 1144 */ 1145 private void computeTimelineDuration() { 1146 mDurationMs = 0; 1147 final int mediaItemsCount = mMediaItems.size(); 1148 for (int i = 0; i < mediaItemsCount; i++) { 1149 final MediaItem mediaItem = mMediaItems.get(i); 1150 mDurationMs += mediaItem.getTimelineDuration(); 1151 if (mediaItem.getEndTransition() != null) { 1152 if (i < mediaItemsCount - 1) { 1153 mDurationMs -= mediaItem.getEndTransition().getDuration(); 1154 } 1155 } 1156 } 1157 } 1158 1159 /** 1160 * Remove transitions associated with the specified media item 1161 * 1162 * @param mediaItem The media item 1163 */ 1164 private void removeAdjacentTransitions(MediaItem mediaItem) { 1165 final Transition beginTransition = mediaItem.getBeginTransition(); 1166 if (beginTransition != null) { 1167 if (beginTransition.getAfterMediaItem() != null) { 1168 beginTransition.getAfterMediaItem().setEndTransition(null); 1169 } 1170 beginTransition.invalidate(); 1171 mTransitions.remove(beginTransition); 1172 } 1173 1174 final Transition endTransition = mediaItem.getEndTransition(); 1175 if (endTransition != null) { 1176 if (endTransition.getBeforeMediaItem() != null) { 1177 endTransition.getBeforeMediaItem().setBeginTransition(null); 1178 } 1179 endTransition.invalidate(); 1180 mTransitions.remove(endTransition); 1181 } 1182 1183 mediaItem.setBeginTransition(null); 1184 mediaItem.setEndTransition(null); 1185 } 1186 1187 /** 1188 * Remove the transition before this media item 1189 * 1190 * @param index The media item index 1191 */ 1192 private void removeTransitionBefore(int index) { 1193 final MediaItem mediaItem = mMediaItems.get(index); 1194 final Iterator<Transition> it = mTransitions.iterator(); 1195 while (it.hasNext()) { 1196 Transition t = it.next(); 1197 if (t.getBeforeMediaItem() == mediaItem) { 1198 it.remove(); 1199 t.invalidate(); 1200 mediaItem.setBeginTransition(null); 1201 if (index > 0) { 1202 mMediaItems.get(index - 1).setEndTransition(null); 1203 } 1204 break; 1205 } 1206 } 1207 } 1208 1209 /** 1210 * Remove the transition after this media item 1211 * 1212 * @param index The media item index 1213 */ 1214 private void removeTransitionAfter(int index) { 1215 final MediaItem mediaItem = mMediaItems.get(index); 1216 final Iterator<Transition> it = mTransitions.iterator(); 1217 while (it.hasNext()) { 1218 Transition t = it.next(); 1219 if (t.getAfterMediaItem() == mediaItem) { 1220 it.remove(); 1221 t.invalidate(); 1222 mediaItem.setEndTransition(null); 1223 // Invalidate the reference in the next media item 1224 if (index < mMediaItems.size() - 1) { 1225 mMediaItems.get(index + 1).setBeginTransition(null); 1226 } 1227 break; 1228 } 1229 } 1230 } 1231} 1232