MediaImageItem.java revision 37f7e0c7bfcfa6684a2012e1f22d1a926ab3d8a3
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.IOException; 20import java.util.ArrayList; 21import java.util.List; 22 23import android.graphics.Bitmap; 24import android.graphics.BitmapFactory; 25import android.graphics.Canvas; 26import android.graphics.Paint; 27import android.graphics.Rect; 28import android.util.Log; 29import android.util.Pair; 30 31/** 32 * This class represents an image item on the storyboard. Note that images are 33 * scaled down to the maximum supported resolution by preserving the native 34 * aspect ratio. To learn the scaled image dimensions use 35 * {@link #getScaledWidth()} and {@link #getScaledHeight()} respectively. 36 * 37 * {@hide} 38 */ 39public class MediaImageItem extends MediaItem { 40 // Logging 41 private static final String TAG = "MediaImageItem"; 42 43 // The resize paint 44 private static final Paint sResizePaint = new Paint(Paint.FILTER_BITMAP_FLAG); 45 46 // Instance variables 47 private final int mWidth; 48 private final int mHeight; 49 private final int mAspectRatio; 50 private long mDurationMs; 51 private int mScaledWidth, mScaledHeight; 52 53 /** 54 * This class cannot be instantiated by using the default constructor 55 */ 56 @SuppressWarnings("unused") 57 private MediaImageItem() throws IOException { 58 this(null, null, null, 0, RENDERING_MODE_BLACK_BORDER); 59 } 60 61 /** 62 * Constructor 63 * 64 * @param editor The video editor reference 65 * @param mediaItemId The media item id 66 * @param filename The image file name 67 * @param durationMs The duration of the image on the storyboard 68 * @param renderingMode The rendering mode 69 * 70 * @throws IOException 71 */ 72 public MediaImageItem(VideoEditor editor, String mediaItemId, String filename, long durationMs, 73 int renderingMode) 74 throws IOException { 75 super(editor, mediaItemId, filename, renderingMode); 76 77 // Determine the dimensions of the image 78 final BitmapFactory.Options dbo = new BitmapFactory.Options(); 79 dbo.inJustDecodeBounds = true; 80 BitmapFactory.decodeFile(filename, dbo); 81 82 mWidth = dbo.outWidth; 83 mHeight = dbo.outHeight; 84 mDurationMs = durationMs; 85 86 // TODO: Determine the aspect ratio from the width and height 87 mAspectRatio = MediaProperties.ASPECT_RATIO_4_3; 88 89 // Images are stored in memory scaled to the maximum resolution to 90 // save memory. 91 final Pair<Integer, Integer>[] resolutions = 92 MediaProperties.getSupportedResolutions(mAspectRatio); 93 // Get the highest resolution 94 final Pair<Integer, Integer> maxResolution = resolutions[resolutions.length - 1]; 95 if (mHeight > maxResolution.second) { 96 // We need to scale the image 97 scaleImage(filename, maxResolution.first, maxResolution.second); 98 mScaledWidth = maxResolution.first; 99 mScaledHeight = maxResolution.second; 100 } else { 101 mScaledWidth = mWidth; 102 mScaledHeight = mHeight; 103 } 104 } 105 106 /* 107 * {@inheritDoc} 108 */ 109 @Override 110 public int getFileType() { 111 if (mFilename.endsWith(".jpg") || mFilename.endsWith(".jpeg")) { 112 return MediaProperties.FILE_JPEG; 113 } else if (mFilename.endsWith(".png")) { 114 return MediaProperties.FILE_PNG; 115 } else { 116 return MediaProperties.FILE_UNSUPPORTED; 117 } 118 } 119 120 /* 121 * {@inheritDoc} 122 */ 123 @Override 124 public int getWidth() { 125 return mWidth; 126 } 127 128 /* 129 * {@inheritDoc} 130 */ 131 @Override 132 public int getHeight() { 133 return mHeight; 134 } 135 136 /** 137 * @return The scaled width of the image. 138 */ 139 public int getScaledWidth() { 140 return mScaledWidth; 141 } 142 143 /** 144 * @return The scaled height of the image. 145 */ 146 public int getScaledHeight() { 147 return mScaledHeight; 148 } 149 150 /* 151 * {@inheritDoc} 152 */ 153 @Override 154 public int getAspectRatio() { 155 return mAspectRatio; 156 } 157 158 /** 159 * This method will adjust the duration of bounding transitions, effects 160 * and overlays if the current duration of the transactions become greater 161 * than the maximum allowable duration. 162 * 163 * @param durationMs The duration of the image in the storyboard timeline 164 */ 165 public void setDuration(long durationMs) { 166 if (durationMs == mDurationMs) { 167 return; 168 } 169 170 // Invalidate the end transitions if necessary. 171 // This invalidation is necessary for the case in which an effect or 172 // an overlay is overlapping with the end transition 173 // (before the duration is changed) and it no longer overlaps with the 174 // transition after the duration is increased. 175 176 // The beginning transition does not need to be invalidated at this time 177 // because an effect or an overlay overlaps with the beginning 178 // transition, the begin transition is unaffected by a media item 179 // duration change. 180 invalidateEndTransition(); 181 182 mDurationMs = durationMs; 183 184 adjustTransitions(); 185 final List<Overlay> adjustedOverlays = adjustOverlays(); 186 final List<Effect> adjustedEffects = adjustEffects(); 187 188 // Invalidate the beginning and end transitions after adjustments. 189 // This invalidation is necessary for the case in which an effect or 190 // an overlay was not overlapping with the beginning or end transitions 191 // before the setDuration reduces the duration of the media item and 192 // causes an overlap of the beginning and/or end transition with the 193 // effect. 194 invalidateBeginTransition(adjustedEffects, adjustedOverlays); 195 invalidateEndTransition(); 196 } 197 198 /* 199 * {@inheritDoc} 200 */ 201 @Override 202 public long getDuration() { 203 return mDurationMs; 204 } 205 206 /* 207 * {@inheritDoc} 208 */ 209 @Override 210 public long getTimelineDuration() { 211 return mDurationMs; 212 } 213 214 /* 215 * {@inheritDoc} 216 */ 217 @Override 218 public Bitmap getThumbnail(int width, int height, long timeMs) throws IOException { 219 return scaleImage(mFilename, width, height); 220 } 221 222 /* 223 * {@inheritDoc} 224 */ 225 @Override 226 public Bitmap[] getThumbnailList(int width, int height, long startMs, long endMs, 227 int thumbnailCount) throws IOException { 228 final Bitmap thumbnail = scaleImage(mFilename, width, height); 229 final Bitmap[] thumbnailArray = new Bitmap[thumbnailCount]; 230 for (int i = 0; i < thumbnailCount; i++) { 231 thumbnailArray[i] = thumbnail; 232 } 233 return thumbnailArray; 234 } 235 236 /* 237 * {@inheritDoc} 238 */ 239 @Override 240 void invalidateTransitions(long startTimeMs, long durationMs) { 241 // Check if the item overlaps with the beginning and end transitions 242 if (mBeginTransition != null) { 243 if (isOverlapping(startTimeMs, durationMs, 0, mBeginTransition.getDuration())) { 244 mBeginTransition.invalidate(); 245 } 246 } 247 248 if (mEndTransition != null) { 249 final long transitionDurationMs = mEndTransition.getDuration(); 250 if (isOverlapping(startTimeMs, durationMs, 251 getDuration() - transitionDurationMs, transitionDurationMs)) { 252 mEndTransition.invalidate(); 253 } 254 } 255 } 256 257 /* 258 * {@inheritDoc} 259 */ 260 @Override 261 void invalidateTransitions(long oldStartTimeMs, long oldDurationMs, long newStartTimeMs, 262 long newDurationMs) { 263 // Check if the item overlaps with the beginning and end transitions 264 if (mBeginTransition != null) { 265 final long transitionDurationMs = mBeginTransition.getDuration(); 266 // If the start time has changed and if the old or the new item 267 // overlaps with the begin transition, invalidate the transition. 268 if (oldStartTimeMs != newStartTimeMs && 269 (isOverlapping(oldStartTimeMs, oldDurationMs, 0, transitionDurationMs) || 270 isOverlapping(newStartTimeMs, newDurationMs, 0, transitionDurationMs))) { 271 mBeginTransition.invalidate(); 272 } 273 } 274 275 if (mEndTransition != null) { 276 final long transitionDurationMs = mEndTransition.getDuration(); 277 // If the start time + duration has changed and if the old or the new 278 // item overlaps the end transition, invalidate the transition/ 279 if (oldStartTimeMs + oldDurationMs != newStartTimeMs + newDurationMs && 280 (isOverlapping(oldStartTimeMs, oldDurationMs, 281 mDurationMs - transitionDurationMs, transitionDurationMs) || 282 isOverlapping(newStartTimeMs, newDurationMs, 283 mDurationMs - transitionDurationMs, transitionDurationMs))) { 284 mEndTransition.invalidate(); 285 } 286 } 287 } 288 289 /** 290 * Invalidate the begin transition if any effects and overlays overlap 291 * with the begin transition. 292 * 293 * @param effects List of effects to check for transition overlap 294 * @param overlays List of overlays to check for transition overlap 295 */ 296 private void invalidateBeginTransition(List<Effect> effects, List<Overlay> overlays) { 297 if (mBeginTransition != null && mBeginTransition.isGenerated()) { 298 final long transitionDurationMs = mBeginTransition.getDuration(); 299 300 // The begin transition must be invalidated if it overlaps with 301 // an effect. 302 for (Effect effect : effects) { 303 // Check if the effect overlaps with the begin transition 304 if (effect.getStartTime() < transitionDurationMs) { 305 mBeginTransition.invalidate(); 306 break; 307 } 308 } 309 310 if (mBeginTransition.isGenerated()) { 311 // The end transition must be invalidated if it overlaps with 312 // an overlay. 313 for (Overlay overlay : overlays) { 314 // Check if the overlay overlaps with the end transition 315 if (overlay.getStartTime() < transitionDurationMs) { 316 mBeginTransition.invalidate(); 317 break; 318 } 319 } 320 } 321 } 322 } 323 324 /** 325 * Invalidate the end transition if any effects and overlays overlap 326 * with the end transition. 327 */ 328 private void invalidateEndTransition() { 329 if (mEndTransition != null && mEndTransition.isGenerated()) { 330 final long transitionDurationMs = mEndTransition.getDuration(); 331 332 // The end transition must be invalidated if it overlaps with 333 // an effect. 334 final List<Effect> effects = getAllEffects(); 335 for (Effect effect : effects) { 336 // Check if the effect overlaps with the end transition 337 if (effect.getStartTime() + effect.getDuration() > 338 mDurationMs - transitionDurationMs) { 339 mEndTransition.invalidate(); 340 break; 341 } 342 } 343 344 if (mEndTransition.isGenerated()) { 345 // The end transition must be invalidated if it overlaps with 346 // an overlay. 347 final List<Overlay> overlays = getAllOverlays(); 348 for (Overlay overlay : overlays) { 349 // Check if the overlay overlaps with the end transition 350 if (overlay.getStartTime() + overlay.getDuration() > 351 mDurationMs - transitionDurationMs) { 352 mEndTransition.invalidate(); 353 break; 354 } 355 } 356 } 357 } 358 } 359 360 /** 361 * Adjust the start time and/or duration of effects. 362 * 363 * @return The list of effects which were adjusted 364 */ 365 private List<Effect> adjustEffects() { 366 final List<Effect> adjustedEffects = new ArrayList<Effect>(); 367 final List<Effect> effects = getAllEffects(); 368 for (Effect effect : effects) { 369 // Adjust the start time if necessary 370 final long effectStartTimeMs; 371 if (effect.getStartTime() > getDuration()) { 372 effectStartTimeMs = 0; 373 } else { 374 effectStartTimeMs = effect.getStartTime(); 375 } 376 377 // Adjust the duration if necessary 378 final long effectDurationMs; 379 if (effectStartTimeMs + effect.getDuration() > getDuration()) { 380 effectDurationMs = getDuration() - effectStartTimeMs; 381 } else { 382 effectDurationMs = effect.getDuration(); 383 } 384 385 if (effectStartTimeMs != effect.getStartTime() || 386 effectDurationMs != effect.getDuration()) { 387 effect.setStartTimeAndDuration(effectStartTimeMs, effectDurationMs); 388 adjustedEffects.add(effect); 389 } 390 } 391 392 return adjustedEffects; 393 } 394 395 /** 396 * Adjust the start time and/or duration of overlays. 397 * 398 * @return The list of overlays which were adjusted 399 */ 400 private List<Overlay> adjustOverlays() { 401 final List<Overlay> adjustedOverlays = new ArrayList<Overlay>(); 402 final List<Overlay> overlays = getAllOverlays(); 403 for (Overlay overlay : overlays) { 404 // Adjust the start time if necessary 405 final long overlayStartTimeMs; 406 if (overlay.getStartTime() > getDuration()) { 407 overlayStartTimeMs = 0; 408 } else { 409 overlayStartTimeMs = overlay.getStartTime(); 410 } 411 412 // Adjust the duration if necessary 413 final long overlayDurationMs; 414 if (overlayStartTimeMs + overlay.getDuration() > getDuration()) { 415 overlayDurationMs = getDuration() - overlayStartTimeMs; 416 } else { 417 overlayDurationMs = overlay.getDuration(); 418 } 419 420 if (overlayStartTimeMs != overlay.getStartTime() || 421 overlayDurationMs != overlay.getDuration()) { 422 overlay.setStartTimeAndDuration(overlayStartTimeMs, overlayDurationMs); 423 adjustedOverlays.add(overlay); 424 } 425 } 426 427 return adjustedOverlays; 428 } 429 430 /** 431 * Resize a bitmap to the specified width and height 432 * 433 * @param filename The filename 434 * @param width The thumbnail width 435 * @param height The thumbnail height 436 * 437 * @return The resized bitmap 438 */ 439 private Bitmap scaleImage(String filename, int width, int height) throws IOException { 440 final BitmapFactory.Options dbo = new BitmapFactory.Options(); 441 dbo.inJustDecodeBounds = true; 442 BitmapFactory.decodeFile(filename, dbo); 443 444 final int nativeWidth = dbo.outWidth; 445 final int nativeHeight = dbo.outHeight; 446 if (Log.isLoggable(TAG, Log.DEBUG)) { 447 Log.d(TAG, "generateThumbnail: Input: " + nativeWidth + "x" + nativeHeight 448 + ", resize to: " + width + "x" + height); 449 } 450 451 final Bitmap srcBitmap; 452 float bitmapWidth, bitmapHeight; 453 if (nativeWidth > width || nativeHeight > height) { 454 float dx = ((float)nativeWidth) / ((float)width); 455 float dy = ((float)nativeHeight) / ((float)height); 456 if (dx > dy) { 457 bitmapWidth = width; 458 bitmapHeight = nativeHeight / dx; 459 } else { 460 bitmapWidth = nativeWidth / dy; 461 bitmapHeight = height; 462 } 463 // Create the bitmap from file 464 if (nativeWidth / bitmapWidth > 1) { 465 final BitmapFactory.Options options = new BitmapFactory.Options(); 466 options.inSampleSize = nativeWidth / (int)bitmapWidth; 467 srcBitmap = BitmapFactory.decodeFile(filename, options); 468 } else { 469 srcBitmap = BitmapFactory.decodeFile(filename); 470 } 471 } else { 472 bitmapWidth = width; 473 bitmapHeight = height; 474 srcBitmap = BitmapFactory.decodeFile(filename); 475 } 476 477 if (srcBitmap == null) { 478 Log.e(TAG, "generateThumbnail: Cannot decode image bytes"); 479 throw new IOException("Cannot decode file: " + mFilename); 480 } 481 482 // Create the canvas bitmap 483 final Bitmap bitmap = Bitmap.createBitmap((int)bitmapWidth, (int)bitmapHeight, 484 Bitmap.Config.ARGB_8888); 485 final Canvas canvas = new Canvas(bitmap); 486 canvas.drawBitmap(srcBitmap, new Rect(0, 0, srcBitmap.getWidth(), srcBitmap.getHeight()), 487 new Rect(0, 0, (int)bitmapWidth, (int)bitmapHeight), sResizePaint); 488 // Release the source bitmap 489 srcBitmap.recycle(); 490 return bitmap; 491 } 492} 493