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