VideoEditorImpl.java revision 731e46575aeffa26b41d7590a0a4de637d792258
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.media.videoeditor;
18
19import java.io.File;
20import java.io.FileInputStream;
21import java.io.FileNotFoundException;
22import java.io.FileOutputStream;
23import java.io.IOException;
24import java.io.StringWriter;
25import java.util.ArrayList;
26import java.util.Iterator;
27import java.util.List;
28import java.util.Map;
29
30import org.xmlpull.v1.XmlPullParser;
31import org.xmlpull.v1.XmlPullParserException;
32import org.xmlpull.v1.XmlSerializer;
33
34import android.graphics.Rect;
35import android.util.Log;
36import android.util.Xml;
37import android.view.SurfaceHolder;
38
39/**
40 * The VideoEditor implementation {@hide}
41 */
42public class VideoEditorImpl implements VideoEditor {
43    // Logging
44    private static final String TAG = "VideoEditorImpl";
45
46    // The project filename
47    private static final String PROJECT_FILENAME = "videoeditor.xml";
48
49    // XML tags
50    private static final String TAG_PROJECT = "project";
51    private static final String TAG_MEDIA_ITEMS = "media_items";
52    private static final String TAG_MEDIA_ITEM = "media_item";
53    private static final String TAG_TRANSITIONS = "transitions";
54    private static final String TAG_TRANSITION = "transition";
55    private static final String TAG_OVERLAYS = "overlays";
56    private static final String TAG_OVERLAY = "overlay";
57    private static final String TAG_OVERLAY_USER_ATTRIBUTES = "overlay_user_attributes";
58    private static final String TAG_EFFECTS = "effects";
59    private static final String TAG_EFFECT = "effect";
60    private static final String TAG_AUDIO_TRACKS = "audio_tracks";
61    private static final String TAG_AUDIO_TRACK = "audio_track";
62
63    private static final String ATTR_ID = "id";
64    private static final String ATTR_FILENAME = "filename";
65    private static final String ATTR_AUDIO_WAVEFORM_FILENAME = "wavefoem";
66    private static final String ATTR_RENDERING_MODE = "rendering_mode";
67    private static final String ATTR_ASPECT_RATIO = "aspect_ratio";
68    private static final String ATTR_TYPE = "type";
69    private static final String ATTR_DURATION = "duration";
70    private static final String ATTR_START_TIME = "start_time";
71    private static final String ATTR_BEGIN_TIME = "begin_time";
72    private static final String ATTR_END_TIME = "end_time";
73    private static final String ATTR_VOLUME = "volume";
74    private static final String ATTR_BEHAVIOR = "behavior";
75    private static final String ATTR_DIRECTION = "direction";
76    private static final String ATTR_BLENDING = "blending";
77    private static final String ATTR_INVERT = "invert";
78    private static final String ATTR_MASK = "mask";
79    private static final String ATTR_BEFORE_MEDIA_ITEM_ID = "before_media_item";
80    private static final String ATTR_AFTER_MEDIA_ITEM_ID = "after_media_item";
81    private static final String ATTR_COLOR_EFFECT_TYPE = "color_type";
82    private static final String ATTR_COLOR_EFFECT_VALUE = "color_value";
83    private static final String ATTR_START_RECT_L = "start_l";
84    private static final String ATTR_START_RECT_T = "start_t";
85    private static final String ATTR_START_RECT_R = "start_r";
86    private static final String ATTR_START_RECT_B = "start_b";
87    private static final String ATTR_END_RECT_L = "end_l";
88    private static final String ATTR_END_RECT_T = "end_t";
89    private static final String ATTR_END_RECT_R = "end_r";
90    private static final String ATTR_END_RECT_B = "end_b";
91    private static final String ATTR_LOOP = "loop";
92    private static final String ATTR_MUTED = "muted";
93    private static final String ATTR_DUCK_ENABLED = "ducking_enabled";
94    private static final String ATTR_DUCK_THRESHOLD = "ducking_threshold";
95    private static final String ATTR_DUCKED_TRACK_VOLUME = "ducking_volume";
96
97    // Instance variables
98    private long mDurationMs;
99    private final String mProjectPath;
100    private final List<MediaItem> mMediaItems = new ArrayList<MediaItem>();
101    private final List<AudioTrack> mAudioTracks = new ArrayList<AudioTrack>();
102    private final List<Transition> mTransitions = new ArrayList<Transition>();
103    private PreviewThread mPreviewThread;
104    private int mAspectRatio;
105
106    /**
107     * The preview thread
108     */
109    private class PreviewThread extends Thread {
110        // Instance variables
111        private final static long FRAME_DURATION = 33;
112
113        // Instance variables
114        private final PreviewProgressListener mListener;
115        private final int mCallbackAfterFrameCount;
116        private final long mFromMs, mToMs;
117        private boolean mRun, mLoop;
118        private long mPositionMs;
119
120        /**
121         * Constructor
122         *
123         * @param fromMs Start preview at this position
124         * @param toMs The time (relative to the timeline) at which the preview
125         *            will stop. Use -1 to play to the end of the timeline
126         * @param callbackAfterFrameCount The listener interface should be
127         *            invoked after the number of frames specified by this
128         *            parameter.
129         * @param loop true if the preview should be looped once it reaches the
130         *            end
131         * @param listener The listener
132         */
133        public PreviewThread(long fromMs, long toMs, boolean loop, int callbackAfterFrameCount,
134                PreviewProgressListener listener) {
135            mPositionMs = mFromMs = fromMs;
136            if (toMs < 0) {
137                mToMs = mDurationMs;
138            } else {
139                mToMs = toMs;
140            }
141            mLoop = loop;
142            mCallbackAfterFrameCount = callbackAfterFrameCount;
143            mListener = listener;
144            mRun = true;
145        }
146
147        /*
148         * {@inheritDoc}
149         */
150        @Override
151        public void run() {
152            if (Log.isLoggable(TAG, Log.DEBUG)) {
153                Log.d(TAG, "===> PreviewThread.run enter");
154            }
155            int frameCount = 0;
156            while (mRun) {
157                try {
158                    sleep(FRAME_DURATION);
159                } catch (InterruptedException ex) {
160                    break;
161                }
162                frameCount++;
163                mPositionMs += FRAME_DURATION;
164
165                if (mPositionMs >= mToMs) {
166                    if (!mLoop) {
167                        if (mListener != null) {
168                            mListener.onProgress(VideoEditorImpl.this, mPositionMs, true);
169                        }
170                        if (Log.isLoggable(TAG, Log.DEBUG)) {
171                            Log.d(TAG, "PreviewThread.run playback complete");
172                        }
173                        break;
174                    } else {
175                        // Fire a notification for the end of the clip
176                        if (mListener != null) {
177                            mListener.onProgress(VideoEditorImpl.this, mToMs, false);
178                        }
179
180                        // Rewind
181                        mPositionMs = mFromMs;
182                        if (mListener != null) {
183                            mListener.onProgress(VideoEditorImpl.this, mPositionMs, false);
184                        }
185                        if (Log.isLoggable(TAG, Log.DEBUG)) {
186                            Log.d(TAG, "PreviewThread.run playback complete");
187                        }
188                        frameCount = 0;
189                    }
190                } else {
191                    if (frameCount == mCallbackAfterFrameCount) {
192                        if (mListener != null) {
193                            mListener.onProgress(VideoEditorImpl.this, mPositionMs, false);
194                        }
195                        frameCount = 0;
196                    }
197                }
198            }
199
200            if (Log.isLoggable(TAG, Log.DEBUG)) {
201                Log.d(TAG, "===> PreviewThread.run exit");
202            }
203        }
204
205        /**
206         * Stop the preview
207         *
208         * @return The stop position
209         */
210        public long stopPreview() {
211            mRun = false;
212            try {
213                join();
214            } catch (InterruptedException ex) {
215            }
216            return mPositionMs;
217        }
218    };
219
220    /**
221     * Constructor
222     *
223     * @param projectPath
224     */
225    public VideoEditorImpl(String projectPath) throws IOException {
226        mProjectPath = projectPath;
227        final File projectXml = new File(projectPath, PROJECT_FILENAME);
228        if (projectXml.exists()) {
229            try {
230                load();
231            } catch (Exception ex) {
232                throw new IOException(ex);
233            }
234        } else {
235            mAspectRatio = MediaProperties.ASPECT_RATIO_16_9;
236            mDurationMs = 0;
237        }
238    }
239
240    /*
241     * {@inheritDoc}
242     */
243    public String getPath() {
244        return mProjectPath;
245    }
246
247    /*
248     * {@inheritDoc}
249     */
250    public synchronized void addMediaItem(MediaItem mediaItem) {
251        if (mPreviewThread != null) {
252            throw new IllegalStateException("Previewing is in progress");
253        }
254
255        if (mMediaItems.contains(mediaItem)) {
256            throw new IllegalArgumentException("Media item already exists: " + mediaItem.getId());
257        }
258
259        // Invalidate the end transition if necessary
260        final int mediaItemsCount = mMediaItems.size();
261        if ( mediaItemsCount > 0) {
262            removeTransitionAfter(mediaItemsCount - 1);
263        }
264
265        // Add the new media item
266        mMediaItems.add(mediaItem);
267
268        computeTimelineDuration();
269    }
270
271    /*
272     * {@inheritDoc}
273     */
274    public synchronized void insertMediaItem(MediaItem mediaItem, String afterMediaItemId) {
275        if (mPreviewThread != null) {
276            throw new IllegalStateException("Previewing is in progress");
277        }
278
279        if (mMediaItems.contains(mediaItem)) {
280            throw new IllegalArgumentException("Media item already exists: " + mediaItem.getId());
281        }
282
283        if (afterMediaItemId == null) {
284            if (mMediaItems.size() > 0) {
285                // Invalidate the transition at the beginning of the timeline
286                removeTransitionBefore(0);
287            }
288            mMediaItems.add(0, mediaItem);
289            computeTimelineDuration();
290        } else {
291            final int mediaItemCount = mMediaItems.size();
292            for (int i = 0; i < mediaItemCount; i++) {
293                final MediaItem mi = mMediaItems.get(i);
294                if (mi.getId().equals(afterMediaItemId)) {
295                    // Invalidate the transition at this position
296                    removeTransitionAfter(i);
297                    // Insert the new media item
298                    mMediaItems.add(i + 1, mediaItem);
299                    computeTimelineDuration();
300                    return;
301                }
302            }
303            throw new IllegalArgumentException("MediaItem not found: " + afterMediaItemId);
304        }
305    }
306
307    /*
308     * {@inheritDoc}
309     */
310    public synchronized void moveMediaItem(String mediaItemId, String afterMediaItemId) {
311        if (mPreviewThread != null) {
312            throw new IllegalStateException("Previewing is in progress");
313        }
314
315        final MediaItem moveMediaItem = removeMediaItem(mediaItemId);
316        if (moveMediaItem == null) {
317            throw new IllegalArgumentException("Target MediaItem not found: " + mediaItemId);
318        }
319
320        if (afterMediaItemId == null) {
321            if (mMediaItems.size() > 0) {
322                // Invalidate adjacent transitions at the insertion point
323                removeTransitionBefore(0);
324
325                // Insert the media item at the new position
326                mMediaItems.add(0, moveMediaItem);
327                computeTimelineDuration();
328            } else {
329                throw new IllegalStateException("Cannot move media item (it is the only item)");
330            }
331        } else {
332            final int mediaItemCount = mMediaItems.size();
333            for (int i = 0; i < mediaItemCount; i++) {
334                final MediaItem mi = mMediaItems.get(i);
335                if (mi.getId().equals(afterMediaItemId)) {
336                    // Invalidate adjacent transitions at the insertion point
337                    removeTransitionAfter(i);
338                    // Insert the media item at the new position
339                    mMediaItems.add(i + 1, moveMediaItem);
340                    computeTimelineDuration();
341                    return;
342                }
343            }
344
345            throw new IllegalArgumentException("MediaItem not found: " + afterMediaItemId);
346        }
347    }
348
349    /*
350     * {@inheritDoc}
351     */
352    public synchronized MediaItem removeMediaItem(String mediaItemId) {
353        if (mPreviewThread != null) {
354            throw new IllegalStateException("Previewing is in progress");
355        }
356
357        final MediaItem mediaItem = getMediaItem(mediaItemId);
358        if (mediaItem != null) {
359            // Remove the media item
360            mMediaItems.remove(mediaItem);
361            // Remove the adjacent transitions
362            removeAdjacentTransitions(mediaItem);
363            computeTimelineDuration();
364        }
365
366        return mediaItem;
367    }
368
369    /*
370     * {@inheritDoc}
371     */
372    public synchronized MediaItem getMediaItem(String mediaItemId) {
373        for (MediaItem mediaItem : mMediaItems) {
374            if (mediaItem.getId().equals(mediaItemId)) {
375                return mediaItem;
376            }
377        }
378
379        return null;
380    }
381
382    /*
383     * {@inheritDoc}
384     */
385    public synchronized List<MediaItem> getAllMediaItems() {
386        return mMediaItems;
387    }
388
389    /*
390     * {@inheritDoc}
391     */
392    public synchronized void removeAllMediaItems() {
393        mMediaItems.clear();
394
395        // Invalidate all transitions
396        for (Transition transition : mTransitions) {
397            transition.invalidate();
398        }
399        mTransitions.clear();
400
401        mDurationMs = 0;
402    }
403
404    /*
405     * {@inheritDoc}
406     */
407    public synchronized void addTransition(Transition transition) {
408        mTransitions.add(transition);
409
410        final MediaItem beforeMediaItem = transition.getBeforeMediaItem();
411        final MediaItem afterMediaItem = transition.getAfterMediaItem();
412
413        // Cross reference the transitions
414        if (afterMediaItem != null) {
415            // If a transition already exists at the specified position then
416            // invalidate it.
417            if (afterMediaItem.getEndTransition() != null) {
418                afterMediaItem.getEndTransition().invalidate();
419            }
420            afterMediaItem.setEndTransition(transition);
421        }
422
423        if (beforeMediaItem != null) {
424            // If a transition already exists at the specified position then
425            // invalidate it.
426            if (beforeMediaItem.getBeginTransition() != null) {
427                beforeMediaItem.getBeginTransition().invalidate();
428            }
429            beforeMediaItem.setBeginTransition(transition);
430        }
431
432        computeTimelineDuration();
433    }
434
435    /*
436     * {@inheritDoc}
437     */
438    public synchronized Transition removeTransition(String transitionId) {
439        if (mPreviewThread != null) {
440            throw new IllegalStateException("Previewing is in progress");
441        }
442
443        final Transition transition = getTransition(transitionId);
444        if (transition == null) {
445            throw new IllegalStateException("Transition not found: " + transitionId);
446        }
447
448        // Remove the transition references
449        final MediaItem afterMediaItem = transition.getAfterMediaItem();
450        if (afterMediaItem != null) {
451            afterMediaItem.setEndTransition(null);
452        }
453
454        final MediaItem beforeMediaItem = transition.getBeforeMediaItem();
455        if (beforeMediaItem != null) {
456            beforeMediaItem.setBeginTransition(null);
457        }
458
459        mTransitions.remove(transition);
460        transition.invalidate();
461        computeTimelineDuration();
462
463        return transition;
464    }
465
466    /*
467     * {@inheritDoc}
468     */
469    public List<Transition> getAllTransitions() {
470        return mTransitions;
471    }
472
473    /*
474     * {@inheritDoc}
475     */
476    public Transition getTransition(String transitionId) {
477        for (Transition transition : mTransitions) {
478            if (transition.getId().equals(transitionId)) {
479                return transition;
480            }
481        }
482
483        return null;
484    }
485
486    /*
487     * {@inheritDoc}
488     */
489    public synchronized void addAudioTrack(AudioTrack audioTrack) {
490        if (mPreviewThread != null) {
491            throw new IllegalStateException("Previewing is in progress");
492        }
493
494        mAudioTracks.add(audioTrack);
495    }
496
497    /*
498     * {@inheritDoc}
499     */
500    public synchronized void insertAudioTrack(AudioTrack audioTrack, String afterAudioTrackId) {
501        if (mPreviewThread != null) {
502            throw new IllegalStateException("Previewing is in progress");
503        }
504
505        if (afterAudioTrackId == null) {
506            mAudioTracks.add(0, audioTrack);
507        } else {
508            final int audioTrackCount = mAudioTracks.size();
509            for (int i = 0; i < audioTrackCount; i++) {
510                AudioTrack at = mAudioTracks.get(i);
511                if (at.getId().equals(afterAudioTrackId)) {
512                    mAudioTracks.add(i + 1, audioTrack);
513                    return;
514                }
515            }
516
517            throw new IllegalArgumentException("AudioTrack not found: " + afterAudioTrackId);
518        }
519    }
520
521    /*
522     * {@inheritDoc}
523     */
524    public synchronized void moveAudioTrack(String audioTrackId, String afterAudioTrackId) {
525        throw new IllegalStateException("Not supported");
526    }
527
528    /*
529     * {@inheritDoc}
530     */
531    public synchronized AudioTrack removeAudioTrack(String audioTrackId) {
532        if (mPreviewThread != null) {
533            throw new IllegalStateException("Previewing is in progress");
534        }
535
536        final AudioTrack audioTrack = getAudioTrack(audioTrackId);
537        if (audioTrack != null) {
538            mAudioTracks.remove(audioTrack);
539        }
540
541        return audioTrack;
542    }
543
544    /*
545     * {@inheritDoc}
546     */
547    public AudioTrack getAudioTrack(String audioTrackId) {
548        for (AudioTrack at : mAudioTracks) {
549            if (at.getId().equals(audioTrackId)) {
550                return at;
551            }
552        }
553
554        return null;
555    }
556
557    /*
558     * {@inheritDoc}
559     */
560    public List<AudioTrack> getAllAudioTracks() {
561        return mAudioTracks;
562    }
563
564    /*
565     * {@inheritDoc}
566     */
567    public void save() throws IOException {
568        final XmlSerializer serializer = Xml.newSerializer();
569        final StringWriter writer = new StringWriter();
570        serializer.setOutput(writer);
571        serializer.startDocument("UTF-8", true);
572        serializer.startTag("", TAG_PROJECT);
573        serializer.attribute("", ATTR_ASPECT_RATIO, Integer.toString(mAspectRatio));
574
575        serializer.startTag("", TAG_MEDIA_ITEMS);
576        for (MediaItem mediaItem : mMediaItems) {
577            serializer.startTag("", TAG_MEDIA_ITEM);
578            serializer.attribute("", ATTR_ID, mediaItem.getId());
579            serializer.attribute("", ATTR_TYPE, mediaItem.getClass().getSimpleName());
580            serializer.attribute("", ATTR_FILENAME, mediaItem.getFilename());
581            serializer.attribute("", ATTR_RENDERING_MODE, Integer.toString(
582                    mediaItem.getRenderingMode()));
583            if (mediaItem instanceof MediaVideoItem) {
584                final MediaVideoItem mvi = (MediaVideoItem)mediaItem;
585                serializer
586                        .attribute("", ATTR_BEGIN_TIME, Long.toString(mvi.getBoundaryBeginTime()));
587                serializer.attribute("", ATTR_END_TIME, Long.toString(mvi.getBoundaryEndTime()));
588                serializer.attribute("", ATTR_VOLUME, Integer.toString(mvi.getVolume()));
589                serializer.attribute("", ATTR_MUTED, Boolean.toString(mvi.isMuted()));
590                if (mvi.getAudioWaveformFilename() != null) {
591                    serializer.attribute("", ATTR_AUDIO_WAVEFORM_FILENAME,
592                            mvi.getAudioWaveformFilename());
593                }
594            } else if (mediaItem instanceof MediaImageItem) {
595                serializer.attribute("", ATTR_DURATION,
596                        Long.toString(mediaItem.getTimelineDuration()));
597            }
598
599            final List<Overlay> overlays = mediaItem.getAllOverlays();
600            if (overlays.size() > 0) {
601                serializer.startTag("", TAG_OVERLAYS);
602                for (Overlay overlay : overlays) {
603                    serializer.startTag("", TAG_OVERLAY);
604                    serializer.attribute("", ATTR_ID, overlay.getId());
605                    serializer.attribute("", ATTR_TYPE, overlay.getClass().getSimpleName());
606                    serializer.attribute("", ATTR_BEGIN_TIME,
607                            Long.toString(overlay.getStartTime()));
608                    serializer.attribute("", ATTR_DURATION, Long.toString(overlay.getDuration()));
609                    if (overlay instanceof OverlayFrame) {
610                        final OverlayFrame overlayFrame = (OverlayFrame)overlay;
611                        overlayFrame.save(getPath());
612                        if (overlayFrame.getFilename() != null) {
613                            serializer.attribute("", ATTR_FILENAME, overlayFrame.getFilename());
614                        }
615                    }
616
617                    // Save the user attributes
618                    serializer.startTag("", TAG_OVERLAY_USER_ATTRIBUTES);
619                    final Map<String, String> userAttributes = overlay.getUserAttributes();
620                    for (String name : userAttributes.keySet()) {
621                        final String value = userAttributes.get(name);
622                        if (value != null) {
623                            serializer.attribute("", name, value);
624                        }
625                    }
626                    serializer.endTag("", TAG_OVERLAY_USER_ATTRIBUTES);
627
628                    serializer.endTag("", TAG_OVERLAY);
629                }
630                serializer.endTag("", TAG_OVERLAYS);
631            }
632
633            final List<Effect> effects = mediaItem.getAllEffects();
634            if (effects.size() > 0) {
635                serializer.startTag("", TAG_EFFECTS);
636                for (Effect effect : effects) {
637                    serializer.startTag("", TAG_EFFECT);
638                    serializer.attribute("", ATTR_ID, effect.getId());
639                    serializer.attribute("", ATTR_TYPE, effect.getClass().getSimpleName());
640                    serializer.attribute("", ATTR_BEGIN_TIME,
641                            Long.toString(effect.getStartTime()));
642                    serializer.attribute("", ATTR_DURATION, Long.toString(effect.getDuration()));
643                    if (effect instanceof EffectColor) {
644                        final EffectColor colorEffect = (EffectColor)effect;
645                        serializer.attribute("", ATTR_COLOR_EFFECT_TYPE,
646                                Integer.toString(colorEffect.getType()));
647                        if (colorEffect.getType() == EffectColor.TYPE_COLOR ||
648                                colorEffect.getType() == EffectColor.TYPE_GRADIENT) {
649                            serializer.attribute("", ATTR_COLOR_EFFECT_VALUE,
650                                    Integer.toString(colorEffect.getColor()));
651                        }
652                    } else if (effect instanceof EffectKenBurns) {
653                        final Rect startRect = ((EffectKenBurns)effect).getStartRect();
654                        serializer.attribute("", ATTR_START_RECT_L,
655                                Integer.toString(startRect.left));
656                        serializer.attribute("", ATTR_START_RECT_T,
657                                Integer.toString(startRect.top));
658                        serializer.attribute("", ATTR_START_RECT_R,
659                                Integer.toString(startRect.right));
660                        serializer.attribute("", ATTR_START_RECT_B,
661                                Integer.toString(startRect.bottom));
662
663                        final Rect endRect = ((EffectKenBurns)effect).getEndRect();
664                        serializer.attribute("", ATTR_END_RECT_L, Integer.toString(endRect.left));
665                        serializer.attribute("", ATTR_END_RECT_T, Integer.toString(endRect.top));
666                        serializer.attribute("", ATTR_END_RECT_R, Integer.toString(endRect.right));
667                        serializer.attribute("", ATTR_END_RECT_B,
668                                Integer.toString(endRect.bottom));
669                    }
670
671                    serializer.endTag("", TAG_EFFECT);
672                }
673                serializer.endTag("", TAG_EFFECTS);
674            }
675
676            serializer.endTag("", TAG_MEDIA_ITEM);
677        }
678        serializer.endTag("", TAG_MEDIA_ITEMS);
679
680        serializer.startTag("", TAG_TRANSITIONS);
681
682        for (Transition transition : mTransitions) {
683            serializer.startTag("", TAG_TRANSITION);
684            serializer.attribute("", ATTR_ID, transition.getId());
685            serializer.attribute("", ATTR_TYPE, transition.getClass().getSimpleName());
686            serializer.attribute("", ATTR_DURATION, Long.toString(transition.getDuration()));
687            serializer.attribute("", ATTR_BEHAVIOR, Integer.toString(transition.getBehavior()));
688            final MediaItem afterMediaItem = transition.getAfterMediaItem();
689            if (afterMediaItem != null) {
690                serializer.attribute("", ATTR_AFTER_MEDIA_ITEM_ID, afterMediaItem.getId());
691            }
692
693            final MediaItem beforeMediaItem = transition.getBeforeMediaItem();
694            if (beforeMediaItem != null) {
695                serializer.attribute("", ATTR_BEFORE_MEDIA_ITEM_ID, beforeMediaItem.getId());
696            }
697
698            if (transition instanceof TransitionSliding) {
699                serializer.attribute("", ATTR_DIRECTION,
700                        Integer.toString(((TransitionSliding)transition).getDirection()));
701            } else if (transition instanceof TransitionAlpha) {
702                TransitionAlpha ta = (TransitionAlpha)transition;
703                serializer.attribute("", ATTR_BLENDING, Integer.toString(ta.getBlendingPercent()));
704                serializer.attribute("", ATTR_INVERT, Boolean.toString(ta.isInvert()));
705                if (ta.getMaskFilename() != null) {
706                    serializer.attribute("", ATTR_MASK, ta.getMaskFilename());
707                }
708            }
709            serializer.endTag("", TAG_TRANSITION);
710        }
711        serializer.endTag("", TAG_TRANSITIONS);
712
713        serializer.startTag("", TAG_AUDIO_TRACKS);
714        for (AudioTrack at : mAudioTracks) {
715            serializer.startTag("", TAG_AUDIO_TRACK);
716            serializer.attribute("", ATTR_ID, at.getId());
717            serializer.attribute("", ATTR_FILENAME, at.getFilename());
718            serializer.attribute("", ATTR_START_TIME, Long.toString(at.getStartTime()));
719            serializer.attribute("", ATTR_BEGIN_TIME, Long.toString(at.getBoundaryBeginTime()));
720            serializer.attribute("", ATTR_END_TIME, Long.toString(at.getBoundaryEndTime()));
721            serializer.attribute("", ATTR_VOLUME, Integer.toString(at.getVolume()));
722            serializer.attribute("", ATTR_DUCK_ENABLED, Boolean.toString(at.isDuckingEnabled()));
723            serializer.attribute("", ATTR_DUCKED_TRACK_VOLUME, Integer.toString(at.getDuckedTrackVolume()));
724            serializer.attribute("", ATTR_DUCK_THRESHOLD, Integer.toString(at.getDuckingThreshhold()));
725            serializer.attribute("", ATTR_MUTED, Boolean.toString(at.isMuted()));
726            serializer.attribute("", ATTR_LOOP, Boolean.toString(at.isLooping()));
727            if (at.getAudioWaveformFilename() != null) {
728                serializer.attribute("", ATTR_AUDIO_WAVEFORM_FILENAME,
729                at.getAudioWaveformFilename());
730            }
731
732            serializer.endTag("", TAG_AUDIO_TRACK);
733        }
734        serializer.endTag("", TAG_AUDIO_TRACKS);
735
736        serializer.endTag("", TAG_PROJECT);
737        serializer.endDocument();
738
739        // Save the metadata XML file
740        final FileOutputStream out = new FileOutputStream(new File(getPath(), PROJECT_FILENAME));
741        out.write(writer.toString().getBytes());
742        out.flush();
743        out.close();
744    }
745
746    /**
747     * Load the project form XML
748     */
749    private void load() throws FileNotFoundException, XmlPullParserException, IOException {
750        final File file = new File(mProjectPath, PROJECT_FILENAME);
751        final FileInputStream fis = new FileInputStream(file);
752
753        try {
754            // Load the metadata
755            final XmlPullParser parser = Xml.newPullParser();
756            parser.setInput(fis, "UTF-8");
757            int eventType = parser.getEventType();
758            String name;
759            MediaItem currentMediaItem = null;
760            Overlay currentOverlay = null;
761            while (eventType != XmlPullParser.END_DOCUMENT) {
762                switch (eventType) {
763                    case XmlPullParser.START_TAG: {
764                        name = parser.getName();
765                        if (TAG_PROJECT.equals(name)) {
766                            mAspectRatio = Integer.parseInt(parser.getAttributeValue("",
767                                    ATTR_ASPECT_RATIO));
768                        } else if (TAG_MEDIA_ITEM.equals(name)) {
769                            final String mediaItemId = parser.getAttributeValue("", ATTR_ID);
770                            final String type = parser.getAttributeValue("", ATTR_TYPE);
771                            final String filename = parser.getAttributeValue("", ATTR_FILENAME);
772                            final int renderingMode = Integer.parseInt(
773                                    parser.getAttributeValue("", ATTR_RENDERING_MODE));
774
775                            if (MediaImageItem.class.getSimpleName().equals(type)) {
776                                final long durationMs = Long.parseLong(
777                                        parser.getAttributeValue("", ATTR_DURATION));
778                                currentMediaItem = new MediaImageItem(this, mediaItemId, filename,
779                                        durationMs, renderingMode);
780                            } else if (MediaVideoItem.class.getSimpleName().equals(type)) {
781                                final long beginMs = Long.parseLong(
782                                        parser.getAttributeValue("", ATTR_BEGIN_TIME));
783                                final long endMs = Long.parseLong(
784                                        parser.getAttributeValue("", ATTR_END_TIME));
785                                final int volume = Integer.parseInt(
786                                        parser.getAttributeValue("", ATTR_VOLUME));
787                                final boolean muted = Boolean.parseBoolean(
788                                        parser.getAttributeValue("", ATTR_MUTED));
789                                final String audioWaveformFilename =
790                                        parser.getAttributeValue("", ATTR_AUDIO_WAVEFORM_FILENAME);
791                                currentMediaItem = new MediaVideoItem(this, mediaItemId, filename,
792                                        renderingMode, beginMs, endMs, volume, muted,
793                                        audioWaveformFilename);
794
795                                final long beginTimeMs = Long.parseLong(
796                                        parser.getAttributeValue("", ATTR_BEGIN_TIME));
797                                final long endTimeMs = Long.parseLong(
798                                        parser.getAttributeValue("", ATTR_END_TIME));
799                                ((MediaVideoItem)currentMediaItem).setExtractBoundaries(
800                                        beginTimeMs, endTimeMs);
801
802                                final int volumePercent = Integer.parseInt(
803                                        parser.getAttributeValue("", ATTR_VOLUME));
804                                ((MediaVideoItem)currentMediaItem).setVolume(volumePercent);
805                            } else {
806                                Log.e(TAG, "Unknown media item type: " + type);
807                                currentMediaItem = null;
808                            }
809
810                            if (currentMediaItem != null) {
811                                mMediaItems.add(currentMediaItem);
812                            }
813                        } else if (TAG_TRANSITION.equals(name)) {
814                            final Transition transition = parseTransition(parser);
815                            if (transition != null) {
816                                mTransitions.add(transition);
817                            }
818                        } else if (TAG_OVERLAY.equals(name)) {
819                            if (currentMediaItem != null) {
820                                currentOverlay = parseOverlay(parser, currentMediaItem);
821                                if (currentOverlay != null) {
822                                    currentMediaItem.addOverlay(currentOverlay);
823                                }
824                            }
825                        } else if (TAG_OVERLAY_USER_ATTRIBUTES.equals(name)) {
826                            if (currentOverlay != null) {
827                                final int attributesCount = parser.getAttributeCount();
828                                for (int i = 0; i < attributesCount; i++) {
829                                    currentOverlay.setUserAttribute(parser.getAttributeName(i),
830                                            parser.getAttributeValue(i));
831                                }
832                            }
833                        } else if (TAG_EFFECT.equals(name)) {
834                            if (currentMediaItem != null) {
835                                final Effect effect = parseEffect(parser, currentMediaItem);
836                                if (effect != null) {
837                                    currentMediaItem.addEffect(effect);
838                                }
839                            }
840                        } else if (TAG_AUDIO_TRACK.equals(name)) {
841                            final AudioTrack audioTrack = parseAudioTrack(parser);
842                            if (audioTrack != null) {
843                                addAudioTrack(audioTrack);
844                            }
845                        }
846                        break;
847                    }
848
849                    case XmlPullParser.END_TAG: {
850                        name = parser.getName();
851                        if (TAG_MEDIA_ITEM.equals(name)) {
852                            currentMediaItem = null;
853                        } else if (TAG_OVERLAY.equals(name)) {
854                            currentOverlay = null;
855                        }
856                        break;
857                    }
858
859                    default: {
860                        break;
861                    }
862                }
863                eventType = parser.next();
864            }
865            computeTimelineDuration();
866        } finally {
867            if (fis != null) {
868                fis.close();
869            }
870        }
871    }
872
873    /**
874     * Parse the transition
875     *
876     * @param parser The parser
877     * @return The transition
878     */
879    private Transition parseTransition(XmlPullParser parser) {
880        final String transitionId = parser.getAttributeValue("", ATTR_ID);
881        final String type = parser.getAttributeValue("", ATTR_TYPE);
882        final long durationMs = Long.parseLong(parser.getAttributeValue("", ATTR_DURATION));
883        final int behavior = Integer.parseInt(parser.getAttributeValue("", ATTR_BEHAVIOR));
884
885        final String beforeMediaItemId = parser.getAttributeValue("", ATTR_BEFORE_MEDIA_ITEM_ID);
886        final MediaItem beforeMediaItem;
887        if (beforeMediaItemId != null) {
888            beforeMediaItem = getMediaItem(beforeMediaItemId);
889        } else {
890            beforeMediaItem = null;
891        }
892
893        final String afterMediaItemId = parser.getAttributeValue("", ATTR_AFTER_MEDIA_ITEM_ID);
894        final MediaItem afterMediaItem;
895        if (afterMediaItemId != null) {
896            afterMediaItem = getMediaItem(afterMediaItemId);
897        } else {
898            afterMediaItem = null;
899        }
900
901        final Transition transition;
902        if (TransitionAlpha.class.getSimpleName().equals(type)) {
903            final int blending = Integer.parseInt(parser.getAttributeValue("", ATTR_BLENDING));
904            final String maskFilename = parser.getAttributeValue("", ATTR_MASK);
905            final boolean invert = Boolean.getBoolean(parser.getAttributeValue("", ATTR_INVERT));
906            transition = new TransitionAlpha(transitionId, afterMediaItem, beforeMediaItem,
907                    durationMs, behavior, maskFilename, blending, invert);
908        } else if (TransitionCrossfade.class.getSimpleName().equals(type)) {
909            transition = new TransitionCrossfade(transitionId, afterMediaItem, beforeMediaItem,
910                    durationMs, behavior);
911        } else if (TransitionSliding.class.getSimpleName().equals(type)) {
912            final int direction = Integer.parseInt(parser.getAttributeValue("", ATTR_DIRECTION));
913            transition = new TransitionSliding(transitionId, afterMediaItem, beforeMediaItem,
914                    durationMs, behavior, direction);
915        } else if (TransitionFadeBlack.class.getSimpleName().equals(type)) {
916            transition = new TransitionFadeBlack(transitionId, afterMediaItem, beforeMediaItem,
917                    durationMs, behavior);
918        } else {
919            transition = null;
920        }
921
922        if (beforeMediaItem != null) {
923            beforeMediaItem.setBeginTransition(transition);
924        }
925
926        if (afterMediaItem != null) {
927            afterMediaItem.setEndTransition(transition);
928        }
929
930        return transition;
931    }
932
933    /**
934     * Parse the overlay
935     *
936     * @param parser The parser
937     * @param mediaItem The media item owner
938     *
939     * @return The overlay
940     */
941    private Overlay parseOverlay(XmlPullParser parser, MediaItem mediaItem) {
942        final String overlayId = parser.getAttributeValue("", ATTR_ID);
943        final String type = parser.getAttributeValue("", ATTR_TYPE);
944        final long durationMs = Long.parseLong(parser.getAttributeValue("", ATTR_DURATION));
945        final long startTimeMs = Long.parseLong(parser.getAttributeValue("", ATTR_BEGIN_TIME));
946
947        final Overlay overlay;
948        if (OverlayFrame.class.getSimpleName().equals(type)) {
949            final String filename = parser.getAttributeValue("", ATTR_FILENAME);
950            overlay = new OverlayFrame(mediaItem, overlayId, filename, startTimeMs, durationMs);
951        } else {
952            overlay = null;
953        }
954
955        return overlay;
956    }
957
958    /**
959     * Parse the effect
960     *
961     * @param parser The parser
962     * @param mediaItem The media item owner
963     *
964     * @return The effect
965     */
966    private Effect parseEffect(XmlPullParser parser, MediaItem mediaItem) {
967        final String effectId = parser.getAttributeValue("", ATTR_ID);
968        final String type = parser.getAttributeValue("", ATTR_TYPE);
969        final long durationMs = Long.parseLong(parser.getAttributeValue("", ATTR_DURATION));
970        final long startTimeMs = Long.parseLong(parser.getAttributeValue("", ATTR_BEGIN_TIME));
971
972        final Effect effect;
973        if (EffectColor.class.getSimpleName().equals(type)) {
974            final int colorEffectType =
975                Integer.parseInt(parser.getAttributeValue("", ATTR_COLOR_EFFECT_TYPE));
976            final int color;
977            if (colorEffectType == EffectColor.TYPE_COLOR
978                    || colorEffectType == EffectColor.TYPE_GRADIENT) {
979                color = Integer.parseInt(parser.getAttributeValue("", ATTR_COLOR_EFFECT_VALUE));
980            } else {
981                color = 0;
982            }
983            effect = new EffectColor(mediaItem, effectId, startTimeMs, durationMs,
984                    colorEffectType, color);
985        } else if (EffectKenBurns.class.getSimpleName().equals(type)) {
986            final Rect startRect = new Rect(
987                    Integer.parseInt(parser.getAttributeValue("", ATTR_START_RECT_L)),
988                    Integer.parseInt(parser.getAttributeValue("", ATTR_START_RECT_T)),
989                    Integer.parseInt(parser.getAttributeValue("", ATTR_START_RECT_R)),
990                    Integer.parseInt(parser.getAttributeValue("", ATTR_START_RECT_B)));
991            final Rect endRect = new Rect(
992                    Integer.parseInt(parser.getAttributeValue("", ATTR_END_RECT_L)),
993                    Integer.parseInt(parser.getAttributeValue("", ATTR_END_RECT_T)),
994                    Integer.parseInt(parser.getAttributeValue("", ATTR_END_RECT_R)),
995                    Integer.parseInt(parser.getAttributeValue("", ATTR_END_RECT_B)));
996            effect = new EffectKenBurns(mediaItem, effectId, startRect, endRect, startTimeMs,
997                    durationMs);
998        } else {
999            effect = null;
1000        }
1001
1002        return effect;
1003    }
1004
1005    /**
1006     * Parse the audio track
1007     *
1008     * @param parser The parser
1009     *
1010     * @return The audio track
1011     */
1012    private AudioTrack parseAudioTrack(XmlPullParser parser) {
1013        final String audioTrackId = parser.getAttributeValue("", ATTR_ID);
1014        final String filename = parser.getAttributeValue("", ATTR_FILENAME);
1015        final long startTimeMs = Long.parseLong(parser.getAttributeValue("", ATTR_START_TIME));
1016        final long beginMs = Long.parseLong(parser.getAttributeValue("", ATTR_BEGIN_TIME));
1017        final long endMs = Long.parseLong(parser.getAttributeValue("", ATTR_END_TIME));
1018        final int volume = Integer.parseInt(parser.getAttributeValue("", ATTR_VOLUME));
1019        final boolean muted = Boolean.parseBoolean(parser.getAttributeValue("", ATTR_MUTED));
1020        final boolean loop = Boolean.parseBoolean(parser.getAttributeValue("", ATTR_LOOP));
1021        final boolean duckingEnabled = Boolean.parseBoolean(parser.getAttributeValue("", ATTR_DUCK_ENABLED));
1022        final int duckThreshold = Integer.parseInt(parser.getAttributeValue("", ATTR_DUCK_THRESHOLD));
1023        final int duckedTrackVolume = Integer.parseInt(parser.getAttributeValue("", ATTR_DUCKED_TRACK_VOLUME));
1024        final String waveformFilename = parser.getAttributeValue("", ATTR_AUDIO_WAVEFORM_FILENAME);
1025        try {
1026            final AudioTrack audioTrack = new AudioTrack(this, audioTrackId, filename, startTimeMs,
1027                    beginMs, endMs, loop, volume, muted, duckingEnabled, duckThreshold, duckedTrackVolume, waveformFilename);
1028
1029            return audioTrack;
1030        } catch (IOException ex) {
1031            return null;
1032        }
1033    }
1034
1035    /*
1036     * {@inheritDoc}
1037     */
1038    public void cancelExport(String filename) {
1039    }
1040
1041    /*
1042     * {@inheritDoc}
1043     */
1044    public void export(String filename, int height, int bitrate, ExportProgressListener listener)
1045            throws IOException {
1046    }
1047
1048    /*
1049     * {@inheritDoc}
1050     */
1051    public void export(String filename, int height, int bitrate, int audioCodec, int videoCodec,
1052            ExportProgressListener listener) throws IOException {
1053    }
1054
1055    /*
1056     * {@inheritDoc}
1057     */
1058    public void generatePreview(MediaProcessingProgressListener listener) {
1059        // Generate all the needed transitions
1060        for (Transition transition : mTransitions) {
1061            if (!transition.isGenerated()) {
1062                transition.generate();
1063            }
1064        }
1065
1066        // This is necessary because the user may had called setDuration on
1067        // MediaImageItems
1068        computeTimelineDuration();
1069    }
1070
1071    /*
1072     * {@inheritDoc}
1073     */
1074    public void release() {
1075        stopPreview();
1076    }
1077
1078    /*
1079     * {@inheritDoc}
1080     */
1081    public long getDuration() {
1082        // Since MediaImageItem can change duration we need to compute the
1083        // duration here
1084        computeTimelineDuration();
1085        return mDurationMs;
1086    }
1087
1088    /*
1089     * {@inheritDoc}
1090     */
1091    public int getAspectRatio() {
1092        return mAspectRatio;
1093    }
1094
1095    /*
1096     * {@inheritDoc}
1097     */
1098    public void setAspectRatio(int aspectRatio) {
1099        mAspectRatio = aspectRatio;
1100
1101        for (Transition transition : mTransitions) {
1102            transition.invalidate();
1103        }
1104    }
1105
1106    /*
1107     * {@inheritDoc}
1108     */
1109    public long renderPreviewFrame(SurfaceHolder surfaceHolder, long timeMs) {
1110        if (mPreviewThread != null) {
1111            throw new IllegalStateException("Previewing is in progress");
1112        }
1113        return timeMs;
1114    }
1115
1116    /*
1117     * {@inheritDoc}
1118     */
1119    public synchronized void startPreview(SurfaceHolder surfaceHolder, long fromMs, long toMs,
1120            boolean loop, int callbackAfterFrameCount, PreviewProgressListener listener) {
1121        if (fromMs >= mDurationMs) {
1122            return;
1123        }
1124        mPreviewThread = new PreviewThread(fromMs, toMs, loop, callbackAfterFrameCount, listener);
1125        mPreviewThread.start();
1126    }
1127
1128    /*
1129     * {@inheritDoc}
1130     */
1131    public synchronized long stopPreview() {
1132        final long stopTimeMs;
1133        if (mPreviewThread != null) {
1134            stopTimeMs = mPreviewThread.stopPreview();
1135            mPreviewThread = null;
1136        } else {
1137            stopTimeMs = 0;
1138        }
1139        return stopTimeMs;
1140    }
1141
1142    /**
1143     * Compute the duration
1144     */
1145    private void computeTimelineDuration() {
1146        mDurationMs = 0;
1147        final int mediaItemsCount = mMediaItems.size();
1148        for (int i = 0; i < mediaItemsCount; i++) {
1149            final MediaItem mediaItem = mMediaItems.get(i);
1150            mDurationMs += mediaItem.getTimelineDuration();
1151            if (mediaItem.getEndTransition() != null) {
1152                if (i < mediaItemsCount - 1) {
1153                    mDurationMs -= mediaItem.getEndTransition().getDuration();
1154                }
1155            }
1156        }
1157    }
1158
1159    /**
1160     * Remove transitions associated with the specified media item
1161     *
1162     * @param mediaItem The media item
1163     */
1164    private void removeAdjacentTransitions(MediaItem mediaItem) {
1165        final Transition beginTransition = mediaItem.getBeginTransition();
1166        if (beginTransition != null) {
1167            if (beginTransition.getAfterMediaItem() != null) {
1168                beginTransition.getAfterMediaItem().setEndTransition(null);
1169            }
1170            beginTransition.invalidate();
1171            mTransitions.remove(beginTransition);
1172        }
1173
1174        final Transition endTransition = mediaItem.getEndTransition();
1175        if (endTransition != null) {
1176            if (endTransition.getBeforeMediaItem() != null) {
1177                endTransition.getBeforeMediaItem().setBeginTransition(null);
1178            }
1179            endTransition.invalidate();
1180            mTransitions.remove(endTransition);
1181        }
1182
1183        mediaItem.setBeginTransition(null);
1184        mediaItem.setEndTransition(null);
1185    }
1186
1187    /**
1188     * Remove the transition before this media item
1189     *
1190     * @param index The media item index
1191     */
1192    private void removeTransitionBefore(int index) {
1193        final MediaItem mediaItem = mMediaItems.get(index);
1194        final Iterator<Transition> it = mTransitions.iterator();
1195        while (it.hasNext()) {
1196            Transition t = it.next();
1197            if (t.getBeforeMediaItem() == mediaItem) {
1198                it.remove();
1199                t.invalidate();
1200                mediaItem.setBeginTransition(null);
1201                if (index > 0) {
1202                    mMediaItems.get(index - 1).setEndTransition(null);
1203                }
1204                break;
1205            }
1206        }
1207    }
1208
1209    /**
1210     * Remove the transition after this media item
1211     *
1212     * @param index The media item index
1213     */
1214    private void removeTransitionAfter(int index) {
1215        final MediaItem mediaItem = mMediaItems.get(index);
1216        final Iterator<Transition> it = mTransitions.iterator();
1217        while (it.hasNext()) {
1218            Transition t = it.next();
1219            if (t.getAfterMediaItem() == mediaItem) {
1220                it.remove();
1221                t.invalidate();
1222                mediaItem.setEndTransition(null);
1223                // Invalidate the reference in the next media item
1224                if (index < mMediaItems.size() - 1) {
1225                    mMediaItems.get(index + 1).setBeginTransition(null);
1226                }
1227                break;
1228            }
1229        }
1230    }
1231}
1232