vintikzzz/ffmpeg-video-info

View on GitHub
ext/ffmpeg_video_info_ext/ffmpeg_video_info_ext.c

Summary

Maintainability
Test Coverage
#include <ruby.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <stdio.h>
#include <libavutil/opt.h>
#include <libavutil/dict.h>
#include <libavutil/pixdesc.h>


void my_dump_metadata(void *ctx, AVDictionary *m, const char *indent, VALUE res)
{
    if(m && !(av_dict_count(m) == 1 && av_dict_get(m, "language", NULL, 0))){
        AVDictionaryEntry *tag=NULL;

        while((tag=av_dict_get(m, "", tag, AV_DICT_IGNORE_SUFFIX))) {
            if(strcmp("language", tag->key)){
                const char *p = tag->value;
                while(*p) {
                    char tmp[256];
                    size_t len = strcspn(p, "\x8\xa\xb\xc\xd");
                    av_strlcpy(tmp, p, FFMIN(sizeof(tmp), len+1));
                    rb_hash_aset(res, rb_str_new2(tag->key), rb_str_new2(tmp));
                    p += len;
                    if (*p) p++;
                }
            }
        }
    }
}

void my_avcodec_string(AVCodecContext *enc, int encode, VALUE res)
{
    const char *codec_type;
    const char *codec_name;
    const char *profile = NULL;
    const AVCodec *p;
    int bitrate;
    AVRational display_aspect_ratio;

    codec_type = av_get_media_type_string(enc->codec_type);
    codec_name = avcodec_get_name(enc->codec_id);
    if (enc->profile != FF_PROFILE_UNKNOWN) {
        if (enc->codec)
            p = enc->codec;
        else
            p = encode ? avcodec_find_encoder(enc->codec_id) :
                        avcodec_find_decoder(enc->codec_id);
        if (p)
            profile = av_get_profile_name(p, enc->profile);
    }

    rb_hash_aset(res, rb_str_new2("type"), rb_str_new2(codec_type ? codec_type : "unknown"));
    rb_hash_aset(res, rb_str_new2("name"), rb_str_new2(codec_name));

    if (enc->codec && strcmp(enc->codec->name, codec_name))
        rb_hash_aset(res, rb_str_new2("codec_name"), rb_str_new2(enc->codec->name));

    if (profile)
        rb_hash_aset(res, rb_str_new2("profile"), rb_str_new2(profile));
    if (enc->codec_tag) {
        char tag_buf[32];
        av_get_codec_tag_string(tag_buf, sizeof(tag_buf), enc->codec_tag);
        rb_hash_aset(res, rb_str_new2("codec_tag"), rb_str_new2(tag_buf));
    }

    switch (enc->codec_type) {
    case AVMEDIA_TYPE_VIDEO:
        // if (enc->pix_fmt != AV_PIX_FMT_NONE) {
        //     const char *colorspace_name;
        //     // rb_hash_aset(res, rb_str_new2("pixel_format"), rb_str_new2(av_get_pix_fmt_name(enc->pix_fmt)));
        //     // if (enc->bits_per_raw_sample &&
        //     //     enc->bits_per_raw_sample <= my_av_pix_fmt_desc_get(enc->pix_fmt)->comp[0].depth_minus1)
        //     //     av_strlcatf(detail, sizeof(detail), "%d bpc, ", enc->bits_per_raw_sample);
        //     // if (enc->color_range != AVCOL_RANGE_UNSPECIFIED)
        //     //     av_strlcatf(detail, sizeof(detail),
        //     //                 enc->color_range == AVCOL_RANGE_MPEG ? "tv, ": "pc, ");

        //     colorspace_name = av_get_colorspace_name(enc->colorspace);
        //     if (colorspace_name)
        //       rb_hash_aset(res, rb_str_new2("color_space"), rb_str_new2(colorspace_name));

        // }
        if (enc->width) {
            rb_hash_aset(res, rb_str_new2("width"), INT2FIX(enc->width));
            rb_hash_aset(res, rb_str_new2("height"), INT2FIX(enc->height));
            if (enc->sample_aspect_ratio.num) {
                av_reduce(&display_aspect_ratio.num, &display_aspect_ratio.den,
                          enc->width * enc->sample_aspect_ratio.num,
                          enc->height * enc->sample_aspect_ratio.den,
                          1024 * 1024);
                VALUE sar = rb_hash_new();
                rb_hash_aset(sar, rb_str_new2("width"), INT2FIX(enc->sample_aspect_ratio.num));
                rb_hash_aset(sar, rb_str_new2("height"), INT2FIX(enc->sample_aspect_ratio.den));
                rb_hash_aset(res, rb_str_new2("SAR"), sar);
                VALUE dar = rb_hash_new();
                rb_hash_aset(dar, rb_str_new2("width"), INT2FIX(display_aspect_ratio.num));
                rb_hash_aset(dar, rb_str_new2("height"), INT2FIX(display_aspect_ratio.den));
                rb_hash_aset(res, rb_str_new2("SAR"), dar);
            }
            // if (av_log_get_level() >= AV_LOG_DEBUG) {
            //     int g = av_gcd(enc->time_base.num, enc->time_base.den);
            //     snprintf(buf + strlen(buf), buf_size - strlen(buf),
            //              ", %d/%d",
            //              enc->time_base.num / g, enc->time_base.den / g);
            // }
        }
        if (encode) {
            rb_hash_aset(res, rb_str_new2("qmin"), INT2FIX(enc->qmin));
            rb_hash_aset(res, rb_str_new2("qmax"), INT2FIX(enc->qmax));
        }
        break;
    case AVMEDIA_TYPE_AUDIO:
        if (enc->sample_rate) {
          rb_hash_aset(res, rb_str_new2("sample_rate"), INT2FIX(enc->sample_rate));
        }
        char layout_buf[32];
        av_get_channel_layout_string(layout_buf, sizeof(layout_buf), enc->channels, enc->channel_layout);
        rb_hash_aset(res, rb_str_new2("channel_layout"), rb_str_new2(layout_buf));
        if (enc->sample_fmt != AV_SAMPLE_FMT_NONE) {
            rb_hash_aset(res, rb_str_new2("sample_format"), rb_str_new2(av_get_sample_fmt_name(enc->sample_fmt)));
        }
        break;
    case AVMEDIA_TYPE_DATA:
        // if (av_log_get_level() >= AV_LOG_DEBUG) {
        //     int g = av_gcd(enc->time_base.num, enc->time_base.den);
        //     if (g)
        //         snprintf(buf + strlen(buf), buf_size - strlen(buf),
        //                  ", %d/%d",
        //                  enc->time_base.num / g, enc->time_base.den / g);
        // }
        break;
    case AVMEDIA_TYPE_SUBTITLE:
        if (enc->width)
            rb_hash_aset(res, rb_str_new2("width"), INT2FIX(enc->width));
            rb_hash_aset(res, rb_str_new2("height"), INT2FIX(enc->height));
        break;
    default:
        return;
    }
    if (encode) {
        if (enc->flags & CODEC_FLAG_PASS1)
            rb_hash_aset(res, rb_str_new2("pass"), INT2FIX(1));
        if (enc->flags & CODEC_FLAG_PASS2)
            rb_hash_aset(res, rb_str_new2("pass"), INT2FIX(2));
    }
    bitrate = get_bit_rate(enc);
    if (bitrate != 0) {
        rb_hash_aset(res, rb_str_new2("bitrate"), INT2FIX(bitrate));
    } else if (enc->rc_max_rate > 0) {
        rb_hash_aset(res, rb_str_new2("bitrate"), INT2FIX(enc->rc_max_rate));
    }
}

void my_dump_stream_format(AVFormatContext *ic, int i, int index, int is_output, VALUE res)
{
    int flags = (is_output ? ic->oformat->flags : ic->iformat->flags);
    AVStream *st = ic->streams[i];
    int g = av_gcd(st->time_base.num, st->time_base.den);
    AVDictionaryEntry *lang = av_dict_get(st->metadata, "language", NULL, 0);
    my_avcodec_string(st->codec, is_output, res);
    /* the pid is an important information, so we display it */
    /* XXX: add a generic system */
    if (flags & AVFMT_SHOW_IDS)
      rb_hash_aset(res, rb_str_new2("show_id"), INT2FIX(st->id));
    if (lang)
      rb_hash_aset(res, rb_str_new2("language"), rb_str_new2(lang->value));
    if (st->sample_aspect_ratio.num && // default
        av_cmp_q(st->sample_aspect_ratio, st->codec->sample_aspect_ratio)) {
        AVRational display_aspect_ratio;
        av_reduce(&display_aspect_ratio.num, &display_aspect_ratio.den,
                  st->codec->width*st->sample_aspect_ratio.num,
                  st->codec->height*st->sample_aspect_ratio.den,
                  1024*1024);
        VALUE sar = rb_hash_new();
        rb_hash_aset(sar, rb_str_new2("width"), INT2FIX(st->sample_aspect_ratio.num));
        rb_hash_aset(sar, rb_str_new2("height"), INT2FIX(st->sample_aspect_ratio.den));
        rb_hash_aset(res, rb_str_new2("SAR"), sar);
        VALUE dar = rb_hash_new();
        rb_hash_aset(dar, rb_str_new2("width"), INT2FIX(display_aspect_ratio.num));
        rb_hash_aset(dar, rb_str_new2("height"), INT2FIX(display_aspect_ratio.den));
        rb_hash_aset(res, rb_str_new2("SAR"), dar);
    }
    if(st->codec->codec_type == AVMEDIA_TYPE_VIDEO){
        if(st->avg_frame_rate.den && st->avg_frame_rate.num)
          rb_hash_aset(res, rb_str_new2("fps"), INT2FIX(av_q2d(st->avg_frame_rate)));
    }
    if (st->disposition & AV_DISPOSITION_DEFAULT)
        rb_hash_aset(res, rb_str_new2("disposition"), rb_str_new2("default"));
    if (st->disposition & AV_DISPOSITION_DUB)
        rb_hash_aset(res, rb_str_new2("disposition"), rb_str_new2("dub"));
    if (st->disposition & AV_DISPOSITION_ORIGINAL)
        rb_hash_aset(res, rb_str_new2("disposition"), rb_str_new2("original"));
    if (st->disposition & AV_DISPOSITION_COMMENT)
        rb_hash_aset(res, rb_str_new2("disposition"), rb_str_new2("comment"));
    if (st->disposition & AV_DISPOSITION_LYRICS)
        rb_hash_aset(res, rb_str_new2("disposition"), rb_str_new2("lyrics"));
    if (st->disposition & AV_DISPOSITION_KARAOKE)
        rb_hash_aset(res, rb_str_new2("disposition"), rb_str_new2("karaoke"));
    if (st->disposition & AV_DISPOSITION_FORCED)
        rb_hash_aset(res, rb_str_new2("disposition"), rb_str_new2("forced"));
    if (st->disposition & AV_DISPOSITION_HEARING_IMPAIRED)
        rb_hash_aset(res, rb_str_new2("disposition"), rb_str_new2("hearing impaired"));
    if (st->disposition & AV_DISPOSITION_VISUAL_IMPAIRED)
        rb_hash_aset(res, rb_str_new2("disposition"), rb_str_new2("visual impaired"));
    if (st->disposition & AV_DISPOSITION_CLEAN_EFFECTS)
        rb_hash_aset(res, rb_str_new2("disposition"), rb_str_new2("clean effects"));
    VALUE metadata = rb_hash_new();
    my_dump_metadata(NULL, st->metadata, "    ", metadata);
    rb_hash_aset(res, rb_str_new2("metadata"), metadata);
}

void my_av_dump_format(AVFormatContext *ic, int index, const char *url, int is_output, VALUE res)
{
    int i;
    uint8_t *printed = ic->nb_streams ? av_mallocz(ic->nb_streams) : NULL;
    if (ic->nb_streams && !printed)
        return;

    rb_hash_aset(res, rb_str_new2("format_name"), rb_str_new2(ic->iformat->name));
    rb_hash_aset(res, rb_str_new2("file_name"), rb_str_new2(url));
    VALUE metadata = rb_hash_new();
    my_dump_metadata(NULL, ic->metadata, "  ", metadata);
    rb_hash_aset(res, rb_str_new2("metadata"), metadata);
    if (!is_output) {
        if (ic->duration != AV_NOPTS_VALUE) {
            int hours, mins, secs, us;
            int64_t duration = ic->duration + 5000;
            secs = duration / AV_TIME_BASE;
            us = duration % AV_TIME_BASE;
            mins = secs / 60;
            secs %= 60;
            hours = mins / 60;
            mins %= 60;
            // av_log(NULL, AV_LOG_INFO, "%02d:%02d:%02d.%02d", hours, mins, secs,
            //        (100 * us) / AV_TIME_BASE);
            rb_hash_aset(res, rb_str_new2("duration"), INT2FIX(hours*3600 + mins*60 + secs));
        }
        if (ic->start_time != AV_NOPTS_VALUE) {
            int secs, us;
            secs = ic->start_time / AV_TIME_BASE;
            us = abs(ic->start_time % AV_TIME_BASE);
            rb_hash_aset(res, rb_str_new2("start"), INT2FIX((int)av_rescale(us, 1000000, AV_TIME_BASE)));
        }
        if (ic->bit_rate) {
            rb_hash_aset(res, rb_str_new2("bitrate"), INT2FIX(ic->bit_rate));
        }
    }
    // for (i = 0; i < ic->nb_chapters; i++) {
    //     AVChapter *ch = ic->chapters[i];
    //     av_log(NULL, AV_LOG_INFO, "    Chapter #%d.%d: ", index, i);
    //     av_log(NULL, AV_LOG_INFO, "start %f, ", ch->start * av_q2d(ch->time_base));
    //     av_log(NULL, AV_LOG_INFO, "end %f\n",   ch->end   * av_q2d(ch->time_base));

    //     VALUE metadata = rb_hash_new();
    //     my_dump_metadata(NULL, ch->metadata, "    ", metadata);
    // }
    // if(ic->nb_programs) {
    //     int j, k, total = 0;
    //     for(j=0; j<ic->nb_programs; j++) {
    //         AVDictionaryEntry *name = av_dict_get(ic->programs[j]->metadata,
    //                                               "name", NULL, 0);
    //         av_log(NULL, AV_LOG_INFO, "  Program %d %s\n", ic->programs[j]->id,
    //                name ? name->value : "");
    //         VALUE metadata = rb_hash_new();
    //         my_dump_metadata(NULL, ic->programs[j]->metadata, "    ", metadata);
    //         for(k=0; k<ic->programs[j]->nb_stream_indexes; k++) {
    //             my_dump_stream_format(ic, ic->programs[j]->stream_index[k], index, is_output, res);
    //             printed[ic->programs[j]->stream_index[k]] = 1;
    //         }
    //         total += ic->programs[j]->nb_stream_indexes;
    //     }
    //     if (total < ic->nb_streams)
    //         av_log(NULL, AV_LOG_INFO, "  No Program\n");
    // }
    VALUE streams = rb_ary_new();
    for(i=0;i<ic->nb_streams;i++)
    {
        if (!printed[i])
        {
            VALUE stream = rb_hash_new();
            my_dump_stream_format(ic, i, index, is_output, stream);
            rb_ary_push(streams, stream);
        }
    }
    rb_hash_aset(res, rb_str_new2("streams"), streams);

    av_free(printed);
}

int get_bit_rate(AVCodecContext *ctx)
{
    int bit_rate;
    int bits_per_sample;

    switch (ctx->codec_type) {
    case AVMEDIA_TYPE_VIDEO:
    case AVMEDIA_TYPE_DATA:
    case AVMEDIA_TYPE_SUBTITLE:
    case AVMEDIA_TYPE_ATTACHMENT:
        bit_rate = ctx->bit_rate;
        break;
    case AVMEDIA_TYPE_AUDIO:
        bits_per_sample = av_get_bits_per_sample(ctx->codec_id);
        bit_rate = bits_per_sample ? ctx->sample_rate * ctx->channels * bits_per_sample : ctx->bit_rate;
        break;
    default:
        bit_rate = 0;
        break;
    }
    return bit_rate;
}

VALUE get_info(VALUE self, VALUE arg)
{
  const char* file = StringValueCStr(arg);
  VALUE res = rb_hash_new();
  // Register all available file formats and codecs
  av_register_all();

  int err;

  // Open video file
  AVFormatContext* format_context = NULL;
  err = avformat_open_input(&format_context, file, NULL, NULL);
  if (err < 0) {
    rb_raise(rb_eRuntimeError, "ffmpeg: Unable to open input file");
  }

  // Retrieve stream information
  err = avformat_find_stream_info(format_context, NULL);
  if (err < 0) {
    rb_raise(rb_eRuntimeError, "ffmpeg: Unable to find stream info");
  }

  // Dump information about file onto standard error
  my_av_dump_format(format_context, 0, file, 0, res);

  // Close the video file
  avformat_close_input(&format_context);

  return res;
}
static VALUE rb_mFFmpegVideoInfo;
void Init_ffmpeg_video_info_ext()
{
  rb_mFFmpegVideoInfo = rb_define_module("FFmpegVideoInfoExt");
  rb_define_singleton_method(rb_mFFmpegVideoInfo, "get", get_info, 1);
}