Android 获取视频画面方式整理
在进行Android音视频开发的时候,我们可能会遇到需要获取视频制定位置的图片的需求。针对这个问题,我们有几种解决方案:分别为Android官方提供的MediaMetadataRetriever、基于FFmpeg封装的FFmpegMediaMetadataRetriever、还有就是基于FFmpeg自研发。
下面我们基于这几个实现方式进行介绍和整理 :
一、MediaMetadataRetriever
/**
* 获取视频某帧的图像,但得到的图像并不一定是指定position的图像。
*
* @param path 视频的本地路径
* @return Bitmap 返回的视频图像
*/
public static Bitmap getVideoFrame(String path) {
Bitmap bmp = null;
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
try {
retriever.setDataSource(path);
String timeString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
// 获取总长度
long totalTime = Long.parseLong(timeString) * 1000;
if (totalTime > 0) {
// 这里为了实现简单,我们直接获取视频中间的画面
bmp = retriever.getFrameAtTime(totalTime / 2, MediaMetadataRetriever.OPTION_CLOSEST);
}
} catch (RuntimeException ex) {
ex.printStackTrace();
} finally {
try {
retriever.release();
} catch (RuntimeException ex) {
ex.printStackTrace();
}
}
return bmp;
}
1.1 本方案优点:
实现方便,因为使用的是系统Api, 不会增加包体积
1.2 本方案缺点:
支持的格式较少,对网络的视频的支持度较低,且在获取指定位置的视频画面的时候,可能因为GOP的大小导致获取的视频位置不准确。
且可能在使用中遇到以下问题:
可能遇到因为视频格式导致的异常:
mindmaptopicMediaMetadataRetrieverJNI: getFrameAtTime: videoFrame is a NULL pointer<br>
可能遇到获取网络图片失败的问题:
java.lang.IllegalArgumentException
at android.media.MediaMetadataRetriever.setDataSource(MediaMetadataRetriever.java:73)
或
java.lang.RuntimeException: setDataSource failed: status = 0x80000000
当使用MediaMetadataRetriever无法满足我们的需求实现的时候,这时候推荐使用FFmpegMediaMetadataRetriever。
二、FFmpegMediaMetadataRetriever
FFmpegMediaMetadataRetriever的开源项目地址为:https://github.com/wseemann/FFmpegMediaMetadataRetriever
FFmpegMediaMetadataRetriever的作者很有心,提供了同MediaMetadataRetriever相同的Api。
2.1 本方案集成方式:
在需要使用的module的build.gradle文件中添加如下配置:
dependencies {
implementation 'com.github.wseemann:FFmpegMediaMetadataRetriever-core:1.0.15'
implementation 'com.github.wseemann:FFmpegMediaMetadataRetriever-native-armeabi-v7a:1.0.15'
implementation 'com.github.wseemann:FFmpegMediaMetadataRetriever-native-x86:1.0.15'
implementation 'com.github.wseemann:FFmpegMediaMetadataRetriever-native-x86_64:1.0.15'
implementation 'com.github.wseemann:FFmpegMediaMetadataRetriever-native-arm64-v8a:1.0.15'
}
使用方式非常像MediaMetadataRetriever,下面是使用方式的代码:
/**
* 获取视频某帧的图像
*
* @param path 视频的路径
* @return Bitmap 返回的视频图像
*/
public static Bitmap getVideoFrameByFMMR(String path) {
Bitmap bmp = null;
FFmpegMediaMetadataRetriever retriever = new FFmpegMediaMetadataRetriever();
try {
retriever.setDataSource(path);
String timeString = retriever.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_DURATION);
// 获取总长度
long totalTime = Long.parseLong(timeString) * 1000;
if (totalTime > 0) {
bmp = retriever.getFrameAtTime(totalTime / 2, FFmpegMediaMetadataRetriever.OPTION_CLOSEST);
}
} catch (RuntimeException ex) {
ex.printStackTrace();
} finally {
try {
retriever.release();
} catch (RuntimeException ex) {
ex.printStackTrace();
}
}
return bmp;
}
需要注意的是,直播流的话,不能使用retriever.getFrameAtTime(long timeUs, int option)的方式获取指定时间的图像。但是可以使用retriever.getFrameAtTime()获取当前时间的画面。
2.2 本方案优点:
支持的格式多;
对网络的视频的支持度好;
且在获取指定位置的视频画面的时候,定位相对准确。
2.3 本方案缺点:
引入了FFmpeg库,会导致打包出来的Apk出现爆炸式的大小增加(native层的库可据实际需要进行精简);
当视频的时长较长或者分辨率较大的时候,可能会导致获取视频画面的耗时较长。
三、基于FFmpeg自研发
基于FFmpeg自研发的方式实现起来和实现播放器差不多,准确来说就是通过seek定位指定的Frame,然后保存为本地图片,执行完成后,由java层的代码加载本地图片为Bitmap。
方案优点:获取画面定位精确,且可根据实际需要实现库的裁剪,且实现流程可控,可定制逻辑。
方案优点缺点:实现起来难度较大。
实现代码:
Java层代码:
/**
* 基于FFmpeg实现的缩略图获取工具类
*/
public class FFmpegThumbnailHelper {
public static Bitmap getVideoThumbnail(String path) {
FFmpegThumbnailHelper helper = new FFmpegThumbnailHelper();
String filePath = AppContextHelper.context.getCacheDir() + File.separator + "11122.jpg";
Log.e(GlobalConfig.LOG_TAG, "filePath = " + filePath);
helper.getThumbnail(path, filePath);
return BitmapFactory.decodeFile(filePath);
}
private native void getThumbnail(String path, String picturePath);
// 加载底层so库
static {
System.loadLibrary("media-editor-lib");
}
}
Native层代码:
#include "iostream"
#include "cstring"
#include "jni.h"
extern "C" {
#include <string>
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
#include "libavutil/opt.h"
#include "libavutil/channel_layout.h"
#include "libavutil/common.h"
#include "libavutil/imgutils.h"
#include "libavutil/mathematics.h"
#include "libavutil/samplefmt.h"
#include "libavutil/time.h"
#include "libavutil/fifo.h"
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavformat/avio.h"
#include "libavfilter/avfilter.h"
#include "libavfilter/buffersink.h"
#include "libavfilter/buffersrc.h"
#include "libswscale/swscale.h"
#include "libswresample/swresample.h"
}
#include <../AndroidLog.h>
AVFormatContext *inputContext;
AVFormatContext *outputContext;
// 用于解码
AVCodecContext *deCodecContext;
// 用于编码
AVCodecContext *enCodecContext;
int video_index = -1;
AVStream *in_stream;
AVStream *out_stream;
void init() {
av_register_all();
avformat_network_init();
av_log_set_level(AV_LOG_INFO);
}
void openInputForDecodec(const char *inputUrl) {
avformat_open_input(&inputContext, inputUrl, NULL, NULL);
avformat_find_stream_info(inputContext, NULL);
for (int i = 0; i < inputContext->nb_streams; i++) {
AVStream *stream = inputContext->streams[i];
/**
* 对于jpg图片来说,它里面就是一路视频流,所以媒体类型就是AVMEDIA_TYPE_VIDEO
*/
if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_index = i;
AVCodec *codec = avcodec_find_decoder(stream->codecpar->codec_id);
// 初始化解码器上下文
deCodecContext = avcodec_alloc_context3(codec);
// 设置解码器参数,从源视频拷贝参数
avcodec_parameters_to_context(deCodecContext, stream->codecpar);
// 初始化解码器
avcodec_open2(deCodecContext, codec, NULL);
}
}
}
// 初始化编码器
void initEncodecContext() {
// 初始化编码器;因为最终是要写入到JPEG,所以使用的编码器ID为AV_CODEC_ID_MJPEG
AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_MJPEG);
enCodecContext = avcodec_alloc_context3(codec);
// 设置编码参数
in_stream = inputContext->streams[video_index];
enCodecContext->width = in_stream->codecpar->width;
enCodecContext->height = in_stream->codecpar->height;
// 如果是编码后写入到图片中,那么比特率可以不用设置,不影响最终的结果(也不会影响图像清晰度)
enCodecContext->bit_rate = in_stream->codecpar->bit_rate;
// 如果是编码后写入到图片中,那么帧率可以不用设置,不影响最终的结果
enCodecContext->framerate = in_stream->r_frame_rate;
enCodecContext->time_base = in_stream->time_base;
// 对于MJPEG编码器来说,它支持的是YUVJ420P/YUVJ422P/YUVJ444P格式的像素
enCodecContext->pix_fmt = AV_PIX_FMT_YUVJ420P;
// 初始化编码器
avcodec_open2(enCodecContext, codec, NULL);
}
void openOutputForEncode(const char *pictureUrl) {
avformat_alloc_output_context2(&outputContext, NULL, NULL, pictureUrl);
out_stream = avformat_new_stream(outputContext, NULL);
avcodec_parameters_from_context(out_stream->codecpar, enCodecContext);
avio_open2(&outputContext->pb, pictureUrl, AVIO_FLAG_READ_WRITE, NULL, NULL);
/** 为输出文件写入头信息 **/
avformat_write_header(outputContext, NULL);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_renhui_player_utils_FFmpegThumbnailHelper_getThumbnail(JNIEnv *env, jobject clazz,
jstring path, jstring picturePath) {
const char *url = env->GetStringUTFChars(path, 0);
const char *pictureUrl = env->GetStringUTFChars(picturePath, 0);
// 设置要截取的时间点
int64_t start_pts = 30;
// FFmpeg的初始化工作
init();
openInputForDecodec(url);
initEncodecContext();
openOutputForEncode(pictureUrl);
// 创建编码解码用的AVFrame
AVFrame *deFrame = av_frame_alloc();
AVPacket *in_pkt = av_packet_alloc();
AVPacket *ou_pkt = av_packet_alloc();
AVRational time_base = inputContext->streams[video_index]->time_base;
AVRational frame_rate = inputContext->streams[video_index]->r_frame_rate;
// 一帧的时间戳
int64_t delt = time_base.den / frame_rate.num;
start_pts *= time_base.den;
/** 因为想要截取的时间处的AVPacket并不一定是I帧,所以想要正确的解码,得先找到离想要截取的时间处往前的最近的I帧
* 开始解码,直到拿到了想要获取的时间处的AVFrame
* AVSEEK_FLAG_BACKWARD 代表如果start_pts指定的时间戳处的AVPacket非I帧,那么就往前移动指针,直到找到I帧,那么
* 当首次调用av_frame_read()函数时返回的AVPacket将为此I帧的AVPacket
*/
av_seek_frame(inputContext, video_index, start_pts, AVSEEK_FLAG_BACKWARD);
bool found = false;
while (av_read_frame(inputContext, in_pkt) == 0) {
if (in_pkt->stream_index != video_index) {
continue;
}
if (found) {
break;
}
// 先解码
avcodec_send_packet(deCodecContext, in_pkt);
while (avcodec_receive_frame(deCodecContext, deFrame) >= 0) {
int got_packet = 0;
avcodec_encode_video2(enCodecContext, ou_pkt, deFrame, &got_packet);
// 因为只编码一帧,所以发送一帧视频后立马清空缓冲区
av_write_frame(outputContext, ou_pkt);
av_packet_unref(in_pkt);
found = true;
break;
}
}
// 写入文件尾对于写入视频文件来说,此函数必须调用,但是对于写入JPG文件来说,不调用此函数也没关系;
av_write_trailer(outputContext);
}