From fdacc8be92cd36f712cfdb0fcf9b0e847f8eeb58 Mon Sep 17 00:00:00 2001 From: Gil Dobjanschi Date: Tue, 7 Sep 2010 11:16:24 -0700 Subject: [PATCH] Initial Video Editor API Change-Id: Iaa91e78d0e50f45ceb943bab93c4f1ea1bdee003 --- .../android/media/videoeditor/AudioTrack.java | 326 ++++++++ .../android/media/videoeditor/Effect.java | 119 +++ .../media/videoeditor/EffectColor.java | 100 +++ .../media/videoeditor/EffectKenBurns.java | 90 ++ .../ExtractAudioWaveformProgressListener.java | 37 + .../media/videoeditor/MediaImageItem.java | 227 +++++ .../android/media/videoeditor/MediaItem.java | 434 ++++++++++ .../media/videoeditor/MediaProperties.java | 256 ++++++ .../media/videoeditor/MediaVideoItem.java | 541 ++++++++++++ .../android/media/videoeditor/Overlay.java | 117 +++ .../media/videoeditor/OverlayFrame.java | 62 ++ .../android/media/videoeditor/Transition.java | 182 ++++ .../media/videoeditor/TransitionAlpha.java | 112 +++ .../media/videoeditor/TransitionAtEnd.java | 81 ++ .../media/videoeditor/TransitionAtStart.java | 90 ++ .../videoeditor/TransitionCrossfade.java | 60 ++ .../videoeditor/TransitionFadeToBlack.java | 59 ++ .../media/videoeditor/TransitionSliding.java | 82 ++ .../media/videoeditor/VideoEditor.java | 493 +++++++++++ .../media/videoeditor/VideoEditorFactory.java | 83 ++ .../videoeditor/VideoEditorTestImpl.java | 777 ++++++++++++++++++ 21 files changed, 4328 insertions(+) create mode 100755 media/java/android/media/videoeditor/AudioTrack.java create mode 100755 media/java/android/media/videoeditor/Effect.java create mode 100755 media/java/android/media/videoeditor/EffectColor.java create mode 100755 media/java/android/media/videoeditor/EffectKenBurns.java create mode 100644 media/java/android/media/videoeditor/ExtractAudioWaveformProgressListener.java create mode 100755 media/java/android/media/videoeditor/MediaImageItem.java create mode 100755 media/java/android/media/videoeditor/MediaItem.java create mode 100755 media/java/android/media/videoeditor/MediaProperties.java create mode 100755 media/java/android/media/videoeditor/MediaVideoItem.java create mode 100755 media/java/android/media/videoeditor/Overlay.java create mode 100755 media/java/android/media/videoeditor/OverlayFrame.java create mode 100755 media/java/android/media/videoeditor/Transition.java create mode 100755 media/java/android/media/videoeditor/TransitionAlpha.java create mode 100755 media/java/android/media/videoeditor/TransitionAtEnd.java create mode 100755 media/java/android/media/videoeditor/TransitionAtStart.java create mode 100755 media/java/android/media/videoeditor/TransitionCrossfade.java create mode 100755 media/java/android/media/videoeditor/TransitionFadeToBlack.java create mode 100755 media/java/android/media/videoeditor/TransitionSliding.java create mode 100755 media/java/android/media/videoeditor/VideoEditor.java create mode 100755 media/java/android/media/videoeditor/VideoEditorFactory.java create mode 100644 media/java/android/media/videoeditor/VideoEditorTestImpl.java diff --git a/media/java/android/media/videoeditor/AudioTrack.java b/media/java/android/media/videoeditor/AudioTrack.java new file mode 100755 index 0000000000000..9d40a784185c8 --- /dev/null +++ b/media/java/android/media/videoeditor/AudioTrack.java @@ -0,0 +1,326 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.videoeditor; + +import java.io.IOException; + +/** + * This class allows to handle an audio track. This audio file is mixed with the + * audio samples of the MediaItems. + * {@hide} + */ +public class AudioTrack { + // Instance variables + private final String mUniqueId; + private final String mFilename; + private final long mDurationMs; + private long mStartTimeMs; + private long mTimelineDurationMs; + private int mVolumePercent; + private long mBeginBoundaryTimeMs; + private long mEndBoundaryTimeMs; + private boolean mLoop; + + // Ducking variables + private int mDuckingThreshold; + private int mDuckingLowVolume; + private boolean mIsDuckingEnabled; + + // The audio waveform filename + private String mAudioWaveformFilename; + + /** + * An object of this type cannot be instantiated by using the default + * constructor + */ + @SuppressWarnings("unused") + private AudioTrack() throws IOException { + this(null, null); + } + + /** + * Constructor + * + * @param audioTrackId The AudioTrack id + * @param filename The absolute file name + * + * @throws IOException if file is not found + * @throws IllegalArgumentException if file format is not supported or if + * the codec is not supported + */ + public AudioTrack(String audioTrackId, String filename) throws IOException { + mUniqueId = audioTrackId; + mFilename = filename; + mStartTimeMs = 0; + // TODO: This value represents to the duration of the audio file + mDurationMs = 300000; + mTimelineDurationMs = mDurationMs; + mVolumePercent = 100; + + // Play the entire audio track + mBeginBoundaryTimeMs = 0; + mEndBoundaryTimeMs = mDurationMs; + + // By default loop is disabled + mLoop = false; + + // Ducking is enabled by default + mDuckingThreshold = 0; + mDuckingLowVolume = 0; + mIsDuckingEnabled = true; + + // The audio waveform file is generated later + mAudioWaveformFilename = null; + } + + /** + * @return The id of the media item + */ + public String getId() { + return mUniqueId; + } + + /** + * Get the filename source for this audio track. + * + * @return The filename as an absolute file name + */ + public String getFilename() { + return mFilename; + } + + /** + * Set the volume of this audio track as percentage of the volume in the + * original audio source file. + * + * @param volumePercent Percentage of the volume to apply. If it is set to + * 0, then volume becomes mute. It it is set to 100, then volume + * is same as original volume. It it is set to 200, then volume + * is doubled (provided that volume amplification is supported) + * + * @throws UnsupportedOperationException if volume amplification is requested + * and is not supported. + */ + public void setVolume(int volumePercent) { + mVolumePercent = volumePercent; + } + + /** + * Get the volume of the audio track as percentage of the volume in the + * original audio source file. + * + * @return The volume in percentage + */ + public int getVolume() { + return mVolumePercent; + } + + /** + * Set the start time of this audio track relative to the storyboard + * timeline. Default value is 0. + * + * @param startTimeMs the start time in milliseconds + */ + public void setStartTime(long startTimeMs) { + mStartTimeMs = startTimeMs; + } + + /** + * Get the start time of this audio track relative to the storyboard + * timeline. + * + * @return The start time in milliseconds + */ + public long getStartTime() { + return mStartTimeMs; + } + + /** + * @return The duration in milliseconds. This value represents the audio + * track duration (not looped) + */ + public long getDuration() { + return mDurationMs; + } + + /** + * @return The timeline duration. If looping is enabled this value + * represents the duration of the looped audio track, otherwise it + * is the duration of the audio track (mDurationMs). + */ + public long getTimelineDuration() { + return mTimelineDurationMs; + } + + /** + * Sets the start and end marks for trimming an audio track + * + * @param beginMs start time in the audio track in milliseconds (relative to + * the beginning of the audio track) + * @param endMs end time in the audio track in milliseconds (relative to the + * beginning of the audio track) + */ + public void setExtractBoundaries(long beginMs, long endMs) { + if (beginMs > mDurationMs) { + throw new IllegalArgumentException("Invalid start time"); + } + if (endMs > mDurationMs) { + throw new IllegalArgumentException("Invalid end time"); + } + + mBeginBoundaryTimeMs = beginMs; + mEndBoundaryTimeMs = endMs; + if (mLoop) { + // TODO: Compute mDurationMs (from the beginning of the loop until + // the end of all the loops. + mTimelineDurationMs = mEndBoundaryTimeMs - mBeginBoundaryTimeMs; + } else { + mTimelineDurationMs = mEndBoundaryTimeMs - mBeginBoundaryTimeMs; + } + } + + /** + * @return The boundary begin time + */ + public long getBoundaryBeginTime() { + return mBeginBoundaryTimeMs; + } + + /** + * @return The boundary end time + */ + public long getBoundaryEndTime() { + return mEndBoundaryTimeMs; + } + + /** + * Enable the loop mode for this audio track. Note that only one of the + * audio tracks in the timeline can have the loop mode enabled. When + * looping is enabled the samples between mBeginBoundaryTimeMs and + * mEndBoundaryTimeMs are looped. + */ + public void enableLoop() { + mLoop = true; + } + + /** + * Disable the loop mode + */ + public void disableLoop() { + mLoop = false; + } + + /** + * @return true if looping is enabled + */ + public boolean isLooping() { + return mLoop; + } + + /** + * Disable the audio duck effect + */ + public void disableDucking() { + mIsDuckingEnabled = false; + } + + /** + * TODO DEFINE + * + * @param threshold + * @param lowVolume + * @param volume + */ + public void enableDucking(int threshold, int lowVolume, int volume) { + mDuckingThreshold = threshold; + mDuckingLowVolume = lowVolume; + mIsDuckingEnabled = true; + } + + /** + * @return true if ducking is enabled + */ + public boolean isDuckingEnabled() { + return mIsDuckingEnabled; + } + + /** + * @return The ducking threshold + */ + public int getDuckingThreshhold() { + return mDuckingThreshold; + } + + /** + * @return The ducking low level + */ + public int getDuckingLowVolume() { + return mDuckingLowVolume; + } + + /** + * This API allows to generate a file containing the sample volume levels of + * this audio track object. This function may take significant time and is + * blocking. The filename can be retrieved using getAudioWaveformFilename(). + * + * @param listener The progress listener + * + * @throws IOException if the output file cannot be created + * @throws IllegalArgumentException if the audio file does not have a valid + * audio track + */ + public void extractAudioWaveform(ExtractAudioWaveformProgressListener listener) + throws IOException { + // TODO: Set mAudioWaveformFilename at the end once the extract is complete + } + + /** + * Get the audio waveform file name if extractAudioWaveform was successful. + * The file format is as following: + * + * + * @return the name of the file, null if the file does not exist + */ + public String getAudioWaveformFilename() { + return mAudioWaveformFilename; + } + + /* + * {@inheritDoc} + */ + @Override + public boolean equals(Object object) { + if (!(object instanceof AudioTrack)) { + return false; + } + return mUniqueId.equals(((AudioTrack)object).mUniqueId); + } + + /* + * {@inheritDoc} + */ + @Override + public int hashCode() { + return mUniqueId.hashCode(); + } +} diff --git a/media/java/android/media/videoeditor/Effect.java b/media/java/android/media/videoeditor/Effect.java new file mode 100755 index 0000000000000..177b8631e85a5 --- /dev/null +++ b/media/java/android/media/videoeditor/Effect.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.videoeditor; + +/** + * This is the super class for all effects. An effect can only be applied to a + * single media item. If one wants to apply the same effect to multiple media + * items, multiple @{MediaItem.addEffect(Effect)} call must be invoked on each + * of the MediaItem objects. + * {@hide} + */ +public abstract class Effect { + // Instance variables + private final String mUniqueId; + protected long mDurationMs; + // The start time of the effect relative to the media item timeline + protected long mStartTimeMs; + + /** + * Default constructor + */ + @SuppressWarnings("unused") + private Effect() { + mUniqueId = null; + mStartTimeMs = 0; + mDurationMs = 0; + } + + /** + * Constructor + * + * @param effectId The effect id + * @param startTimeMs The start time relative to the media item to which it + * is applied + * @param durationMs The effect duration in milliseconds + */ + public Effect(String effectId, long startTimeMs, long durationMs) { + mUniqueId = effectId; + mStartTimeMs = startTimeMs; + mDurationMs = durationMs; + } + + /** + * @return The id of the effect + */ + public String getId() { + return mUniqueId; + } + + /** + * Set the duration of the effect. If a preview or export is in progress, + * then this change is effective for next preview or export session. s + * + * @param durationMs of the effect in milliseconds + */ + public void setDuration(long durationMs) { + mDurationMs = durationMs; + } + + /** + * Get the duration of the effect + * + * @return The duration of the effect in milliseconds + */ + public long getDuration() { + return mDurationMs; + } + + /** + * 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 + * of the media item in milliseconds + */ + public void setStartTime(long startTimeMs) { + mStartTimeMs = startTimeMs; + } + + /** + * @return The start time in milliseconds + */ + public long getStartTime() { + return mStartTimeMs; + } + + /* + * {@inheritDoc} + */ + @Override + public boolean equals(Object object) { + if (!(object instanceof Effect)) { + return false; + } + return mUniqueId.equals(((Effect)object).mUniqueId); + } + + /* + * {@inheritDoc} + */ + @Override + public int hashCode() { + return mUniqueId.hashCode(); + } +} diff --git a/media/java/android/media/videoeditor/EffectColor.java b/media/java/android/media/videoeditor/EffectColor.java new file mode 100755 index 0000000000000..7c616274d92e5 --- /dev/null +++ b/media/java/android/media/videoeditor/EffectColor.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.videoeditor; + +/** + * This class allows to apply color on a media item. + * {@hide} + */ +public class EffectColor extends Effect { + + /** + * Change the video frame color to the RGB color value provided + */ + public static final int TYPE_COLOR = 1; // color as 888 RGB + /** + * Change the video frame color to a gradation from RGB color (at the top of + * the frame) to black (at the bottom of the frame). + */ + public static final int TYPE_GRADIENT = 2; + /** + * Change the video frame color to sepia + */ + public static final int TYPE_SEPIA = 3; + /** + * Invert the video frame color + */ + public static final int TYPE_NEGATIVE = 4; + /** + * Make the video look like as if it was recorded in 50's + */ + public static final int TYPE_FIFTIES = 5; + + // Colors for the color effect + public static final int GREEN = 0x0000ff00; + public static final int PINK = 0x00ff66cc; + public static final int GRAY = 0x007f7f7f; + + // The effect type + private final int mType; + + // The effect parameter + private final int mParam; + + /** + * An object of this type cannot be instantiated by using the default + * constructor + */ + @SuppressWarnings("unused") + private EffectColor() { + this(null, 0, 0, 0, 0); + } + + /** + * Constructor + * + * @param effectId The effect id + * @param startTimeMs The start time relative to the media item to which it + * is applied + * @param durationMs The duration of this effect in milliseconds + * @param type type of the effect. type is one of: TYPE_COLOR, + * TYPE_GRADIENT, TYPE_SEPIA, TYPE_NEGATIVE, TYPE_FIFTIES. If + * type is not supported, the argument is ignored + * @param param if type is TYPE_COLOR, param is the RGB color as 888. + * Otherwise, param is ignored + */ + public EffectColor(String effectId, long startTimeMs, long durationMs, + int type, int param) { + super(effectId, startTimeMs, durationMs); + mType = type; + mParam = param; + } + + /** + * @return The type of this effect + */ + public int getType() { + return mType; + } + + /** + * @return the color as RGB 888 if type is TYPE_COLOR. Otherwise, ignore. + */ + public int getParam() { + return mParam; + } +} diff --git a/media/java/android/media/videoeditor/EffectKenBurns.java b/media/java/android/media/videoeditor/EffectKenBurns.java new file mode 100755 index 0000000000000..c6d22f416f838 --- /dev/null +++ b/media/java/android/media/videoeditor/EffectKenBurns.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.videoeditor; + +import java.io.IOException; + +import android.graphics.Rect; + +/** + * This class represents a Ken Burns effect. + * {@hide} + */ +public class EffectKenBurns extends Effect { + // Instance variables + private Rect mStartRect; + private Rect mEndRect; + + /** + * Objects of this type cannot be instantiated by using the default + * constructor + */ + @SuppressWarnings("unused") + private EffectKenBurns() throws IOException { + this(null, null, null, 0, 0); + } + + /** + * Constructor + * + * @param effectId The effect id + * @param startRect The start rectangle + * @param endRect The end rectangle + * @param startTimeMs The start time + * @param durationMs The duration of the Ken Burns effect in milliseconds + */ + public EffectKenBurns(String effectId, Rect startRect, Rect endRect, long startTime, + long durationMs) + throws IOException { + super(effectId, startTime, durationMs); + + mStartRect = startRect; + mEndRect = endRect; + } + + /** + * @param startRect The start rectangle + * + * @throws IllegalArgumentException if start rectangle is incorrectly set. + */ + public void setStartRect(Rect startRect) { + mStartRect = startRect; + } + + /** + * @return The start rectangle + */ + public Rect getStartRect() { + return mStartRect; + } + + /** + * @param endRect The end rectangle + * + * @throws IllegalArgumentException if end rectangle is incorrectly set. + */ + public void setEndRect(Rect endRect) { + mEndRect = endRect; + } + + /** + * @return The end rectangle + */ + public Rect getEndRect() { + return mEndRect; + } +} diff --git a/media/java/android/media/videoeditor/ExtractAudioWaveformProgressListener.java b/media/java/android/media/videoeditor/ExtractAudioWaveformProgressListener.java new file mode 100644 index 0000000000000..1cce148db5351 --- /dev/null +++ b/media/java/android/media/videoeditor/ExtractAudioWaveformProgressListener.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.videoeditor; + +/** + * This listener interface is used by + * {@link MediaVideoItem#extractAudioWaveform(ExtractAudioWaveformProgressListener listener)} + * or + * {@link AudioTrack#extractAudioWaveform(ExtractAudioWaveformProgressListener listener)} + * {@hide} + */ +public interface ExtractAudioWaveformProgressListener { + /** + * This method notifies the listener of the progress status of + * an extractAudioWaveform operation. + * This method may be called maximum 100 times for one operation. + * + * @param progress The progress in %. At the beginning of the operation, + * this value is set to 0; at the end, the value is set to 100. + */ + public void onProgress(int progress); +} + diff --git a/media/java/android/media/videoeditor/MediaImageItem.java b/media/java/android/media/videoeditor/MediaImageItem.java new file mode 100755 index 0000000000000..db7585a85c310 --- /dev/null +++ b/media/java/android/media/videoeditor/MediaImageItem.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.videoeditor; + +import java.io.IOException; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.util.Log; + +/** + * This class represents an image item on the storyboard. + * {@hide} + */ +public class MediaImageItem extends MediaItem { + // Logging + private static final String TAG = "MediaImageItem"; + + // The resize paint + private static final Paint sResizePaint = new Paint(Paint.FILTER_BITMAP_FLAG); + + // Instance variables + private final int mWidth; + private final int mHeight; + private final int mAspectRatio; + private long mDurationMs; + + /** + * This class cannot be instantiated by using the default constructor + */ + @SuppressWarnings("unused") + private MediaImageItem() throws IOException { + this(null, null, 0, RENDERING_MODE_BLACK_BORDER); + } + + /** + * Constructor + * + * @param mediaItemId The MediaItem id + * @param filename The image file name + * @param durationMs The duration of the image on the storyboard + * @param renderingMode The rendering mode + * + * @throws IOException + */ + public MediaImageItem(String mediaItemId, String filename, long durationMs, int renderingMode) + throws IOException { + super(mediaItemId, filename, renderingMode); + + // Determine the size of the image + final BitmapFactory.Options dbo = new BitmapFactory.Options(); + dbo.inJustDecodeBounds = true; + BitmapFactory.decodeFile(filename, dbo); + + mWidth = dbo.outWidth; + mHeight = dbo.outHeight; + mDurationMs = durationMs; + + // TODO: Determine the aspect ratio from the width and height + mAspectRatio = MediaProperties.ASPECT_RATIO_4_3; + } + + /* + * {@inheritDoc} + */ + @Override + public int getFileType() { + if (mFilename.endsWith(".jpg") || mFilename.endsWith(".jpeg")) { + return MediaProperties.FILE_JPEG; + } else if (mFilename.endsWith(".png")) { + return MediaProperties.FILE_PNG; + } else { + return MediaProperties.FILE_UNSUPPORTED; + } + } + + /* + * {@inheritDoc} + */ + @Override + public int getWidth() { + return mWidth; + } + + /* + * {@inheritDoc} + */ + @Override + public int getHeight() { + return mHeight; + } + + /* + * {@inheritDoc} + */ + @Override + public int getAspectRatio() { + return mAspectRatio; + } + + /** + * @param durationMs The duration of the image in the storyboard timeline + */ + public void setDuration(long durationMs) { + mDurationMs = durationMs; + // TODO: Validate/modify the start and the end time of effects and overlays + } + + /* + * {@inheritDoc} + */ + @Override + public long getDuration() { + return mDurationMs; + } + + /* + * {@inheritDoc} + */ + @Override + public long getTimelineDuration() { + return mDurationMs; + } + + /* + * {@inheritDoc} + */ + @Override + public Bitmap getThumbnail(int width, int height, long timeMs) throws IOException { + return generateImageThumbnail(mFilename, width, height); + } + + /* + * {@inheritDoc} + */ + @Override + public Bitmap[] getThumbnailList(int width, int height, long startMs, long endMs, + int thumbnailCount) throws IOException { + final Bitmap thumbnail = generateImageThumbnail(mFilename, width, height); + final Bitmap[] thumbnailArray = new Bitmap[thumbnailCount]; + for (int i = 0; i < thumbnailCount; i++) { + thumbnailArray[i] = thumbnail; + } + return thumbnailArray; + } + + /** + * Resize a bitmap within an input stream + * + * @param filename The filename + * @param width The thumbnail width + * @param height The thumbnail height + * + * @return The resized bitmap + */ + private Bitmap generateImageThumbnail(String filename, int width, int height) + throws IOException { + final BitmapFactory.Options dbo = new BitmapFactory.Options(); + dbo.inJustDecodeBounds = true; + BitmapFactory.decodeFile(filename, dbo); + + final int nativeWidth = dbo.outWidth; + final int nativeHeight = dbo.outHeight; + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "generateThumbnail: Input: " + nativeWidth + "x" + nativeHeight + + ", resize to: " + width + "x" + height); + } + + final Bitmap srcBitmap; + float bitmapWidth, bitmapHeight; + if (nativeWidth > width || nativeHeight > height) { + float dx = ((float)nativeWidth) / ((float)width); + float dy = ((float)nativeHeight) / ((float)height); + if (dx > dy) { + bitmapWidth = width; + bitmapHeight = nativeHeight / dx; + } else { + bitmapWidth = nativeWidth / dy; + bitmapHeight = height; + } + // Create the bitmap from file + if (nativeWidth / bitmapWidth > 1) { + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = nativeWidth / (int)bitmapWidth; + srcBitmap = BitmapFactory.decodeFile(filename, options); + } else { + srcBitmap = BitmapFactory.decodeFile(filename); + } + } else { + bitmapWidth = width; + bitmapHeight = height; + srcBitmap = BitmapFactory.decodeFile(filename); + } + + if (srcBitmap == null) { + Log.e(TAG, "generateThumbnail: Cannot decode image bytes"); + throw new IOException("Cannot decode file: " + mFilename); + } + + // Create the canvas bitmap + final Bitmap bitmap = Bitmap.createBitmap((int)bitmapWidth, (int)bitmapHeight, + Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(bitmap); + canvas.drawBitmap(srcBitmap, new Rect(0, 0, srcBitmap.getWidth(), srcBitmap.getHeight()), + new Rect(0, 0, (int)bitmapWidth, (int)bitmapHeight), sResizePaint); + // Release the source bitmap + srcBitmap.recycle(); + return bitmap; + } +} diff --git a/media/java/android/media/videoeditor/MediaItem.java b/media/java/android/media/videoeditor/MediaItem.java new file mode 100755 index 0000000000000..9e32744059999 --- /dev/null +++ b/media/java/android/media/videoeditor/MediaItem.java @@ -0,0 +1,434 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.videoeditor; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import android.graphics.Bitmap; + +/** + * This abstract class describes the base class for any MediaItem. Objects are + * defined with a file path as a source data. + * {@hide} +s */ +public abstract class MediaItem { + // A constant which can be used to specify the end of the file (instead of + // providing the actual duration of the media item). + public final static int END_OF_FILE = -1; + + // Rendering modes + /** + * When using the RENDERING_MODE_BLACK_BORDER rendering mode video frames + * are resized by preserving the aspect ratio until the movie matches one of + * the dimensions of the output movie. The areas outside the resized video + * clip are rendered black. + */ + public static final int RENDERING_MODE_BLACK_BORDER = 0; + /** + * When using the RENDERING_MODE_STRETCH rendering mode video frames are + * stretched horizontally or vertically to match the current aspect ratio of + * the movie. + */ + public static final int RENDERING_MODE_STRETCH = 1; + + + // The unique id of the MediaItem + private final String mUniqueId; + + // The name of the file associated with the MediaItem + protected final String mFilename; + + // List of effects + private final List mEffects; + + // List of overlays + private final List mOverlays; + + // The rendering mode + private int mRenderingMode; + + // Beginning and end transitions + private Transition mBeginTransition; + private Transition mEndTransition; + + /** + * Constructor + * + * @param mediaItemId The MediaItem id + * @param filename name of the media file. + * @param renderingMode The rendering mode + * + * @throws IOException if file is not found + * @throws IllegalArgumentException if a capability such as file format is not + * supported the exception object contains the unsupported + * capability + */ + protected MediaItem(String mediaItemId, String filename, int renderingMode) throws IOException { + mUniqueId = mediaItemId; + mFilename = filename; + mRenderingMode = renderingMode; + mEffects = new ArrayList(); + mOverlays = new ArrayList(); + mBeginTransition = null; + mEndTransition = null; + } + + /** + * @return The of the media item + */ + public String getId() { + return mUniqueId; + } + + /** + * @return The media source file name + */ + public String getFilename() { + return mFilename; + } + + /** + * If aspect ratio of the MediaItem is different from the aspect ratio of + * the editor then this API controls the rendering mode. + * + * @param renderingMode rendering mode. It is one of: + * {@link #RENDERING_MODE_BLACK_BORDER}, + * {@link #RENDERING_MODE_STRETCH} + */ + public void setRenderingMode(int renderingMode) { + mRenderingMode = renderingMode; + } + + /** + * @return The rendering mode + */ + public int getRenderingMode() { + return mRenderingMode; + } + + /** + * @param transition The beginning transition + */ + void setBeginTransition(Transition transition) { + mBeginTransition = transition; + } + + /** + * @return The begin transition + */ + public Transition getBeginTransition() { + return mBeginTransition; + } + + /** + * @param transition The end transition + */ + void setEndTransition(Transition transition) { + mEndTransition = transition; + } + + /** + * @return The end transition + */ + public Transition getEndTransition() { + return mEndTransition; + } + + /** + * @return The duration of the media item + */ + public abstract long getDuration(); + + /** + * @return The timeline duration. This is the actual duration in the + * timeline (trimmed duration) + */ + public abstract long getTimelineDuration(); + + /** + * @return The source file type + */ + public abstract int getFileType(); + + /** + * @return Get the native width of the media item + */ + public abstract int getWidth(); + + /** + * @return Get the native height of the media item + */ + public abstract int getHeight(); + + /** + * Get aspect ratio of the source media item. + * + * @return the aspect ratio as described in MediaProperties. + * MediaProperties.ASPECT_RATIO_UNDEFINED if aspect ratio is not + * supported as in MediaProperties + */ + public abstract int getAspectRatio(); + + /** + * Add the specified effect to this media item. + * + * Note that certain types of effects cannot be applied to video and to + * image media items. For example in certain implementation a Ken Burns + * implementation cannot be applied to video media item. + * + * This method invalidates transition video clips if the + * effect overlaps with the beginning and/or the end transition. + * + * @param effect The effect to apply + * @throws IllegalStateException if a preview or an export is in progress + * @throws IllegalArgumentException if the effect start and/or duration are + * invalid or if the effect cannot be applied to this type of media + * item or if the effect id is not unique across all the Effects + * added. + */ + public void addEffect(Effect effect) { + if (mEffects.contains(effect)) { + throw new IllegalArgumentException("Effect already exists: " + effect.getId()); + } + + if (effect.getStartTime() + effect.getDuration() > getDuration()) { + throw new IllegalArgumentException( + "Effect start time + effect duration > media clip duration"); + } + + mEffects.add(effect); + invalidateTransitions(effect); + } + + /** + * Remove the effect with the specified id. + * + * This method invalidates a transition video clip if the effect overlaps + * with a transition. + * + * @param effectId The id of the effect to be removed + * + * @return The effect that was removed + * @throws IllegalStateException if a preview or an export is in progress + */ + public Effect removeEffect(String effectId) { + for (Effect effect : mEffects) { + if (effect.getId().equals(effectId)) { + mEffects.remove(effect); + invalidateTransitions(effect); + return effect; + } + } + + return null; + } + + /** + * Find the effect with the specified id + * + * @param effectId The effect id + * + * @return The effect with the specified id (null if it does not exist) + */ + public Effect getEffect(String effectId) { + for (Effect effect : mEffects) { + if (effect.getId().equals(effectId)) { + return effect; + } + } + + return null; + } + + /** + * Get the list of effects. + * + * @return the effects list. If no effects exist an empty list will be returned. + */ + public List getAllEffects() { + return mEffects; + } + + /** + * Add an overlay to the storyboard. This method invalidates a transition + * video clip if the overlay overlaps with a transition. + * + * @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. + */ + public void addOverlay(Overlay overlay) { + if (mOverlays.contains(overlay)) { + throw new IllegalArgumentException("Overlay already exists: " + overlay.getId()); + } + + if (overlay.getStartTime() + overlay.getDuration() > getDuration()) { + throw new IllegalArgumentException( + "Overlay start time + overlay duration > media clip duration"); + } + + mOverlays.add(overlay); + invalidateTransitions(overlay); + } + + /** + * Remove the overlay with the specified id. + * + * This method invalidates a transition video clip if the overlay overlaps + * with a transition. + * + * @param overlayId The id of the overlay to be removed + * + * @return The overlay that was removed + * @throws IllegalStateException if a preview or an export is in progress + */ + public Overlay removeOverlay(String overlayId) { + for (Overlay overlay : mOverlays) { + if (overlay.getId().equals(overlayId)) { + mOverlays.remove(overlay); + invalidateTransitions(overlay); + return overlay; + } + } + + return null; + } + + /** + * Find the overlay with the specified id + * + * @param overlayId The overlay id + * + * @return The overlay with the specified id (null if it does not exist) + */ + public Overlay getOverlay(String overlayId) { + for (Overlay overlay : mOverlays) { + if (overlay.getId().equals(overlayId)) { + return overlay; + } + } + + return null; + } + + /** + * Get the list of overlays associated with this media item + * + * Note that if any overlay source files are not accessible anymore, + * this method will still provide the full list of overlays. + * + * @return The list of overlays. If no overlays exist an empty list will + * be returned. + */ + public List getAllOverlays() { + return mOverlays; + } + + /** + * Create a thumbnail at specified time in a video stream in Bitmap format + * + * @param width width of the thumbnail in pixels + * @param height height of the thumbnail in pixels + * @param timeMs The time in the source video file at which the thumbnail is + * requested (even if trimmed). + * + * @return The thumbnail as a Bitmap. + * + * @throws IOException if a file error occurs + * @throws IllegalArgumentException if time is out of video duration + */ + public abstract Bitmap getThumbnail(int width, int height, long timeMs) throws IOException; + + /** + * Get the array of Bitmap thumbnails between start and end. + * + * @param width width of the thumbnail in pixels + * @param height height of the thumbnail in pixels + * @param startMs The start of time range in milliseconds + * @param endMs The end of the time range in milliseconds + * @param thumbnailCount The thumbnail count + * + * @return The array of Bitmaps + * + * @throws IOException if a file error occurs + */ + public abstract Bitmap[] getThumbnailList(int width, int height, long startMs, long endMs, + int thumbnailCount) throws IOException; + + /* + * {@inheritDoc} + */ + @Override + public boolean equals(Object object) { + if (!(object instanceof MediaItem)) { + return false; + } + return mUniqueId.equals(((MediaItem)object).mUniqueId); + } + + /* + * {@inheritDoc} + */ + @Override + public int hashCode() { + return mUniqueId.hashCode(); + } + + /** + * Invalidate the start and end transitions if necessary + * + * @param effect The effect that was added or removed + */ + private void invalidateTransitions(Effect effect) { + // Check if the effect overlaps with the beginning and end transitions + if (mBeginTransition != null) { + if (effect.getStartTime() < mBeginTransition.getDuration()) { + mBeginTransition.invalidate(); + } + } + + if (mEndTransition != null) { + if (effect.getStartTime() + effect.getDuration() > getDuration() + - mEndTransition.getDuration()) { + mEndTransition.invalidate(); + } + } + } + + /** + * Invalidate the start and end transitions if necessary + * + * @param overlay The effect that was added or removed + */ + private void invalidateTransitions(Overlay overlay) { + // Check if the overlay overlaps with the beginning and end transitions + if (mBeginTransition != null) { + if (overlay.getStartTime() < mBeginTransition.getDuration()) { + mBeginTransition.invalidate(); + } + } + + if (mEndTransition != null) { + if (overlay.getStartTime() + overlay.getDuration() > getDuration() + - mEndTransition.getDuration()) { + mEndTransition.invalidate(); + } + } + } +} diff --git a/media/java/android/media/videoeditor/MediaProperties.java b/media/java/android/media/videoeditor/MediaProperties.java new file mode 100755 index 0000000000000..c3f5ef7492ece --- /dev/null +++ b/media/java/android/media/videoeditor/MediaProperties.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.videoeditor; + +import android.util.Pair; + +/** + * This class defines all properties of a media file such as supported height, aspect ratio, + * bitrate for export function. + * {@hide} + */ +public class MediaProperties { + // Supported heights + public static final int HEIGHT_144 = 144; + public static final int HEIGHT_360 = 360; + public static final int HEIGHT_480 = 480; + public static final int HEIGHT_720 = 720; + + // Supported aspect ratios + public static final int ASPECT_RATIO_UNDEFINED = 0; + public static final int ASPECT_RATIO_3_2 = 1; + public static final int ASPECT_RATIO_16_9 = 2; + public static final int ASPECT_RATIO_4_3 = 3; + public static final int ASPECT_RATIO_5_3 = 4; + public static final int ASPECT_RATIO_11_9 = 5; + + // The array of supported aspect ratios + private static final int[] ASPECT_RATIOS = new int[] { + ASPECT_RATIO_3_2, + ASPECT_RATIO_16_9, + ASPECT_RATIO_4_3, + ASPECT_RATIO_5_3, + ASPECT_RATIO_11_9 + }; + + // Supported resolutions for specific aspect ratios + @SuppressWarnings({"unchecked"}) + private static final Pair[] ASPECT_RATIO_3_2_RESOLUTIONS = + new Pair[] { + new Pair(720, HEIGHT_480), + new Pair(1080, HEIGHT_720) + }; + + @SuppressWarnings({"unchecked"}) + private static final Pair[] ASPECT_RATIO_4_3_RESOLUTIONS = + new Pair[] { + new Pair(640, HEIGHT_480), + new Pair(960, HEIGHT_720) + }; + + @SuppressWarnings({"unchecked"}) + private static final Pair[] ASPECT_RATIO_5_3_RESOLUTIONS = + new Pair[] { + new Pair(800, HEIGHT_480) + }; + + @SuppressWarnings({"unchecked"}) + private static final Pair[] ASPECT_RATIO_11_9_RESOLUTIONS = + new Pair[] { + new Pair(176, HEIGHT_144) + }; + + @SuppressWarnings({"unchecked"}) + private static final Pair[] ASPECT_RATIO_16_9_RESOLUTIONS = + new Pair[] { + new Pair(640, HEIGHT_360), + new Pair(854, HEIGHT_480), + new Pair(1280, HEIGHT_720), + }; + + + // Bitrate values (in bits per second) + public static final int BITRATE_28K = 28000; + public static final int BITRATE_40K = 40000; + public static final int BITRATE_64K = 64000; + public static final int BITRATE_96K = 96000; + public static final int BITRATE_128K = 128000; + public static final int BITRATE_192K = 192000; + public static final int BITRATE_256K = 256000; + public static final int BITRATE_384K = 384000; + public static final int BITRATE_512K = 512000; + public static final int BITRATE_800K = 800000; + + // The array of supported bitrates + private static final int[] SUPPORTED_BITRATES = new int[] { + BITRATE_28K, + BITRATE_40K, + BITRATE_64K, + BITRATE_96K, + BITRATE_128K, + BITRATE_192K, + BITRATE_256K, + BITRATE_384K, + BITRATE_512K, + BITRATE_800K + }; + + // Video codec types + public static final int VCODEC_H264BP = 1; + public static final int VCODEC_H264MP = 2; + public static final int VCODEC_H263 = 3; + public static final int VCODEC_MPEG4 = 4; + + // The array of supported video codecs + private static final int[] SUPPORTED_VCODECS = new int[] { + VCODEC_H264BP, + VCODEC_H263, + VCODEC_MPEG4, + }; + + // Audio codec types + public static final int ACODEC_AAC_LC = 1; + public static final int ACODEC_AMRNB = 2; + public static final int ACODEC_AMRWB = 3; + public static final int ACODEC_MP3 = 4; + public static final int ACODEC_OGG = 5; + + // The array of supported video codecs + private static final int[] SUPPORTED_ACODECS = new int[] { + ACODEC_AAC_LC, + ACODEC_AMRNB, + ACODEC_AMRWB + }; + + // File format types + public static final int FILE_UNSUPPORTED = 0; + public static final int FILE_3GP = 1; + public static final int FILE_MP4 = 2; + public static final int FILE_JPEG = 3; + public static final int FILE_PNG = 4; + + // The array of the supported file formats + private static final int[] SUPPORTED_VIDEO_FILE_FORMATS = new int[] { + FILE_3GP, + FILE_MP4 + }; + + // The maximum count of audio tracks supported + public static final int AUDIO_MAX_TRACK_COUNT = 1; + + // The maximum volume supported (100 means that no amplification is + // supported, i.e. attenuation only) + public static final int AUDIO_MAX_VOLUME_PERCENT = 100; + + /** + * This class cannot be instantiated + */ + private MediaProperties() { + } + + /** + * @return The array of supported aspect ratios + */ + public static int[] getAllSupportedAspectRatios() { + return ASPECT_RATIOS; + } + + /** + * Get the supported resolutions for the specified aspect ratio. + * + * @param aspectRatio The aspect ratio for which the resolutions are requested + * + * @return The array of width and height pairs + */ + public static Pair[] getSupportedResolutions(int aspectRatio) { + final Pair[] resolutions; + switch(aspectRatio) { + case ASPECT_RATIO_3_2: { + resolutions = ASPECT_RATIO_3_2_RESOLUTIONS; + break; + } + + case ASPECT_RATIO_4_3: { + resolutions = ASPECT_RATIO_4_3_RESOLUTIONS; + break; + } + + case ASPECT_RATIO_5_3: { + resolutions = ASPECT_RATIO_5_3_RESOLUTIONS; + break; + } + + case ASPECT_RATIO_11_9: { + resolutions = ASPECT_RATIO_11_9_RESOLUTIONS; + break; + } + + case ASPECT_RATIO_16_9: { + resolutions = ASPECT_RATIO_16_9_RESOLUTIONS; + break; + } + + default: { + throw new IllegalArgumentException("Unknown aspect ratio: " + aspectRatio); + } + } + + return resolutions; + } + + /** + * @return The array of supported video codecs + */ + public static int[] getSupportedVideoCodecs() { + return SUPPORTED_VCODECS; + } + + /** + * @return The array of supported audio codecs + */ + public static int[] getSupportedAudioCodecs() { + return SUPPORTED_ACODECS; + } + + /** + * @return The array of supported file formats + */ + public static int[] getSupportedVideoFileFormat() { + return SUPPORTED_VIDEO_FILE_FORMATS; + } + + /** + * @return The array of supported video bitrates + */ + public static int[] getSupportedVideoBitrates() { + return SUPPORTED_BITRATES; + } + + /** + * @return The maximum value for the audio volume + */ + public static int getSupportedMaxVolume() { + return MediaProperties.AUDIO_MAX_VOLUME_PERCENT; + } + + /** + * @return The maximum number of audio tracks supported + */ + public static int getSupportedAudioTrackCount() { + return MediaProperties.AUDIO_MAX_TRACK_COUNT; + } +} diff --git a/media/java/android/media/videoeditor/MediaVideoItem.java b/media/java/android/media/videoeditor/MediaVideoItem.java new file mode 100755 index 0000000000000..87e9a2241b295 --- /dev/null +++ b/media/java/android/media/videoeditor/MediaVideoItem.java @@ -0,0 +1,541 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.videoeditor; + +import java.io.IOException; + +import android.graphics.Bitmap; +import android.media.MediaRecorder; +import android.util.Log; +import android.view.SurfaceHolder; + +/** + * This class represents a video clip item on the storyboard + * {@hide} + */ +public class MediaVideoItem extends MediaItem { + // Logging + private static final String TAG = "MediaVideoItem"; + + // Instance variables + private final int mWidth; + private final int mHeight; + private final int mAspectRatio; + private final int mFileType; + private final int mVideoType; + private final int mVideoProfile; + private final int mVideoBitrate; + private final long mDurationMs; + private final int mAudioBitrate; + private final int mFps; + private final int mAudioType; + private final int mAudioChannels; + private final int mAudioSamplingFrequency; + + private long mBeginBoundaryTimeMs; + private long mEndBoundaryTimeMs; + private int mVolumePercentage; + private String mAudioWaveformFilename; + private PlaybackThread mPlaybackThread; + + /** + * This listener interface is used by the MediaVideoItem to emit playback + * progress notifications. This callback should be invoked after the + * number of frames specified by + * {@link #startPlayback(SurfaceHolder surfaceHolder, long fromMs, + * int callbackAfterFrameCount, PlaybackProgressListener listener)} + */ + public interface PlaybackProgressListener { + /** + * This method notifies the listener of the current time position while + * playing a media item + * + * @param mediaItem The media item + * @param timeMs The current playback position (expressed in milliseconds + * since the beginning of the media item). + * @param end true if the end of the media item was reached + */ + public void onProgress(MediaVideoItem mediaItem, long timeMs, boolean end); + } + + /** + * The playback thread + */ + private class PlaybackThread extends Thread { + // Instance variables + private final static long FRAME_DURATION = 33; + private final PlaybackProgressListener mListener; + private final int mCallbackAfterFrameCount; + private final long mFromMs, mToMs; + private boolean mRun, mLoop; + private long mPositionMs; + + /** + * Constructor + * + * @param fromMs The time (relative to the beginning of the media item) + * at which the playback will start + * @param toMs The time (relative to the beginning of the media item) at + * which the playback will stop. Use -1 to play to the end of + * the media item + * @param loop true if the playback should be looped once it reaches the + * end + * @param callbackAfterFrameCount The listener interface should be + * invoked after the number of frames specified by this + * parameter. + * @param listener The listener which will be notified of the playback + * progress + */ + public PlaybackThread(long fromMs, long toMs, boolean loop, int callbackAfterFrameCount, + PlaybackProgressListener listener) { + mPositionMs = mFromMs = fromMs; + if (toMs < 0) { + mToMs = mDurationMs; + } else { + mToMs = toMs; + } + mLoop = loop; + mCallbackAfterFrameCount = callbackAfterFrameCount; + mListener = listener; + mRun = true; + } + + /* + * {@inheritDoc} + */ + @Override + public void run() { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "===> PlaybackThread.run enter"); + } + int frameCount = 0; + while (mRun) { + try { + sleep(FRAME_DURATION); + } catch (InterruptedException ex) { + break; + } + frameCount++; + mPositionMs += FRAME_DURATION; + + if (mPositionMs >= mToMs) { + if (!mLoop) { + if (mListener != null) { + mListener.onProgress(MediaVideoItem.this, mPositionMs, true); + } + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "PlaybackThread.run playback complete"); + } + break; + } else { + // Fire a notification for the end of the clip + if (mListener != null) { + mListener.onProgress(MediaVideoItem.this, mToMs, false); + } + + // Rewind + mPositionMs = mFromMs; + if (mListener != null) { + mListener.onProgress(MediaVideoItem.this, mPositionMs, false); + } + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "PlaybackThread.run playback complete"); + } + frameCount = 0; + } + } else { + if (frameCount == mCallbackAfterFrameCount) { + if (mListener != null) { + mListener.onProgress(MediaVideoItem.this, mPositionMs, false); + } + frameCount = 0; + } + } + } + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "===> PlaybackThread.run exit"); + } + } + + /** + * Stop the playback + * + * @return The stop position + */ + public long stopPlayback() { + mRun = false; + try { + join(); + } catch (InterruptedException ex) { + } + return mPositionMs; + } + }; + + /** + * An object of this type cannot be instantiated with a default constructor + */ + @SuppressWarnings("unused") + private MediaVideoItem() throws IOException { + this(null, null, RENDERING_MODE_BLACK_BORDER); + } + + /** + * Constructor + * + * @param mediaItemId The MediaItem id + * @param filename The image file name + * @param renderingMode The rendering mode + * + * @throws IOException if the file cannot be opened for reading + */ + public MediaVideoItem(String mediaItemId, String filename, int renderingMode) + throws IOException { + this(mediaItemId, filename, renderingMode, null); + } + + /** + * Constructor + * + * @param mediaItemId The MediaItem id + * @param filename The image file name + * @param renderingMode The rendering mode + * @param audioWaveformFilename The name of the audio waveform file + * + * @throws IOException if the file cannot be opened for reading + */ + MediaVideoItem(String mediaItemId, String filename, int renderingMode, + String audioWaveformFilename) throws IOException { + super(mediaItemId, filename, renderingMode); + // TODO: Set these variables correctly + mWidth = 0; + mHeight = 0; + mAspectRatio = MediaProperties.ASPECT_RATIO_3_2; + mFileType = MediaProperties.FILE_MP4; + mVideoType = MediaRecorder.VideoEncoder.H264; + // Do we have predefined values for this variable? + mVideoProfile = 0; + // Can video and audio duration be different? + mDurationMs = 10000; + mVideoBitrate = 800000; + mAudioBitrate = 30000; + mFps = 30; + mAudioType = MediaProperties.ACODEC_AAC_LC; + mAudioChannels = 2; + mAudioSamplingFrequency = 16000; + + mBeginBoundaryTimeMs = 0; + mEndBoundaryTimeMs = mDurationMs; + mVolumePercentage = 100; + mAudioWaveformFilename = audioWaveformFilename; + } + + /** + * Sets the start and end marks for trimming a video media item + * + * @param beginMs Start time in milliseconds. Set to 0 to extract from the + * beginning + * @param endMs End time in milliseconds. Set to {@link #END_OF_FILE} to + * extract until the end + * + * @throws IllegalArgumentException if the start time is greater or equal than + * end time, the end time is beyond the file duration, the start time + * is negative + */ + public void setExtractBoundaries(long beginMs, long endMs) { + if (beginMs > mDurationMs) { + throw new IllegalArgumentException("Invalid start time"); + } + if (endMs > mDurationMs) { + throw new IllegalArgumentException("Invalid end time"); + } + + mBeginBoundaryTimeMs = beginMs; + mEndBoundaryTimeMs = endMs; + // TODO: Validate/modify the start and the end time of effects and overlays + } + + /** + * @return The boundary begin time + */ + public long getBoundaryBeginTime() { + return mBeginBoundaryTimeMs; + } + + /** + * @return The boundary end time + */ + public long getBoundaryEndTime() { + return mEndBoundaryTimeMs; + } + + /* + * {@inheritDoc} + */ + @Override + public void addEffect(Effect effect) { + if (effect instanceof EffectKenBurns) { + throw new IllegalArgumentException("Ken Burns effects cannot be applied to MediaVideoItem"); + } + super.addEffect(effect); + } + + /* + * {@inheritDoc} + */ + @Override + public Bitmap getThumbnail(int width, int height, long timeMs) { + return null; + } + + /* + * {@inheritDoc} + */ + @Override + public Bitmap[] getThumbnailList(int width, int height, long startMs, long endMs, + int thumbnailCount) throws IOException { + return null; + } + + /* + * {@inheritDoc} + */ + @Override + public int getAspectRatio() { + return mAspectRatio; + } + + /* + * {@inheritDoc} + */ + @Override + public int getFileType() { + return mFileType; + } + + /* + * {@inheritDoc} + */ + @Override + public int getWidth() { + return mWidth; + } + + /* + * {@inheritDoc} + */ + @Override + public int getHeight() { + return mHeight; + } + + /* + * {@inheritDoc} + */ + @Override + public long getDuration() { + return mDurationMs; + } + + /** + * @return The timeline duration. This is the actual duration in the + * timeline (trimmed duration) + */ + @Override + public long getTimelineDuration() { + return mEndBoundaryTimeMs - mBeginBoundaryTimeMs; + } + + /** + * Render a frame according to the playback (in the native aspect ratio) for + * the specified media item. All effects and overlays applied to the media + * item are ignored. The extract boundaries are also ignored. This method + * can be used to playback frames when implementing trimming functionality. + * + * @param surfaceHolder SurfaceHolder used by the application + * @param timeMs time corresponding to the frame to display (relative to the + * the beginning of the media item). + * @return The accurate time stamp of the frame that is rendered . + * @throws IllegalStateException if a playback, preview or an export is + * already in progress + * @throws IllegalArgumentException if time is negative or greater than the + * media item duration + */ + public long renderFrame(SurfaceHolder surfaceHolder, long timeMs) { + return timeMs; + } + + /** + * Start the playback of this media item. This method does not block (does + * not wait for the playback to complete). The PlaybackProgressListener + * allows to track the progress at the time interval determined by the + * callbackAfterFrameCount parameter. The SurfaceHolder has to be created + * and ready for use before calling this method. + * + * @param surfaceHolder SurfaceHolder where the frames are rendered. + * @param fromMs The time (relative to the beginning of the media item) at + * which the playback will start + * @param toMs The time (relative to the beginning of the media item) at + * which the playback will stop. Use -1 to play to the end of the + * media item + * @param loop true if the playback should be looped once it reaches the end + * @param callbackAfterFrameCount The listener interface should be invoked + * after the number of frames specified by this parameter. + * @param listener The listener which will be notified of the playback + * progress + * @throws IllegalArgumentException if fromMs or toMs is beyond the playback + * duration + * @throws IllegalStateException if a playback, preview or an export is + * already in progress + */ + public void startPlayback(SurfaceHolder surfaceHolder, long fromMs, long toMs, boolean loop, + int callbackAfterFrameCount, PlaybackProgressListener listener) { + if (fromMs >= mDurationMs) { + return; + } + mPlaybackThread = new PlaybackThread(fromMs, toMs, loop, callbackAfterFrameCount, + listener); + mPlaybackThread.start(); + } + + /** + * Stop the media item playback. This method blocks until the ongoing + * playback is stopped. + * + * @return The accurate current time when stop is effective expressed in + * milliseconds + */ + public long stopPlayback() { + final long stopTimeMs; + if (mPlaybackThread != null) { + stopTimeMs = mPlaybackThread.stopPlayback(); + mPlaybackThread = null; + } else { + stopTimeMs = 0; + } + return stopTimeMs; + } + + /** + * This API allows to generate a file containing the sample volume levels of + * the Audio track of this media item. This function may take significant + * time and is blocking. The file can be retrieved using + * getAudioWaveformFilename(). + * + * @param listener The progress listener + * + * @throws IOException if the output file cannot be created + * @throws IllegalArgumentException if the mediaItem does not have a valid + * Audio track + */ + public void extractAudioWaveform(ExtractAudioWaveformProgressListener listener) + throws IOException { + // TODO: Set mAudioWaveformFilename at the end once the export is complete + } + + /** + * Get the audio waveform file name if {@link #extractAudioWaveform()} was + * successful. The file format is as following: + *
    + *
  • first 4 bytes provide the number of samples for each value, as big-endian signed
  • + *
  • 4 following bytes is the total number of values in the file, as big-endian signed
  • + *
  • all values follow as bytes Name is unique.
  • + *
+ * @return the name of the file, null if the file has not been computed or + * if there is no Audio track in the mediaItem + */ + public String getAudioWaveformFilename() { + return mAudioWaveformFilename; + } + + /** + * Set volume of the Audio track of this mediaItem + * + * @param volumePercent in %/. 100% means no change; 50% means half value, 200% + * means double, 0% means silent. + * @throws UsupportedOperationException if volume value is not supported + */ + public void setVolume(int volumePercent) { + mVolumePercentage = volumePercent; + } + + /** + * Get the volume value of the audio track as percentage. Call of this + * method before calling setVolume will always return 100% + * + * @return the volume in percentage + */ + public int getVolume() { + return mVolumePercentage; + } + + /** + * @return The video type + */ + public int getVideoType() { + return mVideoType; + } + + /** + * @return The video profile + */ + public int getVideoProfile() { + return mVideoProfile; + } + + /** + * @return The video bitrate + */ + public int getVideoBitrate() { + return mVideoBitrate; + } + + /** + * @return The audio bitrate + */ + public int getAudioBitrate() { + return mAudioBitrate; + } + + /** + * @return The number of frames per second + */ + public int getFps() { + return mFps; + } + + /** + * @return The audio codec + */ + public int getAudioType() { + return mAudioType; + } + + /** + * @return The number of audio channels + */ + public int getAudioChannels() { + return mAudioChannels; + } + + /** + * @return The audio sample frequency + */ + public int getAudioSamplingFrequency() { + return mAudioSamplingFrequency; + } +} diff --git a/media/java/android/media/videoeditor/Overlay.java b/media/java/android/media/videoeditor/Overlay.java new file mode 100755 index 0000000000000..506563643039a --- /dev/null +++ b/media/java/android/media/videoeditor/Overlay.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.videoeditor; + + +/** + * This is the super class for all Overlay classes. + * {@hide} + */ +public abstract class Overlay { + // Instance variables + private final String mUniqueId; + + protected long mStartTimeMs; + protected long mDurationMs; + + /** + * Default constructor + */ + @SuppressWarnings("unused") + private Overlay() { + mUniqueId = null; + mStartTimeMs = 0; + mDurationMs = 0; + } + + /** + * Constructor + * + * @param overlayId The overlay id + * @param startTimeMs The start time relative to the media item start time + * @param durationMs The duration + * + * @throws IllegalArgumentException if the file type is not PNG or the + * startTimeMs and durationMs are incorrect. + */ + public Overlay(String overlayId, long startTimeMs, long durationMs) { + mUniqueId = overlayId; + mStartTimeMs = startTimeMs; + mDurationMs = durationMs; + } + + /** + * @return The of the overlay + */ + public String getId() { + return mUniqueId; + } + + /** + * @return The duration of the overlay effect + */ + public long getDuration() { + return mDurationMs; + } + + /** + * If a preview or export is in progress, then this change is effective for + * next preview or export session. + * + * @param durationMs The duration in milliseconds + */ + public void setDuration(long durationMs) { + mDurationMs = durationMs; + } + + /** + * @return the start time of the overlay + */ + public long getStartTime() { + return mStartTimeMs; + } + + /** + * Set the start time for the overlay. If a preview or export is in + * progress, then this change is effective for next preview or export + * session. + * + * @param startTimeMs start time in milliseconds + */ + public void setStartTime(long startTimeMs) { + mStartTimeMs = startTimeMs; + } + + /* + * {@inheritDoc} + */ + @Override + public boolean equals(Object object) { + if (!(object instanceof Overlay)) { + return false; + } + return mUniqueId.equals(((Overlay)object).mUniqueId); + } + + /* + * {@inheritDoc} + */ + @Override + public int hashCode() { + return mUniqueId.hashCode(); + } +} diff --git a/media/java/android/media/videoeditor/OverlayFrame.java b/media/java/android/media/videoeditor/OverlayFrame.java new file mode 100755 index 0000000000000..e5d9b811be5a9 --- /dev/null +++ b/media/java/android/media/videoeditor/OverlayFrame.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.videoeditor; + + +/** + * 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. + * {@hide} + */ +public class OverlayFrame extends Overlay { + // Instance variables + private final String mFilename; + + /** + * An object of this type cannot be instantiated by using the default + * constructor + */ + @SuppressWarnings("unused") + private OverlayFrame() { + this(null, 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 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) { + super(overlayId, startTimeMs, durationMs); + mFilename = filename; + } + + /** + * Get the file name of this overlay + */ + public String getFilename() { + return mFilename; + } +} diff --git a/media/java/android/media/videoeditor/Transition.java b/media/java/android/media/videoeditor/Transition.java new file mode 100755 index 0000000000000..e4bc9a41a3605 --- /dev/null +++ b/media/java/android/media/videoeditor/Transition.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.videoeditor; + +import java.io.File; + +/** + * This class is super class for all transitions. Transitions (with the + * exception of TransitionAtStart and TransitioAtEnd) can only be inserted + * between media items. + * + * Adding a transition between MediaItems makes the + * duration of the storyboard shorter by the duration of the Transition itself. + * As a result, if the duration of the transition is larger than the smaller + * duration of the two MediaItems associated with the Transition, an exception + * will be thrown. + * + * During a transition, the audio track are cross-fading + * automatically. {@hide} + */ +public abstract class Transition { + // The transition behavior + /** The transition starts slowly and speed up */ + public static final int BEHAVIOR_SPEED_UP = 0; + /** The transition start fast and speed down */ + public static final int BEHAVIOR_SPEED_DOWN = 1; + /** The transition speed is constant */ + public static final int BEHAVIOR_LINEAR = 2; + /** The transition starts fast and ends fast with a slow middle */ + public static final int BEHAVIOR_MIDDLE_SLOW = 3; + /** The transition starts slowly and ends slowly with a fast middle */ + public static final int BEHAVIOR_MIDDLE_FAST = 4; + + // The unique id of the transition + private final String mUniqueId; + + // The transition is applied at the end of this media item + private final MediaItem mAfterMediaItem; + // The transition is applied at the beginning of this media item + private final MediaItem mBeforeMediaItem; + + // The transition behavior + protected final int mBehavior; + + // The transition duration + protected long mDurationMs; + + // The transition filename + protected String mFilename; + + /** + * An object of this type cannot be instantiated by using the default + * constructor + */ + @SuppressWarnings("unused") + private Transition() { + this(null, null, null, 0, 0); + } + + /** + * 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 durationMs The duration of the transition in milliseconds + * @param behavior The transition behavior + */ + protected Transition(String transitionId, MediaItem afterMediaItem, MediaItem beforeMediaItem, + long durationMs, int behavior) { + mUniqueId = transitionId; + mAfterMediaItem = afterMediaItem; + mBeforeMediaItem = beforeMediaItem; + mDurationMs = durationMs; + mBehavior = behavior; + } + + /** + * @return The of the transition + */ + public String getId() { + return mUniqueId; + } + + /** + * @return The media item at the end of which the transition is applied + */ + public MediaItem getAfterMediaItem() { + return mAfterMediaItem; + } + + /** + * @return The media item at the beginning of which the transition is applied + */ + public MediaItem getBeforeMediaItem() { + return mBeforeMediaItem; + } + + /** + * Set the duration of the transition. + * + * @param durationMs the duration of the transition in milliseconds + */ + public void setDuration(long durationMs) { + mDurationMs = durationMs; + } + + /** + * @return the duration of the transition in milliseconds + */ + public long getDuration() { + return mDurationMs; + } + + /** + * @return The behavior + */ + public int getBehavior() { + return mBehavior; + } + + /** + * Generate the video clip for the specified transition. + * This method may block for a significant amount of time. + * + * Before the method completes execution it sets the mFilename to + * the name of the newly generated transition video clip file. + */ + abstract void generate(); + + /** + * Remove any resources associated with this transition + */ + void invalidate() { + if (mFilename != null) { + new File(mFilename).delete(); + mFilename = null; + } + } + + /** + * @return true if the transition is generated + */ + boolean isGenerated() { + return (mFilename != null); + } + + /* + * {@inheritDoc} + */ + @Override + public boolean equals(Object object) { + if (!(object instanceof Transition)) { + return false; + } + return mUniqueId.equals(((Transition)object).mUniqueId); + } + + /* + * {@inheritDoc} + */ + @Override + public int hashCode() { + return mUniqueId.hashCode(); + } +} diff --git a/media/java/android/media/videoeditor/TransitionAlpha.java b/media/java/android/media/videoeditor/TransitionAlpha.java new file mode 100755 index 0000000000000..0a4a12f0fe027 --- /dev/null +++ b/media/java/android/media/videoeditor/TransitionAlpha.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.videoeditor; + + +/** + * This class allows to render an "alpha blending" transition according to a + * bitmap mask. The mask shows the shape of the transition all along the + * duration of the transition: just before the transition, video 1 is fully + * displayed. When the transition starts, as the time goes on, pixels of video 2 + * replace pixels of video 1 according to the gray scale pixel value of the + * mask. + * {@hide} + */ +public class TransitionAlpha extends Transition { + /** This is the input JPEG file for the mask */ + private final String mMaskFilename; + + /** + * This is percentage (between 0 and 100) of blending between video 1 and + * video 2 if this value equals 0, then the mask is strictly applied if this + * value equals 100, then the mask is not at all applied (no transition + * effect) + */ + private final int mBlendingPercent; + + /** + * If true, this value inverts the direction of the mask: white pixels of + * the mask show video 2 pixels first black pixels of the mask show video 2 + * pixels last. + */ + private final boolean mIsInvert; + + /** + * An object of this type cannot be instantiated by using the default + * constructor + */ + @SuppressWarnings("unused") + private TransitionAlpha() { + this(null, null, null, 0, 0, null, 0, false); + } + + /** + * 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 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 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. + */ + public TransitionAlpha(String transitionId, MediaItem afterMediaItem, + MediaItem beforeMediaItem, long durationMs, int behavior, String maskFilename, + int blendingPercent, boolean invert) { + super(transitionId, afterMediaItem, beforeMediaItem, durationMs, behavior); + + mMaskFilename = maskFilename; + mBlendingPercent = blendingPercent; + mIsInvert = invert; + } + + /** + * @return The blending percentage + */ + public int getBlendingPercent() { + return mBlendingPercent; + } + + /** + * @return The mask filename + */ + public String getMaskFilename() { + return mMaskFilename; + } + + /** + * @return true if the direction of the alpha blending is inverted + */ + public boolean isInvert() { + return mIsInvert; + } + + /* + * {@inheritDoc} + */ + @Override + public void generate() { + } +} diff --git a/media/java/android/media/videoeditor/TransitionAtEnd.java b/media/java/android/media/videoeditor/TransitionAtEnd.java new file mode 100755 index 0000000000000..7765bd463c1fc --- /dev/null +++ b/media/java/android/media/videoeditor/TransitionAtEnd.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.videoeditor; + + +/** + * TransitionAtEnd is a class useful to manage a predefined transition at the + * end of the movie. + * {@hide} + */ +public class TransitionAtEnd extends Transition { + /** + * This transition fades to black frame using fade out in a certain provided + * duration. This transition is always applied at the end of the movie. + */ + public static final int TYPE_FADE_TO_BLACK = 0; + + /** + * This transition fades to black frame using curtain closing: A black image is + * moved from top to bottom to cover the video. This transition is always + * applied at the end of the movie. + */ + public static final int TYPE_CURTAIN_CLOSING = 1; + + // The transition type + private final int mType; + + /** + * An object of this type cannot be instantiated by using the default + * constructor + */ + @SuppressWarnings("unused") + private TransitionAtEnd() { + this(null, null, 0, 0); + } + + /** + * Constructor. + * + * @param transitionId The transition id + * @param afterMediaItem The transition is applied to the end of this + * media item + * @param durationMs duration of the transition in milliseconds + * @param type type of the transition to apply. + */ + public TransitionAtEnd(String transitionId, MediaItem afterMediaItem, long duration, + int type) { + super(transitionId, afterMediaItem, null, duration, Transition.BEHAVIOR_LINEAR); + mType = type; + } + + /** + * Get the type of this transition + * + * @return The type of the transition + */ + public int getType() { + return mType; + } + + /* + * {@inheritDoc} + */ + @Override + void generate() { + } +} diff --git a/media/java/android/media/videoeditor/TransitionAtStart.java b/media/java/android/media/videoeditor/TransitionAtStart.java new file mode 100755 index 0000000000000..65ebd015f8cb0 --- /dev/null +++ b/media/java/android/media/videoeditor/TransitionAtStart.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.videoeditor; + + +/** + * TransitionAtStart is a class useful to manage a predefined transition at the + * beginning of the movie. + * {@hide} + */ +public class TransitionAtStart extends Transition { + /** + * This transition fades from black using fade-in in a certain provided + * duration. This transition is always applied at the beginning of the + * movie. + */ + public static final int TYPE_FADE_FROM_BLACK = 0; + + /** + * This transition fades from black frame using curtain opening: A black + * image is displayed and moves from bottom to top making the video visible. + * This transition is always applied at the beginning of the movie. + */ + public static final int TYPE_CURTAIN_OPENING = 1; + + // The transition type + private final int mType; + + /** + * An object of this type cannot be instantiated by using the default + * constructor + */ + @SuppressWarnings("unused") + private TransitionAtStart() { + this(null, null, 0, 0); + } + + /** + * Constructor + * + * @param transitionId The transition id + * @param beforeMediaItem The transition is applied to the beginning of + * this media item + * @param durationMs The duration of the transition in milliseconds + * @param type The type of the transition to apply. + */ + public TransitionAtStart(String transitionId, MediaItem beforeMediaItem, long durationMs, + int type) { + super(transitionId, null, beforeMediaItem, durationMs, + Transition.BEHAVIOR_LINEAR); + mType = type; + } + + /** + * Get the type of this transition + * + * @return The type of the transition + */ + public int getType() { + return mType; + } + + /* + * {@inheritDoc} + */ + @Override + public void generate() { + } + + /* + * {@inheritDoc} + */ + @Override + void invalidate() { + } +} diff --git a/media/java/android/media/videoeditor/TransitionCrossfade.java b/media/java/android/media/videoeditor/TransitionCrossfade.java new file mode 100755 index 0000000000000..f8223e88e6949 --- /dev/null +++ b/media/java/android/media/videoeditor/TransitionCrossfade.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.videoeditor; + + +/** + * This class allows to render a crossfade (dissolve) effect transition between + * two videos + * {@hide} + */ +public class TransitionCrossfade extends Transition { + /** + * An object of this type cannot be instantiated by using the default + * constructor + */ + @SuppressWarnings("unused") + private TransitionCrossfade() { + this(null, null, null, 0, 0); + } + + /** + * 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 durationMs duration of the transition in milliseconds + * @param behavior behavior is one of the behavior defined in Transition + * class + * + * @throws IllegalArgumentException if behavior is not supported. + */ + public TransitionCrossfade(String transitionId, MediaItem afterMediaItem, + MediaItem beforeMediaItem, long durationMs, int behavior) { + super(transitionId, afterMediaItem, beforeMediaItem, durationMs, behavior); + } + + /* + * {@inheritDoc} + */ + @Override + void generate() { + } +} diff --git a/media/java/android/media/videoeditor/TransitionFadeToBlack.java b/media/java/android/media/videoeditor/TransitionFadeToBlack.java new file mode 100755 index 0000000000000..9569a65a34656 --- /dev/null +++ b/media/java/android/media/videoeditor/TransitionFadeToBlack.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.videoeditor; + + +/** + * This class is used to render a fade to black transition between two videos. + * {@hide} + */ +public class TransitionFadeToBlack extends Transition { + /** + * An object of this type cannot be instantiated by using the default + * constructor + */ + @SuppressWarnings("unused") + private TransitionFadeToBlack() { + this(null, null, null, 0, 0); + } + + /** + * 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 durationMs duration of the transition + * @param behavior behavior is one of the behavior defined in Transition + * class + * + * @throws IllegalArgumentException if behavior is not supported. + */ + public TransitionFadeToBlack(String transitionId, MediaItem afterMediaItem, + MediaItem beforeMediaItem, long durationMs, int behavior) { + super(transitionId, afterMediaItem, beforeMediaItem, durationMs, behavior); + } + + /* + * {@inheritDoc} + */ + @Override + void generate() { + } +} diff --git a/media/java/android/media/videoeditor/TransitionSliding.java b/media/java/android/media/videoeditor/TransitionSliding.java new file mode 100755 index 0000000000000..cc9f4b287df2a --- /dev/null +++ b/media/java/android/media/videoeditor/TransitionSliding.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.media.videoeditor; + +/** + * This class allows to create sliding transitions + * {@hide} + */ +public class TransitionSliding extends Transition { + + /** Video 1 is pushed to the right while video 2 is coming from left */ + public final static int DIRECTION_RIGHT_OUT_LEFT_IN = 0; + /** Video 1 is pushed to the left while video 2 is coming from right */ + public static final int DIRECTION_LEFT_OUT_RIGHT_IN = 1; + /** Video 1 is pushed to the top while video 2 is coming from bottom */ + public static final int DIRECTION_TOP_OUT_BOTTOM_IN = 2; + /** Video 1 is pushed to the bottom while video 2 is coming from top */ + public static final int DIRECTION_BOTTOM_OUT_TOP_IN = 3; + + // The sliding transitions + private final int mSlidingDirection; + + /** + * An object of this type cannot be instantiated by using the default + * constructor + */ + @SuppressWarnings("unused") + private TransitionSliding() { + this(null, null, null, 0, 0, 0); + } + + /** + * 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 durationMs duration of the transition in milliseconds + * @param behavior behavior is one of the behavior defined in Transition + * class + * @param direction direction shall be one of the supported directions like + * RIGHT_OUT_LEFT_IN + * + * @throws IllegalArgumentException if behavior is not supported. + */ + public TransitionSliding(String transitionId, MediaItem afterMediaItem, + MediaItem beforeMediaItem, long durationMs, int behavior, int direction) { + super(transitionId, afterMediaItem, beforeMediaItem, durationMs, behavior); + mSlidingDirection = direction; + } + + /** + * @return The sliding direction + */ + public int getDirection() { + return mSlidingDirection; + } + + /* + * {@inheritDoc} + */ + @Override + void generate() { + } +} diff --git a/media/java/android/media/videoeditor/VideoEditor.java b/media/java/android/media/videoeditor/VideoEditor.java new file mode 100755 index 0000000000000..aa8f2cb2b538e --- /dev/null +++ b/media/java/android/media/videoeditor/VideoEditor.java @@ -0,0 +1,493 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.videoeditor; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CancellationException; + +import android.view.SurfaceHolder; + +/** + * This is the interface implemented by classes which provide video editing + * functionality. The VideoEditor implementation class manages all input and + * output files. Unless specifically mentioned, methods are blocking. A + * typical editing session may consist of the following sequence of operations: + * + *
    + *
  • Add a set of MediaItems
  • + *
  • Apply a set of Transitions between MediaItems
  • + *
  • Add Effects and Overlays to media items
  • + *
  • Preview the movie at any time
  • + *
  • Save the VideoEditor implementation class internal state
  • + *
  • Release the VideoEditor implementation class instance by invoking + * {@link #release()} + *
+ * The internal VideoEditor state consists of the following elements: + *
    + *
  • Ordered & trimmed MediaItems
  • + *
  • Transition video clips
  • + *
  • Overlays
  • + *
  • Effects
  • + *
  • Audio waveform for the background audio and MediaItems
  • + *
  • Project thumbnail
  • + *
  • Last exported movie.
  • + *
  • Other project specific data such as the current aspect ratio.
  • + *
+ * {@hide} + */ +public interface VideoEditor { + // The file name of the project thumbnail + public static final String THUMBNAIL_FILENAME = "thumbnail.jpg"; + + // Use this value instead of the specific end of the storyboard timeline + // value. + public final static int DURATION_OF_STORYBOARD = -1; + + /** + * This listener interface is used by the VideoEditor to emit preview + * progress notifications. This callback should be invoked after the + * number of frames specified by + * {@link #startPreview(SurfaceHolder surfaceHolder, long fromMs, + * int callbackAfterFrameCount, PreviewProgressListener listener)} + */ + public interface PreviewProgressListener { + /** + * This method notifies the listener of the current time position while + * previewing a project. + * + * @param videoEditor The VideoEditor instance + * @param timeMs The current preview position (expressed in milliseconds + * since the beginning of the storyboard timeline). + * @param end true if the end of the timeline was reached + */ + public void onProgress(VideoEditor videoEditor, long timeMs, boolean end); + } + + /** + * This listener interface is used by the VideoEditor to emit export status + * notifications. + * {@link #export(String filename, ExportProgressListener listener, int height, int bitrate)} + */ + public interface ExportProgressListener { + /** + * This method notifies the listener of the progress status of a export + * operation. + * + * @param videoEditor The VideoEditor instance + * @param filename The name of the file which is in the process of being + * exported. + * @param progress The progress in %. At the beginning of the export, this + * value is set to 0; at the end, the value is set to 100. + */ + public void onProgress(VideoEditor videoEditor, String filename, int progress); + } + + /** + * @return The path where the VideoEditor stores all files related to the + * project + */ + public String getPath(); + + /** + * This method releases all in-memory resources used by the VideoEditor + * instance. All pending operations such as preview, export and extract + * audio waveform must be canceled. + */ + public void release(); + + /** + * Persist the current internal state of VideoEditor to the project path. + * The VideoEditor state may be restored by invoking the + * {@link VideoEditorFactory#load(String)} method. This method does not + * release the internal in-memory state of the VideoEditor. To release + * the in-memory state of the VideoEditor the {@link #release()} method + * must be invoked. + * + * Pending transition generations must be allowed to complete before the + * state is saved. + * Pending audio waveform generations must be allowed to complete. + * Pending export operations must be allowed to continue. + */ + public void save() throws IOException; + + /** + * Create the output movie based on all media items added and the applied + * storyboard items. This method can take a long time to execute and is + * blocking. The application will receive progress notifications via the + * ExportProgressListener. Specific implementations may not support multiple + * simultaneous export operations. + * + * Note that invoking methods which would change the contents of the output + * movie throw an IllegalStateException while an export operation is + * pending. + * + * @param filename The output file name (including the full path) + * @param height The height of the output video file. The supported values + * for height are described in the MediaProperties class, for + * example: HEIGHT_480. The width will be automatically + * computed according to the aspect ratio provided by + * {@link #setAspectRatio(int)} + * @param bitrate The bitrate of the output video file. This is approximate + * value for the output movie. Supported bitrate values are + * described in the MediaProperties class for example: + * BITRATE_384K + * @param listener The listener for progress notifications. Use null if + * export progress notifications are not needed. + * + * @throws IllegalArgumentException if height or bitrate are not supported. + * @throws IOException if output file cannot be created + * @throws IllegalStateException if a preview or an export is in progress or + * if no MediaItem has been added + * @throws CancellationException if export is canceled by calling + * {@link #cancelExport()} + * @throws UnsupportOperationException if multiple simultaneous export() + * are not allowed + */ + public void export(String filename, int height, int bitrate, ExportProgressListener listener) + throws IOException; + + /** + * Cancel the running export operation. This method blocks until the + * export is canceled and the exported file (if any) is deleted. If the + * export completed by the time this method is invoked, the export file + * will be deleted. + * + * @param filename The filename which identifies the export operation to be + * canceled. + **/ + public void cancelExport(String filename); + + /** + * Add a media item at the end of the storyboard. + * + * @param mediaItem The media item object to add + * @throws IllegalStateException if a preview or an export is in progress or + * if the media item id is not unique across all the media items + * added. + */ + public void addMediaItem(MediaItem mediaItem); + + /** + * Insert a media item after the media item with the specified id. + * + * @param mediaItem The media item object to insert + * @param afterMediaItemId Insert the mediaItem after the media item + * identified by this id. If this parameter is null, the media + * item is inserted at the beginning of the timeline. + * + * @throws IllegalStateException if a preview or an export is in progress + * @throws IllegalArgumentException if media item with the specified id does + * not exist (null is a valid value) or if the media item id is + * not unique across all the media items added. + */ + public void insertMediaItem(MediaItem mediaItem, String afterMediaItemId); + + /** + * Move a media item after the media item with the specified id. + * + * Note: The project thumbnail is regenerated if the media item is or + * becomes the first media item in the storyboard timeline. + * + * @param mediaItemId The id of the media item to move + * @param afterMediaItemId Move the media item identified by mediaItemId after + * the media item identified by this parameter. If this parameter + * is null, the media item is moved at the beginning of the + * timeline. + * + * @throws IllegalStateException if a preview or an export is in progress + * @throws IllegalArgumentException if one of media item ids is invalid + * (null is a valid value) + */ + public void moveMediaItem(String mediaItemId, String afterMediaItemId); + + /** + * Remove the media item with the specified id. If there are transitions + * before or after this media item, then this/these transition(s) are + * removed from the storyboard. If the extraction of the audio waveform is + * in progress, the extraction is canceled and the file is deleted. + * + * Effects and overlays associated with the media item will also be + * removed. + * + * Note: The project thumbnail is regenerated if the media item which + * is removed is the first media item in the storyboard or if the + * media item is the only one in the storyboard. If the + * media item is the only one in the storyboard, the project thumbnail + * will be set to a black frame and the aspect ratio will revert to the + * default aspect ratio, and this method is equivalent to + * removeAllMediaItems() in this case. + * + * @param mediaItemId The unique id of the media item to be removed + * + * @return The media item that was removed + * + * @throws IllegalStateException if a preview or an export is in progress + * @throws IllegalArgumentException if media item with the specified id + * does not exist + */ + public MediaItem removeMediaItem(String mediaItemId); + + /** + * Remove all media items in the storyboard. All effects, overlays and all + * transitions are also removed. + * + * Note: The project thumbnail will be set to a black frame and the aspect + * ratio will revert to the default aspect ratio. + * + * @throws IllegalStateException if a preview or an export is in progress + */ + public void removeAllMediaItems(); + + /** + * Get the list of media items in the order in which it they appear in the + * storyboard timeline. + * + * Note that if any media item source files are no longer + * accessible, this method will still provide the full list of media items. + * + * @return The list of media items. If no media item exist an empty list + * will be returned. + */ + public List getAllMediaItems(); + + /** + * Find the media item with the specified id + * + * @param mediaItemId The media item id + * + * @return The media item with the specified id (null if it does not exist) + */ + public MediaItem getMediaItem(String mediaItemId); + + /** + * Add a transition between the media items specified by the transition. + * If a transition existed at the same position it is invalidated and then + * the transition is replaced. Note that the new transition video clip is + * not automatically generated by this method. The + * {@link Transition#generate()} method must be invoked to generate + * the transition video clip. + * + * Note that the TransitionAtEnd and TransitionAtStart are special kinds + * that can not be applied between two media items. + * + * A crossfade audio transition will be automatically applied regardless of + * the video transition. + * + * @param transition The transition to apply + * + * @throws IllegalStateException if a preview or an export is in progress + * @throws IllegalArgumentException if the transition duration is larger + * than the smallest duration of the two media item files or + * if the two media items specified in the transition are not + * adjacent + */ + public void addTransition(Transition transition); + + /** + * Remove the transition with the specified id. + * + * @param transitionId The id of the transition to be removed + * + * @return The transition that was removed + * @throws IllegalStateException if a preview or an export is in progress + * @throws IllegalArgumentException if transition with the specified id does + * not exist + */ + public Transition removeTransition(String transitionId); + + /** + * Get the list of transitions + * + * @return The list of transitions. If no transitions exist an empty list + * will be returned. + */ + public List getAllTransitions(); + + /** + * Find the transition with the specified transition id. + * + * @param transitionId The transition id + * + * @return The transition + */ + public Transition getTransition(String transitionId); + + /** + * Add the specified AudioTrack to the storyboard. Note: Specific + * implementations may support a limited number of audio tracks (e.g. only + * one audio track) + * + * @param audioTrack The AudioTrack to add + * @throws UnsupportedOperationException if the implementation supports a + * limited number of audio tracks. + * @throws IllegalArgumentException if media item is not unique across all + * the audio tracks already added. + */ + public void addAudioTrack(AudioTrack audioTrack); + + /** + * Insert an audio track after the audio track with the specified id. Use + * addAudioTrack to add an audio track at the end of the storyboard + * timeline. + * + * @param audioTrack The audio track object to insert + * @param afterAudioTrackId Insert the audio track after the audio track + * identified by this parameter. If this parameter is null the + * audio track is added at the beginning of the timeline. + * @throws IllegalStateException if a preview or an export is in progress + * @throws IllegalArgumentException if media item with the specified id does + * not exist (null is a valid value). if media item is not + * unique across all the audio tracks already added. + * @throws UnsupportedOperationException if the implementation supports a + * limited number of audio tracks + */ + public void insertAudioTrack(AudioTrack audioTrack, String afterAudioTrackId); + + /** + * Move an AudioTrack after the AudioTrack with the specified id. + * + * @param audioTrackId The id of the AudioTrack to move + * @param afterAudioTrackId Move the AudioTrack identified by audioTrackId + * after the AudioTrack identified by this parameter. If this + * parameter is null the audio track is added at the beginning of + * the timeline. + * @throws IllegalStateException if a preview or an export is in progress + * @throws IllegalArgumentException if one of media item ids is invalid + * (null is a valid value) + */ + public void moveAudioTrack(String audioTrackId, String afterAudioTrackId); + + /** + * Remove the audio track with the specified id. If the extraction of the + * audio waveform is in progress, the extraction is canceled and the file is + * deleted. + * + * @param audioTrackId The id of the audio track to be removed + * + * @return The audio track that was removed + * @throws IllegalStateException if a preview or an export is in progress + */ + public AudioTrack removeAudioTrack(String audioTrackId); + + /** + * Get the list of AudioTracks in order in which they appear in the storyboard. + * + * Note that if any AudioTrack source files are not accessible anymore, + * this method will still provide the full list of audio tracks. + * + * @return The list of AudioTracks. If no audio tracks exist an empty list + * will be returned. + */ + public List getAllAudioTracks(); + + /** + * Find the AudioTrack with the specified id + * + * @param audioTrackId The AudioTrack id + * + * @return The AudioTrack with the specified id (null if it does not exist) + */ + public AudioTrack getAudioTrack(String audioTrackId); + + /** + * Set the aspect ratio used in the preview and the export movie. + * + * The default aspect ratio is ASPECTRATIO_16_9 (16:9). + * + * @param aspectRatio to apply. If aspectRatio is the same as the current + * aspect ratio, then this function just returns. The supported + * aspect ratio are defined in the MediaProperties class for + * example: ASPECTRATIO_16_9 + * + * @throws IllegalStateException if a preview or an export is in progress + * @throws IllegalArgumentException if aspect ratio is not supported + */ + public void setAspectRatio(int aspectRatio); + + /** + * Get current aspect ratio. + * + * @return The aspect ratio as described in MediaProperties + */ + public int getAspectRatio(); + + /** + * Get the preview (and output movie) duration. + * + * @return The duration of the preview (and output movie) + */ + public long getDuration(); + + /** + * Render a frame according to the preview aspect ratio and activating all + * storyboard items relative to the specified time. + * + * @param surfaceHolder SurfaceHolder used by the application + * @param timeMs time corresponding to the frame to display + * + * @return The accurate time stamp of the frame that is rendered + * . + * @throws IllegalStateException if a preview or an export is already + * in progress + * @throws IllegalArgumentException if time is negative or beyond the + * preview duration + */ + public long renderPreviewFrame(SurfaceHolder surfaceHolder, long timeMs); + + /** + * This method must be called after the aspect ratio of the project changes + * and before startPreview is called. Note that this method may block for + * an extensive period of time. + */ + public void generatePreview(); + + /** + * Start the preview of all the storyboard items applied on all MediaItems + * This method does not block (does not wait for the preview to complete). + * The PreviewProgressListener allows to track the progress at the time + * interval determined by the callbackAfterFrameCount parameter. The + * SurfaceHolder has to be created and ready for use before calling this + * method. The method is a no-op if there are no MediaItems in the + * storyboard. + * + * @param surfaceHolder SurfaceHolder where the preview is rendered. + * @param fromMs The time (relative to the timeline) at which the preview + * will start + * @param toMs The time (relative to the timeline) at which the preview will + * stop. Use -1 to play to the end of the timeline + * @param loop true if the preview should be looped once it reaches the end + * @param callbackAfterFrameCount The listener interface should be invoked + * after the number of frames specified by this parameter. + * @param listener The listener which will be notified of the preview + * progress + * @throws IllegalArgumentException if fromMs is beyond the preview duration + * @throws IllegalStateException if a preview or an export is already in + * progress + */ + public void startPreview(SurfaceHolder surfaceHolder, long fromMs, long toMs, boolean loop, + int callbackAfterFrameCount, PreviewProgressListener listener); + + /** + * Stop the current preview. This method blocks until ongoing preview is + * stopped. Ignored if there is no preview running. + * + * @return The accurate current time when stop is effective expressed in + * milliseconds + */ + public long stopPreview(); +} diff --git a/media/java/android/media/videoeditor/VideoEditorFactory.java b/media/java/android/media/videoeditor/VideoEditorFactory.java new file mode 100755 index 0000000000000..2c56fc215d243 --- /dev/null +++ b/media/java/android/media/videoeditor/VideoEditorFactory.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.videoeditor; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + + +/** + * The VideoEditorFactory class must be used to instantiate VideoEditor objects + * by creating a new project {@link #create(String)} or by loading an + * existing project {@link #load(String)}. + * {@hide} + */ +public class VideoEditorFactory { + /** + * This is the factory method for creating a new VideoEditor instance. + * + * @param projectPath The path where all VideoEditor internal + * files are stored. When a project is deleted the application is + * responsible for deleting the path and its contents. + * + * @return The VideoEditor instance + * + * @throws IOException if path does not exist or if the path can + * not be accessed in read/write mode + * @throws IllegalStateException if a previous VideoEditor instance has not + * been released + */ + public static VideoEditor create(String projectPath) throws IOException { + // If the project path does not exist create it + final File dir = new File(projectPath); + if (!dir.exists()) { + if (!dir.mkdirs()) { + throw new FileNotFoundException("Cannot create project path: " + projectPath); + } + } + return new VideoEditorTestImpl(projectPath); + } + + /** + * This is the factory method for instantiating a VideoEditor from the + * internal state previously saved with the + * {@link VideoEditor#save(String)} method. + * + * @param projectPath The path where all VideoEditor internal files + * are stored. When a project is deleted the application is + * responsible for deleting the path and its contents. + * @param generatePreview if set to true the + * {@link MediaEditor#generatePreview()} will be called internally to + * generate any needed transitions. + * + * @return The VideoEditor instance + * + * @throws IOException if path does not exist or if the path can + * not be accessed in read/write mode or if one of the resource + * media files cannot be retrieved + * @throws IllegalStateException if a previous VideoEditor instance has not + * been released + */ + public static VideoEditor load(String projectPath, boolean generatePreview) throws IOException { + final VideoEditorTestImpl videoEditor = new VideoEditorTestImpl(projectPath); + if (generatePreview) { + videoEditor.generatePreview(); + } + return videoEditor; + } +} diff --git a/media/java/android/media/videoeditor/VideoEditorTestImpl.java b/media/java/android/media/videoeditor/VideoEditorTestImpl.java new file mode 100644 index 0000000000000..a23c5c62159d8 --- /dev/null +++ b/media/java/android/media/videoeditor/VideoEditorTestImpl.java @@ -0,0 +1,777 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.videoeditor; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import android.util.Log; +import android.util.Xml; +import android.view.SurfaceHolder; + +/** + * The VideoEditor implementation + * {@hide} + */ +public class VideoEditorTestImpl implements VideoEditor { + // Logging + private static final String TAG = "VideoEditorImpl"; + + // The project filename + private static final String PROJECT_FILENAME = "videoeditor.xml"; + + // XML tags + 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 ATTR_ID = "id"; + private static final String ATTR_FILENAME = "filename"; + private static final String ATTR_AUDIO_WAVEFORM_FILENAME = "wavefoem"; + private static final String ATTR_RENDERING_MODE = "rendering_mode"; + private static final String ATTR_ASPECT_RATIO = "aspect_ratio"; + private static final String ATTR_TYPE = "type"; + private static final String ATTR_DURATION = "duration"; + private static final String ATTR_BEGIN_TIME = "start_time"; + private static final String ATTR_END_TIME = "end_time"; + private static final String ATTR_VOLUME = "volume"; + + private static long mDurationMs; + private final String mProjectPath; + private final List mMediaItems = new ArrayList(); + private final List mAudioTracks = new ArrayList(); + private final List mTransitions = new ArrayList(); + private PreviewThread mPreviewThread; + private int mAspectRatio; + + /** + * The preview thread + */ + private class PreviewThread extends Thread { + // Instance variables + private final static long FRAME_DURATION = 33; + private final PreviewProgressListener mListener; + private final int mCallbackAfterFrameCount; + private final long mFromMs, mToMs; + private boolean mRun, mLoop; + private long mPositionMs; + + /** + * Constructor + * + * @param fromMs Start preview at this position + * @param toMs The time (relative to the timeline) at which the preview + * will stop. Use -1 to play to the end of the timeline + * @param callbackAfterFrameCount The listener interface should be invoked + * after the number of frames specified by this parameter. + * @param loop true if the preview should be looped once it reaches the end + * @param listener The listener + */ + public PreviewThread(long fromMs, long toMs, boolean loop, int callbackAfterFrameCount, + PreviewProgressListener listener) { + mPositionMs = mFromMs = fromMs; + if (toMs < 0) { + mToMs = mDurationMs; + } else { + mToMs = toMs; + } + mLoop = loop; + mCallbackAfterFrameCount = callbackAfterFrameCount; + mListener = listener; + mRun = true; + } + + /* + * {@inheritDoc} + */ + @Override + public void run() { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "===> PreviewThread.run enter"); + } + int frameCount = 0; + while (mRun) { + try { + sleep(FRAME_DURATION); + } catch (InterruptedException ex) { + break; + } + frameCount++; + mPositionMs += FRAME_DURATION; + + if (mPositionMs >= mToMs) { + if (!mLoop) { + if (mListener != null) { + mListener.onProgress(VideoEditorTestImpl.this, mPositionMs, true); + } + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "PreviewThread.run playback complete"); + } + break; + } else { + // Fire a notification for the end of the clip + if (mListener != null) { + mListener.onProgress(VideoEditorTestImpl.this, mToMs, false); + } + + // Rewind + mPositionMs = mFromMs; + if (mListener != null) { + mListener.onProgress(VideoEditorTestImpl.this, mPositionMs, false); + } + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "PreviewThread.run playback complete"); + } + frameCount = 0; + } + } else { + if (frameCount == mCallbackAfterFrameCount) { + if (mListener != null) { + mListener.onProgress(VideoEditorTestImpl.this, mPositionMs, false); + } + frameCount = 0; + } + } + } + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "===> PreviewThread.run exit"); + } + } + + /** + * Stop the preview + * + * @return The stop position + */ + public long stopPreview() { + mRun = false; + try { + join(); + } catch (InterruptedException ex) { + } + return mPositionMs; + } + }; + + /** + * Constructor + * + * @param projectPath + */ + public VideoEditorTestImpl(String projectPath) throws IOException { + mProjectPath = projectPath; + final File projectXml = new File(projectPath, PROJECT_FILENAME); + if (projectXml.exists()) { + try { + load(); + } catch (Exception ex) { + throw new IOException(ex); + } + } else { + mAspectRatio = MediaProperties.ASPECT_RATIO_16_9; + mDurationMs = 0; + } + } + + /* + * {@inheritDoc} + */ + public String getPath() { + return mProjectPath; + } + + /* + * {@inheritDoc} + */ + public synchronized void addMediaItem(MediaItem mediaItem) { + if (mPreviewThread != null) { + throw new IllegalStateException("Previewing is in progress"); + } + + if (mMediaItems.contains(mediaItem)) { + throw new IllegalArgumentException("Media item already exists: " + mediaItem.getId()); + } + + mMediaItems.add(mediaItem); + computeTimelineDuration(); + } + + /* + * {@inheritDoc} + */ + public synchronized void insertMediaItem(MediaItem mediaItem, String afterMediaItemId) { + if (mPreviewThread != null) { + throw new IllegalStateException("Previewing is in progress"); + } + + if (mMediaItems.contains(mediaItem)) { + throw new IllegalArgumentException("Media item already exists: " + mediaItem.getId()); + } + + if (afterMediaItemId == null) { + if (mMediaItems.size() > 0) { + final MediaItem mi = mMediaItems.get(0); + // Invalidate the transition at the beginning of the timeline + removeTransitionBefore(mi); + } + mMediaItems.add(0, mediaItem); + computeTimelineDuration(); + } else { + final int mediaItemCount = mMediaItems.size(); + for (int i = 0; i < mediaItemCount; i++) { + final MediaItem mi = mMediaItems.get(i); + if (mi.getId().equals(afterMediaItemId)) { + // Invalidate the transition at this position + removeTransitionAfter(mi); + // Insert the new media item + mMediaItems.add(i+1, mediaItem); + computeTimelineDuration(); + return; + } + } + throw new IllegalArgumentException("MediaItem not found: " + afterMediaItemId); + } + } + + /* + * {@inheritDoc} + */ + public synchronized void moveMediaItem(String mediaItemId, String afterMediaItemId) { + if (mPreviewThread != null) { + throw new IllegalStateException("Previewing is in progress"); + } + + final MediaItem moveMediaItem = removeMediaItem(mediaItemId); + if (moveMediaItem == null) { + throw new IllegalArgumentException("Target MediaItem not found: " + mediaItemId); + } + + if (afterMediaItemId == null) { + if (mMediaItems.size() > 0) { + final MediaItem mi = mMediaItems.get(0); + // Invalidate adjacent transitions at the insertion point + removeTransitionBefore(mi); + // Insert the media item at the new position + mMediaItems.add(0, moveMediaItem); + computeTimelineDuration(); + } else { + throw new IllegalStateException("Cannot move media item (it is the only item)"); + } + } else { + final int mediaItemCount = mMediaItems.size(); + for (int i = 0; i < mediaItemCount; i++) { + final MediaItem mi = mMediaItems.get(i); + if (mi.getId().equals(afterMediaItemId)) { + // Invalidate adjacent transitions at the insertion point + removeTransitionAfter(mi); + // Insert the media item at the new position + mMediaItems.add(i+1, moveMediaItem); + computeTimelineDuration(); + return; + } + } + + throw new IllegalArgumentException("MediaItem not found: " + afterMediaItemId); + } + } + + /* + * {@inheritDoc} + */ + public synchronized MediaItem removeMediaItem(String mediaItemId) { + if (mPreviewThread != null) { + throw new IllegalStateException("Previewing is in progress"); + } + + final MediaItem mediaItem = getMediaItem(mediaItemId); + if (mediaItem != null) { + // Remove the media item + mMediaItems.remove(mediaItem); + // Remove the adjacent transitions + removeAdjacentTransitions(mediaItem); + computeTimelineDuration(); + } + + return mediaItem; + } + + /* + * {@inheritDoc} + */ + public synchronized MediaItem getMediaItem(String mediaItemId) { + for (MediaItem mediaItem : mMediaItems) { + if (mediaItem.getId().equals(mediaItemId)) { + return mediaItem; + } + } + + return null; + } + + /* + * {@inheritDoc} + */ + public synchronized List getAllMediaItems() { + return mMediaItems; + } + + /* + * {@inheritDoc} + */ + public synchronized void removeAllMediaItems() { + mMediaItems.clear(); + + // Invalidate all transitions + for (Transition transition : mTransitions) { + transition.invalidate(); + } + mTransitions.clear(); + + mDurationMs = 0; + } + + /* + * {@inheritDoc} + */ + public synchronized void addTransition(Transition transition) { + // If a transition already exists at the specified position then + // invalidate it. + final Iterator it = mTransitions.iterator(); + while (it.hasNext()) { + final Transition t = it.next(); + if (t.getAfterMediaItem() == transition.getAfterMediaItem() + || t.getBeforeMediaItem() == transition.getBeforeMediaItem()) { + it.remove(); + t.invalidate(); + break; + } + } + + mTransitions.add(transition); + + // Cross reference the transitions + final MediaItem afterMediaItem = transition.getAfterMediaItem(); + if (afterMediaItem != null) { + afterMediaItem.setEndTransition(transition); + } + final MediaItem beforeMediaItem = transition.getBeforeMediaItem(); + if (beforeMediaItem != null) { + beforeMediaItem.setBeginTransition(transition); + } + computeTimelineDuration(); + } + + /* + * {@inheritDoc} + */ + public synchronized Transition removeTransition(String transitionId) { + if (mPreviewThread != null) { + throw new IllegalStateException("Previewing is in progress"); + } + + final Transition transition = getTransition(transitionId); + if (transition != null) { + mTransitions.remove(transition); + transition.invalidate(); + computeTimelineDuration(); + } + + // Cross reference the transitions + final MediaItem afterMediaItem = transition.getAfterMediaItem(); + if (afterMediaItem != null) { + afterMediaItem.setEndTransition(null); + } + final MediaItem beforeMediaItem = transition.getBeforeMediaItem(); + if (beforeMediaItem != null) { + beforeMediaItem.setBeginTransition(null); + } + + return transition; + } + + /* + * {@inheritDoc} + */ + public List getAllTransitions() { + return mTransitions; + } + + /* + * {@inheritDoc} + */ + public Transition getTransition(String transitionId) { + for (Transition transition : mTransitions) { + if (transition.getId().equals(transitionId)) { + return transition; + } + } + + return null; + } + + /* + * {@inheritDoc} + */ + public synchronized void addAudioTrack(AudioTrack audioTrack) { + if (mPreviewThread != null) { + throw new IllegalStateException("Previewing is in progress"); + } + + mAudioTracks.add(audioTrack); + } + + /* + * {@inheritDoc} + */ + public synchronized void insertAudioTrack(AudioTrack audioTrack, String afterAudioTrackId) { + if (mPreviewThread != null) { + throw new IllegalStateException("Previewing is in progress"); + } + + if (afterAudioTrackId == null) { + mAudioTracks.add(0, audioTrack); + } else { + final int audioTrackCount = mAudioTracks.size(); + for (int i = 0; i < audioTrackCount; i++) { + AudioTrack at = mAudioTracks.get(i); + if (at.getId().equals(afterAudioTrackId)) { + mAudioTracks.add(i+1, audioTrack); + return; + } + } + + throw new IllegalArgumentException("AudioTrack not found: " + afterAudioTrackId); + } + } + + /* + * {@inheritDoc} + */ + public synchronized void moveAudioTrack(String audioTrackId, String afterAudioTrackId) { + throw new IllegalStateException("Not supported"); + } + + /* + * {@inheritDoc} + */ + public synchronized AudioTrack removeAudioTrack(String audioTrackId) { + if (mPreviewThread != null) { + throw new IllegalStateException("Previewing is in progress"); + } + + final AudioTrack audioTrack = getAudioTrack(audioTrackId); + if (audioTrack != null) { + mAudioTracks.remove(audioTrack); + } + + return audioTrack; + } + + /* + * {@inheritDoc} + */ + public AudioTrack getAudioTrack(String audioTrackId) { + if (mPreviewThread != null) { + throw new IllegalStateException("Previewing is in progress"); + } + + final AudioTrack audioTrack = getAudioTrack(audioTrackId); + if (audioTrack != null) { + mAudioTracks.remove(audioTrack); + } + + return audioTrack; + } + + /* + * {@inheritDoc} + */ + public List getAllAudioTracks() { + return mAudioTracks; + } + + /* + * {@inheritDoc} + */ + public void save() throws IOException { + final XmlSerializer serializer = Xml.newSerializer(); + final StringWriter writer = new StringWriter(); + serializer.setOutput(writer); + serializer.startDocument("UTF-8", true); + serializer.startTag("", TAG_PROJECT); + serializer.attribute("", ATTR_ASPECT_RATIO, Integer.toString(mAspectRatio)); + + 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())); + if (mediaItem instanceof MediaVideoItem) { + final MediaVideoItem mvi = (MediaVideoItem)mediaItem; + serializer.attribute("", ATTR_BEGIN_TIME, Long.toString(mvi.getBoundaryBeginTime())); + 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()); + } + } else if (mediaItem instanceof MediaImageItem) { + serializer.attribute("", ATTR_DURATION, Long.toString(mediaItem.getDuration())); + } + serializer.endTag("", TAG_MEDIA_ITEM); + } + serializer.endTag("", TAG_MEDIA_ITEMS); + + serializer.endTag("", TAG_PROJECT); + serializer.endDocument(); + + // Save the metadata XML file + final FileOutputStream out = new FileOutputStream(new File(getPath(), PROJECT_FILENAME)); + out.write(writer.toString().getBytes()); + out.flush(); + out.close(); + } + + /** + * Load the project form XML + */ + private void load() throws FileNotFoundException, XmlPullParserException, IOException { + final File file = new File(mProjectPath, PROJECT_FILENAME); + // Load the metadata + final XmlPullParser parser = Xml.newPullParser(); + parser.setInput(new FileInputStream(file), "UTF-8"); + int eventType = parser.getEventType(); + String name; + while (eventType != XmlPullParser.END_DOCUMENT) { + switch (eventType) { + case XmlPullParser.START_TAG: { + name = parser.getName(); + if (name.equals(TAG_PROJECT)) { + mAspectRatio = Integer.parseInt(parser.getAttributeValue("", + ATTR_ASPECT_RATIO)); + } else if (name.equals(TAG_MEDIA_ITEM)) { + final String mediaItemId = parser.getAttributeValue("", ATTR_ID); + final String type = parser.getAttributeValue("", ATTR_TYPE); + final String filename = parser.getAttributeValue("", ATTR_FILENAME); + final int renderingMode = Integer.parseInt(parser.getAttributeValue("", ATTR_RENDERING_MODE)); + final MediaItem mediaItem; + if (MediaImageItem.class.getSimpleName().equals(type)) { + final long durationMs = Long.parseLong(parser.getAttributeValue("", ATTR_DURATION)); + mediaItem = new MediaImageItem(mediaItemId, filename, durationMs, + renderingMode); + } else if (MediaVideoItem.class.getSimpleName().equals(type)) { + final String audioWaveformFilename = parser.getAttributeValue("", ATTR_AUDIO_WAVEFORM_FILENAME); + mediaItem = new MediaVideoItem(mediaItemId, filename, renderingMode, audioWaveformFilename); + + final long beginTimeMs = Long.parseLong(parser.getAttributeValue("", ATTR_BEGIN_TIME)); + final long endTimeMs = Long.parseLong(parser.getAttributeValue("", ATTR_END_TIME)); + ((MediaVideoItem)mediaItem).setExtractBoundaries(beginTimeMs, endTimeMs); + + final int volumePercent = Integer.parseInt(parser.getAttributeValue("", ATTR_VOLUME)); + ((MediaVideoItem)mediaItem).setVolume(volumePercent); + } else { + Log.e(TAG, "Unknown media item type: " + type); + mediaItem = null; + } + mMediaItems.add(mediaItem); + } + break; + } + + default: { + break; + } + } + eventType = parser.next(); + } + + computeTimelineDuration(); + } + + public void cancelExport(String filename) { + } + + public void export(String filename, int height, int bitrate, ExportProgressListener listener) + throws IOException { + } + + /* + * {@inheritDoc} + */ + public void generatePreview() { + // Generate all the needed transitions + for (Transition transition : mTransitions) { + if (!transition.isGenerated()) { + transition.generate(); + } + } + + // This is necessary because the user may had called setDuration on MediaImageItems + computeTimelineDuration(); + } + + /* + * {@inheritDoc} + */ + public void release() { + stopPreview(); + } + + /* + * {@inheritDoc} + */ + public long getDuration() { + // Since MediaImageItem can change duration we need to compute the duration here + computeTimelineDuration(); + return mDurationMs; + } + + /* + * {@inheritDoc} + */ + public int getAspectRatio() { + return mAspectRatio; + } + + /* + * {@inheritDoc} + */ + public void setAspectRatio(int aspectRatio) { + mAspectRatio = aspectRatio; + } + + /* + * {@inheritDoc} + */ + public long renderPreviewFrame(SurfaceHolder surfaceHolder, long timeMs) { + if (mPreviewThread != null) { + throw new IllegalStateException("Previewing is in progress"); + } + return timeMs; + } + + /* + * {@inheritDoc} + */ + public synchronized void startPreview(SurfaceHolder surfaceHolder, long fromMs, + long toMs, boolean loop, int callbackAfterFrameCount, + PreviewProgressListener listener) { + if (fromMs >= mDurationMs) { + return; + } + mPreviewThread = new PreviewThread(fromMs, toMs, loop, callbackAfterFrameCount, listener); + mPreviewThread.start(); + } + + /* + * {@inheritDoc} + */ + public synchronized long stopPreview() { + final long stopTimeMs; + if (mPreviewThread != null) { + stopTimeMs = mPreviewThread.stopPreview(); + mPreviewThread = null; + } else { + stopTimeMs = 0; + } + return stopTimeMs; + } + + /** + * Compute the duration + */ + private void computeTimelineDuration() { + mDurationMs = 0; + for (MediaItem mediaItem : mMediaItems) { + mDurationMs += mediaItem.getTimelineDuration(); + } + + // Subtract the transition times + for (Transition transition : mTransitions) { + if (!(transition instanceof TransitionAtStart) && !(transition instanceof TransitionAtEnd)) { + mDurationMs -= transition.getDuration(); + } + } + } + + /** + * Remove transitions associated with the specified media item + * + * @param mediaItem The media item + */ + private void removeAdjacentTransitions(MediaItem mediaItem) { + final Iterator it = mTransitions.iterator(); + while (it.hasNext()) { + Transition t = it.next(); + if (t.getAfterMediaItem() == mediaItem || t.getBeforeMediaItem() == mediaItem) { + it.remove(); + t.invalidate(); + mediaItem.setBeginTransition(null); + mediaItem.setEndTransition(null); + break; + } + } + } + + /** + * Remove the transition before this media item + * + * @param mediaItem The media item + */ + private void removeTransitionBefore(MediaItem mediaItem) { + final Iterator it = mTransitions.iterator(); + while (it.hasNext()) { + Transition t = it.next(); + if (t.getBeforeMediaItem() == mediaItem) { + it.remove(); + t.invalidate(); + mediaItem.setBeginTransition(null); + break; + } + } + } + + /** + * Remove the transition after this media item + * + * @param mediaItem The media item + */ + private void removeTransitionAfter(MediaItem mediaItem) { + final Iterator it = mTransitions.iterator(); + while (it.hasNext()) { + Transition t = it.next(); + if (t.getAfterMediaItem() == mediaItem) { + it.remove(); + t.invalidate(); + mediaItem.setEndTransition(null); + break; + } + } + } +}