MediaVideoItem.java revision c4edeb5a60a45af874474ae7244c921a268430f1
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17
18package android.media.videoeditor;
19
20import java.io.File;
21import java.io.IOException;
22import java.lang.ref.SoftReference;
23import android.graphics.Bitmap;
24import android.media.videoeditor.MediaArtistNativeHelper.ClipSettings;
25import android.media.videoeditor.MediaArtistNativeHelper.Properties;
26import android.view.Surface;
27import android.view.SurfaceHolder;
28
29/**
30 * This class represents a video clip item on the storyboard
31 * {@hide}
32 */
33public class MediaVideoItem extends MediaItem {
34
35    /**
36     *  Instance variables
37     */
38    private final int mWidth;
39    private final int mHeight;
40    private final int mAspectRatio;
41    private final int mFileType;
42    private final int mVideoType;
43    private final int mVideoProfile;
44    private final int mVideoBitrate;
45    private final long mDurationMs;
46    private final int mAudioBitrate;
47    private final int mFps;
48    private final int mAudioType;
49    private final int mAudioChannels;
50    private final int mAudioSamplingFrequency;
51    private long mBeginBoundaryTimeMs;
52    private long mEndBoundaryTimeMs;
53    private int mVolumePercentage;
54    private boolean mMuted;
55    private String mAudioWaveformFilename;
56    private MediaArtistNativeHelper mMANativeHelper;
57    private VideoEditorImpl mVideoEditor;
58    /**
59     *  The audio waveform data
60     */
61    private SoftReference<WaveformData> mWaveformData;
62
63    /**
64     * An object of this type cannot be instantiated with a default constructor
65     */
66    @SuppressWarnings("unused")
67    private MediaVideoItem() throws IOException {
68        this(null, null, null, RENDERING_MODE_BLACK_BORDER);
69    }
70
71    /**
72     * Constructor
73     *
74     * @param editor The video editor reference
75     * @param mediaItemId The MediaItem id
76     * @param filename The image file name
77     * @param renderingMode The rendering mode
78     *
79     * @throws IOException if the file cannot be opened for reading
80     */
81    public MediaVideoItem(VideoEditor editor, String mediaItemId, String filename,
82            int renderingMode) throws IOException {
83        this(editor, mediaItemId, filename, renderingMode, 0, END_OF_FILE, 100, false, null);
84    }
85
86    /**
87     * Constructor
88     *
89     * @param editor The video editor reference
90     * @param mediaItemId The MediaItem id
91     * @param filename The image file name
92     * @param renderingMode The rendering mode
93     * @param beginMs Start time in milliseconds. Set to 0 to extract from the
94     *           beginning
95     * @param endMs End time in milliseconds. Set to {@link #END_OF_FILE} to
96     *           extract until the end
97     * @param volumePercent in %/. 100% means no change; 50% means half value, 200%
98     *            means double, 0% means silent.
99     * @param muted true if the audio is muted
100     * @param audioWaveformFilename The name of the audio waveform file
101     *
102     * @throws IOException if the file cannot be opened for reading
103     */
104    MediaVideoItem(VideoEditor editor, String mediaItemId, String filename,
105            int renderingMode, long beginMs, long endMs, int volumePercent, boolean muted,
106            String audioWaveformFilename)  throws IOException {
107        super(editor, mediaItemId, filename, renderingMode);
108
109        if (editor instanceof VideoEditorImpl) {
110            mMANativeHelper = ((VideoEditorImpl)editor).getNativeContext();
111            mVideoEditor = ((VideoEditorImpl)editor);
112        }
113
114        final Properties properties;
115        try {
116             properties = mMANativeHelper.getMediaProperties(filename);
117        } catch ( Exception e) {
118            throw new IllegalArgumentException("Unsupported file or file not found: " + filename);
119        }
120
121        switch (mMANativeHelper.getFileType(properties.fileType)) {
122            case MediaProperties.FILE_3GP:
123                break;
124            case MediaProperties.FILE_MP4:
125                break;
126
127            default:
128                throw new IllegalArgumentException("Unsupported Input File Type");
129        }
130
131        switch (mMANativeHelper.getVideoCodecType(properties.videoFormat)) {
132            case MediaProperties.VCODEC_H263:
133                break;
134            case MediaProperties.VCODEC_H264BP:
135                break;
136            case MediaProperties.VCODEC_H264MP:
137                break;
138            case MediaProperties.VCODEC_MPEG4:
139                break;
140
141            default:
142                throw new IllegalArgumentException("Unsupported Video Codec Format in Input File");
143        }
144
145        mWidth = properties.width;
146        mHeight = properties.height;
147        mAspectRatio = mMANativeHelper.getAspectRatio(properties.width,
148                properties.height);
149        mFileType = mMANativeHelper.getFileType(properties.fileType);
150        mVideoType = mMANativeHelper.getVideoCodecType(properties.videoFormat);
151        mVideoProfile = 0;
152        mDurationMs = properties.videoDuration;
153        mVideoBitrate = properties.videoBitrate;
154        mAudioBitrate = properties.audioBitrate;
155        mFps = (int)properties.averageFrameRate;
156        mAudioType = mMANativeHelper.getAudioCodecType(properties.audioFormat);
157        mAudioChannels = properties.audioChannels;
158        mAudioSamplingFrequency =  properties.audioSamplingFrequency;
159        mBeginBoundaryTimeMs = beginMs;
160        mEndBoundaryTimeMs = endMs == END_OF_FILE ? mDurationMs : endMs;
161        mVolumePercentage = volumePercent;
162        mMuted = muted;
163        mAudioWaveformFilename = audioWaveformFilename;
164        if (audioWaveformFilename != null) {
165            mWaveformData = new SoftReference<WaveformData>(
166                        new WaveformData(audioWaveformFilename));
167        } else {
168            mWaveformData = null;
169        }
170    }
171
172    /**
173     * Sets the start and end marks for trimming a video media item.
174     * This method will adjust the duration of bounding transitions, effects
175     * and overlays if the current duration of the transactions become greater
176     * than the maximum allowable duration.
177     *
178     * @param beginMs Start time in milliseconds. Set to 0 to extract from the
179     *           beginning
180     * @param endMs End time in milliseconds. Set to {@link #END_OF_FILE} to
181     *           extract until the end
182     *
183     * @throws IllegalArgumentException if the start time is greater or equal than
184     *           end time, the end time is beyond the file duration, the start time
185     *           is negative
186     */
187    public void setExtractBoundaries(long beginMs, long endMs) {
188        if (beginMs > mDurationMs) {
189            throw new IllegalArgumentException("setExtractBoundaries: Invalid start time");
190        }
191
192        if (endMs > mDurationMs) {
193            throw new IllegalArgumentException("setExtractBoundaries: Invalid end time");
194        }
195
196        if ((endMs != -1) && (beginMs >= endMs) ) {
197            throw new IllegalArgumentException("setExtractBoundaries: Start time is greater than end time");
198        }
199
200        if ((beginMs < 0) || ((endMs != -1) && (endMs < 0))) {
201            throw new IllegalArgumentException("setExtractBoundaries: Start time or end time is negative");
202        }
203
204        if (beginMs != mBeginBoundaryTimeMs) {
205            if (mBeginTransition != null) {
206                mBeginTransition.invalidate();
207            }
208        }
209
210        if (endMs != mEndBoundaryTimeMs) {
211            if (mEndTransition != null) {
212                mEndTransition.invalidate();
213            }
214        }
215
216        mBeginBoundaryTimeMs = beginMs;
217        mEndBoundaryTimeMs = endMs;
218        mMANativeHelper.setGeneratePreview(true);
219        adjustTransitions();
220        mVideoEditor.updateTimelineDuration();
221        /**
222         *  Note that the start and duration of any effects and overlays are
223         *  not adjusted nor are they automatically removed if they fall
224         *  outside the new boundaries.
225         */
226    }
227
228    /**
229     * @return The boundary begin time
230     */
231    public long getBoundaryBeginTime() {
232        return mBeginBoundaryTimeMs;
233    }
234
235    /**
236     * @return The boundary end time
237     */
238    public long getBoundaryEndTime() {
239        return mEndBoundaryTimeMs;
240    }
241
242    /*
243     * {@inheritDoc}
244     */
245    @Override
246    public void addEffect(Effect effect) {
247        if (effect instanceof EffectKenBurns) {
248            throw new IllegalArgumentException("Ken Burns effects cannot be applied to MediaVideoItem");
249        }
250        super.addEffect(effect);
251    }
252
253    /*
254     * {@inheritDoc}
255     */
256    @Override
257    public Bitmap getThumbnail(int width, int height, long timeMs) {
258        if (timeMs > mDurationMs) {
259            throw new IllegalArgumentException("Time Exceeds duration");
260        }
261
262        if (timeMs < 0) {
263            throw new IllegalArgumentException("Invalid Time duration");
264        }
265
266        if ((width <=0) || (height <= 0)) {
267            throw new IllegalArgumentException("Invalid Dimensions");
268        }
269
270        return mMANativeHelper.getPixels(super.getFilename(),
271                width, height,timeMs);
272    }
273
274    /*
275     * {@inheritDoc}
276     */
277    @Override
278    public Bitmap[] getThumbnailList(int width, int height, long startMs,
279            long endMs, int thumbnailCount) throws IOException {
280        if (startMs > endMs) {
281            throw new IllegalArgumentException("Start time is greater than end time");
282        }
283
284        if (endMs > mDurationMs) {
285            throw new IllegalArgumentException("End time is greater than file duration");
286        }
287
288        if ((height <= 0) || (width <= 0)) {
289            throw new IllegalArgumentException("Invalid dimension");
290        }
291
292        if (startMs == endMs) {
293            final Bitmap[] bitmap = new Bitmap[1];
294            bitmap[0] = mMANativeHelper.getPixels(super.getFilename(), width, height,startMs);
295            return bitmap;
296        }
297
298        return mMANativeHelper.getPixelsList(super.getFilename(), width,
299                height,startMs,endMs,thumbnailCount);
300    }
301
302    /*
303     * {@inheritDoc}
304     */
305    @Override
306    void invalidateTransitions(long startTimeMs, long durationMs) {
307        /**
308         *  Check if the item overlaps with the beginning and end transitions
309         */
310        if (mBeginTransition != null) {
311            if (isOverlapping(startTimeMs, durationMs,
312                    mBeginBoundaryTimeMs, mBeginTransition.getDuration())) {
313                mBeginTransition.invalidate();
314            }
315        }
316
317        if (mEndTransition != null) {
318            final long transitionDurationMs = mEndTransition.getDuration();
319            if (isOverlapping(startTimeMs, durationMs,
320                    mEndBoundaryTimeMs - transitionDurationMs, transitionDurationMs)) {
321                mEndTransition.invalidate();
322            }
323        }
324    }
325
326    /*
327     * {@inheritDoc}
328     */
329    @Override
330    void invalidateTransitions(long oldStartTimeMs, long oldDurationMs, long newStartTimeMs,
331            long newDurationMs) {
332        /**
333         *  Check if the item overlaps with the beginning and end transitions
334         */
335        if (mBeginTransition != null) {
336            final long transitionDurationMs = mBeginTransition.getDuration();
337            final boolean oldOverlap = isOverlapping(oldStartTimeMs, oldDurationMs,
338                    mBeginBoundaryTimeMs, transitionDurationMs);
339            final boolean newOverlap = isOverlapping(newStartTimeMs, newDurationMs,
340                    mBeginBoundaryTimeMs, transitionDurationMs);
341            /**
342             * Invalidate transition if:
343             *
344             * 1. New item overlaps the transition, the old one did not
345             * 2. New item does not overlap the transition, the old one did
346             * 3. New and old item overlap the transition if begin or end
347             * time changed
348             */
349            if (newOverlap != oldOverlap) { // Overlap has changed
350                mBeginTransition.invalidate();
351            } else if (newOverlap) { // Both old and new overlap
352                if ((oldStartTimeMs != newStartTimeMs) ||
353                        !(oldStartTimeMs + oldDurationMs > transitionDurationMs &&
354                        newStartTimeMs + newDurationMs > transitionDurationMs)) {
355                    mBeginTransition.invalidate();
356                }
357            }
358        }
359
360        if (mEndTransition != null) {
361            final long transitionDurationMs = mEndTransition.getDuration();
362            final boolean oldOverlap = isOverlapping(oldStartTimeMs, oldDurationMs,
363                    mEndBoundaryTimeMs - transitionDurationMs, transitionDurationMs);
364            final boolean newOverlap = isOverlapping(newStartTimeMs, newDurationMs,
365                    mEndBoundaryTimeMs - transitionDurationMs, transitionDurationMs);
366            /**
367             * Invalidate transition if:
368             *
369             * 1. New item overlaps the transition, the old one did not
370             * 2. New item does not overlap the transition, the old one did
371             * 3. New and old item overlap the transition if begin or end
372             * time changed
373             */
374            if (newOverlap != oldOverlap) { // Overlap has changed
375                mEndTransition.invalidate();
376            } else if (newOverlap) { // Both old and new overlap
377                if ((oldStartTimeMs + oldDurationMs != newStartTimeMs + newDurationMs) ||
378                        ((oldStartTimeMs > mEndBoundaryTimeMs - transitionDurationMs) ||
379                        newStartTimeMs > mEndBoundaryTimeMs - transitionDurationMs)) {
380                    mEndTransition.invalidate();
381                }
382            }
383        }
384    }
385
386    /*
387     * {@inheritDoc}
388     */
389    @Override
390    public int getAspectRatio() {
391        return mAspectRatio;
392    }
393
394    /*
395     * {@inheritDoc}
396     */
397    @Override
398    public int getFileType() {
399        return mFileType;
400    }
401
402    /*
403     * {@inheritDoc}
404     */
405    @Override
406    public int getWidth() {
407        return mWidth;
408    }
409
410    /*
411     * {@inheritDoc}
412     */
413    @Override
414    public int getHeight() {
415        return mHeight;
416    }
417
418    /*
419     * {@inheritDoc}
420     */
421    @Override
422    public long getDuration() {
423        return mDurationMs;
424    }
425
426    /*
427     * {@inheritDoc}
428     */
429    @Override
430    public long getTimelineDuration() {
431        return mEndBoundaryTimeMs - mBeginBoundaryTimeMs;
432    }
433
434    /**
435     * Render a frame according to the playback (in the native aspect ratio) for
436     * the specified media item. All effects and overlays applied to the media
437     * item are ignored. The extract boundaries are also ignored. This method
438     * can be used to playback frames when implementing trimming functionality.
439     *
440     * @param surfaceHolder SurfaceHolder used by the application
441     * @param timeMs time corresponding to the frame to display (relative to the
442     *            the beginning of the media item).
443     * @return The accurate time stamp of the frame that is rendered .
444     * @throws IllegalStateException if a playback, preview or an export is
445     *             already in progress
446     * @throws IllegalArgumentException if time is negative or greater than the
447     *             media item duration
448     */
449    public long renderFrame(SurfaceHolder surfaceHolder, long timeMs) {
450        if (surfaceHolder == null) {
451            throw new IllegalArgumentException("Surface Holder is null");
452        }
453
454        if (timeMs > mDurationMs || timeMs < 0) {
455            throw new IllegalArgumentException("requested time not correct");
456        }
457
458        final Surface surface = surfaceHolder.getSurface();
459        if (surface == null) {
460            throw new RuntimeException("Surface could not be retrieved from Surface holder");
461        }
462
463        if (mFilename != null) {
464            return mMANativeHelper.renderMediaItemPreviewFrame(surface,
465                    mFilename,timeMs,mWidth,mHeight);
466        } else {
467            return 0;
468        }
469    }
470
471
472    /**
473     * This API allows to generate a file containing the sample volume levels of
474     * the Audio track of this media item. This function may take significant
475     * time and is blocking. The file can be retrieved using
476     * getAudioWaveformFilename().
477     *
478     * @param listener The progress listener
479     *
480     * @throws IOException if the output file cannot be created
481     * @throws IllegalArgumentException if the mediaItem does not have a valid
482     *             Audio track
483     */
484    public void extractAudioWaveform(ExtractAudioWaveformProgressListener listener)
485        throws IOException {
486        int frameDuration = 0;
487        int sampleCount = 0;
488        final String projectPath = mMANativeHelper.getProjectPath();
489        /**
490         *  Waveform file does not exist
491         */
492        if (mAudioWaveformFilename == null ) {
493            /**
494             * Since audioWaveformFilename will not be supplied,it is  generated
495             */
496            String mAudioWaveFileName = null;
497
498            mAudioWaveFileName =
499                String.format(projectPath + "/" + "audioWaveformFile-"+ getId() + ".dat");
500            /**
501             * Logic to get frame duration = (no. of frames per sample * 1000)/
502             * sampling frequency
503             */
504            if (mMANativeHelper.getAudioCodecType(mAudioType) ==
505                MediaProperties.ACODEC_AMRNB ) {
506                frameDuration = (MediaProperties.SAMPLES_PER_FRAME_AMRNB*1000)/
507                MediaProperties.DEFAULT_SAMPLING_FREQUENCY;
508                sampleCount = MediaProperties.SAMPLES_PER_FRAME_AMRNB;
509            } else if (mMANativeHelper.getAudioCodecType(mAudioType) ==
510                MediaProperties.ACODEC_AMRWB ) {
511                frameDuration = (MediaProperties.SAMPLES_PER_FRAME_AMRWB * 1000)/
512                MediaProperties.DEFAULT_SAMPLING_FREQUENCY;
513                sampleCount = MediaProperties.SAMPLES_PER_FRAME_AMRWB;
514            } else if (mMANativeHelper.getAudioCodecType(mAudioType) ==
515                MediaProperties.ACODEC_AAC_LC ) {
516                frameDuration = (MediaProperties.SAMPLES_PER_FRAME_AAC * 1000)/
517                MediaProperties.DEFAULT_SAMPLING_FREQUENCY;
518                sampleCount = MediaProperties.SAMPLES_PER_FRAME_AAC;
519            }
520
521            mMANativeHelper.generateAudioGraph( getId(),
522                    mFilename,
523                    mAudioWaveFileName,
524                    frameDuration,
525                    MediaProperties.DEFAULT_CHANNEL_COUNT,
526                    sampleCount,
527                    listener,
528                    true);
529            /**
530             * Record the generated file name
531             */
532            mAudioWaveformFilename = mAudioWaveFileName;
533        }
534        mWaveformData =
535            new SoftReference<WaveformData>(new WaveformData(mAudioWaveformFilename));
536    }
537
538    /**
539     * Get the audio waveform file name if {@link #extractAudioWaveform()} was
540     * successful. The file format is as following:
541     * <ul>
542     *  <li>first 4 bytes provide the number of samples for each value, as big-endian signed</li>
543     *  <li>4 following bytes is the total number of values in the file, as big-endian signed</li>
544     *  <li>all values follow as bytes Name is unique.</li>
545     *</ul>
546     * @return the name of the file, null if the file has not been computed or
547     *         if there is no Audio track in the mediaItem
548     */
549    String getAudioWaveformFilename() {
550        return mAudioWaveformFilename;
551    }
552
553    /**
554     * Invalidate the AudioWaveform File
555     */
556    void invalidate() {
557        if (mAudioWaveformFilename != null) {
558            new File(mAudioWaveformFilename).delete();
559            mAudioWaveformFilename = null;
560        }
561    }
562
563    /**
564     * @return The waveform data
565     */
566    public WaveformData getWaveformData() throws IOException {
567        if (mWaveformData == null) {
568            return null;
569        }
570
571        WaveformData waveformData = mWaveformData.get();
572        if (waveformData != null) {
573            return waveformData;
574        } else if (mAudioWaveformFilename != null) {
575            try {
576                waveformData = new WaveformData(mAudioWaveformFilename);
577            } catch(IOException e) {
578                throw e;
579            }
580            mWaveformData = new SoftReference<WaveformData>(waveformData);
581            return waveformData;
582        } else {
583            return null;
584        }
585    }
586
587    /**
588     * Set volume of the Audio track of this mediaItem
589     *
590     * @param volumePercent in %/. 100% means no change; 50% means half value, 200%
591     *            means double, 0% means silent.
592     * @throws UsupportedOperationException if volume value is not supported
593     */
594    public void setVolume(int volumePercent) {
595        if ((volumePercent <0) || (volumePercent >100)) {
596            throw new IllegalArgumentException("Invalid volume");
597        }
598
599        mVolumePercentage = volumePercent;
600    }
601
602    /**
603     * Get the volume value of the audio track as percentage. Call of this
604     * method before calling setVolume will always return 100%
605     *
606     * @return the volume in percentage
607     */
608    public int getVolume() {
609        return mVolumePercentage;
610    }
611
612    /**
613     * @param muted true to mute the media item
614     */
615    public void setMute(boolean muted) {
616        mMuted = muted;
617        if (mBeginTransition != null) {
618            mBeginTransition.invalidate();
619        }
620        if (mEndTransition != null) {
621            mEndTransition.invalidate();
622        }
623        mMANativeHelper.setGeneratePreview(true);
624    }
625
626    /**
627     * @return true if the media item is muted
628     */
629    public boolean isMuted() {
630        return mMuted;
631    }
632
633    /**
634     * @return The video type
635     */
636    public int getVideoType() {
637        return mVideoType;
638    }
639
640    /**
641     * @return The video profile
642     */
643    public int getVideoProfile() {
644        return mVideoProfile;
645    }
646
647    /**
648     * @return The video bitrate
649     */
650    public int getVideoBitrate() {
651        return mVideoBitrate;
652    }
653
654    /**
655     * @return The audio bitrate
656     */
657    public int getAudioBitrate() {
658        return mAudioBitrate;
659    }
660
661    /**
662     * @return The number of frames per second
663     */
664    public int getFps() {
665        return mFps;
666    }
667
668    /**
669     * @return The audio codec
670     */
671    public int getAudioType() {
672        return mAudioType;
673    }
674
675    /**
676     * @return The number of audio channels
677     */
678    public int getAudioChannels() {
679        return mAudioChannels;
680    }
681
682    /**
683     * @return The audio sample frequency
684     */
685    public int getAudioSamplingFrequency() {
686        return mAudioSamplingFrequency;
687    }
688
689    /**
690     * @return The Video media item properties in ClipSettings class object
691     * {@link android.media.videoeditor.MediaArtistNativeHelper.ClipSettings}
692     */
693    ClipSettings getVideoClipProperties() {
694        ClipSettings clipSettings = new ClipSettings();
695        clipSettings.clipPath = getFilename();
696        clipSettings.fileType = mMANativeHelper.getMediaItemFileType(getFileType());
697        clipSettings.beginCutTime = (int)getBoundaryBeginTime();
698        clipSettings.endCutTime = (int)getBoundaryEndTime();
699        clipSettings.mediaRendering = mMANativeHelper.getMediaItemRenderingMode(getRenderingMode());
700
701        return clipSettings;
702    }
703}
704