• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 package com.android.videoeditor.service;
18 
19 import java.io.File;
20 import java.io.FileInputStream;
21 import java.io.FileNotFoundException;
22 import java.io.FileOutputStream;
23 import java.io.IOException;
24 import java.io.StringWriter;
25 import java.util.ArrayList;
26 import java.util.List;
27 
28 import org.xmlpull.v1.XmlPullParser;
29 import org.xmlpull.v1.XmlPullParserException;
30 import org.xmlpull.v1.XmlSerializer;
31 
32 import android.media.videoeditor.MediaProperties;
33 import android.media.videoeditor.MediaVideoItem;
34 import android.media.videoeditor.VideoEditor;
35 import android.media.videoeditor.VideoEditor.PreviewProgressListener;
36 import android.net.Uri;
37 import android.util.Xml;
38 import android.view.SurfaceHolder;
39 
40 
41 /**
42  * The video editor project encapsulates the video editor and the project metadata.
43  */
44 public class VideoEditorProject {
45     // The name of the metadata file
46     private final static String PROJECT_METADATA_FILENAME = "metadata.xml";
47 
48     public static final int DEFAULT_ZOOM_LEVEL = 20;
49 
50     // XML definitions
51     private static final String TAG_PROJECT = "project";
52     private static final String TAG_MOVIE = "movie";
53     private static final String TAG_DOWNLOAD = "download";
54     private static final String ATTR_NAME = "name";
55     private static final String ATTR_URI = "uri";
56     private static final String ATTR_SAVED = "saved";
57     private static final String ATTR_THEME = "theme";
58     private static final String ATTR_PLAYHEAD_POSITION = "playhead";
59     private static final String ATTR_DURATION = "duration";
60     private static final String ATTR_ZOOM_LEVEL = "zoom_level";
61     private static final String ATTR_MIME = "mime";
62     private static final String ATTR_FILENAME = "filename";
63     private static final String ATTR_TIME = "time";
64 
65     // Instance variables
66     private final VideoEditor mVideoEditor;
67     private final String mProjectPath;
68     private final long mProjectDurationMs;
69     private final List<Download> mDownloads;
70     private String mProjectName;
71     private long mLastSaved;
72     private Uri mExportedMovieUri;
73     private int mAspectRatio;
74     private String mTheme;
75     private long mPlayheadPosMs;
76     private int mZoomLevel;
77     private List<MovieMediaItem> mMediaItems = new ArrayList<MovieMediaItem>();
78     private List<MovieAudioTrack> mAudioTracks = new ArrayList<MovieAudioTrack>();
79     private boolean mClean;
80 
81     /**
82      * Download item
83      */
84     public static class Download {
85         private final String mMediaUri;
86         private final String mMimeType;
87         private final String mFilename;
88         private final long mTime;
89 
90         /**
91          * Constructor
92          *
93          * @param mediaUri The media URI
94          * @param mimeType The mime type
95          * @param filename The filename
96          * @param time The time when the file was downloaded
97          */
Download(String mediaUri, String mimeType, String filename, long time)98         private Download(String mediaUri, String mimeType, String filename, long time) {
99             mMediaUri = mediaUri;
100             mMimeType = mimeType;
101             mFilename = filename;
102             mTime = time;
103         }
104 
105         /**
106          * @return the media URI
107          */
getMediaUri()108         public String getMediaUri() {
109             return mMediaUri;
110         }
111 
112         /**
113          * @return the mime type
114          */
getMimeType()115         public String getMimeType() {
116             return mMimeType;
117         }
118 
119         /**
120          * @return the filename
121          */
getFilename()122         public String getFilename() {
123             return mFilename;
124         }
125 
126         /**
127          * @return the mTime
128          */
getTime()129         public long getTime() {
130             return mTime;
131         }
132     }
133 
134     /**
135      * Constructor
136      *
137      * @param videoEditor The video editor. Note that this can be null when
138      *  we create the project for the purpose of displaying a project preview.
139      * @param projectPath The project path
140      * @param projectName The project name
141      * @param lastSaved Time when project was last saved
142      * @param playheadPosMs The playhead position
143      * @param durationMs The project duration
144      * @param zoomLevel The zoom level
145      * @param exportedMovieUri The exported movie URI
146      * @param theme The project theme
147      * @param downloads The list of downloads
148      */
VideoEditorProject(VideoEditor videoEditor, String projectPath, String projectName, long lastSaved, long playheadPosMs, long durationMs, int zoomLevel, Uri exportedMovieUri, String theme, List<Download> downloads)149     VideoEditorProject(VideoEditor videoEditor, String projectPath, String projectName,
150             long lastSaved, long playheadPosMs, long durationMs, int zoomLevel,
151             Uri exportedMovieUri, String theme, List<Download> downloads) {
152         mVideoEditor = videoEditor;
153         if (videoEditor != null) {
154             mAspectRatio = videoEditor.getAspectRatio();
155         }
156 
157         if (downloads != null) {
158             mDownloads = downloads;
159         } else {
160             mDownloads = new ArrayList<Download>();
161         }
162         mProjectPath = projectPath;
163         mProjectName = projectName;
164         mLastSaved = lastSaved;
165         mPlayheadPosMs = playheadPosMs;
166         mProjectDurationMs = durationMs;
167         mZoomLevel = zoomLevel;
168         mExportedMovieUri = exportedMovieUri;
169         mTheme = theme;
170         mClean = true;
171     }
172 
173     /**
174      * @param clean true if this is clean
175      */
setClean(boolean clean)176     public void setClean(boolean clean) {
177         mClean = clean;
178     }
179 
180     /**
181      * @return true if no change was made
182      */
isClean()183     public boolean isClean() {
184         return mClean;
185     }
186 
187     /**
188      * @return The project path
189      */
getPath()190     public String getPath() {
191         return mProjectPath;
192     }
193 
194     /**
195      * @param projectName The project name
196      */
setProjectName(String projectName)197     public void setProjectName(String projectName) {
198         mProjectName = projectName;
199         mClean = false;
200     }
201 
202     /**
203      * @return The project name
204      */
getName()205     public String getName() {
206         return mProjectName;
207     }
208 
209     /**
210      * @return Time when time was last saved
211      */
getLastSaved()212     public long getLastSaved() {
213         return mLastSaved;
214     }
215 
216     /**
217      * @return The project duration.
218      *
219      * Note: This method should only be called to retrieve the project duration
220      * as saved on disk. Once a project is opened call computeDuration() to get
221      * the current duration.
222      */
getProjectDuration()223     public long getProjectDuration() {
224         return mProjectDurationMs;
225     }
226 
227     /**
228      * @return The zoom level
229      */
getZoomLevel()230     public int getZoomLevel() {
231         return mZoomLevel;
232     }
233 
234     /**
235      * @param zoomLevel The zoom level
236      */
setZoomLevel(int zoomLevel)237     public void setZoomLevel(int zoomLevel) {
238         mZoomLevel = zoomLevel;
239     }
240 
241     /**
242      * @return The aspect ratio
243      */
getAspectRatio()244     public int getAspectRatio() {
245         return mAspectRatio;
246     }
247 
248     /**
249      * @return The playhead position
250      */
getPlayheadPos()251     public long getPlayheadPos() {
252         return mPlayheadPosMs;
253     }
254 
255     /**
256      * @param playheadPosMs The playhead position
257      */
setPlayheadPos(long playheadPosMs)258     public void setPlayheadPos(long playheadPosMs) {
259         mPlayheadPosMs = playheadPosMs;
260     }
261 
262     /**
263      * @param aspectRatio The aspect ratio
264      */
setAspectRatio(int aspectRatio)265     void setAspectRatio(int aspectRatio) {
266         mAspectRatio = aspectRatio;
267         mClean = false;
268     }
269 
270     /**
271      * Add the URI of an exported movie
272      *
273      * @param uri The movie URI
274      */
addExportedMovieUri(Uri uri)275     void addExportedMovieUri(Uri uri) {
276         mExportedMovieUri = uri;
277         mClean = false;
278     }
279 
280     /**
281      * @return The exported movie URI
282      */
getExportedMovieUri()283     public Uri getExportedMovieUri() {
284         return mExportedMovieUri;
285     }
286 
287     /**
288      * @param theme The theme
289      */
setTheme(String theme)290     void setTheme(String theme) {
291         mTheme = theme;
292         mClean = false;
293     }
294 
295     /**
296      * @return The theme
297      */
getTheme()298     public String getTheme() {
299         return mTheme;
300     }
301 
302     /**
303      * Set the media items
304      *
305      * @param mediaItems The media items
306      */
setMediaItems(List<MovieMediaItem> mediaItems)307     void setMediaItems(List<MovieMediaItem> mediaItems) {
308         mMediaItems = mediaItems;
309         mClean = false;
310     }
311 
312     /**
313      * Insert a media item after the specified media item id
314      *
315      * @param mediaItem The media item
316      * @param afterMediaItemId Insert after this media item id
317      */
insertMediaItem(MovieMediaItem mediaItem, String afterMediaItemId)318     void insertMediaItem(MovieMediaItem mediaItem, String afterMediaItemId) {
319         if (afterMediaItemId == null) {
320             if (mMediaItems.size() > 0) {
321                 // Invalidate the transition at the beginning of the timeline
322                 final MovieMediaItem firstMediaItem = mMediaItems.get(0);
323                 if (firstMediaItem.getBeginTransition() != null) {
324                     firstMediaItem.setBeginTransition(null);
325                 }
326             }
327 
328             mMediaItems.add(0, mediaItem);
329             mClean = false;
330         } else {
331             final int mediaItemCount = mMediaItems.size();
332             for (int i = 0; i < mediaItemCount; i++) {
333                 final MovieMediaItem mi = mMediaItems.get(i);
334                 if (mi.getId().equals(afterMediaItemId)) {
335                     // Invalidate the transition at the end of this media item
336                     mi.setEndTransition(null);
337                     // Invalidate the reference in the next media item (if any)
338                     if (i < mediaItemCount - 1) {
339                         mMediaItems.get(i + 1).setBeginTransition(null);
340                     }
341 
342                     // Insert the new media item
343                     mMediaItems.add(i + 1, mediaItem);
344                     mClean = false;
345                     return;
346                 }
347             }
348 
349             throw new IllegalArgumentException("MediaItem not found: " + afterMediaItemId);
350         }
351     }
352 
353     /**
354      * Update the specified media item
355      *
356      * @param newMediaItem The media item can be a new instance of the media
357      *      item or an updated version of the same instance.
358      */
updateMediaItem(MovieMediaItem newMediaItem)359     void updateMediaItem(MovieMediaItem newMediaItem) {
360         final String newMediaItemId = newMediaItem.getId();
361         final int count = mMediaItems.size();
362         for (int i = 0; i < count; i++) {
363             final MovieMediaItem mediaItem = mMediaItems.get(i);
364             if (mediaItem.getId().equals(newMediaItemId)) {
365                 mMediaItems.set(i, newMediaItem);
366                 mClean = false;
367                 // Update the transitions of the previous and next item
368                 if (i > 0) {
369                     final MovieMediaItem prevMediaItem = mMediaItems.get(i - 1);
370                     prevMediaItem.setEndTransition(newMediaItem.getBeginTransition());
371                 }
372 
373                 if (i < count - 1) {
374                     final MovieMediaItem nextMediaItem = mMediaItems.get(i + 1);
375                     nextMediaItem.setBeginTransition(newMediaItem.getEndTransition());
376                 }
377                 break;
378             }
379         }
380     }
381 
382     /**
383      * Remove the specified media item
384      *
385      * @param mediaItemId The media item id
386      * @param transition The transition to be set between at the delete
387      *      position
388      */
removeMediaItem(String mediaItemId, MovieTransition transition)389     void removeMediaItem(String mediaItemId, MovieTransition transition) {
390         String prevMediaItemId = null;
391         final int count = mMediaItems.size();
392         for (int i = 0; i < count; i++) {
393             final MovieMediaItem mediaItem = mMediaItems.get(i);
394             if (mediaItem.getId().equals(mediaItemId)) {
395                 mMediaItems.remove(i);
396                 mClean = false;
397                 if (transition != null) {
398                     addTransition(transition, prevMediaItemId);
399                 } else {
400                     if (i > 0) {
401                         final MovieMediaItem prevMediaItem = mMediaItems.get(i - 1);
402                         prevMediaItem.setEndTransition(null);
403                     }
404 
405                     if (i < count - 1) {
406                         final MovieMediaItem nextMediaItem = mMediaItems.get(i);
407                         nextMediaItem.setBeginTransition(null);
408                     }
409                 }
410                 break;
411             }
412 
413             prevMediaItemId = mediaItem.getId();
414         }
415     }
416 
417     /**
418      * @return The media items list
419      */
getMediaItems()420     public List<MovieMediaItem> getMediaItems() {
421         return mMediaItems;
422     }
423 
424     /**
425      * @return The media item count
426      */
getMediaItemCount()427     public int getMediaItemCount() {
428         return mMediaItems.size();
429     }
430 
431     /**
432      * @param mediaItemId The media item id
433      *
434      * @return The media item
435      */
getMediaItem(String mediaItemId)436     public MovieMediaItem getMediaItem(String mediaItemId) {
437         for (MovieMediaItem mediaItem : mMediaItems) {
438             if (mediaItem.getId().equals(mediaItemId)) {
439                 return mediaItem;
440             }
441         }
442 
443         return null;
444     }
445 
446     /**
447      * @return The first media item
448      */
getFirstMediaItem()449     public MovieMediaItem getFirstMediaItem() {
450         if (mMediaItems.size() == 0) {
451             return null;
452         } else {
453             return mMediaItems.get(0);
454         }
455     }
456 
457     /**
458      * Check if the specified media item id is the first media item
459      *
460      * @param mediaItemId The media item id
461      *
462      * @return true if this is the first media item
463      */
isFirstMediaItem(String mediaItemId)464     public boolean isFirstMediaItem(String mediaItemId) {
465         final MovieMediaItem mediaItem = getFirstMediaItem();
466         if (mediaItem == null) {
467             return false;
468         } else {
469             return mediaItem.getId().equals(mediaItemId);
470         }
471     }
472 
473     /**
474      * @return The last media item. {@code null} if no item is in the project.
475      */
getLastMediaItem()476     public MovieMediaItem getLastMediaItem() {
477         final int count = mMediaItems.size();
478         if (count == 0) {
479             return null;
480         } else {
481             return mMediaItems.get(count - 1);
482         }
483     }
484 
485     /**
486      * Gets the id of the last media item in this project.
487      *
488      * @return Id of the last media item. {@code null} if no item is in this project.
489      */
getLastMediaItemId()490     public String getLastMediaItemId() {
491         MovieMediaItem lastMediaItem = getLastMediaItem();
492         if (lastMediaItem != null)
493             return lastMediaItem.getId();
494         return null;
495     }
496 
497     /**
498      * Check if the specified media item id is the last media item
499      *
500      * @param mediaItemId The media item id
501      *
502      * @return true if this is the last media item
503      */
isLastMediaItem(String mediaItemId)504     public boolean isLastMediaItem(String mediaItemId) {
505         final MovieMediaItem mediaItem = getLastMediaItem();
506         if (mediaItem == null) {
507             return false;
508         } else {
509             return mediaItem.getId().equals(mediaItemId);
510         }
511     }
512 
513     /**
514      * Find the previous media item with the specified id
515      *
516      * @param mediaItemId The media item id
517      * @return The previous media item
518      */
getPreviousMediaItem(String mediaItemId)519     public MovieMediaItem getPreviousMediaItem(String mediaItemId) {
520         MovieMediaItem prevMediaItem = null;
521         for (MovieMediaItem mediaItem : mMediaItems) {
522             if (mediaItemId.equals(mediaItem.getId())) {
523                 break;
524             } else {
525                 prevMediaItem = mediaItem;
526             }
527         }
528 
529         return prevMediaItem;
530     }
531 
532     /**
533      * Find the next media item with the specified id
534      *
535      * @param mediaItemId The media item id
536      * @return The next media item
537      */
getNextMediaItem(String mediaItemId)538     public MovieMediaItem getNextMediaItem(String mediaItemId) {
539         boolean getNext = false;
540         final int count = mMediaItems.size();
541         for (int i = 0; i < count; i++) {
542             final MovieMediaItem mi = mMediaItems.get(i);
543             if (getNext) {
544                 return mi;
545             } else {
546                 if (mediaItemId.equals(mi.getId())) {
547                     getNext = true;
548                 }
549             }
550         }
551 
552         return null;
553     }
554 
555     /**
556      * Get the previous media item
557      *
558      * @param positionMs The current position in ms
559      * @return The previous media item
560      */
getPreviousMediaItem(long positionMs)561     public MovieMediaItem getPreviousMediaItem(long positionMs) {
562         long startTimeMs = 0;
563         MovieMediaItem prevMediaItem = null;
564         for (MovieMediaItem mediaItem : mMediaItems) {
565             if (positionMs == startTimeMs) {
566                 break;
567             } else if (positionMs > startTimeMs
568                     && positionMs < startTimeMs + mediaItem.getAppTimelineDuration()) {
569                 return mediaItem;
570             } else {
571                 prevMediaItem = mediaItem;
572             }
573 
574             startTimeMs += mediaItem.getAppTimelineDuration();
575             if (mediaItem.getEndTransition() != null) {
576                 startTimeMs -= mediaItem.getEndTransition().getAppDuration();
577             }
578         }
579 
580         return prevMediaItem;
581     }
582 
583     /**
584      * Get the next media item
585      *
586      * @param positionMs The current position in ms
587      * @return The next media item
588      */
getNextMediaItem(long positionMs)589     public MovieMediaItem getNextMediaItem(long positionMs) {
590         long startTimeMs = 0;
591         final int count = mMediaItems.size();
592         for (int i = 0; i < count; i++) {
593             final MovieMediaItem mediaItem = mMediaItems.get(i);
594             if (positionMs >= startTimeMs
595                     && positionMs < startTimeMs + mediaItem.getAppTimelineDuration() -
596                     getEndTransitionDuration(mediaItem)) {
597                 if (i < count - 1) {
598                     return mMediaItems.get(i + 1);
599                 } else {
600                     return null;
601                 }
602             } else if (positionMs >= startTimeMs
603                     && positionMs < startTimeMs + mediaItem.getAppTimelineDuration()) {
604                 if (i < count - 2) {
605                     return mMediaItems.get(i + 2);
606                 } else {
607                     return null;
608                 }
609             } else {
610                 startTimeMs += mediaItem.getAppTimelineDuration();
611                 startTimeMs -= getEndTransitionDuration(mediaItem);
612             }
613         }
614 
615         return null;
616     }
617 
618     /**
619      * Get the beginning media item of the specified transition
620      *
621      * @param transition The transition
622      *
623      * @return The media item
624      */
getPreviousMediaItem(MovieTransition transition)625     public MovieMediaItem getPreviousMediaItem(MovieTransition transition) {
626         final int count = mMediaItems.size();
627         for (int i = 0; i < count; i++) {
628             final MovieMediaItem mediaItem = mMediaItems.get(i);
629             if (i == 0) {
630                 if (mediaItem.getBeginTransition() == transition) {
631                     return null;
632                 }
633             }
634 
635             if (mediaItem.getEndTransition() == transition) {
636                 return mediaItem;
637             }
638         }
639 
640         return null;
641     }
642 
643     /**
644      * Return the end transition duration
645      *
646      * @param mediaItem The media item
647      * @return the end transition duration
648      */
getEndTransitionDuration(MovieMediaItem mediaItem)649     private static long getEndTransitionDuration(MovieMediaItem mediaItem) {
650         if (mediaItem.getEndTransition() != null) {
651             return mediaItem.getEndTransition().getAppDuration();
652         } else {
653             return 0;
654         }
655     }
656 
657     /**
658      * Determine the media item after which a new media item will be inserted.
659      *
660      * @param timeMs The inquiry position
661      *
662      * @return The media item after which the insertion will be performed
663      */
getInsertAfterMediaItem(long timeMs)664     public MovieMediaItem getInsertAfterMediaItem(long timeMs) {
665         long beginMs = 0;
666         long endMs = 0;
667         MovieMediaItem prevMediaItem = null;
668         final int mediaItemsCount = mMediaItems.size();
669         for (int i = 0; i < mediaItemsCount; i++) {
670             final MovieMediaItem mediaItem = mMediaItems.get(i);
671 
672             endMs = beginMs + mediaItem.getAppTimelineDuration();
673 
674             if (mediaItem.getEndTransition() != null) {
675                 if (i < mediaItemsCount - 1) {
676                     endMs -= mediaItem.getEndTransition().getAppDuration();
677                 }
678             }
679 
680             if (timeMs >= beginMs && timeMs <= endMs) {
681                 if (timeMs - beginMs < endMs - timeMs) { // Closer to the beginning
682                     return prevMediaItem;
683                 } else { // Closer to the end
684                     return mediaItem; // Insert after this item
685                 }
686             }
687 
688             beginMs = endMs;
689             prevMediaItem = mediaItem;
690         }
691 
692         return null;
693     }
694 
695     /**
696      * @return true if media items have different aspect ratios
697      */
hasMultipleAspectRatios()698     public boolean hasMultipleAspectRatios() {
699         int aspectRatio = MediaProperties.ASPECT_RATIO_UNDEFINED;
700         for (MovieMediaItem mediaItem : mMediaItems) {
701             if (aspectRatio == MediaProperties.ASPECT_RATIO_UNDEFINED) {
702                 aspectRatio = mediaItem.getAspectRatio();
703             } else if (mediaItem.getAspectRatio() != aspectRatio) {
704                 return true;
705             }
706         }
707 
708         return false;
709     }
710 
711     /**
712      * @return The list of unique aspect ratios
713      */
getUniqueAspectRatiosList()714     public ArrayList<Integer> getUniqueAspectRatiosList() {
715         final ArrayList<Integer> aspectRatiosList = new ArrayList<Integer>();
716         for (MovieMediaItem mediaItem : mMediaItems) {
717             int aspectRatio = mediaItem.getAspectRatio();
718             if (!aspectRatiosList.contains(aspectRatio)) {
719                 aspectRatiosList.add(aspectRatio);
720             }
721         }
722 
723         return aspectRatiosList;
724     }
725 
726     /**
727      * Add a new transition
728      *
729      * @param transition The transition
730      * @param afterMediaItemId Add the transition after this media item
731      */
addTransition(MovieTransition transition, String afterMediaItemId)732     void addTransition(MovieTransition transition, String afterMediaItemId) {
733         final int count = mMediaItems.size();
734         if (afterMediaItemId != null) {
735             MovieMediaItem afterMediaItem = null;
736             int afterMediaItemIndex = -1;
737             for (int i = 0; i < count; i++) {
738                 final MovieMediaItem mediaItem = mMediaItems.get(i);
739                 if (mediaItem.getId().equals(afterMediaItemId)) {
740                     afterMediaItem = mediaItem;
741                     afterMediaItemIndex = i;
742                     break;
743                 }
744             }
745 
746             // Link the transition to the next and previous media items
747             if (afterMediaItem == null) {
748                 throw new IllegalArgumentException("Media item not found: " + afterMediaItemId);
749             }
750 
751             afterMediaItem.setEndTransition(transition);
752 
753             if (afterMediaItemIndex < count - 1) {
754                 final MovieMediaItem beforeMediaItem = mMediaItems.get(afterMediaItemIndex + 1);
755                 beforeMediaItem.setBeginTransition(transition);
756             }
757         } else {
758             if (count == 0) {
759                 throw new IllegalArgumentException("Media item not found at the beginning");
760             }
761 
762             final MovieMediaItem beforeMediaItem = mMediaItems.get(0);
763             beforeMediaItem.setBeginTransition(transition);
764         }
765 
766         mClean = false;
767     }
768 
769     /**
770      * Remove the specified transition
771      *
772      * @param transitionId The transition id
773      */
removeTransition(String transitionId)774     void removeTransition(String transitionId) {
775         final int count = mMediaItems.size();
776         for (int i = 0; i < count; i++) {
777             final MovieMediaItem mediaItem = mMediaItems.get(i);
778             final MovieTransition beginTransition = mediaItem.getBeginTransition();
779             if (beginTransition != null && beginTransition.getId().equals(transitionId)) {
780                 mediaItem.setBeginTransition(null);
781                 break;
782             }
783 
784             final MovieTransition endTransition = mediaItem.getEndTransition();
785             if (endTransition != null && endTransition.getId().equals(transitionId)) {
786                 mediaItem.setEndTransition(null);
787             }
788         }
789 
790         mClean = false;
791     }
792 
793     /**
794      * Find the transition with the specified id
795      *
796      * @param transitionId The transition id
797      * @return The transition
798      */
getTransition(String transitionId)799     public MovieTransition getTransition(String transitionId) {
800         final MovieMediaItem firstMediaItem = getFirstMediaItem();
801         if (firstMediaItem == null) {
802             return null;
803         }
804 
805         final MovieTransition beginTransition = firstMediaItem.getBeginTransition();
806         if (beginTransition != null && beginTransition.getId().equals(transitionId)) {
807             return beginTransition;
808         }
809 
810         for (MovieMediaItem mediaItem : mMediaItems) {
811             final MovieTransition endTransition = mediaItem.getEndTransition();
812             if (endTransition != null && endTransition.getId().equals(transitionId)) {
813                 return endTransition;
814             }
815         }
816 
817         return null;
818     }
819 
820     /**
821      * Add the overlay
822      *
823      * @param mediaItemId The media item id
824      * @param overlay The overlay
825      */
addOverlay(String mediaItemId, MovieOverlay overlay)826     void addOverlay(String mediaItemId, MovieOverlay overlay) {
827         final MovieMediaItem mediaItem = getMediaItem(mediaItemId);
828 
829         // Remove an existing overlay (if any)
830         final MovieOverlay oldOverlay = mediaItem.getOverlay();
831         if (oldOverlay != null) {
832             mediaItem.removeOverlay(oldOverlay.getId());
833         }
834 
835         mediaItem.addOverlay(overlay);
836         mClean = false;
837     }
838 
839     /**
840      * Remove the specified overlay
841      *
842      * @param mediaItemId The media item id
843      * @param overlayId The overlay id
844      */
removeOverlay(String mediaItemId, String overlayId)845     void removeOverlay(String mediaItemId, String overlayId) {
846         final MovieMediaItem mediaItem = getMediaItem(mediaItemId);
847         mediaItem.removeOverlay(overlayId);
848         mClean = false;
849     }
850 
851     /**
852      * Get the specified overlay
853      *
854      * @param mediaItemId The media item id
855      * @param overlayId The overlay id
856      * @return The movie overlay
857      */
getOverlay(String mediaItemId, String overlayId)858     public MovieOverlay getOverlay(String mediaItemId, String overlayId) {
859         final MovieMediaItem mediaItem = getMediaItem(mediaItemId);
860         return mediaItem.getOverlay();
861     }
862 
863     /**
864      * Add the effect
865      *
866      * @param mediaItemId The media item id
867      * @param effect The effect
868      */
addEffect(String mediaItemId, MovieEffect effect)869     void addEffect(String mediaItemId, MovieEffect effect) {
870         final MovieMediaItem mediaItem = getMediaItem(mediaItemId);
871         // Remove an existing effect
872         final MovieEffect oldEffect = mediaItem.getEffect();
873         if (oldEffect != null) {
874             mediaItem.removeEffect(oldEffect.getId());
875         }
876 
877         mediaItem.addEffect(effect);
878         mClean = false;
879     }
880 
881     /**
882      * Remove the specified effect
883      *
884      * @param mediaItemId The media item id
885      * @param effectId The effect id
886      */
removeEffect(String mediaItemId, String effectId)887     void removeEffect(String mediaItemId, String effectId) {
888         final MovieMediaItem mediaItem = getMediaItem(mediaItemId);
889         mediaItem.removeEffect(effectId);
890         mClean = false;
891     }
892 
893     /**
894      * Get the specified effect
895      *
896      * @param mediaItemId The media item id
897      * @param effectId The effect id
898      * @return The movie effect
899      */
getEffect(String mediaItemId, String effectId)900     public MovieEffect getEffect(String mediaItemId, String effectId) {
901         final MovieMediaItem mediaItem = getMediaItem(mediaItemId);
902         return mediaItem.getEffect();
903     }
904 
905     /**
906      * Set the audio tracks
907      *
908      * @param audioTracks The audio tracks
909      */
setAudioTracks(List<MovieAudioTrack> audioTracks)910     void setAudioTracks(List<MovieAudioTrack> audioTracks) {
911         mAudioTracks = audioTracks;
912         mClean = false;
913     }
914 
915     /**
916      * Add an audio track
917      *
918      * @param audioTrack The audio track
919      */
addAudioTrack(MovieAudioTrack audioTrack)920     void addAudioTrack(MovieAudioTrack audioTrack) {
921         mAudioTracks.add(audioTrack);
922         mClean = false;
923     }
924 
925     /**
926      * Remove the specified audio track
927      *
928      * @param audioTrackId The audio track id
929      */
removeAudioTrack(String audioTrackId)930     void removeAudioTrack(String audioTrackId) {
931         final int count = mAudioTracks.size();
932         for (int i = 0; i < count; i++) {
933             final MovieAudioTrack audioTrack = mAudioTracks.get(i);
934             if (audioTrack.getId().equals(audioTrackId)) {
935                 mAudioTracks.remove(i);
936                 mClean = false;
937                 break;
938             }
939         }
940     }
941 
942     /**
943      * @return The audio tracks
944      */
getAudioTracks()945     public List<MovieAudioTrack> getAudioTracks() {
946         return mAudioTracks;
947     }
948 
949     /**
950      * @param audioTrackId The audio track id
951      * @return The audio track
952      */
getAudioTrack(String audioTrackId)953     public MovieAudioTrack getAudioTrack(String audioTrackId) {
954         for (MovieAudioTrack audioTrack : mAudioTracks) {
955             if (audioTrack.getId().equals(audioTrackId)) {
956                 return audioTrack;
957             }
958         }
959 
960         return null;
961     }
962 
963     /**
964      * Compute the begin time for this media item
965      *
966      * @param mediaItemId The media item id for which we compute the begin time
967      *
968      * @return The begin time for this media item
969      */
getMediaItemBeginTime(String mediaItemId)970     public long getMediaItemBeginTime(String mediaItemId) {
971         long beginMs = 0;
972         final int mediaItemsCount = mMediaItems.size();
973         for (int i = 0; i < mediaItemsCount; i++) {
974             final MovieMediaItem mi = mMediaItems.get(i);
975             if (mi.getId().equals(mediaItemId)) {
976                 break;
977             }
978 
979             beginMs += mi.getAppTimelineDuration();
980 
981             if (mi.getEndTransition() != null) {
982                 if (i < mediaItemsCount - 1) {
983                     beginMs -= mi.getEndTransition().getAppDuration();
984                 }
985             }
986         }
987 
988         return beginMs;
989     }
990 
991     /**
992      * @return The total duration
993      */
computeDuration()994     public long computeDuration() {
995         long totalDurationMs = 0;
996         final int mediaItemsCount = mMediaItems.size();
997         for (int i = 0; i < mediaItemsCount; i++) {
998             final MovieMediaItem mediaItem = mMediaItems.get(i);
999             totalDurationMs += mediaItem.getAppTimelineDuration();
1000 
1001             if (mediaItem.getEndTransition() != null) {
1002                 if (i < mediaItemsCount - 1) {
1003                     totalDurationMs -= mediaItem.getEndTransition().getAppDuration();
1004                 }
1005             }
1006         }
1007 
1008         return totalDurationMs;
1009     }
1010 
1011     /**
1012      * Render a frame according to the preview aspect ratio and activating all
1013      * storyboard items relative to the specified time.
1014      *
1015      * @param surfaceHolder SurfaceHolder used by the application
1016      * @param timeMs time corresponding to the frame to display
1017      * @param overlayData The overlay data
1018      *
1019      * @return The accurate time stamp of the frame that is rendered.
1020      * @throws IllegalStateException if a preview or an export is already in
1021      *             progress
1022      * @throws IllegalArgumentException if time is negative or beyond the
1023      *             preview duration
1024      */
renderPreviewFrame(SurfaceHolder surfaceHolder, long timeMs, VideoEditor.OverlayData overlayData)1025     public long renderPreviewFrame(SurfaceHolder surfaceHolder, long timeMs,
1026             VideoEditor.OverlayData overlayData) {
1027         if (mVideoEditor != null) {
1028             return mVideoEditor.renderPreviewFrame(surfaceHolder, timeMs, overlayData);
1029         } else {
1030             return 0;
1031         }
1032     }
1033 
1034     /**
1035      * Render a frame of a media item.
1036      *
1037      * @param surfaceHolder SurfaceHolder used by the application
1038      * @param mediaItemId The media item id
1039      * @param timeMs time corresponding to the frame to display
1040      *
1041      * @return The accurate time stamp of the frame that is rendered .
1042      * @throws IllegalStateException if a preview or an export is already in
1043      *             progress
1044      * @throws IllegalArgumentException if time is negative or beyond the
1045      *             preview duration
1046      */
renderMediaItemFrame(SurfaceHolder surfaceHolder, String mediaItemId, long timeMs)1047     public long renderMediaItemFrame(SurfaceHolder surfaceHolder, String mediaItemId,
1048             long timeMs) {
1049         if (mVideoEditor != null) {
1050             final MediaVideoItem mediaItem =
1051                 (MediaVideoItem)mVideoEditor.getMediaItem(mediaItemId);
1052             if (mediaItem != null) {
1053                 return mediaItem.renderFrame(surfaceHolder, timeMs);
1054             } else {
1055                 return -1;
1056             }
1057         } else {
1058             return 0;
1059         }
1060     }
1061 
1062     /**
1063      * Start the preview of all the storyboard items applied on all MediaItems
1064      * This method does not block (does not wait for the preview to complete).
1065      * The PreviewProgressListener allows to track the progress at the time
1066      * interval determined by the callbackAfterFrameCount parameter. The
1067      * SurfaceHolder has to be created and ready for use before calling this
1068      * method. The method is a no-op if there are no MediaItems in the
1069      * storyboard.
1070      *
1071      * @param surfaceHolder SurfaceHolder where the preview is rendered.
1072      * @param fromMs The time (relative to the timeline) at which the preview
1073      *            will start
1074      * @param toMs The time (relative to the timeline) at which the preview will
1075      *            stop. Use -1 to play to the end of the timeline
1076      * @param loop true if the preview should be looped once it reaches the end
1077      * @param callbackAfterFrameCount The listener interface should be invoked
1078      *            after the number of frames specified by this parameter.
1079      * @param listener The listener which will be notified of the preview
1080      *            progress
1081      *
1082      * @throws IllegalArgumentException if fromMs is beyond the preview duration
1083      * @throws IllegalStateException if a preview or an export is already in
1084      *             progress
1085      */
startPreview(SurfaceHolder surfaceHolder, long fromMs, long toMs, boolean loop, int callbackAfterFrameCount, PreviewProgressListener listener)1086     public void startPreview(SurfaceHolder surfaceHolder, long fromMs, long toMs, boolean loop,
1087             int callbackAfterFrameCount, PreviewProgressListener listener) {
1088         if (mVideoEditor != null) {
1089             mVideoEditor.startPreview(surfaceHolder, fromMs, toMs, loop, callbackAfterFrameCount,
1090                     listener);
1091         }
1092     }
1093 
1094     /**
1095      * Stop the current preview. This method blocks until ongoing preview is
1096      * stopped. Ignored if there is no preview running.
1097      *
1098      * @return The accurate current time when stop is effective expressed in
1099      *         milliseconds
1100      */
stopPreview()1101     public long stopPreview() {
1102         if (mVideoEditor != null) {
1103             return mVideoEditor.stopPreview();
1104         } else {
1105             return 0;
1106         }
1107     }
1108 
1109     /**
1110      * Clear the surface
1111      *
1112      * @param surfaceHolder SurfaceHolder where the preview is rendered.
1113      */
clearSurface(SurfaceHolder surfaceHolder)1114     public void clearSurface(SurfaceHolder surfaceHolder) {
1115         if (mVideoEditor != null) {
1116             mVideoEditor.clearSurface(surfaceHolder);
1117         }
1118     }
1119 
1120     /**
1121      * Release the project
1122      */
release()1123     public void release() {
1124     }
1125 
1126     /**
1127      * Add a new download to the project
1128      *
1129      * @param mediaUri The media URI
1130      * @param mimeType The mime type
1131      * @param filename The local filename
1132      */
addDownload(String mediaUri, String mimeType, String filename)1133     public void addDownload(String mediaUri, String mimeType, String filename) {
1134         mDownloads.add(new Download(mediaUri, mimeType, filename, System.currentTimeMillis()));
1135         mClean = false;
1136     }
1137 
1138     /**
1139      * Remove a download
1140      *
1141      * @param mediaUri The media URI
1142      */
removeDownload(String mediaUri)1143     public void removeDownload(String mediaUri) {
1144         final int count = mDownloads.size();
1145         for (int i = 0; i < count; i++) {
1146             final Download download = mDownloads.get(i);
1147             final String uri = download.getMediaUri();
1148             if (mediaUri.equals(uri)) {
1149                 // Delete the file associated with the download
1150                 final String filename = download.getFilename();
1151                 new File(filename).delete();
1152 
1153                 // Remove the download from the list
1154                 mDownloads.remove(i);
1155                 mClean = false;
1156                 break;
1157             }
1158         }
1159     }
1160 
1161     /**
1162      * @return The list of downloads
1163      */
getDownloads()1164     public List<Download> getDownloads() {
1165         return mDownloads;
1166     }
1167 
1168     /**
1169      * Load metadata from file
1170      *
1171      * @param videoEditor The video editor
1172      * @param projectPath The project path
1173      *
1174      * @return A new instance of the VideoEditorProject
1175      */
fromXml(VideoEditor videoEditor, String projectPath)1176     public static VideoEditorProject fromXml(VideoEditor videoEditor, String projectPath)
1177             throws XmlPullParserException, FileNotFoundException, IOException {
1178         final File file = new File(projectPath, PROJECT_METADATA_FILENAME);
1179         final FileInputStream fis = new FileInputStream(file);
1180         final List<Download> downloads = new ArrayList<Download>();
1181         try {
1182             // Load the metadata
1183             final XmlPullParser parser = Xml.newPullParser();
1184             parser.setInput(fis, "UTF-8");
1185             int eventType = parser.getEventType();
1186 
1187             String projectName = null;
1188             String themeId = null;
1189             Uri exportedMovieUri = null;
1190             long lastSaved = 0;
1191             long playheadPosMs = 0;
1192             long durationMs = 0;
1193             int zoomLevel = DEFAULT_ZOOM_LEVEL;
1194             while (eventType != XmlPullParser.END_DOCUMENT) {
1195                 String name = null;
1196                 switch (eventType) {
1197                     case XmlPullParser.START_TAG: {
1198                         name = parser.getName();
1199                         if (name.equalsIgnoreCase(TAG_PROJECT)) {
1200                             projectName = parser.getAttributeValue("", ATTR_NAME);
1201                             themeId = parser.getAttributeValue("", ATTR_THEME);
1202                             lastSaved = Long.parseLong(parser.getAttributeValue("", ATTR_SAVED));
1203                             playheadPosMs = Long.parseLong(parser.getAttributeValue("",
1204                                     ATTR_PLAYHEAD_POSITION));
1205                             durationMs = Long.parseLong(parser.getAttributeValue("",
1206                                     ATTR_DURATION));
1207                             zoomLevel = Integer.parseInt(parser.getAttributeValue("",
1208                                     ATTR_ZOOM_LEVEL));
1209                         } else if (name.equalsIgnoreCase(TAG_MOVIE)) {
1210                             exportedMovieUri = Uri.parse(parser.getAttributeValue("", ATTR_URI));
1211                         } else if (name.equalsIgnoreCase(TAG_DOWNLOAD)) {
1212                             downloads.add(new Download(parser.getAttributeValue("", ATTR_URI),
1213                                     parser.getAttributeValue("", ATTR_MIME),
1214                                     parser.getAttributeValue("", ATTR_FILENAME),
1215                                     Long.parseLong(parser.getAttributeValue("", ATTR_TIME))));
1216                         }
1217 
1218                         break;
1219                     }
1220 
1221                     default: {
1222                         break;
1223                     }
1224                 }
1225                 eventType = parser.next();
1226             }
1227 
1228             return new VideoEditorProject(videoEditor, projectPath, projectName, lastSaved,
1229                     playheadPosMs, durationMs, zoomLevel, exportedMovieUri, themeId, downloads);
1230         } finally {
1231             if (fis != null) {
1232                 fis.close();
1233             }
1234         }
1235     }
1236 
1237     /**
1238      * Save the content to XML
1239      */
saveToXml()1240     public void saveToXml() throws IOException {
1241         // Save the project metadata
1242         final XmlSerializer serializer = Xml.newSerializer();
1243         final StringWriter writer = new StringWriter();
1244         serializer.setOutput(writer);
1245         serializer.startDocument("UTF-8", true);
1246         serializer.startTag("", TAG_PROJECT);
1247         if (mProjectName != null) {
1248             serializer.attribute("", ATTR_NAME, mProjectName);
1249         }
1250         if (mTheme != null) {
1251             serializer.attribute("", ATTR_THEME, mTheme);
1252         }
1253 
1254         serializer.attribute("", ATTR_PLAYHEAD_POSITION, Long.toString(mPlayheadPosMs));
1255         serializer.attribute("", ATTR_DURATION, Long.toString(computeDuration()));
1256         serializer.attribute("", ATTR_ZOOM_LEVEL, Integer.toString(mZoomLevel));
1257 
1258         mLastSaved = System.currentTimeMillis();
1259         serializer.attribute("", ATTR_SAVED, Long.toString(mLastSaved));
1260         if (mExportedMovieUri != null) {
1261             serializer.startTag("", TAG_MOVIE);
1262             serializer.attribute("", ATTR_URI, mExportedMovieUri.toString());
1263             serializer.endTag("", TAG_MOVIE);
1264         }
1265 
1266         for (Download download : mDownloads) {
1267             serializer.startTag("", TAG_DOWNLOAD);
1268             serializer.attribute("", ATTR_URI, download.getMediaUri());
1269             serializer.attribute("", ATTR_MIME, download.getMimeType());
1270             serializer.attribute("", ATTR_FILENAME, download.getFilename());
1271             serializer.attribute("", ATTR_TIME, Long.toString(download.getTime()));
1272             serializer.endTag("", TAG_DOWNLOAD);
1273         }
1274         serializer.endTag("", TAG_PROJECT);
1275         serializer.endDocument();
1276 
1277         // Save the metadata XML file
1278         final FileOutputStream out = new FileOutputStream(new File(mVideoEditor.getPath(),
1279                 PROJECT_METADATA_FILENAME));
1280         out.write(writer.toString().getBytes("UTF-8"));
1281         out.flush();
1282         out.close();
1283     }
1284 }
1285