內(nèi)容如題:
代碼在文末:
核心使用:
這里使用了安卓自帶的類(MediaMetadataRetriever)實(shí)現(xiàn):
思路與細(xì)節(jié):
(1)既然做一個(gè)工具欄,首先要判斷能否讀取視頻,換而言之,就是視頻文件的判斷。
(2)獲取視頻幀,外部調(diào)用,一般是兩種,一個(gè)就是時(shí)長(zhǎng)間隔獲取,一個(gè)就是數(shù)量多少來(lái)獲取,本質(zhì)上,還是一個(gè)時(shí)長(zhǎng)間隔的判斷,這里注意時(shí)長(zhǎng)單位以及精度問(wèn)題。
(3)獲取了圖片,則需要保存,保存的時(shí)候,要注意安卓系統(tǒng)版本引起的問(wèn)題。
(4)獲取圖片的過(guò)程,是否需要多線程支持,多線程支持又如何處理等細(xì)節(jié)。
(5)內(nèi)存的釋放,避免內(nèi)存泄漏。
核心代碼如下圖:
package com.marvhong.videoeditor.snap.utils;
import android.content.Context;
import android.graphics.Bitmap;
import android.media.MediaMetadataRetriever;
import android.text.TextUtils;
import android.util.Log;
import com.marvhong.videoeditor.snap.thread.LibVideoExecutorsManager;
import java.io.File;
import java.io.FileOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* FileName: VideoSnapUtils
* Author: lzt
* Date: 2022/4/15 14:39
* 視頻截圖工具欄
*/
public class VideoSnapUtils implements Serializable {
private final String TAG = VideoSnapUtils.class.getSimpleName();
private VideoSnapListener mListener;
private Context mContext;
private final String mRootPath = "LibVideoSnap";
//多線程數(shù)量
private final int THREAD_SIZE = 3;
public VideoSnapUtils(Context context) {
this.mContext = context;
}
//內(nèi)部func-------------------------------------------------------------------------------------
public <T> List<List<T>> splitList(List<T> list, int groupSize) {
if (list.size() <= groupSize) {
List<List<T>> newList = new ArrayList<>(1);
newList.add(list);
return newList;
}
int length = list.size();
// 計(jì)算可以分成多少組
int num = (length + groupSize - 1) / groupSize;
List<List<T>> newList = new ArrayList<>(num);
for (int i = 0; i < num; i++) {
// 開始位置
int fromIndex = i * groupSize;
// 結(jié)束位置
int toIndex = (i + 1) * groupSize < length ? (i + 1) * groupSize : length;
newList.add(list.subList(fromIndex, toIndex));
}
return newList;
}
private String saveBitmap(Bitmap bm, String path, String name) throws Exception {
Log.e(TAG, "保存圖片");
File pathFile = new File(path);
if (!pathFile.exists()) {
createFile(pathFile, false);
}
File file = new File(path + name);
if (!file.exists()) {
createFile(file, true);
}
FileOutputStream out = new FileOutputStream(file);
bm.compress(Bitmap.CompressFormat.PNG, 90, out);
out.flush();
out.close();
return path + name;
}
private void createFile(File file, boolean isFile) throws Exception {// 創(chuàng)建文件
if (!file.exists()) {// 如果文件不存在
if (!file.getParentFile().exists()) {// 如果文件父目錄不存在
createFile(file.getParentFile(), false);
} else {// 存在文件父目錄
if (isFile) {// 創(chuàng)建文件
file.createNewFile();// 創(chuàng)建新文件
} else {
file.mkdir();// 創(chuàng)建目錄
}
}
}
}
/**
* 視頻獲取截圖
* 處理特殊情況,interval或者duration為0的時(shí)候
*/
private void startSnap(String path, long duration, long interval) throws Exception {
String parentPath = mContext.getExternalCacheDir().getPath() + "/" + mRootPath + "/";
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(path);
final List<String> resultList = new ArrayList<>();
if ((duration == 0 || interval == 0) || (interval >= duration)) {
//獲取視頻第一幀
if (mListener != null) {
mListener.start(1);
}
Bitmap firstBit = retriever.getFrameAtTime();
retriever.release();
String firstSavePath = saveBitmap(firstBit, parentPath, System.currentTimeMillis() + ".png");
if (!TextUtils.isEmpty(firstSavePath)) {
resultList.add(firstSavePath);
if (mListener != null) {
mListener.progress(1, 1, firstSavePath);
mListener.finish(resultList);
}
} else {
if (mListener != null) {
mListener.error("save error path is empty");
}
}
if (!firstBit.isRecycled()) {
firstBit.recycle();
}
return;
}
//計(jì)算出每個(gè)截圖的位置
long rest = duration % interval;
final int totalCount;
if (rest > 0) {
totalCount = (int) (duration / interval) + 1;
} else {
totalCount = (int) (duration / interval);
}
if (mListener != null) {
mListener.start(totalCount);
}
List<Long> snapTimeList = new ArrayList<>();
for (int i = 0; i < totalCount; i++) {
snapTimeList.add(i * interval * 1000);
}
//分割list--多線程
long startTime = System.currentTimeMillis();
AtomicInteger countSize = new AtomicInteger(0);
List<List<Long>> splitList = splitList(snapTimeList, THREAD_SIZE);
for (List<Long> cache : splitList) {
LibVideoExecutorsManager.getInstance().getCacheExecutors(TAG).execute(new Runnable() {
@Override
public void run() {
for (int i = 0; i < cache.size(); i++) {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(path);
try {
Bitmap cacheBitmap = retriever.getFrameAtTime(cache.get(i), MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
String savePath = saveBitmap(cacheBitmap, parentPath, System.currentTimeMillis() + String.valueOf(i) + ".png");
if (!cacheBitmap.isRecycled()) {
cacheBitmap.recycle();
}
if (!TextUtils.isEmpty(savePath)) {
resultList.add(savePath);
if (mListener != null) {
if (cache.get(i) == 0) {
mListener.progress(totalCount, 1, savePath);
} else {
mListener.progress(totalCount, (int) (cache.get(i) / 1000 / interval), savePath);
}
if (countSize.incrementAndGet() == totalCount) {
mListener.finish(resultList);
Log.i("snapVideo", "finish time: " + (System.currentTimeMillis() -startTime));
}
}
} else {
throw new Exception("save pic error,path is save failed");
}
retriever.release();
} catch (Exception e) {
retriever.release();
if (mListener != null) {
mListener.error("save error path is empty");
if (countSize.incrementAndGet() == totalCount) {
mListener.finish(resultList);
Log.i("snapVideo", "finish time: " + (System.currentTimeMillis() -startTime));
}
}
break;
}
}
}
});
}
}
//外部func-------------------------------------------------------------------------------------
/**
* 截圖
*
* @param path 視頻路徑
* @param interval 時(shí)間間距
*/
public void startByInterval(String path, long interval) {
LibVideoExecutorsManager.getInstance().getCacheExecutors(TAG).execute(new Runnable() {
@Override
public void run() {
try {
//根據(jù)數(shù)量,計(jì)算間隔時(shí)長(zhǎng)
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(path);
long duration = Long.parseLong(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
retriever.release();
startSnap(path, duration, interval);
} catch (Exception e) {
if (mListener != null) {
mListener.error(e.getMessage());
}
}
}
});
}
/**
* 截圖
*
* @param path 視頻路徑
* @param count 圖片數(shù)量
*/
public void startByCount(String path, int count) {
LibVideoExecutorsManager.getInstance().getCacheExecutors(TAG).execute(new Runnable() {
@Override
public void run() {
try {
//根據(jù)數(shù)量,計(jì)算間隔時(shí)長(zhǎng)
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(path);
long duration = Long.parseLong(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
retriever.release();
long interval = duration / count;
startSnap(path, duration, interval);
} catch (Exception e) {
if (mListener != null) {
mListener.error(e.getMessage());
}
}
}
});
}
/**
* 停止
*/
public void stop() {
removeSnapListener();
LibVideoExecutorsManager.getInstance().closeCacheExecutors(TAG);
}
/**
* 監(jiān)聽
*/
public VideoSnapUtils setOnSnapListener(VideoSnapListener listener) {
this.mListener = listener;
return this;
}
private void removeSnapListener() {
this.mListener = null;
}
}
LibVideoExecutorsManager--就是線程池管理類
VideoSnapListener--就是監(jiān)聽類
that's all--------------------------------------------------------------------------
項(xiàng)目地址