J.Chou

说说Android的视频裁剪(二)

30 December 2016

有关视频裁剪我在前面一篇博客中只是做了简单的说明,主要涉及到的知识准备在这篇博客中拿出来说一下。有兴趣的可以fork我的视频裁剪的项目源码,一起学习进步。

Github:https://github.com/iknow4/Android-Video-Trimmer

视频裁剪页如下图所示: videoTrim

图上面的视频播放用是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。

videoTrim

/**
     * 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