MediaItem.java revision a573b563b3c6a3edc60393543dc9adb7ade4f188
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17
18package android.media.videoeditor;
19
20import java.io.File;
21import java.io.FileNotFoundException;
22import java.io.FileOutputStream;
23import java.io.IOException;
24import java.util.ArrayList;
25import java.util.List;
26
27import java.io.DataOutputStream;
28import java.nio.ByteBuffer;
29import java.nio.IntBuffer;
30
31import android.graphics.Bitmap;
32import android.media.videoeditor.MediaArtistNativeHelper.ClipSettings;
33import android.media.videoeditor.MediaArtistNativeHelper.FileType;
34import android.media.videoeditor.MediaArtistNativeHelper.MediaRendering;
35
36/**
37 * This abstract class describes the base class for any MediaItem. Objects are
38 * defined with a file path as a source data.
39 * {@hide}
40 */
41public abstract class MediaItem {
42    /**
43     *  A constant which can be used to specify the end of the file (instead of
44     *  providing the actual duration of the media item).
45     */
46    public final static int END_OF_FILE = -1;
47
48    /**
49     *  Rendering modes
50     */
51    /**
52     * When using the RENDERING_MODE_BLACK_BORDER rendering mode video frames
53     * are resized by preserving the aspect ratio until the movie matches one of
54     * the dimensions of the output movie. The areas outside the resized video
55     * clip are rendered black.
56     */
57    public static final int RENDERING_MODE_BLACK_BORDER = 0;
58
59    /**
60     * When using the RENDERING_MODE_STRETCH rendering mode video frames are
61     * stretched horizontally or vertically to match the current aspect ratio of
62     * the video editor.
63     */
64    public static final int RENDERING_MODE_STRETCH = 1;
65
66    /**
67     * When using the RENDERING_MODE_CROPPING rendering mode video frames are
68     * scaled horizontally or vertically by preserving the original aspect ratio
69     * of the media item.
70     */
71    public static final int RENDERING_MODE_CROPPING = 2;
72
73    /**
74     *  The unique id of the MediaItem
75     */
76    private final String mUniqueId;
77
78    /**
79     *  The name of the file associated with the MediaItem
80     */
81    protected final String mFilename;
82
83    /**
84     *  List of effects
85     */
86    private final List<Effect> mEffects;
87
88    /**
89     *  List of overlays
90     */
91    private final List<Overlay> mOverlays;
92
93    /**
94     *  The rendering mode
95     */
96    private int mRenderingMode;
97
98    private final MediaArtistNativeHelper mMANativeHelper;
99
100    private final String mProjectPath;
101
102    /**
103     *  Beginning and end transitions
104     */
105    protected Transition mBeginTransition;
106
107    protected Transition mEndTransition;
108
109    protected String mGeneratedImageClip;
110
111    protected boolean mRegenerateClip;
112
113    private boolean mBlankFrameGenerated = false;
114
115    private String mBlankFrameFilename = null;
116
117    /**
118     * Constructor
119     *
120     * @param editor The video editor reference
121     * @param mediaItemId The MediaItem id
122     * @param filename name of the media file.
123     * @param renderingMode The rendering mode
124     * @throws IOException if file is not found
125     * @throws IllegalArgumentException if a capability such as file format is
126     *             not supported the exception object contains the unsupported
127     *             capability
128     */
129    protected MediaItem(VideoEditor editor, String mediaItemId, String filename,
130                        int renderingMode) throws IOException {
131        if (filename == null) {
132            throw new IllegalArgumentException("MediaItem : filename is null");
133        }
134        mUniqueId = mediaItemId;
135        mFilename = filename;
136        mRenderingMode = renderingMode;
137        mEffects = new ArrayList<Effect>();
138        mOverlays = new ArrayList<Overlay>();
139        mBeginTransition = null;
140        mEndTransition = null;
141        mMANativeHelper = ((VideoEditorImpl)editor).getNativeContext();
142        mProjectPath = editor.getPath();
143        mRegenerateClip = false;
144        mGeneratedImageClip = null;
145    }
146
147    /**
148     * @return The id of the media item
149     */
150    public String getId() {
151        return mUniqueId;
152    }
153
154    /**
155     * @return The media source file name
156     */
157    public String getFilename() {
158        return mFilename;
159    }
160
161    /**
162     * If aspect ratio of the MediaItem is different from the aspect ratio of
163     * the editor then this API controls the rendering mode.
164     *
165     * @param renderingMode rendering mode. It is one of:
166     *            {@link #RENDERING_MODE_BLACK_BORDER},
167     *            {@link #RENDERING_MODE_STRETCH}
168     */
169    public void setRenderingMode(int renderingMode) {
170        switch (renderingMode) {
171            case RENDERING_MODE_BLACK_BORDER:
172            case RENDERING_MODE_STRETCH:
173            case RENDERING_MODE_CROPPING:
174                break;
175
176            default:
177                throw new IllegalArgumentException("Invalid Rendering Mode");
178        }
179
180        mMANativeHelper.setGeneratePreview(true);
181
182        mRenderingMode = renderingMode;
183        if (mBeginTransition != null) {
184            mBeginTransition.invalidate();
185        }
186
187        if (mEndTransition != null) {
188            mEndTransition.invalidate();
189        }
190    }
191
192    /**
193     * @return The rendering mode
194     */
195    public int getRenderingMode() {
196        return mRenderingMode;
197    }
198
199    /**
200     * @param transition The beginning transition
201     */
202    void setBeginTransition(Transition transition) {
203        mBeginTransition = transition;
204    }
205
206    /**
207     * @return The begin transition
208     */
209    public Transition getBeginTransition() {
210        return mBeginTransition;
211    }
212
213    /**
214     * @param transition The end transition
215     */
216    void setEndTransition(Transition transition) {
217        mEndTransition = transition;
218    }
219
220    /**
221     * @return The end transition
222     */
223    public Transition getEndTransition() {
224        return mEndTransition;
225    }
226
227    /**
228     * @return The timeline duration. This is the actual duration in the
229     *         timeline (trimmed duration)
230     */
231    public abstract long getTimelineDuration();
232
233    /**
234     * @return The is the full duration of the media item (not trimmed)
235     */
236    public abstract long getDuration();
237
238    /**
239     * @return The source file type
240     */
241    public abstract int getFileType();
242
243    /**
244     * @return Get the native width of the media item
245     */
246    public abstract int getWidth();
247
248    /**
249     * @return Get the native height of the media item
250     */
251    public abstract int getHeight();
252
253    /**
254     * Get aspect ratio of the source media item.
255     *
256     * @return the aspect ratio as described in MediaProperties.
257     *         MediaProperties.ASPECT_RATIO_UNDEFINED if aspect ratio is not
258     *         supported as in MediaProperties
259     */
260    public abstract int getAspectRatio();
261
262    /**
263     * Add the specified effect to this media item.
264     *
265     * Note that certain types of effects cannot be applied to video and to
266     * image media items. For example in certain implementation a Ken Burns
267     * implementation cannot be applied to video media item.
268     *
269     * This method invalidates transition video clips if the
270     * effect overlaps with the beginning and/or the end transition.
271     *
272     * @param effect The effect to apply
273     * @throws IllegalStateException if a preview or an export is in progress
274     * @throws IllegalArgumentException if the effect start and/or duration are
275     *      invalid or if the effect cannot be applied to this type of media
276     *      item or if the effect id is not unique across all the Effects
277     *      added.
278     */
279    public void addEffect(Effect effect) {
280
281        if (effect == null) {
282            throw new IllegalArgumentException("NULL effect cannot be applied");
283        }
284
285        if (effect.getMediaItem() != this) {
286            throw new IllegalArgumentException("Media item mismatch");
287        }
288
289        if (mEffects.contains(effect)) {
290            throw new IllegalArgumentException("Effect already exists: " + effect.getId());
291        }
292
293        if (effect.getStartTime() + effect.getDuration() > getDuration()) {
294            throw new IllegalArgumentException(
295            "Effect start time + effect duration > media clip duration");
296        }
297
298        mMANativeHelper.setGeneratePreview(true);
299
300        mEffects.add(effect);
301
302        invalidateTransitions(effect.getStartTime(), effect.getDuration());
303
304        if (effect instanceof EffectKenBurns) {
305            mRegenerateClip = true;
306        }
307    }
308
309    /**
310     * Remove the effect with the specified id.
311     *
312     * This method invalidates a transition video clip if the effect overlaps
313     * with a transition.
314     *
315     * @param effectId The id of the effect to be removed
316     *
317     * @return The effect that was removed
318     * @throws IllegalStateException if a preview or an export is in progress
319     */
320    public Effect removeEffect(String effectId) {
321        for (Effect effect : mEffects) {
322            if (effect.getId().equals(effectId)) {
323                mMANativeHelper.setGeneratePreview(true);
324
325                mEffects.remove(effect);
326
327                invalidateTransitions(effect.getStartTime(), effect.getDuration());
328                if (effect instanceof EffectKenBurns) {
329                    if (mGeneratedImageClip != null) {
330                        /**
331                         *  Delete the file
332                         */
333                        new File(mGeneratedImageClip).delete();
334                        /**
335                         *  Invalidate the filename
336                         */
337                        mGeneratedImageClip = null;
338                    }
339                    mRegenerateClip = false;
340                }
341                return effect;
342            }
343        }
344        return null;
345    }
346
347    /**
348     * Set the filepath of the generated image clip when the effect is added.
349     *
350     * @param The filepath of the generated image clip.
351     */
352    void setGeneratedImageClip(String generatedFilePath) {
353        mGeneratedImageClip = generatedFilePath;
354    }
355
356    /**
357     * Get the filepath of the generated image clip when the effect is added.
358     *
359     * @return The filepath of the generated image clip (null if it does not
360     *         exist)
361     */
362    String getGeneratedImageClip() {
363        return mGeneratedImageClip;
364    }
365
366    /**
367     * Find the effect with the specified id
368     *
369     * @param effectId The effect id
370     * @return The effect with the specified id (null if it does not exist)
371     */
372    public Effect getEffect(String effectId) {
373        for (Effect effect : mEffects) {
374            if (effect.getId().equals(effectId)) {
375                return effect;
376            }
377        }
378        return null;
379    }
380
381    /**
382     * Get the list of effects.
383     *
384     * @return the effects list. If no effects exist an empty list will be
385     *         returned.
386     */
387    public List<Effect> getAllEffects() {
388        return mEffects;
389    }
390
391    /**
392     * Add an overlay to the storyboard. This method invalidates a transition
393     * video clip if the overlay overlaps with a transition.
394     *
395     * @param overlay The overlay to add
396     * @throws IllegalStateException if a preview or an export is in progress or
397     *             if the overlay id is not unique across all the overlays added
398     *             or if the bitmap is not specified or if the dimensions of the
399     *             bitmap do not match the dimensions of the media item
400     * @throws FileNotFoundException, IOException if overlay could not be saved
401     *             to project path
402     */
403    public void addOverlay(Overlay overlay) throws FileNotFoundException, IOException {
404        if (overlay == null) {
405            throw new IllegalArgumentException("NULL Overlay cannot be applied");
406        }
407
408        if (overlay.getMediaItem() != this) {
409            throw new IllegalArgumentException("Media item mismatch");
410        }
411
412        if (mOverlays.contains(overlay)) {
413            throw new IllegalArgumentException("Overlay already exists: " + overlay.getId());
414        }
415
416        if (overlay.getStartTime() + overlay.getDuration() > getDuration()) {
417            throw new IllegalArgumentException(
418            "Overlay start time + overlay duration > media clip duration");
419        }
420
421        if (overlay instanceof OverlayFrame) {
422            final OverlayFrame frame = (OverlayFrame)overlay;
423            final Bitmap bitmap = frame.getBitmap();
424            if (bitmap == null) {
425                throw new IllegalArgumentException("Overlay bitmap not specified");
426            }
427
428            final int scaledWidth, scaledHeight;
429            if (this instanceof MediaVideoItem) {
430                scaledWidth = getWidth();
431                scaledHeight = getHeight();
432            } else {
433                scaledWidth = ((MediaImageItem)this).getScaledWidth();
434                scaledHeight = ((MediaImageItem)this).getScaledHeight();
435            }
436
437            /**
438             * The dimensions of the overlay bitmap must be the same as the
439             * media item dimensions
440             */
441            if (bitmap.getWidth() != scaledWidth || bitmap.getHeight() != scaledHeight) {
442                throw new IllegalArgumentException(
443                "Bitmap dimensions must match media item dimensions");
444            }
445
446            mMANativeHelper.setGeneratePreview(true);
447            ((OverlayFrame)overlay).save(mProjectPath);
448
449            mOverlays.add(overlay);
450            invalidateTransitions(overlay.getStartTime(), overlay.getDuration());
451
452        } else {
453            throw new IllegalArgumentException("Overlay not supported");
454        }
455    }
456
457    /**
458     * @param flag The flag to indicate if regeneration of clip is true or
459     *            false.
460     */
461    void setRegenerateClip(boolean flag) {
462        mRegenerateClip = flag;
463    }
464
465    /**
466     * @return flag The flag to indicate if regeneration of clip is true or
467     *         false.
468     */
469    boolean getRegenerateClip() {
470        return mRegenerateClip;
471    }
472
473    /**
474     * Remove the overlay with the specified id.
475     *
476     * This method invalidates a transition video clip if the overlay overlaps
477     * with a transition.
478     *
479     * @param overlayId The id of the overlay to be removed
480     *
481     * @return The overlay that was removed
482     * @throws IllegalStateException if a preview or an export is in progress
483     */
484    public Overlay removeOverlay(String overlayId) {
485        for (Overlay overlay : mOverlays) {
486            if (overlay.getId().equals(overlayId)) {
487                mMANativeHelper.setGeneratePreview(true);
488
489                mOverlays.remove(overlay);
490                if (overlay instanceof OverlayFrame) {
491                    ((OverlayFrame)overlay).invalidate();
492                }
493                invalidateTransitions(overlay.getStartTime(), overlay.getDuration());
494                return overlay;
495            }
496        }
497        return null;
498    }
499
500    /**
501     * Find the overlay with the specified id
502     *
503     * @param overlayId The overlay id
504     *
505     * @return The overlay with the specified id (null if it does not exist)
506     */
507    public Overlay getOverlay(String overlayId) {
508        for (Overlay overlay : mOverlays) {
509            if (overlay.getId().equals(overlayId)) {
510                return overlay;
511            }
512        }
513
514        return null;
515    }
516
517    /**
518     * Get the list of overlays associated with this media item
519     *
520     * Note that if any overlay source files are not accessible anymore,
521     * this method will still provide the full list of overlays.
522     *
523     * @return The list of overlays. If no overlays exist an empty list will
524     *         be returned.
525     */
526    public List<Overlay> getAllOverlays() {
527        return mOverlays;
528    }
529
530    /**
531     * Create a thumbnail at specified time in a video stream in Bitmap format
532     *
533     * @param width width of the thumbnail in pixels
534     * @param height height of the thumbnail in pixels
535     * @param timeMs The time in the source video file at which the thumbnail is
536     *            requested (even if trimmed).
537     *
538     * @return The thumbnail as a Bitmap.
539     *
540     * @throws IOException if a file error occurs
541     * @throws IllegalArgumentException if time is out of video duration
542     */
543    public abstract Bitmap getThumbnail(int width, int height, long timeMs)
544                                        throws IOException;
545
546    /**
547     * Get the array of Bitmap thumbnails between start and end.
548     *
549     * @param width width of the thumbnail in pixels
550     * @param height height of the thumbnail in pixels
551     * @param startMs The start of time range in milliseconds
552     * @param endMs The end of the time range in milliseconds
553     * @param thumbnailCount The thumbnail count
554     *
555     * @return The array of Bitmaps
556     *
557     * @throws IOException if a file error occurs
558     */
559    public abstract Bitmap[] getThumbnailList(int width, int height,
560                                              long startMs, long endMs,
561                                              int thumbnailCount)
562                                              throws IOException;
563
564    /*
565     * {@inheritDoc}
566     */
567    @Override
568    public boolean equals(Object object) {
569        if (!(object instanceof MediaItem)) {
570            return false;
571        }
572        return mUniqueId.equals(((MediaItem)object).mUniqueId);
573    }
574
575    /*
576     * {@inheritDoc}
577     */
578    @Override
579    public int hashCode() {
580        return mUniqueId.hashCode();
581    }
582
583    /**
584     * Invalidate the start and end transitions if necessary
585     *
586     * @param startTimeMs The start time of the effect or overlay
587     * @param durationMs The duration of the effect or overlay
588     */
589    abstract void invalidateTransitions(long startTimeMs, long durationMs);
590
591    /**
592     * Invalidate the start and end transitions if necessary. This method is
593     * typically called when the start time and/or duration of an overlay or
594     * effect is changing.
595     *
596     * @param oldStartTimeMs The old start time of the effect or overlay
597     * @param oldDurationMs The old duration of the effect or overlay
598     * @param newStartTimeMs The new start time of the effect or overlay
599     * @param newDurationMs The new duration of the effect or overlay
600     */
601    abstract void invalidateTransitions(long oldStartTimeMs, long oldDurationMs,
602            long newStartTimeMs, long newDurationMs);
603
604    /**
605     * Check if two items overlap in time
606     *
607     * @param startTimeMs1 Item 1 start time
608     * @param durationMs1 Item 1 duration
609     * @param startTimeMs2 Item 2 start time
610     * @param durationMs2 Item 2 end time
611     * @return true if the two items overlap
612     */
613    protected boolean isOverlapping(long startTimeMs1, long durationMs1,
614                                    long startTimeMs2, long durationMs2) {
615        if (startTimeMs1 + durationMs1 <= startTimeMs2) {
616            return false;
617        } else if (startTimeMs1 >= startTimeMs2 + durationMs2) {
618            return false;
619        }
620
621        return true;
622    }
623
624    /**
625     * Adjust the duration transitions.
626     */
627    protected void adjustTransitions() {
628        /**
629         *  Check if the duration of transitions need to be adjusted
630         */
631        if (mBeginTransition != null) {
632            final long maxDurationMs = mBeginTransition.getMaximumDuration();
633            if (mBeginTransition.getDuration() > maxDurationMs) {
634                mBeginTransition.setDuration(maxDurationMs);
635            }
636        }
637
638        if (mEndTransition != null) {
639            final long maxDurationMs = mEndTransition.getMaximumDuration();
640            if (mEndTransition.getDuration() > maxDurationMs) {
641                mEndTransition.setDuration(maxDurationMs);
642            }
643        }
644    }
645
646    /**
647     * @return MediaArtistNativeHleper context
648     */
649    MediaArtistNativeHelper getNativeContext() {
650        return mMANativeHelper;
651    }
652
653    /**
654     * Initialises ClipSettings fields to default value
655     *
656     * @param ClipSettings object
657     *{@link android.media.videoeditor.MediaArtistNativeHelper.ClipSettings}
658     */
659    void initClipSettings(ClipSettings clipSettings) {
660        clipSettings.clipPath = null;
661        clipSettings.clipDecodedPath = null;
662        clipSettings.clipOriginalPath = null;
663        clipSettings.fileType = 0;
664        clipSettings.endCutTime = 0;
665        clipSettings.beginCutTime = 0;
666        clipSettings.beginCutPercent = 0;
667        clipSettings.endCutPercent = 0;
668        clipSettings.panZoomEnabled = false;
669        clipSettings.panZoomPercentStart = 0;
670        clipSettings.panZoomTopLeftXStart = 0;
671        clipSettings.panZoomTopLeftYStart = 0;
672        clipSettings.panZoomPercentEnd = 0;
673        clipSettings.panZoomTopLeftXEnd = 0;
674        clipSettings.panZoomTopLeftYEnd = 0;
675        clipSettings.mediaRendering = 0;
676        clipSettings.rgbWidth = 0;
677        clipSettings.rgbHeight = 0;
678    }
679
680    /**
681     * @return ClipSettings object with populated data
682     *{@link android.media.videoeditor.MediaArtistNativeHelper.ClipSettings}
683     */
684    ClipSettings getClipSettings() {
685        MediaVideoItem mVI = null;
686        MediaImageItem mII = null;
687        ClipSettings clipSettings = new ClipSettings();
688        initClipSettings(clipSettings);
689        if (this instanceof MediaVideoItem) {
690            mVI = (MediaVideoItem)this;
691            clipSettings.clipPath = mVI.getFilename();
692            clipSettings.fileType = mMANativeHelper.getMediaItemFileType(mVI.
693                                                                 getFileType());
694            clipSettings.beginCutTime = (int)mVI.getBoundaryBeginTime();
695            clipSettings.endCutTime = (int)mVI.getBoundaryEndTime();
696            clipSettings.mediaRendering = mMANativeHelper.
697                                          getMediaItemRenderingMode(mVI
698                                          .getRenderingMode());
699        } else if (this instanceof MediaImageItem) {
700            mII = (MediaImageItem)this;
701            clipSettings = mII.getImageClipProperties();
702        }
703        return clipSettings;
704    }
705
706    /**
707     * Generates a black frame to be used for generating
708     * begin transition at first media item in storyboard
709     * or end transition at last media item in storyboard
710     *
711     * @param ClipSettings object
712     *{@link android.media.videoeditor.MediaArtistNativeHelper.ClipSettings}
713     */
714    void generateBlankFrame(ClipSettings clipSettings) {
715        if (!mBlankFrameGenerated) {
716            int mWidth = 64;
717            int mHeight = 64;
718            mBlankFrameFilename = String.format(mProjectPath + "/" + "ghost.rgb");
719            FileOutputStream fl = null;
720            try {
721                 fl = new FileOutputStream(mBlankFrameFilename);
722            } catch (IOException e) {
723                /* catch IO exception */
724            }
725            final DataOutputStream dos = new DataOutputStream(fl);
726
727            final int [] framingBuffer = new int[mWidth];
728
729            ByteBuffer byteBuffer = ByteBuffer.allocate(framingBuffer.length * 4);
730            IntBuffer intBuffer;
731
732            byte[] array = byteBuffer.array();
733            int tmp = 0;
734            while(tmp < mHeight) {
735                intBuffer = byteBuffer.asIntBuffer();
736                intBuffer.put(framingBuffer,0,mWidth);
737                try {
738                    dos.write(array);
739                } catch (IOException e) {
740                    /* catch file write error */
741                }
742                tmp += 1;
743            }
744
745            try {
746                fl.close();
747            } catch (IOException e) {
748                /* file close error */
749            }
750            mBlankFrameGenerated = true;
751        }
752
753        clipSettings.clipPath = mBlankFrameFilename;
754        clipSettings.fileType = FileType.JPG;
755        clipSettings.beginCutTime = 0;
756        clipSettings.endCutTime = 0;
757        clipSettings.mediaRendering = MediaRendering.RESIZING;
758
759        clipSettings.rgbWidth = 64;
760        clipSettings.rgbHeight = 64;
761    }
762
763    /**
764     * Invalidates the blank frame generated
765     */
766    void invalidateBlankFrame() {
767        if (mBlankFrameFilename != null) {
768            if (new File(mBlankFrameFilename).exists()) {
769                new File(mBlankFrameFilename).delete();
770                mBlankFrameFilename = null;
771            }
772        }
773    }
774
775}
776