From 21e9da6f446301756ddabbfb9d61155db5480366 Mon Sep 17 00:00:00 2001 From: Gil Dobjanschi Date: Wed, 22 Sep 2010 20:03:58 -0700 Subject: [PATCH] 1. Added OverlayFrame constuctor 2. Invalidate transitions when trimming video 3. Remove image file when removing an OverlayFrame 4. Bug fixes in the VideoEditor implementation Change-Id: I7bd3c888848edaf659a4faef14ad1c5a6603c3cc --- .../android/media/videoeditor/Effect.java | 2 +- .../android/media/videoeditor/MediaItem.java | 35 +++- .../media/videoeditor/MediaVideoItem.java | 15 +- .../media/videoeditor/OverlayFrame.java | 86 +++++++- .../media/videoeditor/TransitionAlpha.java | 14 +- .../videoeditor/VideoEditorTestImpl.java | 192 +++++++++--------- 6 files changed, 231 insertions(+), 113 deletions(-) diff --git a/media/java/android/media/videoeditor/Effect.java b/media/java/android/media/videoeditor/Effect.java index 177b8631e85a5..8c3957767efac 100755 --- a/media/java/android/media/videoeditor/Effect.java +++ b/media/java/android/media/videoeditor/Effect.java @@ -84,7 +84,7 @@ public abstract class Effect { * Set start time of the effect. If a preview or export is in progress, then * this change is effective for next preview or export session. * - * @param startTimeMs The start time of the effect relative to the begining + * @param startTimeMs The start time of the effect relative to the beginning * of the media item in milliseconds */ public void setStartTime(long startTimeMs) { diff --git a/media/java/android/media/videoeditor/MediaItem.java b/media/java/android/media/videoeditor/MediaItem.java index 9e32744059999..e7be35dd7d9ec 100755 --- a/media/java/android/media/videoeditor/MediaItem.java +++ b/media/java/android/media/videoeditor/MediaItem.java @@ -64,8 +64,8 @@ public abstract class MediaItem { private int mRenderingMode; // Beginning and end transitions - private Transition mBeginTransition; - private Transition mEndTransition; + protected Transition mBeginTransition; + protected Transition mEndTransition; /** * Constructor @@ -113,6 +113,13 @@ public abstract class MediaItem { */ public void setRenderingMode(int renderingMode) { mRenderingMode = renderingMode; + if (mBeginTransition != null) { + mBeginTransition.invalidate(); + } + + if (mEndTransition != null) { + mEndTransition.invalidate(); + } } /** @@ -271,7 +278,9 @@ public abstract class MediaItem { * * @param overlay The overlay to add * @throws IllegalStateException if a preview or an export is in progress or - * if the overlay id is not unique across all the overlays added. + * if the overlay id is not unique across all the overlays + * added or if the bitmap is not specified or if the dimensions of + * the bitmap do not match the dimensions of the media item */ public void addOverlay(Overlay overlay) { if (mOverlays.contains(overlay)) { @@ -283,6 +292,23 @@ public abstract class MediaItem { "Overlay start time + overlay duration > media clip duration"); } + if (overlay instanceof OverlayFrame) { + final OverlayFrame frame = (OverlayFrame)overlay; + final Bitmap bitmap = frame.getBitmap(); + if (bitmap == null) { + throw new IllegalArgumentException("Overlay bitmap not specified"); + } + + // The dimensions of the overlay bitmap must be the same as the + // media item dimensions + if (bitmap.getWidth() != getWidth() || bitmap.getHeight() != getHeight()) { + throw new IllegalArgumentException( + "Bitmap dimensions must match media item dimensions"); + } + } else { + throw new IllegalArgumentException("Overlay not supported"); + } + mOverlays.add(overlay); invalidateTransitions(overlay); } @@ -302,6 +328,9 @@ public abstract class MediaItem { for (Overlay overlay : mOverlays) { if (overlay.getId().equals(overlayId)) { mOverlays.remove(overlay); + if (overlay instanceof OverlayFrame) { + ((OverlayFrame)overlay).invalidate(); + } invalidateTransitions(overlay); return overlay; } diff --git a/media/java/android/media/videoeditor/MediaVideoItem.java b/media/java/android/media/videoeditor/MediaVideoItem.java index 87e9a2241b295..af50b839833d6 100755 --- a/media/java/android/media/videoeditor/MediaVideoItem.java +++ b/media/java/android/media/videoeditor/MediaVideoItem.java @@ -264,8 +264,19 @@ public class MediaVideoItem extends MediaItem { throw new IllegalArgumentException("Invalid end time"); } - mBeginBoundaryTimeMs = beginMs; - mEndBoundaryTimeMs = endMs; + if (beginMs != mBeginBoundaryTimeMs) { + mBeginBoundaryTimeMs = beginMs; + if (mBeginTransition != null) { + mBeginTransition.invalidate(); + } + } + + if (endMs == mEndBoundaryTimeMs) { + mEndBoundaryTimeMs = endMs; + if (mEndTransition != null) { + mEndTransition.invalidate(); + } + } // TODO: Validate/modify the start and the end time of effects and overlays } diff --git a/media/java/android/media/videoeditor/OverlayFrame.java b/media/java/android/media/videoeditor/OverlayFrame.java index e5d9b811be5a9..3abac6c41393c 100755 --- a/media/java/android/media/videoeditor/OverlayFrame.java +++ b/media/java/android/media/videoeditor/OverlayFrame.java @@ -16,16 +16,24 @@ package android.media.videoeditor; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Bitmap.CompressFormat; + /** - * This class is used to overlay an image on top of a media item. This class - * does not manage deletion of the overlay file so application may use - * {@link #getFilename()} for this purpose. + * This class is used to overlay an image on top of a media item. * {@hide} */ public class OverlayFrame extends Overlay { // Instance variables - private final String mFilename; + private final Bitmap mBitmap; + private String mFilename; /** * An object of this type cannot be instantiated by using the default @@ -33,30 +41,90 @@ public class OverlayFrame extends Overlay { */ @SuppressWarnings("unused") private OverlayFrame() { - this(null, null, 0, 0); + this(null, (String)null, 0, 0); } /** * Constructor for an OverlayFrame * * @param overlayId The overlay id - * @param filename The file name that contains the overlay. Only PNG - * supported. + * @param bitmap The bitmap to be used as an overlay. The size of the + * bitmap must equal to the size of the media item to which it is + * added. The bitmap is typically a decoded PNG file. * @param startTimeMs The overlay start time in milliseconds * @param durationMs The overlay duration in milliseconds * * @throws IllegalArgumentException if the file type is not PNG or the * startTimeMs and durationMs are incorrect. */ - public OverlayFrame(String overlayId, String filename, long startTimeMs, long durationMs) { + public OverlayFrame(String overlayId, Bitmap bitmap, long startTimeMs, + long durationMs) { + super(overlayId, startTimeMs, durationMs); + mBitmap = bitmap; + mFilename = null; + } + + /** + * Constructor for an OverlayFrame. This constructor can be used to + * restore the overlay after it was saved internally by the video editor. + * + * @param overlayId The overlay id + * @param filename The file name that contains the overlay. + * @param startTimeMs The overlay start time in milliseconds + * @param durationMs The overlay duration in milliseconds + * + * @throws IllegalArgumentException if the file type is not PNG or the + * startTimeMs and durationMs are incorrect. + */ + OverlayFrame(String overlayId, String filename, long startTimeMs, long durationMs) { super(overlayId, startTimeMs, durationMs); mFilename = filename; + mBitmap = BitmapFactory.decodeFile(mFilename); + } + + /** + * @return Get the overlay bitmap + */ + public Bitmap getBitmap() { + return mBitmap; } /** * Get the file name of this overlay */ - public String getFilename() { + String getFilename() { return mFilename; } + + /** + * Save the overlay to the project folder + * + * @param editor The video editor + * + * @return + * @throws FileNotFoundException if the bitmap cannot be saved + * @throws IOException if the bitmap file cannot be saved + */ + String save(VideoEditor editor) throws FileNotFoundException, IOException { + if (mFilename != null) { + return mFilename; + } + + mFilename = editor.getPath() + "/" + getId() + ".png"; + // Save the image to a local file + final FileOutputStream out = new FileOutputStream(mFilename); + mBitmap.compress(CompressFormat.PNG, 100, out); + out.flush(); + out.close(); + return mFilename; + } + + /** + * Delete the overlay file + */ + void invalidate() { + if (mFilename != null) { + new File(mFilename).delete(); + } + } } diff --git a/media/java/android/media/videoeditor/TransitionAlpha.java b/media/java/android/media/videoeditor/TransitionAlpha.java index 0a4a12f0fe027..30e66fca9d2b1 100755 --- a/media/java/android/media/videoeditor/TransitionAlpha.java +++ b/media/java/android/media/videoeditor/TransitionAlpha.java @@ -58,17 +58,19 @@ public class TransitionAlpha extends Transition { * Constructor * * @param transitionId The transition id - * @param afterMediaItem The transition is applied to the end of this - * media item - * @param beforeMediaItem The transition is applied to the beginning of - * this media item + * @param afterMediaItem The transition is applied to the end of this media + * item + * @param beforeMediaItem The transition is applied to the beginning of this + * media item * @param durationMs duration of the transition in milliseconds * @param behavior behavior is one of the behavior defined in Transition * class - * @param maskFilename JPEG file name + * @param maskFilename JPEG file name. The dimension of the image + * corresponds to 720p (16:9 aspect ratio). Mask files are + * shared between video editors and can be created in the + * projects folder (the parent folder for all projects). * @param blendingPercent The blending percent applied * @param invert true to invert the direction of the alpha blending - * * @throws IllegalArgumentException if behavior is not supported, or if * direction are not supported. */ diff --git a/media/java/android/media/videoeditor/VideoEditorTestImpl.java b/media/java/android/media/videoeditor/VideoEditorTestImpl.java index 14e26588b6990..7226d5d7b9ab2 100644 --- a/media/java/android/media/videoeditor/VideoEditorTestImpl.java +++ b/media/java/android/media/videoeditor/VideoEditorTestImpl.java @@ -48,8 +48,8 @@ public class VideoEditorTestImpl implements VideoEditor { private static final String TAG_PROJECT = "project"; private static final String TAG_MEDIA_ITEMS = "media_items"; private static final String TAG_MEDIA_ITEM = "media_item"; - private static final String TAG_BEGIN_TRANSITION = "begin_transition"; - private static final String TAG_END_TRANSITION = "end_transition"; + private static final String TAG_TRANSITIONS = "transitions"; + private static final String TAG_TRANSITION = "transition"; private static final String ATTR_ID = "id"; private static final String ATTR_FILENAME = "filename"; private static final String ATTR_AUDIO_WAVEFORM_FILENAME = "wavefoem"; @@ -65,6 +65,8 @@ public class VideoEditorTestImpl implements VideoEditor { private static final String ATTR_BLENDING = "blending"; private static final String ATTR_INVERT = "invert"; private static final String ATTR_MASK = "mask"; + private static final String ATTR_BEFORE_MEDIA_ITEM_ID = "before_media_item"; + private static final String ATTR_AFTER_MEDIA_ITEM_ID = "after_media_item"; // Instance variables private long mDurationMs; @@ -400,6 +402,7 @@ public class VideoEditorTestImpl implements VideoEditor { } beforeMediaItem.setBeginTransition(transition); } + computeTimelineDuration(); } @@ -412,22 +415,25 @@ public class VideoEditorTestImpl implements VideoEditor { } final Transition transition = getTransition(transitionId); - if (transition != null) { - mTransitions.remove(transition); - transition.invalidate(); - computeTimelineDuration(); + if (transition == null) { + throw new IllegalStateException("Transition not found: " + transitionId); } - // Cross reference the transitions + // Remove the transition references final MediaItem afterMediaItem = transition.getAfterMediaItem(); if (afterMediaItem != null) { afterMediaItem.setEndTransition(null); } + final MediaItem beforeMediaItem = transition.getBeforeMediaItem(); if (beforeMediaItem != null) { beforeMediaItem.setBeginTransition(null); } + mTransitions.remove(transition); + transition.invalidate(); + computeTimelineDuration(); + return transition; } @@ -543,15 +549,14 @@ public class VideoEditorTestImpl implements VideoEditor { serializer.startTag("", TAG_PROJECT); serializer.attribute("", ATTR_ASPECT_RATIO, Integer.toString(mAspectRatio)); - boolean firstMediaItem = true; serializer.startTag("", TAG_MEDIA_ITEMS); for (MediaItem mediaItem : mMediaItems) { serializer.startTag("", TAG_MEDIA_ITEM); serializer.attribute("", ATTR_ID, mediaItem.getId()); serializer.attribute("", ATTR_TYPE, mediaItem.getClass().getSimpleName()); serializer.attribute("", ATTR_FILENAME, mediaItem.getFilename()); - serializer.attribute("", ATTR_RENDERING_MODE, Integer.toString(mediaItem - .getRenderingMode())); + serializer.attribute("", ATTR_RENDERING_MODE, Integer.toString( + mediaItem.getRenderingMode())); if (mediaItem instanceof MediaVideoItem) { final MediaVideoItem mvi = (MediaVideoItem)mediaItem; serializer @@ -559,55 +564,50 @@ public class VideoEditorTestImpl implements VideoEditor { serializer.attribute("", ATTR_END_TIME, Long.toString(mvi.getBoundaryEndTime())); serializer.attribute("", ATTR_VOLUME, Integer.toString(mvi.getVolume())); if (mvi.getAudioWaveformFilename() != null) { - serializer.attribute("", ATTR_AUDIO_WAVEFORM_FILENAME, mvi - .getAudioWaveformFilename()); + serializer.attribute("", ATTR_AUDIO_WAVEFORM_FILENAME, + mvi.getAudioWaveformFilename()); } } else if (mediaItem instanceof MediaImageItem) { serializer.attribute("", ATTR_DURATION, Long.toString(mediaItem.getDuration())); } - if (firstMediaItem) { - firstMediaItem = false; - final Transition beginTransition = mediaItem.getBeginTransition(); - if (beginTransition != null) { - serializer.startTag("", TAG_BEGIN_TRANSITION); - serializer.attribute("", ATTR_ID, beginTransition.getId()); - serializer.attribute("", ATTR_TYPE, beginTransition.getClass() - .getSimpleName()); - serializer.attribute("", ATTR_DURATION, Long.toString(beginTransition - .getDuration())); - serializer.attribute("", ATTR_BEHAVIOR, Integer.toString(beginTransition - .getBehavior())); - serializer.endTag("", TAG_BEGIN_TRANSITION); - } - } - - final Transition endTransition = mediaItem.getEndTransition(); - if (endTransition != null) { - serializer.startTag("", TAG_END_TRANSITION); - serializer.attribute("", ATTR_ID, endTransition.getId()); - serializer.attribute("", ATTR_TYPE, endTransition.getClass().getSimpleName()); - serializer.attribute("", ATTR_DURATION, Long.toString(endTransition - .getDuration())); - serializer.attribute("", ATTR_BEHAVIOR, Integer.toString(endTransition - .getBehavior())); - if (endTransition instanceof TransitionSliding) { - serializer.attribute("", ATTR_DIRECTION, Integer - .toString(((TransitionSliding)endTransition).getDirection())); - } else if (endTransition instanceof TransitionAlpha) { - TransitionAlpha ta = (TransitionAlpha)endTransition; - serializer.attribute("", ATTR_BLENDING, Integer.toString(ta - .getBlendingPercent())); - serializer.attribute("", ATTR_INVERT, Boolean.toString(ta.isInvert())); - serializer.attribute("", ATTR_MASK, ta.getMaskFilename()); - } - serializer.endTag("", TAG_END_TRANSITION); - } - serializer.endTag("", TAG_MEDIA_ITEM); } serializer.endTag("", TAG_MEDIA_ITEMS); + serializer.startTag("", TAG_TRANSITIONS); + + for (Transition transition : mTransitions) { + serializer.startTag("", TAG_TRANSITION); + serializer.attribute("", ATTR_ID, transition.getId()); + serializer.attribute("", ATTR_TYPE, transition.getClass().getSimpleName()); + serializer.attribute("", ATTR_DURATION, Long.toString(transition.getDuration())); + serializer.attribute("", ATTR_BEHAVIOR, Integer.toString(transition.getBehavior())); + final MediaItem afterMediaItem = transition.getAfterMediaItem(); + if (afterMediaItem != null) { + serializer.attribute("", ATTR_AFTER_MEDIA_ITEM_ID, afterMediaItem.getId()); + } + + final MediaItem beforeMediaItem = transition.getBeforeMediaItem(); + if (beforeMediaItem != null) { + serializer.attribute("", ATTR_BEFORE_MEDIA_ITEM_ID, beforeMediaItem.getId()); + } + + if (transition instanceof TransitionSliding) { + serializer.attribute("", ATTR_DIRECTION, + Integer.toString(((TransitionSliding)transition).getDirection())); + } else if (transition instanceof TransitionAlpha) { + TransitionAlpha ta = (TransitionAlpha)transition; + serializer.attribute("", ATTR_BLENDING, Integer.toString(ta.getBlendingPercent())); + serializer.attribute("", ATTR_INVERT, Boolean.toString(ta.isInvert())); + if (ta.getMaskFilename() != null) { + serializer.attribute("", ATTR_MASK, ta.getMaskFilename()); + } + } + serializer.endTag("", TAG_TRANSITION); + } + serializer.endTag("", TAG_TRANSITIONS); + serializer.endTag("", TAG_PROJECT); serializer.endDocument(); @@ -628,8 +628,6 @@ public class VideoEditorTestImpl implements VideoEditor { parser.setInput(new FileInputStream(file), "UTF-8"); int eventType = parser.getEventType(); String name; - MediaItem currentMediaItem = null; - MediaItem previousMediaItem = null; while (eventType != XmlPullParser.END_DOCUMENT) { switch (eventType) { case XmlPullParser.START_TAG: { @@ -644,6 +642,7 @@ public class VideoEditorTestImpl implements VideoEditor { final int renderingMode = Integer.parseInt(parser.getAttributeValue("", ATTR_RENDERING_MODE)); + MediaItem currentMediaItem; if (MediaImageItem.class.getSimpleName().equals(type)) { final long durationMs = Long.parseLong(parser.getAttributeValue("", ATTR_DURATION)); @@ -671,29 +670,13 @@ public class VideoEditorTestImpl implements VideoEditor { } if (currentMediaItem != null) { - if (previousMediaItem != null) { - currentMediaItem.setBeginTransition(previousMediaItem - .getEndTransition()); - } mMediaItems.add(currentMediaItem); } - } else if (name.equals(TAG_BEGIN_TRANSITION)) { - final Transition transition = parseTransition(parser, currentMediaItem, - null); - currentMediaItem.setBeginTransition(transition); - } else if (name.equals(TAG_END_TRANSITION)) { - final Transition transition = parseTransition(parser, previousMediaItem, - currentMediaItem); - currentMediaItem.setEndTransition(transition); - } - break; - } - - case XmlPullParser.END_TAG: { - name = parser.getName(); - if (name.equals(TAG_MEDIA_ITEM)) { - previousMediaItem = currentMediaItem; - currentMediaItem = null; + } else if (name.equals(TAG_TRANSITION)) { + final Transition transition = parseTransition(parser); + if (transition != null) { + mTransitions.add(transition); + } } break; } @@ -712,47 +695,72 @@ public class VideoEditorTestImpl implements VideoEditor { * Parse the transition * * @param parser The parser - * @param afterMediaItem The transition is at the end of this media item - * @param beforeMediaItem The transition is at the beginning of this media - * item * @return The transition */ - private Transition parseTransition(XmlPullParser parser, MediaItem beforeMediaItem, - MediaItem afterMediaItem) { + private Transition parseTransition(XmlPullParser parser) { final String transitionId = parser.getAttributeValue("", ATTR_ID); final String type = parser.getAttributeValue("", ATTR_TYPE); final long durationMs = Long.parseLong(parser.getAttributeValue("", ATTR_DURATION)); final int behavior = Integer.parseInt(parser.getAttributeValue("", ATTR_BEHAVIOR)); + + final String beforeMediaItemId = parser.getAttributeValue("", ATTR_BEFORE_MEDIA_ITEM_ID); + final MediaItem beforeMediaItem; + if (beforeMediaItemId != null) { + beforeMediaItem = getMediaItem(beforeMediaItemId); + } else { + beforeMediaItem = null; + } + + final String afterMediaItemId = parser.getAttributeValue("", ATTR_AFTER_MEDIA_ITEM_ID); + final MediaItem afterMediaItem; + if (afterMediaItemId != null) { + afterMediaItem = getMediaItem(afterMediaItemId); + } else { + afterMediaItem = null; + } + + final Transition transition; if (TransitionStartCurtainOpening.class.getSimpleName().equals(type)) { - return new TransitionStartCurtainOpening(transitionId, beforeMediaItem, durationMs, - behavior); + transition = new TransitionStartCurtainOpening(transitionId, beforeMediaItem, + durationMs, behavior); } else if (TransitionStartFadeFromBlack.class.getSimpleName().equals(type)) { - return new TransitionStartFadeFromBlack(transitionId, beforeMediaItem, durationMs, - behavior); + transition = new TransitionStartFadeFromBlack(transitionId, beforeMediaItem, + durationMs, behavior); } else if (TransitionAlpha.class.getSimpleName().equals(type)) { final int blending = Integer.parseInt(parser.getAttributeValue("", ATTR_BLENDING)); final String maskFilename = parser.getAttributeValue("", ATTR_MASK); final boolean invert = Boolean.getBoolean(parser.getAttributeValue("", ATTR_INVERT)); - return new TransitionAlpha(transitionId, afterMediaItem, beforeMediaItem, durationMs, - behavior, maskFilename, blending, invert); - } else if (TransitionAlpha.class.getSimpleName().equals(type)) { - return new TransitionCrossfade(transitionId, afterMediaItem, beforeMediaItem, + transition = new TransitionAlpha(transitionId, afterMediaItem, beforeMediaItem, + durationMs, behavior, maskFilename, blending, invert); + } else if (TransitionCrossfade.class.getSimpleName().equals(type)) { + transition = new TransitionCrossfade(transitionId, afterMediaItem, beforeMediaItem, durationMs, behavior); } else if (TransitionSliding.class.getSimpleName().equals(type)) { final int direction = Integer.parseInt(parser.getAttributeValue("", ATTR_DIRECTION)); - return new TransitionSliding(transitionId, afterMediaItem, beforeMediaItem, durationMs, - behavior, direction); + transition = new TransitionSliding(transitionId, afterMediaItem, beforeMediaItem, + durationMs, behavior, direction); } else if (TransitionFadeToBlack.class.getSimpleName().equals(type)) { - return new TransitionFadeToBlack(transitionId, afterMediaItem, beforeMediaItem, + transition = new TransitionFadeToBlack(transitionId, afterMediaItem, beforeMediaItem, durationMs, behavior); } else if (TransitionEndCurtainClosing.class.getSimpleName().equals(type)) { - return new TransitionEndCurtainClosing(transitionId, beforeMediaItem, durationMs, + transition = new TransitionEndCurtainClosing(transitionId, afterMediaItem, durationMs, behavior); } else if (TransitionEndFadeToBlack.class.getSimpleName().equals(type)) { - return new TransitionEndFadeToBlack(transitionId, beforeMediaItem, durationMs, behavior); + transition = new TransitionEndFadeToBlack(transitionId, afterMediaItem, durationMs, + behavior); + } else { + transition = null; } - return null; + if (beforeMediaItem != null) { + beforeMediaItem.setBeginTransition(transition); + } + + if (afterMediaItem != null) { + afterMediaItem.setEndTransition(transition); + } + + return transition; } public void cancelExport(String filename) {