FFmpeg裏面有一個模塊FFprobe(https://ffmpeg.org/ffprobe.html)專門用來檢測多媒體格式數據,它的作用類似Android中的MediaMetadataRetriever。FFprobe支持檢測format、streams、frames,用法與FFmpeg類似,我們可以單獨使用,也可以結合在一起使用,下面舉例說明一下:
1、format
僅是顯示format:ffprobe -i xxx.mp4 -show_format
既顯示又打印format:ffprobe -i xxx.mp4 -show_format -print_format json
2、streams
顯示音視頻流:ffprobe -i xxx.mp4 -show_streams
3、frames
顯示視頻幀:ffprobe -i xxx.mp4 -show_frames
4、format與streams
ffprobe -i xxx.mp4 -show_streams -show_format -print_format json
最終打印出來是json格式(也可以選擇xml格式),我們需要解析一下json數據,提取我們需要的信息,最終數據如下圖:
看到上圖,夥伴們有木有恍然大悟,感覺到與Android的MediaMetadataRetriever的功能似曾相識呢?FFprobe可以檢測的多媒體信息包括:時長、碼率、文件大小、封裝格式、多媒體流的個數、視頻分辨率、寬高比、像素格式、幀率、視頻編碼器、音頻採樣率、聲道個數、聲道佈局、音頻編碼器等等。它比Android自帶的MediaMetadataRetriever的優勢在於:ffprobe可支持更多多媒體格式,兼容性好;而MediaMetadataRetriever取決於系統的硬解碼器,不同手機的MediaCodec編解碼能力存在差異。下面我們來具體討論下FFprobe檢測多媒體信息的實現過程:
一、修改ffprobe源碼
在ffprobe.c源碼中,是沒有對外方法提供jni調用的,也沒有返回字符串結果。所以我們需要修改下ffprobe.c源碼:首先把main方法改爲ffprobe_run方法,並且在ffprobe.h裏面聲明該方法,然後增加字符串打印方法。
//ffprobe主函數入口
char* ffprobe_run(int argc, char **argv)
{
const Writer *w;
WriterContext *wctx;
char *buf;
char *w_name = NULL, *w_args = NULL;
int ret, i;
//動態申請內存
buffer_length = 0;
if(print_buffer == NULL) {
print_buffer = av_malloc(sizeof(char) * buffer_size);
}
memset(print_buffer, '\0', (size_t) buffer_size);
...
return print_buffer;
}
編寫打印json字符串方法:
void frank_printf_json(char *fmt, ...)
{
va_list args;
va_start(args, fmt);
int length = printf_json(print_buffer + buffer_length, buffer_size - buffer_length, fmt, args);
buffer_length += length;
va_end(args);
}
在解析多媒體格式時,調用打印json字符串方法,把數據寫入內存裏:
static void json_print_section_header(WriterContext *wctx)
{
JSONContext *json = wctx->priv;
AVBPrint buf;
const struct section *section = wctx->section[wctx->level];
const struct section *parent_section = wctx->level ?
wctx->section[wctx->level-1] : NULL;
if (wctx->level && wctx->nb_item[wctx->level-1])
frank_printf_json(",\n");
if (section->flags & SECTION_FLAG_IS_WRAPPER) {
frank_printf_json("{\n");
json->indent_level++;
} else {
av_bprint_init(&buf, 1, AV_BPRINT_SIZE_UNLIMITED);
json_escape_str(&buf, section->name, wctx);
JSON_INDENT();
json->indent_level++;
if (section->flags & SECTION_FLAG_IS_ARRAY) {
frank_printf_json("\"%s\": [\n", buf.str);
} else if (parent_section && !(parent_section->flags & SECTION_FLAG_IS_ARRAY)) {
frank_printf_json("\"%s\": {%s", buf.str, json->item_start_end);
} else {
frank_printf_json("{%s", json->item_start_end);
/* this is required so the parser can distinguish between packets and frames */
if (parent_section && parent_section->id == SECTION_ID_PACKETS_AND_FRAMES) {
if (!json->compact)
JSON_INDENT();
frank_printf_json("\"type\": \"%s\"%s", section->name, json->item_sep);
}
}
av_bprint_finalize(&buf, NULL);
}
}
static void json_print_section_footer(WriterContext *wctx)
{
JSONContext *json = wctx->priv;
const struct section *section = wctx->section[wctx->level];
if (wctx->level == 0) {
json->indent_level--;
frank_printf_json("\n}\n");
} else if (section->flags & SECTION_FLAG_IS_ARRAY) {
frank_printf_json("\n");
json->indent_level--;
JSON_INDENT();
frank_printf_json("]");
} else {
frank_printf_json("%s", json->item_start_end);
json->indent_level--;
if (!json->compact)
JSON_INDENT();
frank_printf_json("}");
}
}
static inline void json_print_item_str(WriterContext *wctx,
const char *key, const char *value)
{
AVBPrint buf;
av_bprint_init(&buf, 1, AV_BPRINT_SIZE_UNLIMITED);
frank_printf_json("\"%s\":", json_escape_str(&buf, key, wctx));
av_bprint_clear(&buf);
frank_printf_json(" \"%s\"", json_escape_str(&buf, value, wctx));
av_bprint_finalize(&buf, NULL);
}
static void json_print_str(WriterContext *wctx, const char *key, const char *value)
{
JSONContext *json = wctx->priv;
if (wctx->nb_item[wctx->level])
frank_printf_json("%s", json->item_sep);
if (!json->compact)
JSON_INDENT();
json_print_item_str(wctx, key, value);
}
static void json_print_int(WriterContext *wctx, const char *key, long long int value)
{
JSONContext *json = wctx->priv;
AVBPrint buf;
if (wctx->nb_item[wctx->level])
frank_printf_json("%s", json->item_sep);
if (!json->compact)
JSON_INDENT();
av_bprint_init(&buf, 1, AV_BPRINT_SIZE_UNLIMITED);
frank_printf_json("\"%s\": %lld", json_escape_str(&buf, key, wctx), value);
av_bprint_finalize(&buf, NULL);
}
二、封裝jni方法
提供jni方法,供java層調用ffprobe_run(),實現多媒體格式的解析:
FFMPEG_FUNC(jstring , handleProbe, jobjectArray commands) {
int argc = (*env)->GetArrayLength(env, commands);
char **argv = (char**)malloc(argc * sizeof(char*));
int i;
for (i = 0; i < argc; i++) {
jstring jstr = (jstring) (*env)->GetObjectArrayElement(env, commands, i);
char* temp = (char*) (*env)->GetStringUTFChars(env, jstr, 0);
argv[i] = malloc(1024);
strcpy(argv[i], temp);
(*env)->ReleaseStringUTFChars(env, jstr, temp);
}
//execute ffprobe command
char* result = ffprobe_run(argc, argv);
//release memory
for (i = 0; i < argc; i++) {
free(argv[i]);
}
free(argv);
return (*env)->NewStringUTF(env, result);
}
三、java層調用jni方法
在java層使用native關鍵字聲明jni方法,開啓子線程調用:
/**
* execute probe cmd internal
* @param commands commands
* @param onHandleListener onHandleListener
*/
public static void executeProbe(final String[] commands, final OnHandleListener onHandleListener) {
new Thread(new Runnable() {
@Override
public void run() {
if(onHandleListener != null) {
onHandleListener.onBegin();
}
//execute ffprobe
String result = handleProbe(commands);
int resultCode = !TextUtils.isEmpty(result) ? RESULT_SUCCESS : RESULT_ERROR;
if(onHandleListener != null) {
onHandleListener.onEnd(resultCode, result);
}
}
}).start();
}
private native static String handleProbe(String[] commands);
四、監聽FFprobe運行狀態
傳入字符串參數,調用executeFFprobeCmd方法,實現onHandlerListener監聽:
/**
* execute probe cmd
* @param commandLine commandLine
*/
public void executeFFprobeCmd(final String[] commandLine) {
if(commandLine == null) {
return;
}
FFmpegCmd.executeProbe(commandLine, new OnHandleListener() {
@Override
public void onBegin() {
mHandler.obtainMessage(MSG_BEGIN).sendToTarget();
}
@Override
public void onEnd(int resultCode, String resultMsg) {
MediaBean mediaBean = null;
if(resultMsg != null && !resultMsg.isEmpty()) {
mediaBean = JsonParseTool.parseMediaFormat(resultMsg);
}
mHandler.obtainMessage(MSG_FINISH, mediaBean).sendToTarget();
}
});
}
五、封裝FFprobe命令
在文章的開頭,我們已經介紹過ffprobe命令,更多更詳細的命令請參考文檔:https://ffmpeg.org/ffprobe.html.ffprobe命令結構分爲三部分:ffprobe+(-i filePath)輸入文件路徑+(-show_streams -show_frames -show_format)執行主體。
public static String[] probeFormat(String inputFile) {
String ffprobeCmd = "ffprobe -i %s -show_streams -show_format -print_format json";
ffprobeCmd = String.format(Locale.getDefault(), ffprobeCmd, inputFile);
return ffprobeCmd.split(" ");
}
六、解析json數據
調用FFprobe函數,返回json字符串結果後,我們需要進一步解析,提取我們需要的信息:
public static MediaBean parseMediaFormat(String mediaFormat) {
if (mediaFormat == null || mediaFormat.isEmpty()) {
return null;
}
MediaBean mediaBean = null;
try {
JSONObject jsonMedia = new JSONObject(mediaFormat);
JSONObject jsonMediaFormat = jsonMedia.getJSONObject("format");
mediaBean = new MediaBean();
int streamNum = jsonMediaFormat.optInt("nb_streams");
mediaBean.setStreamNum(streamNum);
String formatName = jsonMediaFormat.optString("format_name");
mediaBean.setFormatName(formatName);
String bitRateStr = jsonMediaFormat.optString("bit_rate");
if (!TextUtils.isEmpty(bitRateStr)) {
mediaBean.setBitRate(Integer.valueOf(bitRateStr));
}
String sizeStr = jsonMediaFormat.optString("size");
if (!TextUtils.isEmpty(sizeStr)) {
mediaBean.setSize(Long.valueOf(sizeStr));
}
String durationStr = jsonMediaFormat.optString("duration");
if (!TextUtils.isEmpty(durationStr)) {
float duration = Float.valueOf(durationStr);
mediaBean.setDuration((long) duration);
}
JSONArray jsonMediaStream = jsonMedia.getJSONArray("streams");
if (jsonMediaStream == null) {
return mediaBean;
}
for (int index = 0; index < jsonMediaStream.length(); index ++) {
JSONObject jsonMediaStreamItem = jsonMediaStream.optJSONObject(index);
if (jsonMediaStreamItem == null) continue;
String codecType = jsonMediaStreamItem.optString("codec_type");
if (codecType == null) continue;
if (codecType.equals(TYPE_VIDEO)) {
VideoBean videoBean = new VideoBean();
mediaBean.setVideoBean(videoBean);
String codecName = jsonMediaStreamItem.optString("codec_tag_string");
videoBean.setVideoCodec(codecName);
int width = jsonMediaStreamItem.optInt("width");
videoBean.setWidth(width);
int height = jsonMediaStreamItem.optInt("height");
videoBean.setHeight(height);
String aspectRatio = jsonMediaStreamItem.optString("display_aspect_ratio");
videoBean.setDisplayAspectRatio(aspectRatio);
String pixelFormat = jsonMediaStreamItem.optString("pix_fmt");
videoBean.setPixelFormat(pixelFormat);
String profile = jsonMediaStreamItem.optString("profile");
videoBean.setProfile(profile);
int level = jsonMediaStreamItem.optInt("level");
videoBean.setLevel(level);
String frameRateStr = jsonMediaStreamItem.optString("r_frame_rate");
if (!TextUtils.isEmpty(frameRateStr)) {
String[] frameRateArray = frameRateStr.split("/");
double frameRate = Math.ceil(Double.valueOf(frameRateArray[0]) / Double.valueOf(frameRateArray[1]));
videoBean.setFrameRate((int) frameRate);
}
} else if (codecType.equals(TYPE_AUDIO)) {
AudioBean audioBean = new AudioBean();
mediaBean.setAudioBean(audioBean);
String codecName = jsonMediaStreamItem.optString("codec_tag_string");
audioBean.setAudioCodec(codecName);
String sampleRateStr = jsonMediaStreamItem.optString("sample_rate");
if (!TextUtils.isEmpty(sampleRateStr)) {
audioBean.setSampleRate(Integer.valueOf(sampleRateStr));
}
int channels = jsonMediaStreamItem.optInt("channels");
audioBean.setChannels(channels);
String channelLayout = jsonMediaStreamItem.optString("channel_layout");
audioBean.setChannelLayout(channelLayout);
}
}
} catch (Exception e) {
Log.e(TAG, "parse error=" + e.toString());
}
return mediaBean;
}
折騰這麼久,終於解析到多媒體格式相關數據了,詳細源碼可以到Github查看:https://github.com/xufuji456/FFmpegAndroid