MediaItem.java revision a573b563b3c6a3edc60393543dc9adb7ade4f188
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 17 18package android.media.videoeditor; 19 20import java.io.File; 21import java.io.FileNotFoundException; 22import java.io.FileOutputStream; 23import java.io.IOException; 24import java.util.ArrayList; 25import java.util.List; 26 27import java.io.DataOutputStream; 28import java.nio.ByteBuffer; 29import java.nio.IntBuffer; 30 31import android.graphics.Bitmap; 32import android.media.videoeditor.MediaArtistNativeHelper.ClipSettings; 33import android.media.videoeditor.MediaArtistNativeHelper.FileType; 34import android.media.videoeditor.MediaArtistNativeHelper.MediaRendering; 35 36/** 37 * This abstract class describes the base class for any MediaItem. Objects are 38 * defined with a file path as a source data. 39 * {@hide} 40 */ 41public abstract class MediaItem { 42 /** 43 * A constant which can be used to specify the end of the file (instead of 44 * providing the actual duration of the media item). 45 */ 46 public final static int END_OF_FILE = -1; 47 48 /** 49 * Rendering modes 50 */ 51 /** 52 * When using the RENDERING_MODE_BLACK_BORDER rendering mode video frames 53 * are resized by preserving the aspect ratio until the movie matches one of 54 * the dimensions of the output movie. The areas outside the resized video 55 * clip are rendered black. 56 */ 57 public static final int RENDERING_MODE_BLACK_BORDER = 0; 58 59 /** 60 * When using the RENDERING_MODE_STRETCH rendering mode video frames are 61 * stretched horizontally or vertically to match the current aspect ratio of 62 * the video editor. 63 */ 64 public static final int RENDERING_MODE_STRETCH = 1; 65 66 /** 67 * When using the RENDERING_MODE_CROPPING rendering mode video frames are 68 * scaled horizontally or vertically by preserving the original aspect ratio 69 * of the media item. 70 */ 71 public static final int RENDERING_MODE_CROPPING = 2; 72 73 /** 74 * The unique id of the MediaItem 75 */ 76 private final String mUniqueId; 77 78 /** 79 * The name of the file associated with the MediaItem 80 */ 81 protected final String mFilename; 82 83 /** 84 * List of effects 85 */ 86 private final List<Effect> mEffects; 87 88 /** 89 * List of overlays 90 */ 91 private final List<Overlay> mOverlays; 92 93 /** 94 * The rendering mode 95 */ 96 private int mRenderingMode; 97 98 private final MediaArtistNativeHelper mMANativeHelper; 99 100 private final String mProjectPath; 101 102 /** 103 * Beginning and end transitions 104 */ 105 protected Transition mBeginTransition; 106 107 protected Transition mEndTransition; 108 109 protected String mGeneratedImageClip; 110 111 protected boolean mRegenerateClip; 112 113 private boolean mBlankFrameGenerated = false; 114 115 private String mBlankFrameFilename = null; 116 117 /** 118 * Constructor 119 * 120 * @param editor The video editor reference 121 * @param mediaItemId The MediaItem id 122 * @param filename name of the media file. 123 * @param renderingMode The rendering mode 124 * @throws IOException if file is not found 125 * @throws IllegalArgumentException if a capability such as file format is 126 * not supported the exception object contains the unsupported 127 * capability 128 */ 129 protected MediaItem(VideoEditor editor, String mediaItemId, String filename, 130 int renderingMode) throws IOException { 131 if (filename == null) { 132 throw new IllegalArgumentException("MediaItem : filename is null"); 133 } 134 mUniqueId = mediaItemId; 135 mFilename = filename; 136 mRenderingMode = renderingMode; 137 mEffects = new ArrayList<Effect>(); 138 mOverlays = new ArrayList<Overlay>(); 139 mBeginTransition = null; 140 mEndTransition = null; 141 mMANativeHelper = ((VideoEditorImpl)editor).getNativeContext(); 142 mProjectPath = editor.getPath(); 143 mRegenerateClip = false; 144 mGeneratedImageClip = null; 145 } 146 147 /** 148 * @return The id of the media item 149 */ 150 public String getId() { 151 return mUniqueId; 152 } 153 154 /** 155 * @return The media source file name 156 */ 157 public String getFilename() { 158 return mFilename; 159 } 160 161 /** 162 * If aspect ratio of the MediaItem is different from the aspect ratio of 163 * the editor then this API controls the rendering mode. 164 * 165 * @param renderingMode rendering mode. It is one of: 166 * {@link #RENDERING_MODE_BLACK_BORDER}, 167 * {@link #RENDERING_MODE_STRETCH} 168 */ 169 public void setRenderingMode(int renderingMode) { 170 switch (renderingMode) { 171 case RENDERING_MODE_BLACK_BORDER: 172 case RENDERING_MODE_STRETCH: 173 case RENDERING_MODE_CROPPING: 174 break; 175 176 default: 177 throw new IllegalArgumentException("Invalid Rendering Mode"); 178 } 179 180 mMANativeHelper.setGeneratePreview(true); 181 182 mRenderingMode = renderingMode; 183 if (mBeginTransition != null) { 184 mBeginTransition.invalidate(); 185 } 186 187 if (mEndTransition != null) { 188 mEndTransition.invalidate(); 189 } 190 } 191 192 /** 193 * @return The rendering mode 194 */ 195 public int getRenderingMode() { 196 return mRenderingMode; 197 } 198 199 /** 200 * @param transition The beginning transition 201 */ 202 void setBeginTransition(Transition transition) { 203 mBeginTransition = transition; 204 } 205 206 /** 207 * @return The begin transition 208 */ 209 public Transition getBeginTransition() { 210 return mBeginTransition; 211 } 212 213 /** 214 * @param transition The end transition 215 */ 216 void setEndTransition(Transition transition) { 217 mEndTransition = transition; 218 } 219 220 /** 221 * @return The end transition 222 */ 223 public Transition getEndTransition() { 224 return mEndTransition; 225 } 226 227 /** 228 * @return The timeline duration. This is the actual duration in the 229 * timeline (trimmed duration) 230 */ 231 public abstract long getTimelineDuration(); 232 233 /** 234 * @return The is the full duration of the media item (not trimmed) 235 */ 236 public abstract long getDuration(); 237 238 /** 239 * @return The source file type 240 */ 241 public abstract int getFileType(); 242 243 /** 244 * @return Get the native width of the media item 245 */ 246 public abstract int getWidth(); 247 248 /** 249 * @return Get the native height of the media item 250 */ 251 public abstract int getHeight(); 252 253 /** 254 * Get aspect ratio of the source media item. 255 * 256 * @return the aspect ratio as described in MediaProperties. 257 * MediaProperties.ASPECT_RATIO_UNDEFINED if aspect ratio is not 258 * supported as in MediaProperties 259 */ 260 public abstract int getAspectRatio(); 261 262 /** 263 * Add the specified effect to this media item. 264 * 265 * Note that certain types of effects cannot be applied to video and to 266 * image media items. For example in certain implementation a Ken Burns 267 * implementation cannot be applied to video media item. 268 * 269 * This method invalidates transition video clips if the 270 * effect overlaps with the beginning and/or the end transition. 271 * 272 * @param effect The effect to apply 273 * @throws IllegalStateException if a preview or an export is in progress 274 * @throws IllegalArgumentException if the effect start and/or duration are 275 * invalid or if the effect cannot be applied to this type of media 276 * item or if the effect id is not unique across all the Effects 277 * added. 278 */ 279 public void addEffect(Effect effect) { 280 281 if (effect == null) { 282 throw new IllegalArgumentException("NULL effect cannot be applied"); 283 } 284 285 if (effect.getMediaItem() != this) { 286 throw new IllegalArgumentException("Media item mismatch"); 287 } 288 289 if (mEffects.contains(effect)) { 290 throw new IllegalArgumentException("Effect already exists: " + effect.getId()); 291 } 292 293 if (effect.getStartTime() + effect.getDuration() > getDuration()) { 294 throw new IllegalArgumentException( 295 "Effect start time + effect duration > media clip duration"); 296 } 297 298 mMANativeHelper.setGeneratePreview(true); 299 300 mEffects.add(effect); 301 302 invalidateTransitions(effect.getStartTime(), effect.getDuration()); 303 304 if (effect instanceof EffectKenBurns) { 305 mRegenerateClip = true; 306 } 307 } 308 309 /** 310 * Remove the effect with the specified id. 311 * 312 * This method invalidates a transition video clip if the effect overlaps 313 * with a transition. 314 * 315 * @param effectId The id of the effect to be removed 316 * 317 * @return The effect that was removed 318 * @throws IllegalStateException if a preview or an export is in progress 319 */ 320 public Effect removeEffect(String effectId) { 321 for (Effect effect : mEffects) { 322 if (effect.getId().equals(effectId)) { 323 mMANativeHelper.setGeneratePreview(true); 324 325 mEffects.remove(effect); 326 327 invalidateTransitions(effect.getStartTime(), effect.getDuration()); 328 if (effect instanceof EffectKenBurns) { 329 if (mGeneratedImageClip != null) { 330 /** 331 * Delete the file 332 */ 333 new File(mGeneratedImageClip).delete(); 334 /** 335 * Invalidate the filename 336 */ 337 mGeneratedImageClip = null; 338 } 339 mRegenerateClip = false; 340 } 341 return effect; 342 } 343 } 344 return null; 345 } 346 347 /** 348 * Set the filepath of the generated image clip when the effect is added. 349 * 350 * @param The filepath of the generated image clip. 351 */ 352 void setGeneratedImageClip(String generatedFilePath) { 353 mGeneratedImageClip = generatedFilePath; 354 } 355 356 /** 357 * Get the filepath of the generated image clip when the effect is added. 358 * 359 * @return The filepath of the generated image clip (null if it does not 360 * exist) 361 */ 362 String getGeneratedImageClip() { 363 return mGeneratedImageClip; 364 } 365 366 /** 367 * Find the effect with the specified id 368 * 369 * @param effectId The effect id 370 * @return The effect with the specified id (null if it does not exist) 371 */ 372 public Effect getEffect(String effectId) { 373 for (Effect effect : mEffects) { 374 if (effect.getId().equals(effectId)) { 375 return effect; 376 } 377 } 378 return null; 379 } 380 381 /** 382 * Get the list of effects. 383 * 384 * @return the effects list. If no effects exist an empty list will be 385 * returned. 386 */ 387 public List<Effect> getAllEffects() { 388 return mEffects; 389 } 390 391 /** 392 * Add an overlay to the storyboard. This method invalidates a transition 393 * video clip if the overlay overlaps with a transition. 394 * 395 * @param overlay The overlay to add 396 * @throws IllegalStateException if a preview or an export is in progress or 397 * if the overlay id is not unique across all the overlays added 398 * or if the bitmap is not specified or if the dimensions of the 399 * bitmap do not match the dimensions of the media item 400 * @throws FileNotFoundException, IOException if overlay could not be saved 401 * to project path 402 */ 403 public void addOverlay(Overlay overlay) throws FileNotFoundException, IOException { 404 if (overlay == null) { 405 throw new IllegalArgumentException("NULL Overlay cannot be applied"); 406 } 407 408 if (overlay.getMediaItem() != this) { 409 throw new IllegalArgumentException("Media item mismatch"); 410 } 411 412 if (mOverlays.contains(overlay)) { 413 throw new IllegalArgumentException("Overlay already exists: " + overlay.getId()); 414 } 415 416 if (overlay.getStartTime() + overlay.getDuration() > getDuration()) { 417 throw new IllegalArgumentException( 418 "Overlay start time + overlay duration > media clip duration"); 419 } 420 421 if (overlay instanceof OverlayFrame) { 422 final OverlayFrame frame = (OverlayFrame)overlay; 423 final Bitmap bitmap = frame.getBitmap(); 424 if (bitmap == null) { 425 throw new IllegalArgumentException("Overlay bitmap not specified"); 426 } 427 428 final int scaledWidth, scaledHeight; 429 if (this instanceof MediaVideoItem) { 430 scaledWidth = getWidth(); 431 scaledHeight = getHeight(); 432 } else { 433 scaledWidth = ((MediaImageItem)this).getScaledWidth(); 434 scaledHeight = ((MediaImageItem)this).getScaledHeight(); 435 } 436 437 /** 438 * The dimensions of the overlay bitmap must be the same as the 439 * media item dimensions 440 */ 441 if (bitmap.getWidth() != scaledWidth || bitmap.getHeight() != scaledHeight) { 442 throw new IllegalArgumentException( 443 "Bitmap dimensions must match media item dimensions"); 444 } 445 446 mMANativeHelper.setGeneratePreview(true); 447 ((OverlayFrame)overlay).save(mProjectPath); 448 449 mOverlays.add(overlay); 450 invalidateTransitions(overlay.getStartTime(), overlay.getDuration()); 451 452 } else { 453 throw new IllegalArgumentException("Overlay not supported"); 454 } 455 } 456 457 /** 458 * @param flag The flag to indicate if regeneration of clip is true or 459 * false. 460 */ 461 void setRegenerateClip(boolean flag) { 462 mRegenerateClip = flag; 463 } 464 465 /** 466 * @return flag The flag to indicate if regeneration of clip is true or 467 * false. 468 */ 469 boolean getRegenerateClip() { 470 return mRegenerateClip; 471 } 472 473 /** 474 * Remove the overlay with the specified id. 475 * 476 * This method invalidates a transition video clip if the overlay overlaps 477 * with a transition. 478 * 479 * @param overlayId The id of the overlay to be removed 480 * 481 * @return The overlay that was removed 482 * @throws IllegalStateException if a preview or an export is in progress 483 */ 484 public Overlay removeOverlay(String overlayId) { 485 for (Overlay overlay : mOverlays) { 486 if (overlay.getId().equals(overlayId)) { 487 mMANativeHelper.setGeneratePreview(true); 488 489 mOverlays.remove(overlay); 490 if (overlay instanceof OverlayFrame) { 491 ((OverlayFrame)overlay).invalidate(); 492 } 493 invalidateTransitions(overlay.getStartTime(), overlay.getDuration()); 494 return overlay; 495 } 496 } 497 return null; 498 } 499 500 /** 501 * Find the overlay with the specified id 502 * 503 * @param overlayId The overlay id 504 * 505 * @return The overlay with the specified id (null if it does not exist) 506 */ 507 public Overlay getOverlay(String overlayId) { 508 for (Overlay overlay : mOverlays) { 509 if (overlay.getId().equals(overlayId)) { 510 return overlay; 511 } 512 } 513 514 return null; 515 } 516 517 /** 518 * Get the list of overlays associated with this media item 519 * 520 * Note that if any overlay source files are not accessible anymore, 521 * this method will still provide the full list of overlays. 522 * 523 * @return The list of overlays. If no overlays exist an empty list will 524 * be returned. 525 */ 526 public List<Overlay> getAllOverlays() { 527 return mOverlays; 528 } 529 530 /** 531 * Create a thumbnail at specified time in a video stream in Bitmap format 532 * 533 * @param width width of the thumbnail in pixels 534 * @param height height of the thumbnail in pixels 535 * @param timeMs The time in the source video file at which the thumbnail is 536 * requested (even if trimmed). 537 * 538 * @return The thumbnail as a Bitmap. 539 * 540 * @throws IOException if a file error occurs 541 * @throws IllegalArgumentException if time is out of video duration 542 */ 543 public abstract Bitmap getThumbnail(int width, int height, long timeMs) 544 throws IOException; 545 546 /** 547 * Get the array of Bitmap thumbnails between start and end. 548 * 549 * @param width width of the thumbnail in pixels 550 * @param height height of the thumbnail in pixels 551 * @param startMs The start of time range in milliseconds 552 * @param endMs The end of the time range in milliseconds 553 * @param thumbnailCount The thumbnail count 554 * 555 * @return The array of Bitmaps 556 * 557 * @throws IOException if a file error occurs 558 */ 559 public abstract Bitmap[] getThumbnailList(int width, int height, 560 long startMs, long endMs, 561 int thumbnailCount) 562 throws IOException; 563 564 /* 565 * {@inheritDoc} 566 */ 567 @Override 568 public boolean equals(Object object) { 569 if (!(object instanceof MediaItem)) { 570 return false; 571 } 572 return mUniqueId.equals(((MediaItem)object).mUniqueId); 573 } 574 575 /* 576 * {@inheritDoc} 577 */ 578 @Override 579 public int hashCode() { 580 return mUniqueId.hashCode(); 581 } 582 583 /** 584 * Invalidate the start and end transitions if necessary 585 * 586 * @param startTimeMs The start time of the effect or overlay 587 * @param durationMs The duration of the effect or overlay 588 */ 589 abstract void invalidateTransitions(long startTimeMs, long durationMs); 590 591 /** 592 * Invalidate the start and end transitions if necessary. This method is 593 * typically called when the start time and/or duration of an overlay or 594 * effect is changing. 595 * 596 * @param oldStartTimeMs The old start time of the effect or overlay 597 * @param oldDurationMs The old duration of the effect or overlay 598 * @param newStartTimeMs The new start time of the effect or overlay 599 * @param newDurationMs The new duration of the effect or overlay 600 */ 601 abstract void invalidateTransitions(long oldStartTimeMs, long oldDurationMs, 602 long newStartTimeMs, long newDurationMs); 603 604 /** 605 * Check if two items overlap in time 606 * 607 * @param startTimeMs1 Item 1 start time 608 * @param durationMs1 Item 1 duration 609 * @param startTimeMs2 Item 2 start time 610 * @param durationMs2 Item 2 end time 611 * @return true if the two items overlap 612 */ 613 protected boolean isOverlapping(long startTimeMs1, long durationMs1, 614 long startTimeMs2, long durationMs2) { 615 if (startTimeMs1 + durationMs1 <= startTimeMs2) { 616 return false; 617 } else if (startTimeMs1 >= startTimeMs2 + durationMs2) { 618 return false; 619 } 620 621 return true; 622 } 623 624 /** 625 * Adjust the duration transitions. 626 */ 627 protected void adjustTransitions() { 628 /** 629 * Check if the duration of transitions need to be adjusted 630 */ 631 if (mBeginTransition != null) { 632 final long maxDurationMs = mBeginTransition.getMaximumDuration(); 633 if (mBeginTransition.getDuration() > maxDurationMs) { 634 mBeginTransition.setDuration(maxDurationMs); 635 } 636 } 637 638 if (mEndTransition != null) { 639 final long maxDurationMs = mEndTransition.getMaximumDuration(); 640 if (mEndTransition.getDuration() > maxDurationMs) { 641 mEndTransition.setDuration(maxDurationMs); 642 } 643 } 644 } 645 646 /** 647 * @return MediaArtistNativeHleper context 648 */ 649 MediaArtistNativeHelper getNativeContext() { 650 return mMANativeHelper; 651 } 652 653 /** 654 * Initialises ClipSettings fields to default value 655 * 656 * @param ClipSettings object 657 *{@link android.media.videoeditor.MediaArtistNativeHelper.ClipSettings} 658 */ 659 void initClipSettings(ClipSettings clipSettings) { 660 clipSettings.clipPath = null; 661 clipSettings.clipDecodedPath = null; 662 clipSettings.clipOriginalPath = null; 663 clipSettings.fileType = 0; 664 clipSettings.endCutTime = 0; 665 clipSettings.beginCutTime = 0; 666 clipSettings.beginCutPercent = 0; 667 clipSettings.endCutPercent = 0; 668 clipSettings.panZoomEnabled = false; 669 clipSettings.panZoomPercentStart = 0; 670 clipSettings.panZoomTopLeftXStart = 0; 671 clipSettings.panZoomTopLeftYStart = 0; 672 clipSettings.panZoomPercentEnd = 0; 673 clipSettings.panZoomTopLeftXEnd = 0; 674 clipSettings.panZoomTopLeftYEnd = 0; 675 clipSettings.mediaRendering = 0; 676 clipSettings.rgbWidth = 0; 677 clipSettings.rgbHeight = 0; 678 } 679 680 /** 681 * @return ClipSettings object with populated data 682 *{@link android.media.videoeditor.MediaArtistNativeHelper.ClipSettings} 683 */ 684 ClipSettings getClipSettings() { 685 MediaVideoItem mVI = null; 686 MediaImageItem mII = null; 687 ClipSettings clipSettings = new ClipSettings(); 688 initClipSettings(clipSettings); 689 if (this instanceof MediaVideoItem) { 690 mVI = (MediaVideoItem)this; 691 clipSettings.clipPath = mVI.getFilename(); 692 clipSettings.fileType = mMANativeHelper.getMediaItemFileType(mVI. 693 getFileType()); 694 clipSettings.beginCutTime = (int)mVI.getBoundaryBeginTime(); 695 clipSettings.endCutTime = (int)mVI.getBoundaryEndTime(); 696 clipSettings.mediaRendering = mMANativeHelper. 697 getMediaItemRenderingMode(mVI 698 .getRenderingMode()); 699 } else if (this instanceof MediaImageItem) { 700 mII = (MediaImageItem)this; 701 clipSettings = mII.getImageClipProperties(); 702 } 703 return clipSettings; 704 } 705 706 /** 707 * Generates a black frame to be used for generating 708 * begin transition at first media item in storyboard 709 * or end transition at last media item in storyboard 710 * 711 * @param ClipSettings object 712 *{@link android.media.videoeditor.MediaArtistNativeHelper.ClipSettings} 713 */ 714 void generateBlankFrame(ClipSettings clipSettings) { 715 if (!mBlankFrameGenerated) { 716 int mWidth = 64; 717 int mHeight = 64; 718 mBlankFrameFilename = String.format(mProjectPath + "/" + "ghost.rgb"); 719 FileOutputStream fl = null; 720 try { 721 fl = new FileOutputStream(mBlankFrameFilename); 722 } catch (IOException e) { 723 /* catch IO exception */ 724 } 725 final DataOutputStream dos = new DataOutputStream(fl); 726 727 final int [] framingBuffer = new int[mWidth]; 728 729 ByteBuffer byteBuffer = ByteBuffer.allocate(framingBuffer.length * 4); 730 IntBuffer intBuffer; 731 732 byte[] array = byteBuffer.array(); 733 int tmp = 0; 734 while(tmp < mHeight) { 735 intBuffer = byteBuffer.asIntBuffer(); 736 intBuffer.put(framingBuffer,0,mWidth); 737 try { 738 dos.write(array); 739 } catch (IOException e) { 740 /* catch file write error */ 741 } 742 tmp += 1; 743 } 744 745 try { 746 fl.close(); 747 } catch (IOException e) { 748 /* file close error */ 749 } 750 mBlankFrameGenerated = true; 751 } 752 753 clipSettings.clipPath = mBlankFrameFilename; 754 clipSettings.fileType = FileType.JPG; 755 clipSettings.beginCutTime = 0; 756 clipSettings.endCutTime = 0; 757 clipSettings.mediaRendering = MediaRendering.RESIZING; 758 759 clipSettings.rgbWidth = 64; 760 clipSettings.rgbHeight = 64; 761 } 762 763 /** 764 * Invalidates the blank frame generated 765 */ 766 void invalidateBlankFrame() { 767 if (mBlankFrameFilename != null) { 768 if (new File(mBlankFrameFilename).exists()) { 769 new File(mBlankFrameFilename).delete(); 770 mBlankFrameFilename = null; 771 } 772 } 773 } 774 775} 776