MediaVideoItem.java revision 1f5de3833daddfa1d0ebc7dee1f9e007d4ed36a0
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 void getThumbnailList(int width, int height, 297 long startMs, long endMs, 298 int thumbnailCount, 299 int[] indices, 300 GetThumbnailListCallback callback) 301 throws IOException { 302 if (startMs > endMs) { 303 throw new IllegalArgumentException("Start time is greater than end time"); 304 } 305 306 if (endMs > mDurationMs) { 307 throw new IllegalArgumentException("End time is greater than file duration"); 308 } 309 310 if ((height <= 0) || (width <= 0)) { 311 throw new IllegalArgumentException("Invalid dimension"); 312 } 313 314 mMANativeHelper.getPixelsList(super.getFilename(), width, 315 height, startMs, endMs, thumbnailCount, indices, callback); 316 } 317 318 /* 319 * {@inheritDoc} 320 */ 321 @Override 322 void invalidateTransitions(long startTimeMs, long durationMs) { 323 /** 324 * Check if the item overlaps with the beginning and end transitions 325 */ 326 if (mBeginTransition != null) { 327 if (isOverlapping(startTimeMs, durationMs, 328 mBeginBoundaryTimeMs, mBeginTransition.getDuration())) { 329 mBeginTransition.invalidate(); 330 } 331 } 332 333 if (mEndTransition != null) { 334 final long transitionDurationMs = mEndTransition.getDuration(); 335 if (isOverlapping(startTimeMs, durationMs, 336 mEndBoundaryTimeMs - transitionDurationMs, transitionDurationMs)) { 337 mEndTransition.invalidate(); 338 } 339 } 340 } 341 342 /* 343 * {@inheritDoc} 344 */ 345 @Override 346 void invalidateTransitions(long oldStartTimeMs, long oldDurationMs, long newStartTimeMs, 347 long newDurationMs) { 348 /** 349 * Check if the item overlaps with the beginning and end transitions 350 */ 351 if (mBeginTransition != null) { 352 final long transitionDurationMs = mBeginTransition.getDuration(); 353 final boolean oldOverlap = isOverlapping(oldStartTimeMs, oldDurationMs, 354 mBeginBoundaryTimeMs, transitionDurationMs); 355 final boolean newOverlap = isOverlapping(newStartTimeMs, newDurationMs, 356 mBeginBoundaryTimeMs, transitionDurationMs); 357 /** 358 * Invalidate transition if: 359 * 360 * 1. New item overlaps the transition, the old one did not 361 * 2. New item does not overlap the transition, the old one did 362 * 3. New and old item overlap the transition if begin or end 363 * time changed 364 */ 365 if (newOverlap != oldOverlap) { // Overlap has changed 366 mBeginTransition.invalidate(); 367 } else if (newOverlap) { // Both old and new overlap 368 if ((oldStartTimeMs != newStartTimeMs) || 369 !(oldStartTimeMs + oldDurationMs > transitionDurationMs && 370 newStartTimeMs + newDurationMs > transitionDurationMs)) { 371 mBeginTransition.invalidate(); 372 } 373 } 374 } 375 376 if (mEndTransition != null) { 377 final long transitionDurationMs = mEndTransition.getDuration(); 378 final boolean oldOverlap = isOverlapping(oldStartTimeMs, oldDurationMs, 379 mEndBoundaryTimeMs - transitionDurationMs, transitionDurationMs); 380 final boolean newOverlap = isOverlapping(newStartTimeMs, newDurationMs, 381 mEndBoundaryTimeMs - transitionDurationMs, transitionDurationMs); 382 /** 383 * Invalidate transition if: 384 * 385 * 1. New item overlaps the transition, the old one did not 386 * 2. New item does not overlap the transition, the old one did 387 * 3. New and old item overlap the transition if begin or end 388 * time changed 389 */ 390 if (newOverlap != oldOverlap) { // Overlap has changed 391 mEndTransition.invalidate(); 392 } else if (newOverlap) { // Both old and new overlap 393 if ((oldStartTimeMs + oldDurationMs != newStartTimeMs + newDurationMs) || 394 ((oldStartTimeMs > mEndBoundaryTimeMs - transitionDurationMs) || 395 newStartTimeMs > mEndBoundaryTimeMs - transitionDurationMs)) { 396 mEndTransition.invalidate(); 397 } 398 } 399 } 400 } 401 402 /* 403 * {@inheritDoc} 404 */ 405 @Override 406 public int getAspectRatio() { 407 return mAspectRatio; 408 } 409 410 /* 411 * {@inheritDoc} 412 */ 413 @Override 414 public int getFileType() { 415 return mFileType; 416 } 417 418 /* 419 * {@inheritDoc} 420 */ 421 @Override 422 public int getWidth() { 423 return mWidth; 424 } 425 426 /* 427 * {@inheritDoc} 428 */ 429 @Override 430 public int getHeight() { 431 return mHeight; 432 } 433 434 /* 435 * {@inheritDoc} 436 */ 437 @Override 438 public long getDuration() { 439 return mDurationMs; 440 } 441 442 /* 443 * {@inheritDoc} 444 */ 445 @Override 446 public long getTimelineDuration() { 447 return mEndBoundaryTimeMs - mBeginBoundaryTimeMs; 448 } 449 450 /** 451 * Render a frame according to the playback (in the native aspect ratio) for 452 * the specified media item. All effects and overlays applied to the media 453 * item are ignored. The extract boundaries are also ignored. This method 454 * can be used to playback frames when implementing trimming functionality. 455 * 456 * @param surfaceHolder SurfaceHolder used by the application 457 * @param timeMs time corresponding to the frame to display (relative to the 458 * the beginning of the media item). 459 * @return The accurate time stamp of the frame that is rendered . 460 * @throws IllegalStateException if a playback, preview or an export is 461 * already in progress 462 * @throws IllegalArgumentException if time is negative or greater than the 463 * media item duration 464 */ 465 public long renderFrame(SurfaceHolder surfaceHolder, long timeMs) { 466 if (surfaceHolder == null) { 467 throw new IllegalArgumentException("Surface Holder is null"); 468 } 469 470 if (timeMs > mDurationMs || timeMs < 0) { 471 throw new IllegalArgumentException("requested time not correct"); 472 } 473 474 final Surface surface = surfaceHolder.getSurface(); 475 if (surface == null) { 476 throw new RuntimeException("Surface could not be retrieved from Surface holder"); 477 } 478 479 if (mFilename != null) { 480 return mMANativeHelper.renderMediaItemPreviewFrame(surface, 481 mFilename,timeMs,mWidth,mHeight); 482 } else { 483 return 0; 484 } 485 } 486 487 488 /** 489 * This API allows to generate a file containing the sample volume levels of 490 * the Audio track of this media item. This function may take significant 491 * time and is blocking. The file can be retrieved using 492 * getAudioWaveformFilename(). 493 * 494 * @param listener The progress listener 495 * 496 * @throws IOException if the output file cannot be created 497 * @throws IllegalArgumentException if the mediaItem does not have a valid 498 * Audio track 499 */ 500 public void extractAudioWaveform(ExtractAudioWaveformProgressListener listener) 501 throws IOException { 502 int frameDuration = 0; 503 int sampleCount = 0; 504 final String projectPath = mMANativeHelper.getProjectPath(); 505 /** 506 * Waveform file does not exist 507 */ 508 if (mAudioWaveformFilename == null ) { 509 /** 510 * Since audioWaveformFilename will not be supplied,it is generated 511 */ 512 String mAudioWaveFileName = null; 513 514 mAudioWaveFileName = 515 String.format(projectPath + "/" + "audioWaveformFile-"+ getId() + ".dat"); 516 /** 517 * Logic to get frame duration = (no. of frames per sample * 1000)/ 518 * sampling frequency 519 */ 520 if (mMANativeHelper.getAudioCodecType(mAudioType) == 521 MediaProperties.ACODEC_AMRNB ) { 522 frameDuration = (MediaProperties.SAMPLES_PER_FRAME_AMRNB*1000)/ 523 MediaProperties.DEFAULT_SAMPLING_FREQUENCY; 524 sampleCount = MediaProperties.SAMPLES_PER_FRAME_AMRNB; 525 } else if (mMANativeHelper.getAudioCodecType(mAudioType) == 526 MediaProperties.ACODEC_AMRWB ) { 527 frameDuration = (MediaProperties.SAMPLES_PER_FRAME_AMRWB * 1000)/ 528 MediaProperties.DEFAULT_SAMPLING_FREQUENCY; 529 sampleCount = MediaProperties.SAMPLES_PER_FRAME_AMRWB; 530 } else if (mMANativeHelper.getAudioCodecType(mAudioType) == 531 MediaProperties.ACODEC_AAC_LC ) { 532 frameDuration = (MediaProperties.SAMPLES_PER_FRAME_AAC * 1000)/ 533 MediaProperties.DEFAULT_SAMPLING_FREQUENCY; 534 sampleCount = MediaProperties.SAMPLES_PER_FRAME_AAC; 535 } 536 537 mMANativeHelper.generateAudioGraph( getId(), 538 mFilename, 539 mAudioWaveFileName, 540 frameDuration, 541 MediaProperties.DEFAULT_CHANNEL_COUNT, 542 sampleCount, 543 listener, 544 true); 545 /** 546 * Record the generated file name 547 */ 548 mAudioWaveformFilename = mAudioWaveFileName; 549 } 550 mWaveformData = 551 new SoftReference<WaveformData>(new WaveformData(mAudioWaveformFilename)); 552 } 553 554 /** 555 * Get the audio waveform file name if {@link #extractAudioWaveform()} was 556 * successful. The file format is as following: 557 * <ul> 558 * <li>first 4 bytes provide the number of samples for each value, as big-endian signed</li> 559 * <li>4 following bytes is the total number of values in the file, as big-endian signed</li> 560 * <li>all values follow as bytes Name is unique.</li> 561 *</ul> 562 * @return the name of the file, null if the file has not been computed or 563 * if there is no Audio track in the mediaItem 564 */ 565 String getAudioWaveformFilename() { 566 return mAudioWaveformFilename; 567 } 568 569 /** 570 * Invalidate the AudioWaveform File 571 */ 572 void invalidate() { 573 if (mAudioWaveformFilename != null) { 574 new File(mAudioWaveformFilename).delete(); 575 mAudioWaveformFilename = null; 576 } 577 } 578 579 /** 580 * @return The waveform data 581 */ 582 public WaveformData getWaveformData() throws IOException { 583 if (mWaveformData == null) { 584 return null; 585 } 586 587 WaveformData waveformData = mWaveformData.get(); 588 if (waveformData != null) { 589 return waveformData; 590 } else if (mAudioWaveformFilename != null) { 591 try { 592 waveformData = new WaveformData(mAudioWaveformFilename); 593 } catch(IOException e) { 594 throw e; 595 } 596 mWaveformData = new SoftReference<WaveformData>(waveformData); 597 return waveformData; 598 } else { 599 return null; 600 } 601 } 602 603 /** 604 * Set volume of the Audio track of this mediaItem 605 * 606 * @param volumePercent in %/. 100% means no change; 50% means half value, 200% 607 * means double, 0% means silent. 608 * @throws UsupportedOperationException if volume value is not supported 609 */ 610 public void setVolume(int volumePercent) { 611 if ((volumePercent <0) || (volumePercent >100)) { 612 throw new IllegalArgumentException("Invalid volume"); 613 } 614 615 mVolumePercentage = volumePercent; 616 } 617 618 /** 619 * Get the volume value of the audio track as percentage. Call of this 620 * method before calling setVolume will always return 100% 621 * 622 * @return the volume in percentage 623 */ 624 public int getVolume() { 625 return mVolumePercentage; 626 } 627 628 /** 629 * @param muted true to mute the media item 630 */ 631 public void setMute(boolean muted) { 632 mMANativeHelper.setGeneratePreview(true); 633 mMuted = muted; 634 if (mBeginTransition != null) { 635 mBeginTransition.invalidate(); 636 } 637 if (mEndTransition != null) { 638 mEndTransition.invalidate(); 639 } 640 } 641 642 /** 643 * @return true if the media item is muted 644 */ 645 public boolean isMuted() { 646 return mMuted; 647 } 648 649 /** 650 * @return The video type 651 */ 652 public int getVideoType() { 653 return mVideoType; 654 } 655 656 /** 657 * @return The video profile 658 */ 659 public int getVideoProfile() { 660 return mVideoProfile; 661 } 662 663 /** 664 * @return The video bitrate 665 */ 666 public int getVideoBitrate() { 667 return mVideoBitrate; 668 } 669 670 /** 671 * @return The audio bitrate 672 */ 673 public int getAudioBitrate() { 674 return mAudioBitrate; 675 } 676 677 /** 678 * @return The number of frames per second 679 */ 680 public int getFps() { 681 return mFps; 682 } 683 684 /** 685 * @return The audio codec 686 */ 687 public int getAudioType() { 688 return mAudioType; 689 } 690 691 /** 692 * @return The number of audio channels 693 */ 694 public int getAudioChannels() { 695 return mAudioChannels; 696 } 697 698 /** 699 * @return The audio sample frequency 700 */ 701 public int getAudioSamplingFrequency() { 702 return mAudioSamplingFrequency; 703 } 704 705 /** 706 * @return The Video media item properties in ClipSettings class object 707 * {@link android.media.videoeditor.MediaArtistNativeHelper.ClipSettings} 708 */ 709 ClipSettings getVideoClipProperties() { 710 ClipSettings clipSettings = new ClipSettings(); 711 clipSettings.clipPath = getFilename(); 712 clipSettings.fileType = mMANativeHelper.getMediaItemFileType(getFileType()); 713 clipSettings.beginCutTime = (int)getBoundaryBeginTime(); 714 clipSettings.endCutTime = (int)getBoundaryEndTime(); 715 clipSettings.mediaRendering = mMANativeHelper.getMediaItemRenderingMode(getRenderingMode()); 716 717 return clipSettings; 718 } 719} 720