/*
* 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<WaveformData> 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<WaveformData>(
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<WaveformData>(new WaveformData(mAudioWaveformFilename));
}
/**
* Get the audio waveform file name if {@link #extractAudioWaveform()} was
* successful. The file format is as following:
* <ul>
* <li>first 4 bytes provide the number of samples for each value, as big-endian signed</li>
* <li>4 following bytes is the total number of values in the file, as big-endian signed</li>
* <li>all values follow as bytes Name is unique.</li>
*</ul>
* @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>(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;
}
}