Android音视频开发之三种方式获取视频某一帧

2023年 9月 2日 54.8k 0

Android音视频开发之三种方式获取视频某一帧

Android开发三种方式获取视频某一帧.png

前言:

在Android开发中获取视频的某一帧很常见,相信大家都用过腾讯视频、爱奇艺、优酷等,里面会有直播或者列表播放视频时暂停时显示当前一帧的封面,或者视频暂停时从视频列表进入详情然后再返回时显示之前暂停的界面。今天先说下三种方式实现获取视频某一帧显示封面,然后说遇到的问题.

1.使用Glide方式获取视频某一帧:

/**
 * 使用Glide方式获取视频某一帧
 * @param context 上下文
 * @param uri 视频地址
 * @param imageView 设置image
 * @param frameTimeMicros 获取某一时间帧.
 */
public static void loadVideoScreenshot(final Context context, String uri, ImageView imageView, long frameTimeMicros) {
    RequestOptions requestOptions = RequestOptions.frameOf(frameTimeMicros);
    requestOptions.set(FRAME_OPTION, MediaMetadataRetriever.OPTION_CLOSEST);
    requestOptions.transform(new BitmapTransformation() {
        @Override
        protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) {
            Log.d("--使用glide方式--", "高度为" + toTransform.getHeight() + "寛度为" + toTransform.getWidth());
            return toTransform;
        }
        @Override
        public void updateDiskCacheKey(MessageDigest messageDigest) {
            try {
                messageDigest.update((context.getPackageName() + "RotateTransform").getBytes("utf-8"));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });
    Glide.with(context).load(uri).apply(requestOptions).into(imageView);
}

1.1打断点可以看到bitmap图片信息如下:

4d217214a47840cab0ebd4a6eac81a93.png

1.2 glide输出图片宽高:

1f4e2254f01241f0a6ef67b154d5acdb.png

1.3 输出的日志如下:

e6ddacd024364871a22898d51670fde8.png

2.使用MediaMetadataRetriever方式获取视频某一帧:

/**
 * 使用MediaMetadataRetriever获取视频指定微秒处的帧图片
 * url  网络url
 *timeUs 微秒 
 */
@SuppressLint("LongLogTag")
public static Bitmap GetFramePictures(String url, long timeUs){
    Bitmap videoShortCut = null;
    String width;
    String height;
    MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
    try {
        if (url != null) {
            HashMap headers;
            headers = new HashMap();
            headers.put("User-Agent", "Mozilla/5.0 (Linux; U; Android 4.4.2; zh-CN; MW-KW-001 Build/JRO03C) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 UCBrowser/1.0.0.001 U4/0.8.0 Mobile Safari/533.1");
            mediaMetadataRetriever.setDataSource(url, headers);
        } else {
            //mmr.setDataSource(mFD, mOffset, mLength);
        }
        videoShortCut = mediaMetadataRetriever.getFrameAtTime(timeUs, MediaMetadataRetriever.OPTION_CLOSEST);
        width = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
        height = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
        Log.d("--使用MediaMetadata方式--","图片高度为:" + width + "图片宽度为:" + height);
​
    } catch (IllegalArgumentException e) {
        Log.e("setFrameAtTimeWithView ERROR:" , e.toString(), e);
    }
    mediaMetadataRetriever.release();
    return  videoShortCut;
}

2.1 断点截图如下:

34a1988a46f347b69d56459ec31912ca.png

图2-1

30ec0e0d203b4fd5ae274bc5b8b1bf36.png

图2-2

2.2 日志打印如下:

e6ddacd024364871a22898d51670fde8.png

图2-3

3.使用TextureView方式获取视频某一帧:

public Bitmap getFrontCoverBitmap() {
    Bitmap bitmap = null;
    if (textureView != null) {
        bitmap = textureView.getBitmap();
    }
    return bitmap;
}

3.1 调试输出图片bitmap信息:

29cadc88000141f781ec68936a01d831.png

图3-1

78dffe3ac25f4b7c91dd6fd15990393b.png

图3-2

4ccae9e84cf14669bf0896000b427cfa.png

图3-3

3.2 日志打印图片宽高为:

e09b629467db4901a1b23c267e3f54bb.png

4.工具类如下:

package com.example.videotimedemo.util;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.media.MediaMetadataRetriever;
import android.util.Log;
import android.widget.ImageView;

import androidx.annotation.NonNull;

import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
import com.bumptech.glide.request.RequestOptions;

import java.security.MessageDigest;
import java.util.HashMap;

import static com.bumptech.glide.load.resource.bitmap.VideoDecoder.FRAME_OPTION;

public class VideoUtils {
    public static long videoDuration;
    private static long durationTime;

    /**
     * 根据url查询视频时长和宽高
     *
     * @param url
     * @return
     */
    public static long getPlayTime(String url) {
        android.media.MediaMetadataRetriever metadataRetriever = new android.media.MediaMetadataRetriever();
        try {
            if (url != null) {
                HashMap headers;
                headers = new HashMap();
                headers.put("User-Agent", "Mozilla/5.0 (Linux; U; Android 4.4.2; zh-CN; MW-KW-001 Build/JRO03C) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 UCBrowser/1.0.0.001 U4/0.8.0 Mobile Safari/533.1");
                metadataRetriever.setDataSource(url, headers);
            } else {
                //mmr.setDataSource(mFD, mOffset, mLength);
            }
            durationTime = Long.valueOf(metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));//时长(毫秒)
            String width = metadataRetriever.extractMetadata(android.media.MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);//宽
            String height = metadataRetriever.extractMetadata(android.media.MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);//高
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            metadataRetriever.release();
        }
        return durationTime;
    }

    /**
     * 使用MediaMetadataRetriever获取视频指定微秒处的帧图片
     * url  网络url
     *timeUs 微秒 1秒=1000毫秒=1000000微秒,若时间不乘以1000则显示的第一帧的图片
     */
    @SuppressLint("LongLogTag")
    public static Bitmap GetFramePictures(String url, long timeUs){
        Bitmap videoShortCut = null;
        String width;
        String height;
        MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
        try {
            if (url != null) {
                HashMap headers;
                headers = new HashMap();
                headers.put("User-Agent", "Mozilla/5.0 (Linux; U; Android 4.4.2; zh-CN; MW-KW-001 Build/JRO03C) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 UCBrowser/1.0.0.001 U4/0.8.0 Mobile Safari/533.1");
                mediaMetadataRetriever.setDataSource(url, headers);
            } else {
                //mmr.setDataSource(mFD, mOffset, mLength);
            }
            videoShortCut = mediaMetadataRetriever.getFrameAtTime(timeUs , MediaMetadataRetriever.OPTION_CLOSEST);
            width = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
            height = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
            Log.d("--使用MediaMetadata方式--","图片高度为:" + width + "图片宽度为:" + height);

        } catch (IllegalArgumentException e) {
            Log.e("setFrameAtTimeWithView ERROR:" , e.toString(), e);
        }
        mediaMetadataRetriever.release();
        return  videoShortCut;
    }

    /**
     * 使用Glide方式获取视频某一帧
     * @param context 上下文
     * @param uri 视频地址
     * @param imageView 设置image
     * @param frameTimeMicros 获取某一时间帧.
     */
    public static void loadVideoScreenshot(final Context context, String uri, ImageView imageView, long frameTimeMicros) {
        RequestOptions requestOptions = RequestOptions.frameOf(frameTimeMicros* 1000);
        requestOptions.set(FRAME_OPTION, MediaMetadataRetriever.OPTION_CLOSEST);
        requestOptions.transform(new BitmapTransformation() {
            @Override
            protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) {
                Log.d("--使用glide方式--", "高度为" + toTransform.getHeight() + "寛度为" + toTransform.getWidth());
                return toTransform;
            }
            @Override
            public void updateDiskCacheKey(MessageDigest messageDigest) {
                try {
                    messageDigest.update((context.getPackageName() + "RotateTransform").getBytes("utf-8"));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        Glide.with(context).load(uri).apply(requestOptions).into(imageView);
    }


    private static void setVideoDuration(long duration) {
        videoDuration = duration;
    }

    public static long getVideoDuration() {
        return videoDuration;
    }
}

5.完整测试代码如下:

package com.example.videotimedemo;

import android.graphics.Bitmap;
import android.media.MediaMetadataRetriever;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;

import androidx.appcompat.app.AppCompatActivity;

import com.example.videotimedemo.app.App;
import com.example.videotimedemo.util.VideoUtils;

import java.util.HashMap;

import cn.jzvd.JZMediaSystem;
import cn.jzvd.JZUtils;
import cn.jzvd.Jzvd;
import cn.jzvd.JzvdStd;
import io.reactivex.ObservableEmitter;
import io.reactivex.ObservableOnSubscribe;
import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;

public class MainActivity extends AppCompatActivity {
    public static final String videoUrl = "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4";
    private Handler mHandler;
    private JzvdStd jzvdStd;
    private static final String TAG = MainActivity.class.getName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mHandler = new Handler();
        initView();
        initData();
    }

    private void initData() {
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                long time = VideoUtils.getPlayTime(videoUrl);
                Log.d("--time--", time + "");
                jzvdStd.totalTimeTextView.setText(JZUtils.stringForTime(time));
            }
        }, 200);
        getDuration(videoUrl);
    }

    public void getDuration(String url) {
        io.reactivex.Observable observable = io.reactivex.Observable.create(new ObservableOnSubscribe() {
            @Override
            public void subscribe(ObservableEmitter e) throws Exception {
                android.media.MediaMetadataRetriever metadataRetriever = new android.media.MediaMetadataRetriever();
                try {
                    if (url != null) {
                        HashMap headers;
                        headers = new HashMap();
                        headers.put("User-Agent", "Mozilla/5.0 (Linux; U; Android 4.4.2; zh-CN; MW-KW-001 Build/JRO03C) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 UCBrowser/1.0.0.001 U4/0.8.0 Mobile Safari/533.1");
                        metadataRetriever.setDataSource(url, headers);
                    } else {
                        //mmr.setDataSource(mFD, mOffset, mLength);
                    }
                    long durationTime = Long.parseLong(metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));//时长(毫秒)
                    String width = metadataRetriever.extractMetadata(android.media.MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);//宽
                    String height = metadataRetriever.extractMetadata(android.media.MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);//高
                    e.onNext(durationTime);
                } catch (Exception ex) {
                    ex.printStackTrace();
                } finally {
                    metadataRetriever.release();
                }
            }
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
        observable.subscribe(new Observer() {
            @Override
            public void onSubscribe(Disposable d) {

            }

            @Override
            public void onNext(Long time) {
                Log.d("--time--", time.toString());
                jzvdStd.totalTimeTextView.setText(JZUtils.stringForTime(time));
            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onComplete() {

            }
        });
    }

    private void initView() {
        jzvdStd = findViewById(R.id.videoplayer);
        if (jzvdStd != null) {

            jzvdStd.postDelayed(new Runnable() {
                @Override
                public void run() {
                    Bitmap bitmap = VideoUtils.GetFramePictures(videoUrl,jzvdStd.getCurrentPositionWhenPlaying());
                    Log.d(TAG,"视频封面为:"+ bitmap);
                }
            },100);
             String proxyUrl = App.getProxy(this).getProxyUrl(videoUrl);
             jzvdStd.setUp(proxyUrl, "视频默认没有播放", JzvdStd.SCREEN_NORMAL, JZMediaSystem.class);
            jzvdStd.startVideo();
           // jzvdStd.postDelayed(() -> ),100);
            //VideoUtils.loadVideoScreenshot(this, videoUrl, jzvdStd.thumbImageView, jzvdStd.getDuration());
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        Jzvd.goOnPlayOnPause();
    }

    @Override
    protected void onResume() {
        super.onResume();
        Jzvd.goOnPlayOnResume();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Jzvd.releaseAllVideos();
    }


}

6.布局如下:




    


7.遇到问题如下:

7.1 使用glide和MediaMetadataRetriever方式时由于时间是微妙所以需要换算成毫秒*1000

7.2 使用glide方式会有延迟获取到的封面可能不准确

7.3 使用MediaMetadataRetriever方式会出现卡顿现象,由于是网络在线查询,所以网络不好时查询很慢,一般会用rxjava异步去查询,列表不建议用这个。

8.从上面3-1、3-2、3-3的截图可以看出:

封面是实时显示的,很符合需求视频暂停到那一秒就显示当前某一帧的封面,而且不会出现卡顿.所以这种方式是最好的,也是最符合用户体验的.

9.总结:

以上就是今天的内容,使用三种方式实现获取视频的某一帧,最后选择的是第三种,大家可以根据需要自行选择,如果小伙伴们有更好的方法欢迎留言,后面有时间会整理出来一个列表播放暂停进入详情页再次返回的demo,谢谢大家!当然还有一种是利用ffmpeg实现获取视频的某一帧后面也会整理出来.

10.项目源码如下:

videotimedemo: 视频播放时间 (gitee.com)

相关文章

服务器端口转发,带你了解服务器端口转发
服务器开放端口,服务器开放端口的步骤
产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
如何使用 WinGet 下载 Microsoft Store 应用
百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

发布评论