MediaVideoItem.java revision 600acf14ff12eaf139f0ac644fb7e17849af65fa
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 Bitmap[] getThumbnailList(int width, int height, long startMs,
297            long endMs, int thumbnailCount) throws IOException {
298        if (startMs > endMs) {
299            throw new IllegalArgumentException("Start time is greater than end time");
300        }
301
302        if (endMs > mDurationMs) {
303            throw new IllegalArgumentException("End time is greater than file duration");
304        }
305
306        if ((height <= 0) || (width <= 0)) {
307            throw new IllegalArgumentException("Invalid dimension");
308        }
309
310        if (startMs == endMs) {
311            final Bitmap[] bitmap = new Bitmap[1];
312            bitmap[0] = mMANativeHelper.getPixels(super.getFilename(), width, height,startMs);
313            return bitmap;
314        }
315
316        return mMANativeHelper.getPixelsList(super.getFilename(), width,
317                height,startMs,endMs,thumbnailCount);
318    }
319
320    /*
321     * {@inheritDoc}
322     */
323    @Override
324    void invalidateTransitions(long startTimeMs, long durationMs) {
325        /**
326         *  Check if the item overlaps with the beginning and end transitions
327         */
328        if (mBeginTransition != null) {
329            if (isOverlapping(startTimeMs, durationMs,
330                    mBeginBoundaryTimeMs, mBeginTransition.getDuration())) {
331                mBeginTransition.invalidate();
332            }
333        }
334
335        if (mEndTransition != null) {
336            final long transitionDurationMs = mEndTransition.getDuration();
337            if (isOverlapping(startTimeMs, durationMs,
338                    mEndBoundaryTimeMs - transitionDurationMs, transitionDurationMs)) {
339                mEndTransition.invalidate();
340            }
341        }
342    }
343
344    /*
345     * {@inheritDoc}
346     */
347    @Override
348    void invalidateTransitions(long oldStartTimeMs, long oldDurationMs, long newStartTimeMs,
349            long newDurationMs) {
350        /**
351         *  Check if the item overlaps with the beginning and end transitions
352         */
353        if (mBeginTransition != null) {
354            final long transitionDurationMs = mBeginTransition.getDuration();
355            final boolean oldOverlap = isOverlapping(oldStartTimeMs, oldDurationMs,
356                    mBeginBoundaryTimeMs, transitionDurationMs);
357            final boolean newOverlap = isOverlapping(newStartTimeMs, newDurationMs,
358                    mBeginBoundaryTimeMs, transitionDurationMs);
359            /**
360             * Invalidate transition if:
361             *
362             * 1. New item overlaps the transition, the old one did not
363             * 2. New item does not overlap the transition, the old one did
364             * 3. New and old item overlap the transition if begin or end
365             * time changed
366             */
367            if (newOverlap != oldOverlap) { // Overlap has changed
368                mBeginTransition.invalidate();
369            } else if (newOverlap) { // Both old and new overlap
370                if ((oldStartTimeMs != newStartTimeMs) ||
371                        !(oldStartTimeMs + oldDurationMs > transitionDurationMs &&
372                        newStartTimeMs + newDurationMs > transitionDurationMs)) {
373                    mBeginTransition.invalidate();
374                }
375            }
376        }
377
378        if (mEndTransition != null) {
379            final long transitionDurationMs = mEndTransition.getDuration();
380            final boolean oldOverlap = isOverlapping(oldStartTimeMs, oldDurationMs,
381                    mEndBoundaryTimeMs - transitionDurationMs, transitionDurationMs);
382            final boolean newOverlap = isOverlapping(newStartTimeMs, newDurationMs,
383                    mEndBoundaryTimeMs - transitionDurationMs, transitionDurationMs);
384            /**
385             * Invalidate transition if:
386             *
387             * 1. New item overlaps the transition, the old one did not
388             * 2. New item does not overlap the transition, the old one did
389             * 3. New and old item overlap the transition if begin or end
390             * time changed
391             */
392            if (newOverlap != oldOverlap) { // Overlap has changed
393                mEndTransition.invalidate();
394            } else if (newOverlap) { // Both old and new overlap
395                if ((oldStartTimeMs + oldDurationMs != newStartTimeMs + newDurationMs) ||
396                        ((oldStartTimeMs > mEndBoundaryTimeMs - transitionDurationMs) ||
397                        newStartTimeMs > mEndBoundaryTimeMs - transitionDurationMs)) {
398                    mEndTransition.invalidate();
399                }
400            }
401        }
402    }
403
404    /*
405     * {@inheritDoc}
406     */
407    @Override
408    public int getAspectRatio() {
409        return mAspectRatio;
410    }
411
412    /*
413     * {@inheritDoc}
414     */
415    @Override
416    public int getFileType() {
417        return mFileType;
418    }
419
420    /*
421     * {@inheritDoc}
422     */
423    @Override
424    public int getWidth() {
425        return mWidth;
426    }
427
428    /*
429     * {@inheritDoc}
430     */
431    @Override
432    public int getHeight() {
433        return mHeight;
434    }
435
436    /*
437     * {@inheritDoc}
438     */
439    @Override
440    public long getDuration() {
441        return mDurationMs;
442    }
443
444    /*
445     * {@inheritDoc}
446     */
447    @Override
448    public long getTimelineDuration() {
449        return mEndBoundaryTimeMs - mBeginBoundaryTimeMs;
450    }
451
452    /**
453     * Render a frame according to the playback (in the native aspect ratio) for
454     * the specified media item. All effects and overlays applied to the media
455     * item are ignored. The extract boundaries are also ignored. This method
456     * can be used to playback frames when implementing trimming functionality.
457     *
458     * @param surfaceHolder SurfaceHolder used by the application
459     * @param timeMs time corresponding to the frame to display (relative to the
460     *            the beginning of the media item).
461     * @return The accurate time stamp of the frame that is rendered .
462     * @throws IllegalStateException if a playback, preview or an export is
463     *             already in progress
464     * @throws IllegalArgumentException if time is negative or greater than the
465     *             media item duration
466     */
467    public long renderFrame(SurfaceHolder surfaceHolder, long timeMs) {
468        if (surfaceHolder == null) {
469            throw new IllegalArgumentException("Surface Holder is null");
470        }
471
472        if (timeMs > mDurationMs || timeMs < 0) {
473            throw new IllegalArgumentException("requested time not correct");
474        }
475
476        final Surface surface = surfaceHolder.getSurface();
477        if (surface == null) {
478            throw new RuntimeException("Surface could not be retrieved from Surface holder");
479        }
480
481        if (mFilename != null) {
482            return mMANativeHelper.renderMediaItemPreviewFrame(surface,
483                    mFilename,timeMs,mWidth,mHeight);
484        } else {
485            return 0;
486        }
487    }
488
489
490    /**
491     * This API allows to generate a file containing the sample volume levels of
492     * the Audio track of this media item. This function may take significant
493     * time and is blocking. The file can be retrieved using
494     * getAudioWaveformFilename().
495     *
496     * @param listener The progress listener
497     *
498     * @throws IOException if the output file cannot be created
499     * @throws IllegalArgumentException if the mediaItem does not have a valid
500     *             Audio track
501     */
502    public void extractAudioWaveform(ExtractAudioWaveformProgressListener listener)
503        throws IOException {
504        int frameDuration = 0;
505        int sampleCount = 0;
506        final String projectPath = mMANativeHelper.getProjectPath();
507        /**
508         *  Waveform file does not exist
509         */
510        if (mAudioWaveformFilename == null ) {
511            /**
512             * Since audioWaveformFilename will not be supplied,it is  generated
513             */
514            String mAudioWaveFileName = null;
515
516            mAudioWaveFileName =
517                String.format(projectPath + "/" + "audioWaveformFile-"+ getId() + ".dat");
518            /**
519             * Logic to get frame duration = (no. of frames per sample * 1000)/
520             * sampling frequency
521             */
522            if (mMANativeHelper.getAudioCodecType(mAudioType) ==
523                MediaProperties.ACODEC_AMRNB ) {
524                frameDuration = (MediaProperties.SAMPLES_PER_FRAME_AMRNB*1000)/
525                MediaProperties.DEFAULT_SAMPLING_FREQUENCY;
526                sampleCount = MediaProperties.SAMPLES_PER_FRAME_AMRNB;
527            } else if (mMANativeHelper.getAudioCodecType(mAudioType) ==
528                MediaProperties.ACODEC_AMRWB ) {
529                frameDuration = (MediaProperties.SAMPLES_PER_FRAME_AMRWB * 1000)/
530                MediaProperties.DEFAULT_SAMPLING_FREQUENCY;
531                sampleCount = MediaProperties.SAMPLES_PER_FRAME_AMRWB;
532            } else if (mMANativeHelper.getAudioCodecType(mAudioType) ==
533                MediaProperties.ACODEC_AAC_LC ) {
534                frameDuration = (MediaProperties.SAMPLES_PER_FRAME_AAC * 1000)/
535                MediaProperties.DEFAULT_SAMPLING_FREQUENCY;
536                sampleCount = MediaProperties.SAMPLES_PER_FRAME_AAC;
537            }
538
539            mMANativeHelper.generateAudioGraph( getId(),
540                    mFilename,
541                    mAudioWaveFileName,
542                    frameDuration,
543                    MediaProperties.DEFAULT_CHANNEL_COUNT,
544                    sampleCount,
545                    listener,
546                    true);
547            /**
548             * Record the generated file name
549             */
550            mAudioWaveformFilename = mAudioWaveFileName;
551        }
552        mWaveformData =
553            new SoftReference<WaveformData>(new WaveformData(mAudioWaveformFilename));
554    }
555
556    /**
557     * Get the audio waveform file name if {@link #extractAudioWaveform()} was
558     * successful. The file format is as following:
559     * <ul>
560     *  <li>first 4 bytes provide the number of samples for each value, as big-endian signed</li>
561     *  <li>4 following bytes is the total number of values in the file, as big-endian signed</li>
562     *  <li>all values follow as bytes Name is unique.</li>
563     *</ul>
564     * @return the name of the file, null if the file has not been computed or
565     *         if there is no Audio track in the mediaItem
566     */
567    String getAudioWaveformFilename() {
568        return mAudioWaveformFilename;
569    }
570
571    /**
572     * Invalidate the AudioWaveform File
573     */
574    void invalidate() {
575        if (mAudioWaveformFilename != null) {
576            new File(mAudioWaveformFilename).delete();
577            mAudioWaveformFilename = null;
578        }
579    }
580
581    /**
582     * @return The waveform data
583     */
584    public WaveformData getWaveformData() throws IOException {
585        if (mWaveformData == null) {
586            return null;
587        }
588
589        WaveformData waveformData = mWaveformData.get();
590        if (waveformData != null) {
591            return waveformData;
592        } else if (mAudioWaveformFilename != null) {
593            try {
594                waveformData = new WaveformData(mAudioWaveformFilename);
595            } catch(IOException e) {
596                throw e;
597            }
598            mWaveformData = new SoftReference<WaveformData>(waveformData);
599            return waveformData;
600        } else {
601            return null;
602        }
603    }
604
605    /**
606     * Set volume of the Audio track of this mediaItem
607     *
608     * @param volumePercent in %/. 100% means no change; 50% means half value, 200%
609     *            means double, 0% means silent.
610     * @throws UsupportedOperationException if volume value is not supported
611     */
612    public void setVolume(int volumePercent) {
613        if ((volumePercent <0) || (volumePercent >100)) {
614            throw new IllegalArgumentException("Invalid volume");
615        }
616
617        mVolumePercentage = volumePercent;
618    }
619
620    /**
621     * Get the volume value of the audio track as percentage. Call of this
622     * method before calling setVolume will always return 100%
623     *
624     * @return the volume in percentage
625     */
626    public int getVolume() {
627        return mVolumePercentage;
628    }
629
630    /**
631     * @param muted true to mute the media item
632     */
633    public void setMute(boolean muted) {
634        mMANativeHelper.setGeneratePreview(true);
635        mMuted = muted;
636        if (mBeginTransition != null) {
637            mBeginTransition.invalidate();
638        }
639        if (mEndTransition != null) {
640            mEndTransition.invalidate();
641        }
642    }
643
644    /**
645     * @return true if the media item is muted
646     */
647    public boolean isMuted() {
648        return mMuted;
649    }
650
651    /**
652     * @return The video type
653     */
654    public int getVideoType() {
655        return mVideoType;
656    }
657
658    /**
659     * @return The video profile
660     */
661    public int getVideoProfile() {
662        return mVideoProfile;
663    }
664
665    /**
666     * @return The video bitrate
667     */
668    public int getVideoBitrate() {
669        return mVideoBitrate;
670    }
671
672    /**
673     * @return The audio bitrate
674     */
675    public int getAudioBitrate() {
676        return mAudioBitrate;
677    }
678
679    /**
680     * @return The number of frames per second
681     */
682    public int getFps() {
683        return mFps;
684    }
685
686    /**
687     * @return The audio codec
688     */
689    public int getAudioType() {
690        return mAudioType;
691    }
692
693    /**
694     * @return The number of audio channels
695     */
696    public int getAudioChannels() {
697        return mAudioChannels;
698    }
699
700    /**
701     * @return The audio sample frequency
702     */
703    public int getAudioSamplingFrequency() {
704        return mAudioSamplingFrequency;
705    }
706
707    /**
708     * @return The Video media item properties in ClipSettings class object
709     * {@link android.media.videoeditor.MediaArtistNativeHelper.ClipSettings}
710     */
711    ClipSettings getVideoClipProperties() {
712        ClipSettings clipSettings = new ClipSettings();
713        clipSettings.clipPath = getFilename();
714        clipSettings.fileType = mMANativeHelper.getMediaItemFileType(getFileType());
715        clipSettings.beginCutTime = (int)getBoundaryBeginTime();
716        clipSettings.endCutTime = (int)getBoundaryEndTime();
717        clipSettings.mediaRendering = mMANativeHelper.getMediaItemRenderingMode(getRenderingMode());
718
719        return clipSettings;
720    }
721}
722