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