MediaVideoItem.java revision 2b06e56031ef8bd1b5155ec3225f37248d4833b4
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.IOException; 22import java.lang.ref.SoftReference; 23import android.graphics.Bitmap; 24import android.media.videoeditor.MediaArtistNativeHelper.ClipSettings; 25import android.media.videoeditor.MediaArtistNativeHelper.Properties; 26import android.view.Surface; 27import android.view.SurfaceHolder; 28 29/** 30 * This class represents a video clip item on the storyboard 31 * {@hide} 32 */ 33public class MediaVideoItem extends MediaItem { 34 35 /** 36 * Instance variables 37 */ 38 private final int mWidth; 39 private final int mHeight; 40 private final int mAspectRatio; 41 private final int mFileType; 42 private final int mVideoType; 43 private final int mVideoProfile; 44 private final int mVideoBitrate; 45 private final long mDurationMs; 46 private final int mAudioBitrate; 47 private final int mFps; 48 private final int mAudioType; 49 private final int mAudioChannels; 50 private final int mAudioSamplingFrequency; 51 private long mBeginBoundaryTimeMs; 52 private long mEndBoundaryTimeMs; 53 private int mVolumePercentage; 54 private boolean mMuted; 55 private String mAudioWaveformFilename; 56 private MediaArtistNativeHelper mMANativeHelper; 57 private VideoEditorImpl mVideoEditor; 58 /** 59 * The audio waveform data 60 */ 61 private SoftReference<WaveformData> mWaveformData; 62 63 /** 64 * An object of this type cannot be instantiated with a default constructor 65 */ 66 @SuppressWarnings("unused") 67 private MediaVideoItem() throws IOException { 68 this(null, null, null, RENDERING_MODE_BLACK_BORDER); 69 } 70 71 /** 72 * Constructor 73 * 74 * @param editor The video editor reference 75 * @param mediaItemId The MediaItem id 76 * @param filename The image file name 77 * @param renderingMode The rendering mode 78 * 79 * @throws IOException if the file cannot be opened for reading 80 */ 81 public MediaVideoItem(VideoEditor editor, String mediaItemId, String filename, 82 int renderingMode) throws IOException { 83 this(editor, mediaItemId, filename, renderingMode, 0, END_OF_FILE, 100, false, null); 84 } 85 86 /** 87 * Constructor 88 * 89 * @param editor The video editor reference 90 * @param mediaItemId The MediaItem id 91 * @param filename The image file name 92 * @param renderingMode The rendering mode 93 * @param beginMs Start time in milliseconds. Set to 0 to extract from the 94 * beginning 95 * @param endMs End time in milliseconds. Set to {@link #END_OF_FILE} to 96 * extract until the end 97 * @param volumePercent in %/. 100% means no change; 50% means half value, 200% 98 * means double, 0% means silent. 99 * @param muted true if the audio is muted 100 * @param audioWaveformFilename The name of the audio waveform file 101 * 102 * @throws IOException if the file cannot be opened for reading 103 */ 104 MediaVideoItem(VideoEditor editor, String mediaItemId, String filename, 105 int renderingMode, long beginMs, long endMs, int volumePercent, boolean muted, 106 String audioWaveformFilename) throws IOException { 107 super(editor, mediaItemId, filename, renderingMode); 108 109 if (editor instanceof VideoEditorImpl) { 110 mMANativeHelper = ((VideoEditorImpl)editor).getNativeContext(); 111 mVideoEditor = ((VideoEditorImpl)editor); 112 } 113 114 final Properties properties; 115 try { 116 properties = mMANativeHelper.getMediaProperties(filename); 117 } catch ( Exception e) { 118 throw new IllegalArgumentException("Unsupported file or file not found: " + filename); 119 } 120 121 switch (mMANativeHelper.getFileType(properties.fileType)) { 122 case MediaProperties.FILE_3GP: 123 case MediaProperties.FILE_MP4: 124 case MediaProperties.FILE_M4V: 125 break; 126 127 default: 128 throw new IllegalArgumentException("Unsupported Input File Type"); 129 } 130 131 switch (mMANativeHelper.getVideoCodecType(properties.videoFormat)) { 132 case MediaProperties.VCODEC_H263: 133 case MediaProperties.VCODEC_H264BP: 134 case MediaProperties.VCODEC_H264MP: 135 case MediaProperties.VCODEC_MPEG4: 136 break; 137 138 default: 139 throw new IllegalArgumentException("Unsupported Video Codec Format in Input File"); 140 } 141 142 /* Check if the profile is unsupported. */ 143 if (properties.profileAndLevel == MediaProperties.UNDEFINED_VIDEO_PROFILE) { 144 throw new IllegalArgumentException("Unsupported Video Codec Profile in Input File"); 145 } 146 147 mWidth = properties.width; 148 mHeight = properties.height; 149 mAspectRatio = mMANativeHelper.getAspectRatio(properties.width, 150 properties.height); 151 mFileType = mMANativeHelper.getFileType(properties.fileType); 152 mVideoType = mMANativeHelper.getVideoCodecType(properties.videoFormat); 153 mVideoProfile = properties.profileAndLevel; 154 mDurationMs = properties.videoDuration; 155 mVideoBitrate = properties.videoBitrate; 156 mAudioBitrate = properties.audioBitrate; 157 mFps = (int)properties.averageFrameRate; 158 mAudioType = mMANativeHelper.getAudioCodecType(properties.audioFormat); 159 mAudioChannels = properties.audioChannels; 160 mAudioSamplingFrequency = properties.audioSamplingFrequency; 161 mBeginBoundaryTimeMs = beginMs; 162 mEndBoundaryTimeMs = endMs == END_OF_FILE ? mDurationMs : endMs; 163 mVolumePercentage = volumePercent; 164 mMuted = muted; 165 mAudioWaveformFilename = audioWaveformFilename; 166 if (audioWaveformFilename != null) { 167 mWaveformData = new SoftReference<WaveformData>( 168 new WaveformData(audioWaveformFilename)); 169 } else { 170 mWaveformData = null; 171 } 172 } 173 174 /** 175 * Sets the start and end marks for trimming a video media item. 176 * This method will adjust the duration of bounding transitions, effects 177 * and overlays if the current duration of the transactions become greater 178 * than the maximum allowable duration. 179 * 180 * @param beginMs Start time in milliseconds. Set to 0 to extract from the 181 * beginning 182 * @param endMs End time in milliseconds. Set to {@link #END_OF_FILE} to 183 * extract until the end 184 * 185 * @throws IllegalArgumentException if the start time is greater or equal than 186 * end time, the end time is beyond the file duration, the start time 187 * is negative 188 */ 189 public void setExtractBoundaries(long beginMs, long endMs) { 190 if (beginMs > mDurationMs) { 191 throw new IllegalArgumentException("setExtractBoundaries: Invalid start time"); 192 } 193 194 if (endMs > mDurationMs) { 195 throw new IllegalArgumentException("setExtractBoundaries: Invalid end time"); 196 } 197 198 if ((endMs != -1) && (beginMs >= endMs) ) { 199 throw new IllegalArgumentException("setExtractBoundaries: Start time is greater than end time"); 200 } 201 202 if ((beginMs < 0) || ((endMs != -1) && (endMs < 0))) { 203 throw new IllegalArgumentException("setExtractBoundaries: Start time or end time is negative"); 204 } 205 206 mMANativeHelper.setGeneratePreview(true); 207 208 if (beginMs != mBeginBoundaryTimeMs) { 209 if (mBeginTransition != null) { 210 mBeginTransition.invalidate(); 211 } 212 } 213 214 if (endMs != mEndBoundaryTimeMs) { 215 if (mEndTransition != null) { 216 mEndTransition.invalidate(); 217 } 218 } 219 220 mBeginBoundaryTimeMs = beginMs; 221 mEndBoundaryTimeMs = endMs; 222 adjustTransitions(); 223 mVideoEditor.updateTimelineDuration(); 224 /** 225 * Note that the start and duration of any effects and overlays are 226 * not adjusted nor are they automatically removed if they fall 227 * outside the new boundaries. 228 */ 229 } 230 231 /** 232 * @return The boundary begin time 233 */ 234 public long getBoundaryBeginTime() { 235 return mBeginBoundaryTimeMs; 236 } 237 238 /** 239 * @return The boundary end time 240 */ 241 public long getBoundaryEndTime() { 242 return mEndBoundaryTimeMs; 243 } 244 245 /* 246 * {@inheritDoc} 247 */ 248 @Override 249 public void addEffect(Effect effect) { 250 if (effect instanceof EffectKenBurns) { 251 throw new IllegalArgumentException("Ken Burns effects cannot be applied to MediaVideoItem"); 252 } 253 super.addEffect(effect); 254 } 255 256 /* 257 * {@inheritDoc} 258 */ 259 @Override 260 public Bitmap getThumbnail(int width, int height, long timeMs) { 261 if (timeMs > mDurationMs) { 262 throw new IllegalArgumentException("Time Exceeds duration"); 263 } 264 265 if (timeMs < 0) { 266 throw new IllegalArgumentException("Invalid Time duration"); 267 } 268 269 if ((width <= 0) || (height <= 0)) { 270 throw new IllegalArgumentException("Invalid Dimensions"); 271 } 272 273 return mMANativeHelper.getPixels(super.getFilename(), width, height,timeMs); 274 } 275 276 /* 277 * {@inheritDoc} 278 */ 279 @Override 280 public Bitmap[] getThumbnailList(int width, int height, long startMs, 281 long endMs, int thumbnailCount) throws IOException { 282 if (startMs > endMs) { 283 throw new IllegalArgumentException("Start time is greater than end time"); 284 } 285 286 if (endMs > mDurationMs) { 287 throw new IllegalArgumentException("End time is greater than file duration"); 288 } 289 290 if ((height <= 0) || (width <= 0)) { 291 throw new IllegalArgumentException("Invalid dimension"); 292 } 293 294 if (startMs == endMs) { 295 final Bitmap[] bitmap = new Bitmap[1]; 296 bitmap[0] = mMANativeHelper.getPixels(super.getFilename(), width, height,startMs); 297 return bitmap; 298 } 299 300 return mMANativeHelper.getPixelsList(super.getFilename(), width, 301 height,startMs,endMs,thumbnailCount); 302 } 303 304 /* 305 * {@inheritDoc} 306 */ 307 @Override 308 void invalidateTransitions(long startTimeMs, long durationMs) { 309 /** 310 * Check if the item overlaps with the beginning and end transitions 311 */ 312 if (mBeginTransition != null) { 313 if (isOverlapping(startTimeMs, durationMs, 314 mBeginBoundaryTimeMs, mBeginTransition.getDuration())) { 315 mBeginTransition.invalidate(); 316 } 317 } 318 319 if (mEndTransition != null) { 320 final long transitionDurationMs = mEndTransition.getDuration(); 321 if (isOverlapping(startTimeMs, durationMs, 322 mEndBoundaryTimeMs - transitionDurationMs, transitionDurationMs)) { 323 mEndTransition.invalidate(); 324 } 325 } 326 } 327 328 /* 329 * {@inheritDoc} 330 */ 331 @Override 332 void invalidateTransitions(long oldStartTimeMs, long oldDurationMs, long newStartTimeMs, 333 long newDurationMs) { 334 /** 335 * Check if the item overlaps with the beginning and end transitions 336 */ 337 if (mBeginTransition != null) { 338 final long transitionDurationMs = mBeginTransition.getDuration(); 339 final boolean oldOverlap = isOverlapping(oldStartTimeMs, oldDurationMs, 340 mBeginBoundaryTimeMs, transitionDurationMs); 341 final boolean newOverlap = isOverlapping(newStartTimeMs, newDurationMs, 342 mBeginBoundaryTimeMs, transitionDurationMs); 343 /** 344 * Invalidate transition if: 345 * 346 * 1. New item overlaps the transition, the old one did not 347 * 2. New item does not overlap the transition, the old one did 348 * 3. New and old item overlap the transition if begin or end 349 * time changed 350 */ 351 if (newOverlap != oldOverlap) { // Overlap has changed 352 mBeginTransition.invalidate(); 353 } else if (newOverlap) { // Both old and new overlap 354 if ((oldStartTimeMs != newStartTimeMs) || 355 !(oldStartTimeMs + oldDurationMs > transitionDurationMs && 356 newStartTimeMs + newDurationMs > transitionDurationMs)) { 357 mBeginTransition.invalidate(); 358 } 359 } 360 } 361 362 if (mEndTransition != null) { 363 final long transitionDurationMs = mEndTransition.getDuration(); 364 final boolean oldOverlap = isOverlapping(oldStartTimeMs, oldDurationMs, 365 mEndBoundaryTimeMs - transitionDurationMs, transitionDurationMs); 366 final boolean newOverlap = isOverlapping(newStartTimeMs, newDurationMs, 367 mEndBoundaryTimeMs - transitionDurationMs, transitionDurationMs); 368 /** 369 * Invalidate transition if: 370 * 371 * 1. New item overlaps the transition, the old one did not 372 * 2. New item does not overlap the transition, the old one did 373 * 3. New and old item overlap the transition if begin or end 374 * time changed 375 */ 376 if (newOverlap != oldOverlap) { // Overlap has changed 377 mEndTransition.invalidate(); 378 } else if (newOverlap) { // Both old and new overlap 379 if ((oldStartTimeMs + oldDurationMs != newStartTimeMs + newDurationMs) || 380 ((oldStartTimeMs > mEndBoundaryTimeMs - transitionDurationMs) || 381 newStartTimeMs > mEndBoundaryTimeMs - transitionDurationMs)) { 382 mEndTransition.invalidate(); 383 } 384 } 385 } 386 } 387 388 /* 389 * {@inheritDoc} 390 */ 391 @Override 392 public int getAspectRatio() { 393 return mAspectRatio; 394 } 395 396 /* 397 * {@inheritDoc} 398 */ 399 @Override 400 public int getFileType() { 401 return mFileType; 402 } 403 404 /* 405 * {@inheritDoc} 406 */ 407 @Override 408 public int getWidth() { 409 return mWidth; 410 } 411 412 /* 413 * {@inheritDoc} 414 */ 415 @Override 416 public int getHeight() { 417 return mHeight; 418 } 419 420 /* 421 * {@inheritDoc} 422 */ 423 @Override 424 public long getDuration() { 425 return mDurationMs; 426 } 427 428 /* 429 * {@inheritDoc} 430 */ 431 @Override 432 public long getTimelineDuration() { 433 return mEndBoundaryTimeMs - mBeginBoundaryTimeMs; 434 } 435 436 /** 437 * Render a frame according to the playback (in the native aspect ratio) for 438 * the specified media item. All effects and overlays applied to the media 439 * item are ignored. The extract boundaries are also ignored. This method 440 * can be used to playback frames when implementing trimming functionality. 441 * 442 * @param surfaceHolder SurfaceHolder used by the application 443 * @param timeMs time corresponding to the frame to display (relative to the 444 * the beginning of the media item). 445 * @return The accurate time stamp of the frame that is rendered . 446 * @throws IllegalStateException if a playback, preview or an export is 447 * already in progress 448 * @throws IllegalArgumentException if time is negative or greater than the 449 * media item duration 450 */ 451 public long renderFrame(SurfaceHolder surfaceHolder, long timeMs) { 452 if (surfaceHolder == null) { 453 throw new IllegalArgumentException("Surface Holder is null"); 454 } 455 456 if (timeMs > mDurationMs || timeMs < 0) { 457 throw new IllegalArgumentException("requested time not correct"); 458 } 459 460 final Surface surface = surfaceHolder.getSurface(); 461 if (surface == null) { 462 throw new RuntimeException("Surface could not be retrieved from Surface holder"); 463 } 464 465 if (mFilename != null) { 466 return mMANativeHelper.renderMediaItemPreviewFrame(surface, 467 mFilename,timeMs,mWidth,mHeight); 468 } else { 469 return 0; 470 } 471 } 472 473 474 /** 475 * This API allows to generate a file containing the sample volume levels of 476 * the Audio track of this media item. This function may take significant 477 * time and is blocking. The file can be retrieved using 478 * getAudioWaveformFilename(). 479 * 480 * @param listener The progress listener 481 * 482 * @throws IOException if the output file cannot be created 483 * @throws IllegalArgumentException if the mediaItem does not have a valid 484 * Audio track 485 */ 486 public void extractAudioWaveform(ExtractAudioWaveformProgressListener listener) 487 throws IOException { 488 int frameDuration = 0; 489 int sampleCount = 0; 490 final String projectPath = mMANativeHelper.getProjectPath(); 491 /** 492 * Waveform file does not exist 493 */ 494 if (mAudioWaveformFilename == null ) { 495 /** 496 * Since audioWaveformFilename will not be supplied,it is generated 497 */ 498 String mAudioWaveFileName = null; 499 500 mAudioWaveFileName = 501 String.format(projectPath + "/" + "audioWaveformFile-"+ getId() + ".dat"); 502 /** 503 * Logic to get frame duration = (no. of frames per sample * 1000)/ 504 * sampling frequency 505 */ 506 if (mMANativeHelper.getAudioCodecType(mAudioType) == 507 MediaProperties.ACODEC_AMRNB ) { 508 frameDuration = (MediaProperties.SAMPLES_PER_FRAME_AMRNB*1000)/ 509 MediaProperties.DEFAULT_SAMPLING_FREQUENCY; 510 sampleCount = MediaProperties.SAMPLES_PER_FRAME_AMRNB; 511 } else if (mMANativeHelper.getAudioCodecType(mAudioType) == 512 MediaProperties.ACODEC_AMRWB ) { 513 frameDuration = (MediaProperties.SAMPLES_PER_FRAME_AMRWB * 1000)/ 514 MediaProperties.DEFAULT_SAMPLING_FREQUENCY; 515 sampleCount = MediaProperties.SAMPLES_PER_FRAME_AMRWB; 516 } else if (mMANativeHelper.getAudioCodecType(mAudioType) == 517 MediaProperties.ACODEC_AAC_LC ) { 518 frameDuration = (MediaProperties.SAMPLES_PER_FRAME_AAC * 1000)/ 519 MediaProperties.DEFAULT_SAMPLING_FREQUENCY; 520 sampleCount = MediaProperties.SAMPLES_PER_FRAME_AAC; 521 } 522 523 mMANativeHelper.generateAudioGraph( getId(), 524 mFilename, 525 mAudioWaveFileName, 526 frameDuration, 527 MediaProperties.DEFAULT_CHANNEL_COUNT, 528 sampleCount, 529 listener, 530 true); 531 /** 532 * Record the generated file name 533 */ 534 mAudioWaveformFilename = mAudioWaveFileName; 535 } 536 mWaveformData = 537 new SoftReference<WaveformData>(new WaveformData(mAudioWaveformFilename)); 538 } 539 540 /** 541 * Get the audio waveform file name if {@link #extractAudioWaveform()} was 542 * successful. The file format is as following: 543 * <ul> 544 * <li>first 4 bytes provide the number of samples for each value, as big-endian signed</li> 545 * <li>4 following bytes is the total number of values in the file, as big-endian signed</li> 546 * <li>all values follow as bytes Name is unique.</li> 547 *</ul> 548 * @return the name of the file, null if the file has not been computed or 549 * if there is no Audio track in the mediaItem 550 */ 551 String getAudioWaveformFilename() { 552 return mAudioWaveformFilename; 553 } 554 555 /** 556 * Invalidate the AudioWaveform File 557 */ 558 void invalidate() { 559 if (mAudioWaveformFilename != null) { 560 new File(mAudioWaveformFilename).delete(); 561 mAudioWaveformFilename = null; 562 } 563 } 564 565 /** 566 * @return The waveform data 567 */ 568 public WaveformData getWaveformData() throws IOException { 569 if (mWaveformData == null) { 570 return null; 571 } 572 573 WaveformData waveformData = mWaveformData.get(); 574 if (waveformData != null) { 575 return waveformData; 576 } else if (mAudioWaveformFilename != null) { 577 try { 578 waveformData = new WaveformData(mAudioWaveformFilename); 579 } catch(IOException e) { 580 throw e; 581 } 582 mWaveformData = new SoftReference<WaveformData>(waveformData); 583 return waveformData; 584 } else { 585 return null; 586 } 587 } 588 589 /** 590 * Set volume of the Audio track of this mediaItem 591 * 592 * @param volumePercent in %/. 100% means no change; 50% means half value, 200% 593 * means double, 0% means silent. 594 * @throws UsupportedOperationException if volume value is not supported 595 */ 596 public void setVolume(int volumePercent) { 597 if ((volumePercent <0) || (volumePercent >100)) { 598 throw new IllegalArgumentException("Invalid volume"); 599 } 600 601 mVolumePercentage = volumePercent; 602 } 603 604 /** 605 * Get the volume value of the audio track as percentage. Call of this 606 * method before calling setVolume will always return 100% 607 * 608 * @return the volume in percentage 609 */ 610 public int getVolume() { 611 return mVolumePercentage; 612 } 613 614 /** 615 * @param muted true to mute the media item 616 */ 617 public void setMute(boolean muted) { 618 mMANativeHelper.setGeneratePreview(true); 619 mMuted = muted; 620 if (mBeginTransition != null) { 621 mBeginTransition.invalidate(); 622 } 623 if (mEndTransition != null) { 624 mEndTransition.invalidate(); 625 } 626 } 627 628 /** 629 * @return true if the media item is muted 630 */ 631 public boolean isMuted() { 632 return mMuted; 633 } 634 635 /** 636 * @return The video type 637 */ 638 public int getVideoType() { 639 return mVideoType; 640 } 641 642 /** 643 * @return The video profile 644 */ 645 public int getVideoProfile() { 646 return mVideoProfile; 647 } 648 649 /** 650 * @return The video bitrate 651 */ 652 public int getVideoBitrate() { 653 return mVideoBitrate; 654 } 655 656 /** 657 * @return The audio bitrate 658 */ 659 public int getAudioBitrate() { 660 return mAudioBitrate; 661 } 662 663 /** 664 * @return The number of frames per second 665 */ 666 public int getFps() { 667 return mFps; 668 } 669 670 /** 671 * @return The audio codec 672 */ 673 public int getAudioType() { 674 return mAudioType; 675 } 676 677 /** 678 * @return The number of audio channels 679 */ 680 public int getAudioChannels() { 681 return mAudioChannels; 682 } 683 684 /** 685 * @return The audio sample frequency 686 */ 687 public int getAudioSamplingFrequency() { 688 return mAudioSamplingFrequency; 689 } 690 691 /** 692 * @return The Video media item properties in ClipSettings class object 693 * {@link android.media.videoeditor.MediaArtistNativeHelper.ClipSettings} 694 */ 695 ClipSettings getVideoClipProperties() { 696 ClipSettings clipSettings = new ClipSettings(); 697 clipSettings.clipPath = getFilename(); 698 clipSettings.fileType = mMANativeHelper.getMediaItemFileType(getFileType()); 699 clipSettings.beginCutTime = (int)getBoundaryBeginTime(); 700 clipSettings.endCutTime = (int)getBoundaryEndTime(); 701 clipSettings.mediaRendering = mMANativeHelper.getMediaItemRenderingMode(getRenderingMode()); 702 703 return clipSettings; 704 } 705} 706