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