/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.media.videoeditor; import java.io.File; import java.io.IOException; import java.lang.ref.SoftReference; import android.graphics.Bitmap; import android.media.videoeditor.MediaArtistNativeHelper.ClipSettings; import android.media.videoeditor.MediaArtistNativeHelper.Properties; import android.media.videoeditor.VideoEditorProfile; import android.view.Surface; import android.view.SurfaceHolder; /** * This class represents a video clip item on the storyboard * {@hide} */ public class MediaVideoItem extends MediaItem { /** * Instance variables */ private final int mWidth; private final int mHeight; private final int mAspectRatio; private final int mFileType; private final int mVideoType; private final int mVideoProfile; private final int mVideoLevel; private final int mVideoBitrate; private final long mDurationMs; private final int mAudioBitrate; private final int mFps; private final int mAudioType; private final int mAudioChannels; private final int mAudioSamplingFrequency; private long mBeginBoundaryTimeMs; private long mEndBoundaryTimeMs; private int mVolumePercentage; private boolean mMuted; private String mAudioWaveformFilename; private MediaArtistNativeHelper mMANativeHelper; private VideoEditorImpl mVideoEditor; private final int mVideoRotationDegree; /** * The audio waveform data */ private SoftReference mWaveformData; /** * An object of this type cannot be instantiated with a default constructor */ @SuppressWarnings("unused") private MediaVideoItem() throws IOException { this(null, null, null, RENDERING_MODE_BLACK_BORDER); } /** * Constructor * * @param editor The video editor reference * @param mediaItemId The MediaItem id * @param filename The image file name * @param renderingMode The rendering mode * * @throws IOException if the file cannot be opened for reading */ public MediaVideoItem(VideoEditor editor, String mediaItemId, String filename, int renderingMode) throws IOException { this(editor, mediaItemId, filename, renderingMode, 0, END_OF_FILE, 100, false, null); } /** * Constructor * * @param editor The video editor reference * @param mediaItemId The MediaItem id * @param filename The image file name * @param renderingMode The rendering mode * @param beginMs Start time in milliseconds. Set to 0 to extract from the * beginning * @param endMs End time in milliseconds. Set to {@link #END_OF_FILE} to * extract until the end * @param volumePercent in %/. 100% means no change; 50% means half value, 200% * means double, 0% means silent. * @param muted true if the audio is muted * @param audioWaveformFilename The name of the audio waveform file * * @throws IOException if the file cannot be opened for reading */ MediaVideoItem(VideoEditor editor, String mediaItemId, String filename, int renderingMode, long beginMs, long endMs, int volumePercent, boolean muted, String audioWaveformFilename) throws IOException { super(editor, mediaItemId, filename, renderingMode); if (editor instanceof VideoEditorImpl) { mMANativeHelper = ((VideoEditorImpl)editor).getNativeContext(); mVideoEditor = ((VideoEditorImpl)editor); } final Properties properties; try { properties = mMANativeHelper.getMediaProperties(filename); } catch ( Exception e) { throw new IllegalArgumentException(e.getMessage() + " : " + filename); } /** Check the platform specific maximum import resolution */ VideoEditorProfile veProfile = VideoEditorProfile.get(); if (veProfile == null) { throw new RuntimeException("Can't get the video editor profile"); } final int maxInputWidth = veProfile.maxInputVideoFrameWidth; final int maxInputHeight = veProfile.maxInputVideoFrameHeight; if ((properties.width > maxInputWidth) || (properties.height > maxInputHeight)) { throw new IllegalArgumentException( "Unsupported import resolution. Supported maximum width:" + maxInputWidth + " height:" + maxInputHeight + ", current width:" + properties.width + " height:" + properties.height); } /** Check the platform specific maximum video profile and level */ if (!properties.profileSupported) { throw new IllegalArgumentException( "Unsupported video profile " + properties.profile); } if (!properties.levelSupported) { throw new IllegalArgumentException( "Unsupported video level " + properties.level); } switch (mMANativeHelper.getFileType(properties.fileType)) { case MediaProperties.FILE_3GP: case MediaProperties.FILE_MP4: case MediaProperties.FILE_M4V: break; default: throw new IllegalArgumentException("Unsupported Input File Type"); } switch (mMANativeHelper.getVideoCodecType(properties.videoFormat)) { case MediaProperties.VCODEC_H263: case MediaProperties.VCODEC_H264: case MediaProperties.VCODEC_MPEG4: break; default: throw new IllegalArgumentException("Unsupported Video Codec Format in Input File"); } mWidth = properties.width; mHeight = properties.height; mAspectRatio = mMANativeHelper.getAspectRatio(properties.width, properties.height); mFileType = mMANativeHelper.getFileType(properties.fileType); mVideoType = mMANativeHelper.getVideoCodecType(properties.videoFormat); mVideoProfile = properties.profile; mVideoLevel = properties.level; mDurationMs = properties.videoDuration; mVideoBitrate = properties.videoBitrate; mAudioBitrate = properties.audioBitrate; mFps = (int)properties.averageFrameRate; mAudioType = mMANativeHelper.getAudioCodecType(properties.audioFormat); mAudioChannels = properties.audioChannels; mAudioSamplingFrequency = properties.audioSamplingFrequency; mBeginBoundaryTimeMs = beginMs; mEndBoundaryTimeMs = endMs == END_OF_FILE ? mDurationMs : endMs; mVolumePercentage = volumePercent; mMuted = muted; mAudioWaveformFilename = audioWaveformFilename; if (audioWaveformFilename != null) { mWaveformData = new SoftReference( new WaveformData(audioWaveformFilename)); } else { mWaveformData = null; } mVideoRotationDegree = properties.videoRotation; } /** * Sets the start and end marks for trimming a video media item. * This method will adjust the duration of bounding transitions, effects * and overlays if the current duration of the transactions become greater * than the maximum allowable duration. * * @param beginMs Start time in milliseconds. Set to 0 to extract from the * beginning * @param endMs End time in milliseconds. Set to {@link #END_OF_FILE} to * extract until the end * * @throws IllegalArgumentException if the start time is greater or equal than * end time, the end time is beyond the file duration, the start time * is negative */ public void setExtractBoundaries(long beginMs, long endMs) { if (beginMs > mDurationMs) { throw new IllegalArgumentException("setExtractBoundaries: Invalid start time"); } if (endMs > mDurationMs) { throw new IllegalArgumentException("setExtractBoundaries: Invalid end time"); } if ((endMs != -1) && (beginMs >= endMs) ) { throw new IllegalArgumentException("setExtractBoundaries: Start time is greater than end time"); } if ((beginMs < 0) || ((endMs != -1) && (endMs < 0))) { throw new IllegalArgumentException("setExtractBoundaries: Start time or end time is negative"); } mMANativeHelper.setGeneratePreview(true); if (beginMs != mBeginBoundaryTimeMs) { if (mBeginTransition != null) { mBeginTransition.invalidate(); } } if (endMs != mEndBoundaryTimeMs) { if (mEndTransition != null) { mEndTransition.invalidate(); } } mBeginBoundaryTimeMs = beginMs; mEndBoundaryTimeMs = endMs; adjustTransitions(); mVideoEditor.updateTimelineDuration(); /** * Note that the start and duration of any effects and overlays are * not adjusted nor are they automatically removed if they fall * outside the new boundaries. */ } /** * @return The boundary begin time */ public long getBoundaryBeginTime() { return mBeginBoundaryTimeMs; } /** * @return The boundary end time */ public long getBoundaryEndTime() { return mEndBoundaryTimeMs; } /* * {@inheritDoc} */ @Override public void addEffect(Effect effect) { if (effect instanceof EffectKenBurns) { throw new IllegalArgumentException("Ken Burns effects cannot be applied to MediaVideoItem"); } super.addEffect(effect); } /* * {@inheritDoc} */ @Override public Bitmap getThumbnail(int width, int height, long timeMs) { if (timeMs > mDurationMs) { throw new IllegalArgumentException("Time Exceeds duration"); } if (timeMs < 0) { throw new IllegalArgumentException("Invalid Time duration"); } if ((width <= 0) || (height <= 0)) { throw new IllegalArgumentException("Invalid Dimensions"); } if (mVideoRotationDegree == 90 || mVideoRotationDegree == 270) { int temp = width; width = height; height = temp; } return mMANativeHelper.getPixels( getFilename(), width, height, timeMs, mVideoRotationDegree); } /* * {@inheritDoc} */ @Override public void getThumbnailList(int width, int height, long startMs, long endMs, int thumbnailCount, int[] indices, GetThumbnailListCallback callback) throws IOException { if (startMs > endMs) { throw new IllegalArgumentException("Start time is greater than end time"); } if (endMs > mDurationMs) { throw new IllegalArgumentException("End time is greater than file duration"); } if ((height <= 0) || (width <= 0)) { throw new IllegalArgumentException("Invalid dimension"); } if (mVideoRotationDegree == 90 || mVideoRotationDegree == 270) { int temp = width; width = height; height = temp; } mMANativeHelper.getPixelsList(getFilename(), width, height, startMs, endMs, thumbnailCount, indices, callback, mVideoRotationDegree); } /* * {@inheritDoc} */ @Override void invalidateTransitions(long startTimeMs, long durationMs) { /** * Check if the item overlaps with the beginning and end transitions */ if (mBeginTransition != null) { if (isOverlapping(startTimeMs, durationMs, mBeginBoundaryTimeMs, mBeginTransition.getDuration())) { mBeginTransition.invalidate(); } } if (mEndTransition != null) { final long transitionDurationMs = mEndTransition.getDuration(); if (isOverlapping(startTimeMs, durationMs, mEndBoundaryTimeMs - transitionDurationMs, transitionDurationMs)) { mEndTransition.invalidate(); } } } /* * {@inheritDoc} */ @Override void invalidateTransitions(long oldStartTimeMs, long oldDurationMs, long newStartTimeMs, long newDurationMs) { /** * Check if the item overlaps with the beginning and end transitions */ if (mBeginTransition != null) { final long transitionDurationMs = mBeginTransition.getDuration(); final boolean oldOverlap = isOverlapping(oldStartTimeMs, oldDurationMs, mBeginBoundaryTimeMs, transitionDurationMs); final boolean newOverlap = isOverlapping(newStartTimeMs, newDurationMs, mBeginBoundaryTimeMs, transitionDurationMs); /** * Invalidate transition if: * * 1. New item overlaps the transition, the old one did not * 2. New item does not overlap the transition, the old one did * 3. New and old item overlap the transition if begin or end * time changed */ if (newOverlap != oldOverlap) { // Overlap has changed mBeginTransition.invalidate(); } else if (newOverlap) { // Both old and new overlap if ((oldStartTimeMs != newStartTimeMs) || !(oldStartTimeMs + oldDurationMs > transitionDurationMs && newStartTimeMs + newDurationMs > transitionDurationMs)) { mBeginTransition.invalidate(); } } } if (mEndTransition != null) { final long transitionDurationMs = mEndTransition.getDuration(); final boolean oldOverlap = isOverlapping(oldStartTimeMs, oldDurationMs, mEndBoundaryTimeMs - transitionDurationMs, transitionDurationMs); final boolean newOverlap = isOverlapping(newStartTimeMs, newDurationMs, mEndBoundaryTimeMs - transitionDurationMs, transitionDurationMs); /** * Invalidate transition if: * * 1. New item overlaps the transition, the old one did not * 2. New item does not overlap the transition, the old one did * 3. New and old item overlap the transition if begin or end * time changed */ if (newOverlap != oldOverlap) { // Overlap has changed mEndTransition.invalidate(); } else if (newOverlap) { // Both old and new overlap if ((oldStartTimeMs + oldDurationMs != newStartTimeMs + newDurationMs) || ((oldStartTimeMs > mEndBoundaryTimeMs - transitionDurationMs) || newStartTimeMs > mEndBoundaryTimeMs - transitionDurationMs)) { mEndTransition.invalidate(); } } } } /* * {@inheritDoc} */ @Override public int getAspectRatio() { return mAspectRatio; } /* * {@inheritDoc} */ @Override public int getFileType() { return mFileType; } /* * {@inheritDoc} */ @Override public int getWidth() { if (mVideoRotationDegree == 90 || mVideoRotationDegree == 270) { return mHeight; } else { return mWidth; } } /* * {@inheritDoc} */ @Override public int getHeight() { if (mVideoRotationDegree == 90 || mVideoRotationDegree == 270) { return mWidth; } else { return mHeight; } } /* * {@inheritDoc} */ @Override public long getDuration() { return mDurationMs; } /* * {@inheritDoc} */ @Override public long getTimelineDuration() { return mEndBoundaryTimeMs - mBeginBoundaryTimeMs; } /** * Render a frame according to the playback (in the native aspect ratio) for * the specified media item. All effects and overlays applied to the media * item are ignored. The extract boundaries are also ignored. This method * can be used to playback frames when implementing trimming functionality. * * @param surfaceHolder SurfaceHolder used by the application * @param timeMs time corresponding to the frame to display (relative to the * the beginning of the media item). * @return The accurate time stamp of the frame that is rendered . * @throws IllegalStateException if a playback, preview or an export is * already in progress * @throws IllegalArgumentException if time is negative or greater than the * media item duration */ public long renderFrame(SurfaceHolder surfaceHolder, long timeMs) { if (surfaceHolder == null) { throw new IllegalArgumentException("Surface Holder is null"); } if (timeMs > mDurationMs || timeMs < 0) { throw new IllegalArgumentException("requested time not correct"); } final Surface surface = surfaceHolder.getSurface(); if (surface == null) { throw new RuntimeException("Surface could not be retrieved from Surface holder"); } if (mFilename != null) { return mMANativeHelper.renderMediaItemPreviewFrame(surface, mFilename,timeMs,mWidth,mHeight); } else { return 0; } } /** * This API allows to generate a file containing the sample volume levels of * the Audio track of this media item. This function may take significant * time and is blocking. The file can be retrieved using * getAudioWaveformFilename(). * * @param listener The progress listener * * @throws IOException if the output file cannot be created * @throws IllegalArgumentException if the mediaItem does not have a valid * Audio track */ public void extractAudioWaveform(ExtractAudioWaveformProgressListener listener) throws IOException { int frameDuration = 0; int sampleCount = 0; final String projectPath = mMANativeHelper.getProjectPath(); /** * Waveform file does not exist */ if (mAudioWaveformFilename == null ) { /** * Since audioWaveformFilename will not be supplied,it is generated */ String mAudioWaveFileName = null; mAudioWaveFileName = String.format(projectPath + "/" + "audioWaveformFile-"+ getId() + ".dat"); /** * Logic to get frame duration = (no. of frames per sample * 1000)/ * sampling frequency */ if (mMANativeHelper.getAudioCodecType(mAudioType) == MediaProperties.ACODEC_AMRNB ) { frameDuration = (MediaProperties.SAMPLES_PER_FRAME_AMRNB*1000)/ MediaProperties.DEFAULT_SAMPLING_FREQUENCY; sampleCount = MediaProperties.SAMPLES_PER_FRAME_AMRNB; } else if (mMANativeHelper.getAudioCodecType(mAudioType) == MediaProperties.ACODEC_AMRWB ) { frameDuration = (MediaProperties.SAMPLES_PER_FRAME_AMRWB * 1000)/ MediaProperties.DEFAULT_SAMPLING_FREQUENCY; sampleCount = MediaProperties.SAMPLES_PER_FRAME_AMRWB; } else if (mMANativeHelper.getAudioCodecType(mAudioType) == MediaProperties.ACODEC_AAC_LC ) { frameDuration = (MediaProperties.SAMPLES_PER_FRAME_AAC * 1000)/ MediaProperties.DEFAULT_SAMPLING_FREQUENCY; sampleCount = MediaProperties.SAMPLES_PER_FRAME_AAC; } mMANativeHelper.generateAudioGraph( getId(), mFilename, mAudioWaveFileName, frameDuration, MediaProperties.DEFAULT_CHANNEL_COUNT, sampleCount, listener, true); /** * Record the generated file name */ mAudioWaveformFilename = mAudioWaveFileName; } mWaveformData = new SoftReference(new WaveformData(mAudioWaveformFilename)); } /** * Get the audio waveform file name if {@link #extractAudioWaveform()} was * successful. The file format is as following: *
    *
  • first 4 bytes provide the number of samples for each value, as big-endian signed
  • *
  • 4 following bytes is the total number of values in the file, as big-endian signed
  • *
  • all values follow as bytes Name is unique.
  • *
* @return the name of the file, null if the file has not been computed or * if there is no Audio track in the mediaItem */ String getAudioWaveformFilename() { return mAudioWaveformFilename; } /** * Invalidate the AudioWaveform File */ void invalidate() { if (mAudioWaveformFilename != null) { new File(mAudioWaveformFilename).delete(); mAudioWaveformFilename = null; } } /** * @return The waveform data */ public WaveformData getWaveformData() throws IOException { if (mWaveformData == null) { return null; } WaveformData waveformData = mWaveformData.get(); if (waveformData != null) { return waveformData; } else if (mAudioWaveformFilename != null) { try { waveformData = new WaveformData(mAudioWaveformFilename); } catch(IOException e) { throw e; } mWaveformData = new SoftReference(waveformData); return waveformData; } else { return null; } } /** * Set volume of the Audio track of this mediaItem * * @param volumePercent in %/. 100% means no change; 50% means half value, 200% * means double, 0% means silent. * @throws UsupportedOperationException if volume value is not supported */ public void setVolume(int volumePercent) { if ((volumePercent <0) || (volumePercent >100)) { throw new IllegalArgumentException("Invalid volume"); } mVolumePercentage = volumePercent; } /** * Get the volume value of the audio track as percentage. Call of this * method before calling setVolume will always return 100% * * @return the volume in percentage */ public int getVolume() { return mVolumePercentage; } /** * @param muted true to mute the media item */ public void setMute(boolean muted) { mMANativeHelper.setGeneratePreview(true); mMuted = muted; if (mBeginTransition != null) { mBeginTransition.invalidate(); } if (mEndTransition != null) { mEndTransition.invalidate(); } } /** * @return true if the media item is muted */ public boolean isMuted() { return mMuted; } /** * @return The video type */ public int getVideoType() { return mVideoType; } /** * @return The video profile */ public int getVideoProfile() { return mVideoProfile; } /** * @return The video profile */ public int getVideoLevel() { return mVideoLevel; } /** * @return The video bitrate */ public int getVideoBitrate() { return mVideoBitrate; } /** * @return The audio bitrate */ public int getAudioBitrate() { return mAudioBitrate; } /** * @return The number of frames per second */ public int getFps() { return mFps; } /** * @return The audio codec */ public int getAudioType() { return mAudioType; } /** * @return The number of audio channels */ public int getAudioChannels() { return mAudioChannels; } /** * @return The audio sample frequency */ public int getAudioSamplingFrequency() { return mAudioSamplingFrequency; } /** * @return The Video media item properties in ClipSettings class object * {@link android.media.videoeditor.MediaArtistNativeHelper.ClipSettings} */ ClipSettings getVideoClipProperties() { ClipSettings clipSettings = new ClipSettings(); clipSettings.clipPath = getFilename(); clipSettings.fileType = mMANativeHelper.getMediaItemFileType(getFileType()); clipSettings.beginCutTime = (int)getBoundaryBeginTime(); clipSettings.endCutTime = (int)getBoundaryEndTime(); clipSettings.mediaRendering = mMANativeHelper.getMediaItemRenderingMode(getRenderingMode()); clipSettings.rotationDegree = mVideoRotationDegree; return clipSettings; } }