30 December 2016
有关视频裁剪我在前面一篇博客中只是做了简单的说明,主要涉及到的知识准备在这篇博客中拿出来说一下。有兴趣的可以fork我的视频裁剪的项目源码,一起学习进步。
图上面的视频播放用是VideoView来实现的,比较简单。我主要说说视频帧的读取和视频时间的选取。 视频帧的获取可以通过Android提供的MediaMetadataRetriever类来实现。
MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
mediaMetadataRetriever.setDataSource(context, videoUri);
在MediaMetadataRetriever类中会调用到media_jni的这个库中的setDataSource()方法,最终使得我们可以获取视频的meta data,meta data是非常丰富的,足足有26种,足够满足我们的需求了。
/**
* The metadata key to retrieve the numeric string describing the
* order of the audio data source on its original recording.
*/
public static final int METADATA_KEY_CD_TRACK_NUMBER = 0;
/**
* The metadata key to retrieve the information about the album title
* of the data source.
*/
public static final int METADATA_KEY_ALBUM = 1;
/**
* The metadata key to retrieve the information about the artist of
* the data source.
*/
public static final int METADATA_KEY_ARTIST = 2;
/**
* The metadata key to retrieve the information about the author of
* the data source.
*/
public static final int METADATA_KEY_AUTHOR = 3;
/**
* The metadata key to retrieve the information about the composer of
* the data source.
*/
public static final int METADATA_KEY_COMPOSER = 4;
/**
* The metadata key to retrieve the date when the data source was created
* or modified.
*/
public static final int METADATA_KEY_DATE = 5;
/**
* The metadata key to retrieve the content type or genre of the data
* source.
*/
public static final int METADATA_KEY_GENRE = 6;
/**
* The metadata key to retrieve the data source title.
*/
public static final int METADATA_KEY_TITLE = 7;
/**
* The metadata key to retrieve the year when the data source was created
* or modified.
*/
public static final int METADATA_KEY_YEAR = 8;
/**
* The metadata key to retrieve the playback duration of the data source.
*/
public static final int METADATA_KEY_DURATION = 9;
/**
* The metadata key to retrieve the number of tracks, such as audio, video,
* text, in the data source, such as a mp4 or 3gpp file.
*/
public static final int METADATA_KEY_NUM_TRACKS = 10;
/**
* The metadata key to retrieve the information of the writer (such as
* lyricist) of the data source.
*/
public static final int METADATA_KEY_WRITER = 11;
/**
* The metadata key to retrieve the mime type of the data source. Some
* example mime types include: "video/mp4", "audio/mp4", "audio/amr-wb",
* etc.
*/
public static final int METADATA_KEY_MIMETYPE = 12;
/**
* The metadata key to retrieve the information about the performers or
* artist associated with the data source.
*/
public static final int METADATA_KEY_ALBUMARTIST = 13;
/**
* The metadata key to retrieve the numberic string that describes which
* part of a set the audio data source comes from.
*/
public static final int METADATA_KEY_DISC_NUMBER = 14;
/**
* The metadata key to retrieve the music album compilation status.
*/
public static final int METADATA_KEY_COMPILATION = 15;
/**
* If this key exists the media contains audio content.
*/
public static final int METADATA_KEY_HAS_AUDIO = 16;
/**
* If this key exists the media contains video content.
*/
public static final int METADATA_KEY_HAS_VIDEO = 17;
/**
* If the media contains video, this key retrieves its width.
*/
public static final int METADATA_KEY_VIDEO_WIDTH = 18;
/**
* If the media contains video, this key retrieves its height.
*/
public static final int METADATA_KEY_VIDEO_HEIGHT = 19;
/**
* This key retrieves the average bitrate (in bits/sec), if available.
*/
public static final int METADATA_KEY_BITRATE = 20;
/**
* This key retrieves the language code of text tracks, if available.
* If multiple text tracks present, the return value will look like:
* "eng:chi"
* @hide
*/
public static final int METADATA_KEY_TIMED_TEXT_LANGUAGES = 21;
/**
* If this key exists the media is drm-protected.
* @hide
*/
public static final int METADATA_KEY_IS_DRM = 22;
/**
* This key retrieves the location information, if available.
* The location should be specified according to ISO-6709 standard, under
* a mp4/3gp box "@xyz". Location with longitude of -90 degrees and latitude
* of 180 degrees will be retrieved as "-90.0000+180.0000", for instance.
*/
public static final int METADATA_KEY_LOCATION = 23;
/**
* This key retrieves the video rotation angle in degrees, if available.
* The video rotation angle may be 0, 90, 180, or 270 degrees.
*/
public static final int METADATA_KEY_VIDEO_ROTATION = 24;
/**
* This key retrieves the original capture framerate, if it's
* available. The capture framerate will be a floating point
* number.
*/
public static final int METADATA_KEY_CAPTURE_FRAMERATE = 25;
// Add more here...
了解这些meta 信息,就可以知道一个视频所能给我们提供的信息有哪些了。 在这里,我们是要获取视频的帧(Frame),帧其实就是图片,一帧可能会有N多张图片组成。通过调用MediaMetadataRetriever类中的getFrameAtTime()获取视频的帧的Bitmap。
/**
* Call this method after setDataSource(). This method finds a
* representative frame close to the given time position by considering
* the given option if possible, and returns it as a bitmap. This is
* useful for generating a thumbnail for an input data source or just
* obtain and display a frame at the given time position.
*
* @param timeUs The time position where the frame will be retrieved.
* When retrieving the frame at the given time position, there is no
* guarantee that the data source has a frame located at the position.
* When this happens, a frame nearby will be returned. If timeUs is
* negative, time position and option will ignored, and any frame
* that the implementation considers as representative may be returned.
*
* @param option a hint on how the frame is found. Use
* {@link #OPTION_PREVIOUS_SYNC} if one wants to retrieve a sync frame
* that has a timestamp earlier than or the same as timeUs. Use
* {@link #OPTION_NEXT_SYNC} if one wants to retrieve a sync frame
* that has a timestamp later than or the same as timeUs. Use
* {@link #OPTION_CLOSEST_SYNC} if one wants to retrieve a sync frame
* that has a timestamp closest to or the same as timeUs. Use
* {@link #OPTION_CLOSEST} if one wants to retrieve a frame that may
* or may not be a sync frame but is closest to or the same as timeUs.
* {@link #OPTION_CLOSEST} often has larger performance overhead compared
* to the other options if there is no sync frame located at timeUs.
*
* @return A Bitmap containing a representative video frame, which
* can be null, if such a frame cannot be retrieved.
*/
public Bitmap getFrameAtTime(long timeUs, int option) {
if (option < OPTION_PREVIOUS_SYNC ||
option > OPTION_CLOSEST) {
throw new IllegalArgumentException("Unsupported option: " + option);
}
return _getFrameAtTime(timeUs, option);
}
这里有两个参数: timeUs 表示每一帧的开始时间位置,给定的时间的帧不一定存在,但是系统会将临近的一帧返回给我们。 option 参数表示获取帧的方式
以下是我调用的代码块,代码块是放在单独的线程中执行的。
try {
MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
mediaMetadataRetriever.setDataSource(context, videoUri);
// Retrieve media data use microsecond
long videoLengthInMs = Long.parseLong(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)) * 1000;
long numThumbs = videoLengthInMs < one_frame_time ? 1 : (videoLengthInMs / one_frame_time);
final long interval = videoLengthInMs / numThumbs;
//每次截取到3帧之后上报
for (long i = 0; i < numThumbs; ++i) {
Bitmap bitmap = mediaMetadataRetriever.getFrameAtTime(i * interval, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
try {
bitmap = Bitmap.createScaledBitmap(bitmap, thumb_Width, thumb_Height, false);
} catch (Exception e) {
e.printStackTrace();
}
thumbnailList.add(bitmap);
if (thumbnailList.size() == 3) {
callback.onSingleCallback((ArrayList<Bitmap>) thumbnailList.clone(), (int) interval);
thumbnailList.clear();
}
}
if (thumbnailList.size() > 0) {
callback.onSingleCallback((ArrayList<Bitmap>) thumbnailList.clone(), (int) interval);
thumbnailList.clear();
}
mediaMetadataRetriever.release();
} catch (final Throwable e) {
Thread.getDefaultUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
}
子线程会在每截取到三帧之后将数据上报给主线程,主线程拿到数据之后会进行ui的更新操作,将bitmap显示出来。
当视频源时间比较长的时候,我们会截取到很多帧图片,所以需要将帧图片可以左右滑动。项目中用到的控件是:VideoThumbHorizontalListView (可水平滚动的ListView)
videoThumbListView.setOnScrollStateChangedListener(new VideoThumbHorizontalListView.OnScrollStateChangedListener({
@Override
public void onScrollStateChanged(ScrollState scrollState, int scrollDirection) {
if (videoThumbListView.getCurrentX() == 0)
return;
switch (scrollState) {
case SCROLL_STATE_FLING:
case SCROLL_STATE_IDLE:
case SCROLL_STATE_TOUCH_SCROLL:
if (scrolledOffset < 0) {
mScrolledOffset = mScrolledOffset - Math.abs(scrolledOffset);
if (mScrolledOffset <= 0)
mScrolledOffset = 0;
} else {
if(PixToTime(mScrolledOffset + SCREEN_WIDTH) <= mDuration)//根据时间来判断还是否可以向左滚动
mScrolledOffset = mScrolledOffset + scrolledOffset;
}
onVideoReset();
onSeekThumbs(0, mScrolledOffset + leftThumbValue);
onSeekThumbs(1, mScrolledOffset + rightThumbValue);
mRangeSeekBarView.invalidate();
break;
}
} );
我会去监听水平ListView的左右滚动事件,以获取它的滚动距离,从而算出并显示当前截取到的视频时间的长度。这里我们需要将视频总长度与屏幕的宽度做一个计算,形成一个映射关系,这是一个比较麻烦的事情,处理不好,会导致在ListView滚动过程中时间显示不准确的问题,哈哈,我的项目就存在着这样的bug。
这个项目只是一个粗糙的项目,页面上会存在一些小问题,不过可以拿来简单的学习一下。
欢迎有兴趣的同学star和fork。
Github:https://github.com/iknow4/Android-Video-Trimmer
Thanks for reading. To help others please click ❤ to recommend this article if you found it helpful.
— J.Chou