/* * 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.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.io.DataOutputStream; import java.nio.ByteBuffer; import java.nio.IntBuffer; import android.graphics.Bitmap; import android.media.videoeditor.MediaArtistNativeHelper.ClipSettings; import android.media.videoeditor.MediaArtistNativeHelper.FileType; import android.media.videoeditor.MediaArtistNativeHelper.MediaRendering; /** * This abstract class describes the base class for any MediaItem. Objects are * defined with a file path as a source data. * {@hide} */ 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 video editor. */ public static final int RENDERING_MODE_STRETCH = 1; /** * When using the RENDERING_MODE_CROPPING rendering mode video frames are * scaled horizontally or vertically by preserving the original aspect ratio * of the media item. */ public static final int RENDERING_MODE_CROPPING = 2; /** * 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<Effect> mEffects; /** * List of overlays */ private final List<Overlay> mOverlays; /** * The rendering mode */ private int mRenderingMode; private final MediaArtistNativeHelper mMANativeHelper; private final String mProjectPath; /** * Beginning and end transitions */ protected Transition mBeginTransition; protected Transition mEndTransition; protected String mGeneratedImageClip; protected boolean mRegenerateClip; private boolean mBlankFrameGenerated = false; private String mBlankFrameFilename = null; /** * Constructor * * @param editor The video editor reference * @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(VideoEditor editor, String mediaItemId, String filename, int renderingMode) throws IOException { if (filename == null) { throw new IllegalArgumentException("MediaItem : filename is null"); } File file = new File(filename); if (!file.exists()) { throw new IOException(filename + " not found ! "); } /*Compare file_size with 2GB*/ if (VideoEditor.MAX_SUPPORTED_FILE_SIZE <= file.length()) { throw new IllegalArgumentException("File size is more than 2GB"); } mUniqueId = mediaItemId; mFilename = filename; mRenderingMode = renderingMode; mEffects = new ArrayList<Effect>(); mOverlays = new ArrayList<Overlay>(); mBeginTransition = null; mEndTransition = null; mMANativeHelper = ((VideoEditorImpl)editor).getNativeContext(); mProjectPath = editor.getPath(); mRegenerateClip = false; mGeneratedImageClip = null; } /** * @return The id 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) { switch (renderingMode) { case RENDERING_MODE_BLACK_BORDER: case RENDERING_MODE_STRETCH: case RENDERING_MODE_CROPPING: break; default: throw new IllegalArgumentException("Invalid Rendering Mode"); } mMANativeHelper.setGeneratePreview(true); mRenderingMode = renderingMode; if (mBeginTransition != null) { mBeginTransition.invalidate(); } if (mEndTransition != null) { mEndTransition.invalidate(); } for (Overlay overlay : mOverlays) { ((OverlayFrame)overlay).invalidateGeneratedFiles(); } } /** * @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 timeline duration. This is the actual duration in the * timeline (trimmed duration) */ public abstract long getTimelineDuration(); /** * @return The is the full duration of the media item (not trimmed) */ public abstract long getDuration(); /** * @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 (effect == null) { throw new IllegalArgumentException("NULL effect cannot be applied"); } if (effect.getMediaItem() != this) { throw new IllegalArgumentException("Media item mismatch"); } 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"); } mMANativeHelper.setGeneratePreview(true); mEffects.add(effect); invalidateTransitions(effect.getStartTime(), effect.getDuration()); if (effect instanceof EffectKenBurns) { mRegenerateClip = true; } } /** * 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)) { mMANativeHelper.setGeneratePreview(true); mEffects.remove(effect); invalidateTransitions(effect.getStartTime(), effect.getDuration()); if (effect instanceof EffectKenBurns) { if (mGeneratedImageClip != null) { /** * Delete the file */ new File(mGeneratedImageClip).delete(); /** * Invalidate the filename */ mGeneratedImageClip = null; } mRegenerateClip = false; } return effect; } } return null; } /** * Set the filepath of the generated image clip when the effect is added. * * @param The filepath of the generated image clip. */ void setGeneratedImageClip(String generatedFilePath) { mGeneratedImageClip = generatedFilePath; } /** * Get the filepath of the generated image clip when the effect is added. * * @return The filepath of the generated image clip (null if it does not * exist) */ String getGeneratedImageClip() { return mGeneratedImageClip; } /** * 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<Effect> 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 * or if the bitmap is not specified or if the dimensions of the * bitmap do not match the dimensions of the media item * @throws FileNotFoundException, IOException if overlay could not be saved * to project path */ public void addOverlay(Overlay overlay) throws FileNotFoundException, IOException { if (overlay == null) { throw new IllegalArgumentException("NULL Overlay cannot be applied"); } if (overlay.getMediaItem() != this) { throw new IllegalArgumentException("Media item mismatch"); } 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"); } if (overlay instanceof OverlayFrame) { final OverlayFrame frame = (OverlayFrame)overlay; final Bitmap bitmap = frame.getBitmap(); if (bitmap == null) { throw new IllegalArgumentException("Overlay bitmap not specified"); } final int scaledWidth, scaledHeight; if (this instanceof MediaVideoItem) { scaledWidth = getWidth(); scaledHeight = getHeight(); } else { scaledWidth = ((MediaImageItem)this).getScaledWidth(); scaledHeight = ((MediaImageItem)this).getScaledHeight(); } /** * The dimensions of the overlay bitmap must be the same as the * media item dimensions */ if (bitmap.getWidth() != scaledWidth || bitmap.getHeight() != scaledHeight) { throw new IllegalArgumentException( "Bitmap dimensions must match media item dimensions"); } mMANativeHelper.setGeneratePreview(true); ((OverlayFrame)overlay).save(mProjectPath); mOverlays.add(overlay); invalidateTransitions(overlay.getStartTime(), overlay.getDuration()); } else { throw new IllegalArgumentException("Overlay not supported"); } } /** * @param flag The flag to indicate if regeneration of clip is true or * false. */ void setRegenerateClip(boolean flag) { mRegenerateClip = flag; } /** * @return flag The flag to indicate if regeneration of clip is true or * false. */ boolean getRegenerateClip() { return mRegenerateClip; } /** * 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)) { mMANativeHelper.setGeneratePreview(true); mOverlays.remove(overlay); if (overlay instanceof OverlayFrame) { ((OverlayFrame)overlay).invalidate(); } invalidateTransitions(overlay.getStartTime(), overlay.getDuration()); 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<Overlay> 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 * @param indices The indices of the thumbnails wanted * @param callback The callback used to pass back the bitmaps * * @throws IOException if a file error occurs */ public abstract void getThumbnailList(int width, int height, long startMs, long endMs, int thumbnailCount, int[] indices, GetThumbnailListCallback callback) throws IOException; public interface GetThumbnailListCallback { public void onThumbnail(Bitmap bitmap, int index); } // This is for compatibility, only used in tests. public Bitmap[] getThumbnailList(int width, int height, long startMs, long endMs, int thumbnailCount) throws IOException { final Bitmap[] bitmaps = new Bitmap[thumbnailCount]; int[] indices = new int[thumbnailCount]; for (int i = 0; i < thumbnailCount; i++) { indices[i] = i; } getThumbnailList(width, height, startMs, endMs, thumbnailCount, indices, new GetThumbnailListCallback() { public void onThumbnail(Bitmap bitmap, int index) { bitmaps[index] = bitmap; } }); return bitmaps; } /* * {@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 startTimeMs The start time of the effect or overlay * @param durationMs The duration of the effect or overlay */ abstract void invalidateTransitions(long startTimeMs, long durationMs); /** * Invalidate the start and end transitions if necessary. This method is * typically called when the start time and/or duration of an overlay or * effect is changing. * * @param oldStartTimeMs The old start time of the effect or overlay * @param oldDurationMs The old duration of the effect or overlay * @param newStartTimeMs The new start time of the effect or overlay * @param newDurationMs The new duration of the effect or overlay */ abstract void invalidateTransitions(long oldStartTimeMs, long oldDurationMs, long newStartTimeMs, long newDurationMs); /** * Check if two items overlap in time * * @param startTimeMs1 Item 1 start time * @param durationMs1 Item 1 duration * @param startTimeMs2 Item 2 start time * @param durationMs2 Item 2 end time * @return true if the two items overlap */ protected boolean isOverlapping(long startTimeMs1, long durationMs1, long startTimeMs2, long durationMs2) { if (startTimeMs1 + durationMs1 <= startTimeMs2) { return false; } else if (startTimeMs1 >= startTimeMs2 + durationMs2) { return false; } return true; } /** * Adjust the duration transitions. */ protected void adjustTransitions() { /** * Check if the duration of transitions need to be adjusted */ if (mBeginTransition != null) { final long maxDurationMs = mBeginTransition.getMaximumDuration(); if (mBeginTransition.getDuration() > maxDurationMs) { mBeginTransition.setDuration(maxDurationMs); } } if (mEndTransition != null) { final long maxDurationMs = mEndTransition.getMaximumDuration(); if (mEndTransition.getDuration() > maxDurationMs) { mEndTransition.setDuration(maxDurationMs); } } } /** * @return MediaArtistNativeHleper context */ MediaArtistNativeHelper getNativeContext() { return mMANativeHelper; } /** * Initialises ClipSettings fields to default value * * @param ClipSettings object *{@link android.media.videoeditor.MediaArtistNativeHelper.ClipSettings} */ void initClipSettings(ClipSettings clipSettings) { clipSettings.clipPath = null; clipSettings.clipDecodedPath = null; clipSettings.clipOriginalPath = null; clipSettings.fileType = 0; clipSettings.endCutTime = 0; clipSettings.beginCutTime = 0; clipSettings.beginCutPercent = 0; clipSettings.endCutPercent = 0; clipSettings.panZoomEnabled = false; clipSettings.panZoomPercentStart = 0; clipSettings.panZoomTopLeftXStart = 0; clipSettings.panZoomTopLeftYStart = 0; clipSettings.panZoomPercentEnd = 0; clipSettings.panZoomTopLeftXEnd = 0; clipSettings.panZoomTopLeftYEnd = 0; clipSettings.mediaRendering = 0; clipSettings.rgbWidth = 0; clipSettings.rgbHeight = 0; } /** * @return ClipSettings object with populated data *{@link android.media.videoeditor.MediaArtistNativeHelper.ClipSettings} */ ClipSettings getClipSettings() { MediaVideoItem mVI = null; MediaImageItem mII = null; ClipSettings clipSettings = new ClipSettings(); initClipSettings(clipSettings); if (this instanceof MediaVideoItem) { mVI = (MediaVideoItem)this; clipSettings.clipPath = mVI.getFilename(); clipSettings.fileType = mMANativeHelper.getMediaItemFileType(mVI. getFileType()); clipSettings.beginCutTime = (int)mVI.getBoundaryBeginTime(); clipSettings.endCutTime = (int)mVI.getBoundaryEndTime(); clipSettings.mediaRendering = mMANativeHelper. getMediaItemRenderingMode(mVI .getRenderingMode()); } else if (this instanceof MediaImageItem) { mII = (MediaImageItem)this; clipSettings = mII.getImageClipProperties(); } return clipSettings; } /** * Generates a black frame to be used for generating * begin transition at first media item in storyboard * or end transition at last media item in storyboard * * @param ClipSettings object *{@link android.media.videoeditor.MediaArtistNativeHelper.ClipSettings} */ void generateBlankFrame(ClipSettings clipSettings) { if (!mBlankFrameGenerated) { int mWidth = 64; int mHeight = 64; mBlankFrameFilename = String.format(mProjectPath + "/" + "ghost.rgb"); FileOutputStream fl = null; try { fl = new FileOutputStream(mBlankFrameFilename); } catch (IOException e) { /* catch IO exception */ } final DataOutputStream dos = new DataOutputStream(fl); final int [] framingBuffer = new int[mWidth]; ByteBuffer byteBuffer = ByteBuffer.allocate(framingBuffer.length * 4); IntBuffer intBuffer; byte[] array = byteBuffer.array(); int tmp = 0; while(tmp < mHeight) { intBuffer = byteBuffer.asIntBuffer(); intBuffer.put(framingBuffer,0,mWidth); try { dos.write(array); } catch (IOException e) { /* catch file write error */ } tmp += 1; } try { fl.close(); } catch (IOException e) { /* file close error */ } mBlankFrameGenerated = true; } clipSettings.clipPath = mBlankFrameFilename; clipSettings.fileType = FileType.JPG; clipSettings.beginCutTime = 0; clipSettings.endCutTime = 0; clipSettings.mediaRendering = MediaRendering.RESIZING; clipSettings.rgbWidth = 64; clipSettings.rgbHeight = 64; } /** * Invalidates the blank frame generated */ void invalidateBlankFrame() { if (mBlankFrameFilename != null) { if (new File(mBlankFrameFilename).exists()) { new File(mBlankFrameFilename).delete(); mBlankFrameFilename = null; } } } }