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