MediaVideoItem.java revision 9bcedf7cf3e9c981837f2d8ec98cd118efad3f01
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,
82            String filename,
83            int renderingMode)
84    throws IOException {
85        this(editor, mediaItemId, filename, renderingMode, 0, END_OF_FILE,
86                100, false, null);
87    }
88
89    /**
90     * Constructor
91     *
92     * @param editor The video editor reference
93     * @param mediaItemId The MediaItem id
94     * @param filename The image file name
95     * @param renderingMode The rendering mode
96     * @param beginMs Start time in milliseconds. Set to 0 to extract from the
97     *           beginning
98     * @param endMs End time in milliseconds. Set to {@link #END_OF_FILE} to
99     *           extract until the end
100     * @param volumePercent in %/. 100% means no change; 50% means half value, 200%
101     *            means double, 0% means silent.
102     * @param muted true if the audio is muted
103     * @param audioWaveformFilename The name of the audio waveform file
104     *
105     * @throws IOException if the file cannot be opened for reading
106     */
107    MediaVideoItem(VideoEditor editor, String mediaItemId, String filename,
108            int renderingMode,
109            long beginMs, long endMs, int volumePercent, boolean muted,
110            String audioWaveformFilename)  throws IOException {
111        super(editor, mediaItemId, filename, renderingMode);
112        if (editor instanceof VideoEditorImpl) {
113            mMANativeHelper = ((VideoEditorImpl)editor).getNativeContext();
114            mVideoEditor = ((VideoEditorImpl)editor);
115        }
116        Properties properties = null;
117        try {
118             properties = mMANativeHelper.getMediaProperties(filename);
119        } catch ( Exception e) {
120            throw new IllegalArgumentException("Unsupported file or file not found");
121        }
122        switch (mMANativeHelper.getFileType(properties.fileType)) {
123            case MediaProperties.FILE_3GP:
124                break;
125            case MediaProperties.FILE_MP4:
126                break;
127
128            default:
129                throw new IllegalArgumentException("Unsupported Input File Type");
130        }
131
132        switch (mMANativeHelper.getVideoCodecType(properties.videoFormat)) {
133            case MediaProperties.VCODEC_H263:
134                break;
135            case MediaProperties.VCODEC_H264BP:
136                break;
137            case MediaProperties.VCODEC_H264MP:
138                break;
139            case MediaProperties.VCODEC_MPEG4:
140                break;
141
142            default:
143                throw new IllegalArgumentException("Unsupported Video Codec Format in Input File");
144        }
145
146        mWidth = properties.width;
147        mHeight = properties.height;
148        mAspectRatio = mMANativeHelper.getAspectRatio(properties.width,
149                properties.height);
150        mFileType = mMANativeHelper.getFileType(properties.fileType);
151        mVideoType = mMANativeHelper.getVideoCodecType(properties.videoFormat);
152        mVideoProfile = 0;
153        mDurationMs = properties.videoDuration;
154        mVideoBitrate = properties.videoBitrate;
155        mAudioBitrate = properties.audioBitrate;
156        mFps = (int)properties.averageFrameRate;
157        mAudioType = mMANativeHelper.getAudioCodecType(properties.audioFormat);
158        mAudioChannels = properties.audioChannels;
159        mAudioSamplingFrequency =  properties.audioSamplingFrequency;
160        mBeginBoundaryTimeMs = beginMs;
161        mEndBoundaryTimeMs = endMs == END_OF_FILE ? mDurationMs : endMs;
162        mVolumePercentage = volumePercent;
163        mMuted = muted;
164        mAudioWaveformFilename = audioWaveformFilename;
165        if (audioWaveformFilename != null) {
166            mWaveformData =
167                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        if (endMs > mDurationMs) {
194            throw new IllegalArgumentException("setExtractBoundaries: Invalid end time");
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        {
260            throw new IllegalArgumentException("Time Exceeds duration");
261        }
262        if (timeMs < 0)
263        {
264            throw new IllegalArgumentException("Invalid Time duration");
265        }
266        if ((width <=0) || (height <= 0))
267        {
268            throw new IllegalArgumentException("Invalid Dimensions");
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        if (endMs > mDurationMs) {
284            throw new IllegalArgumentException("End time is greater than file duration");
285        }
286        if ((height <= 0) || (width <= 0)) {
287            throw new IllegalArgumentException("Invalid dimension");
288        }
289        if (startMs == endMs) {
290            Bitmap[] bitmap = new Bitmap[1];
291            bitmap[0] = mMANativeHelper.getPixels(super.getFilename(),
292                    width, height,startMs);
293            return bitmap;
294        }
295        return mMANativeHelper.getPixelsList(super.getFilename(), width,
296                height,startMs,endMs,thumbnailCount);
297    }
298
299    /*
300     * {@inheritDoc}
301     */
302    @Override
303    void invalidateTransitions(long startTimeMs, long durationMs) {
304        /**
305         *  Check if the item overlaps with the beginning and end transitions
306         */
307        if (mBeginTransition != null) {
308            if (isOverlapping(startTimeMs, durationMs,
309                    mBeginBoundaryTimeMs, mBeginTransition.getDuration())) {
310                mBeginTransition.invalidate();
311            }
312        }
313
314        if (mEndTransition != null) {
315            final long transitionDurationMs = mEndTransition.getDuration();
316            if (isOverlapping(startTimeMs, durationMs,
317                    mEndBoundaryTimeMs - transitionDurationMs, transitionDurationMs)) {
318                mEndTransition.invalidate();
319            }
320        }
321    }
322
323    /*
324     * {@inheritDoc}
325     */
326    @Override
327    void invalidateTransitions(long oldStartTimeMs, long oldDurationMs,
328            long newStartTimeMs,
329            long newDurationMs) {
330        /**
331         *  Check if the item overlaps with the beginning and end transitions
332         */
333        if (mBeginTransition != null) {
334            final long transitionDurationMs = mBeginTransition.getDuration();
335            /**
336             *  If the start time has changed and if the old or the new item
337             *  overlaps with the begin transition, invalidate the transition.
338             */
339            if (((oldStartTimeMs != newStartTimeMs)
340                    || (oldDurationMs != newDurationMs) )&&
341                    (isOverlapping(oldStartTimeMs, oldDurationMs,
342                            mBeginBoundaryTimeMs, transitionDurationMs) ||
343                            isOverlapping(newStartTimeMs, newDurationMs,
344                                    mBeginBoundaryTimeMs, transitionDurationMs))) {
345                mBeginTransition.invalidate();
346            }
347        }
348
349        if (mEndTransition != null) {
350            final long transitionDurationMs = mEndTransition.getDuration();
351            /**
352             *  If the start time + duration has changed and if the old or the new
353             *  item overlaps the end transition, invalidate the transition
354             */
355            if (oldStartTimeMs + oldDurationMs != newStartTimeMs + newDurationMs &&
356                    (isOverlapping(oldStartTimeMs, oldDurationMs,
357                            mEndBoundaryTimeMs - transitionDurationMs, transitionDurationMs) ||
358                            isOverlapping(newStartTimeMs, newDurationMs,
359                                    mEndBoundaryTimeMs - transitionDurationMs, transitionDurationMs))) {
360                mEndTransition.invalidate();
361            }
362        }
363    }
364
365    /*
366     * {@inheritDoc}
367     */
368    @Override
369    public int getAspectRatio() {
370        return mAspectRatio;
371    }
372
373    /*
374     * {@inheritDoc}
375     */
376    @Override
377    public int getFileType() {
378        return mFileType;
379    }
380
381    /*
382     * {@inheritDoc}
383     */
384    @Override
385    public int getWidth() {
386        return mWidth;
387    }
388
389    /*
390     * {@inheritDoc}
391     */
392    @Override
393    public int getHeight() {
394        return mHeight;
395    }
396
397    /*
398     * {@inheritDoc}
399     */
400    @Override
401    public long getDuration() {
402        return mDurationMs;
403    }
404
405    /*
406     * {@inheritDoc}
407     */
408    @Override
409    public long getTimelineDuration() {
410        return mEndBoundaryTimeMs - mBeginBoundaryTimeMs;
411    }
412
413    /**
414     * Render a frame according to the playback (in the native aspect ratio) for
415     * the specified media item. All effects and overlays applied to the media
416     * item are ignored. The extract boundaries are also ignored. This method
417     * can be used to playback frames when implementing trimming functionality.
418     *
419     * @param surfaceHolder SurfaceHolder used by the application
420     * @param timeMs time corresponding to the frame to display (relative to the
421     *            the beginning of the media item).
422     * @return The accurate time stamp of the frame that is rendered .
423     * @throws IllegalStateException if a playback, preview or an export is
424     *             already in progress
425     * @throws IllegalArgumentException if time is negative or greater than the
426     *             media item duration
427     */
428    public long renderFrame(SurfaceHolder surfaceHolder, long timeMs) {
429        if (surfaceHolder == null) {
430            throw new IllegalArgumentException("Surface Holder is null");
431        }
432
433        if (timeMs > mDurationMs || timeMs < 0) {
434            throw new IllegalArgumentException("requested time not correct");
435        }
436
437        Surface surface = surfaceHolder.getSurface();
438        if (surface == null) {
439            throw new RuntimeException("Surface could not be retrieved from Surface holder");
440        }
441
442        if (mFilename != null) {
443            return mMANativeHelper.renderMediaItemPreviewFrame(surface,
444                    mFilename,timeMs,mWidth,mHeight);
445        }
446        else {
447            return 0;
448        }
449    }
450
451
452    /**
453     * This API allows to generate a file containing the sample volume levels of
454     * the Audio track of this media item. This function may take significant
455     * time and is blocking. The file can be retrieved using
456     * getAudioWaveformFilename().
457     *
458     * @param listener The progress listener
459     *
460     * @throws IOException if the output file cannot be created
461     * @throws IllegalArgumentException if the mediaItem does not have a valid
462     *             Audio track
463     */
464    public void extractAudioWaveform(ExtractAudioWaveformProgressListener listener)
465    throws IOException {
466        int frameDuration = 0;
467        int sampleCount = 0;
468        final String projectPath = mMANativeHelper.getProjectPath();
469        /**
470         *  Waveform file does not exist
471         */
472        if (mAudioWaveformFilename == null ) {
473            /**
474             * Since audioWaveformFilename will not be supplied,it is  generated
475             */
476            String mAudioWaveFileName = null;
477
478            mAudioWaveFileName =
479                String.format(projectPath + "/" + "audioWaveformFile-"+ getId() + ".dat");
480            /**
481             * Logic to get frame duration = (no. of frames per sample * 1000)/
482             * sampling frequency
483             */
484            if ( mMANativeHelper.getAudioCodecType(mAudioType) ==
485                MediaProperties.ACODEC_AMRNB ) {
486                frameDuration = (MediaProperties.SAMPLES_PER_FRAME_AMRNB*1000)/
487                MediaProperties.DEFAULT_SAMPLING_FREQUENCY;
488                sampleCount = MediaProperties.SAMPLES_PER_FRAME_AMRNB;
489            }
490            else if ( mMANativeHelper.getAudioCodecType(mAudioType) ==
491                MediaProperties.ACODEC_AMRWB ) {
492                frameDuration = (MediaProperties.SAMPLES_PER_FRAME_AMRWB * 1000)/
493                MediaProperties.DEFAULT_SAMPLING_FREQUENCY;
494                sampleCount = MediaProperties.SAMPLES_PER_FRAME_AMRWB;
495            }
496            else if ( mMANativeHelper.getAudioCodecType(mAudioType) ==
497                MediaProperties.ACODEC_AAC_LC ) {
498                frameDuration = (MediaProperties.SAMPLES_PER_FRAME_AAC * 1000)/
499                MediaProperties.DEFAULT_SAMPLING_FREQUENCY;
500                sampleCount = MediaProperties.SAMPLES_PER_FRAME_AAC;
501            }
502
503            mMANativeHelper.generateAudioGraph( getId(),
504                    mFilename,
505                    mAudioWaveFileName,
506                    frameDuration,
507                    MediaProperties.DEFAULT_CHANNEL_COUNT,
508                    sampleCount,
509                    listener,
510                    true);
511            /**
512             * Record the generated file name
513             */
514            mAudioWaveformFilename = mAudioWaveFileName;
515        }
516        mWaveformData =
517            new SoftReference<WaveformData>(new WaveformData(mAudioWaveformFilename));
518    }
519
520    /**
521     * Get the audio waveform file name if {@link #extractAudioWaveform()} was
522     * successful. The file format is as following:
523     * <ul>
524     *  <li>first 4 bytes provide the number of samples for each value, as big-endian signed</li>
525     *  <li>4 following bytes is the total number of values in the file, as big-endian signed</li>
526     *  <li>all values follow as bytes Name is unique.</li>
527     *</ul>
528     * @return the name of the file, null if the file has not been computed or
529     *         if there is no Audio track in the mediaItem
530     */
531    String getAudioWaveformFilename() {
532        return mAudioWaveformFilename;
533    }
534
535    /**
536     * Invalidate the AudioWaveform File
537     */
538    void invalidate() {
539        if (mAudioWaveformFilename != null) {
540            new File(mAudioWaveformFilename).delete();
541            mAudioWaveformFilename = null;
542        }
543    }
544
545    /**
546     * @return The waveform data
547     */
548    public WaveformData getWaveformData() throws IOException {
549        if (mWaveformData == null) {
550            return null;
551        }
552
553        WaveformData waveformData = mWaveformData.get();
554        if (waveformData != null) {
555            return waveformData;
556        } else if (mAudioWaveformFilename != null) {
557            try {
558                waveformData = new WaveformData(mAudioWaveformFilename);
559            } catch(IOException e) {
560                throw e;
561            }
562            mWaveformData = new SoftReference<WaveformData>(waveformData);
563            return waveformData;
564        } else {
565            return null;
566        }
567    }
568
569    /**
570     * Set volume of the Audio track of this mediaItem
571     *
572     * @param volumePercent in %/. 100% means no change; 50% means half value, 200%
573     *            means double, 0% means silent.
574     * @throws UsupportedOperationException if volume value is not supported
575     */
576    public void setVolume(int volumePercent) {
577        if ((volumePercent <0) || (volumePercent >100)) {
578            throw new IllegalArgumentException("Invalid volume");
579        }
580
581        mVolumePercentage = volumePercent;
582    }
583
584    /**
585     * Get the volume value of the audio track as percentage. Call of this
586     * method before calling setVolume will always return 100%
587     *
588     * @return the volume in percentage
589     */
590    public int getVolume() {
591        return mVolumePercentage;
592    }
593
594    /**
595     * @param muted true to mute the media item
596     */
597    public void setMute(boolean muted) {
598        mMuted = muted;
599        if (mBeginTransition != null) {
600            mBeginTransition.invalidate();
601        }
602        if (mEndTransition != null) {
603            mEndTransition.invalidate();
604        }
605        mMANativeHelper.setGeneratePreview(true);
606    }
607
608    /**
609     * @return true if the media item is muted
610     */
611    public boolean isMuted() {
612        return mMuted;
613    }
614
615    /**
616     * @return The video type
617     */
618    public int getVideoType() {
619        return mVideoType;
620    }
621
622    /**
623     * @return The video profile
624     */
625    public int getVideoProfile() {
626        return mVideoProfile;
627    }
628
629    /**
630     * @return The video bitrate
631     */
632    public int getVideoBitrate() {
633        return mVideoBitrate;
634    }
635
636    /**
637     * @return The audio bitrate
638     */
639    public int getAudioBitrate() {
640        return mAudioBitrate;
641    }
642
643    /**
644     * @return The number of frames per second
645     */
646    public int getFps() {
647        return mFps;
648    }
649
650    /**
651     * @return The audio codec
652     */
653    public int getAudioType() {
654        return mAudioType;
655    }
656
657    /**
658     * @return The number of audio channels
659     */
660    public int getAudioChannels() {
661        return mAudioChannels;
662    }
663
664    /**
665     * @return The audio sample frequency
666     */
667    public int getAudioSamplingFrequency() {
668        return mAudioSamplingFrequency;
669    }
670
671    /**
672     * @return The Video media item properties in ClipSettings class object
673     * {@link android.media.videoeditor.MediaArtistNativeHelper.ClipSettings}
674     */
675    ClipSettings getVideoClipProperties() {
676        ClipSettings clipSettings = new ClipSettings();
677        clipSettings.clipPath = getFilename();
678        clipSettings.fileType = mMANativeHelper.getMediaItemFileType(getFileType());
679        clipSettings.beginCutTime = (int)getBoundaryBeginTime();
680        clipSettings.endCutTime = (int)getBoundaryEndTime();
681        clipSettings.mediaRendering = mMANativeHelper.getMediaItemRenderingMode(getRenderingMode());
682
683        return clipSettings;
684    }
685
686}
687