FFmpeg之FFprobe检测多媒体格式

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数据,提取我们需要的信息,最终数据如下图:

FFmpeg之FFprobe检测多媒体格式_第1张图片

看到上图,伙伴们有木有恍然大悟,感觉到与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

你可能感兴趣的:(音视频开发,android开发,ndk开发)