关于前端:使用ffmpeg生成视频缩略图

5次阅读

共计 3919 个字符,预计需要花费 10 分钟才能阅读完成。

整个我的项目在 https://github.com/ximikang/ffmpegThumbnail 公布

生成缩略图的步骤

  1. 应用 ffmpeg 解码视频
  2. 帧格局转换
  3. 依据缩略图的数量从视频流中取帧
  4. 应用 opencv 建设画布并生成缩略图

ffmpeg 解码视频

依据缩略图的数量从视频流中取帧

  1. 获取图片之间的工夫距离
    // Read media file and read the header information from container format
    AVFormatContext* pFormatContext = avformat_alloc_context();
    if (!pFormatContext) {logging("ERROR could not allocate memory for format context");
        return -1;
    }
    
    if (avformat_open_input(&pFormatContext, inputFilePath.string().c_str(), NULL, NULL) != 0) {logging("ERROR could not open media file");
    }
    
    logging("format %s, duration %lld us, bit_rate %lld", pFormatContext->iformat->name, pFormatContext->duration, pFormatContext->bit_rate);
    cout << "视频时常:" << pFormatContext->duration / 1000.0 / 1000.0 << "s" << endl;
    int64_t video_duration = pFormatContext->duration;
    int sum_count = rowNums * colNums;
    // 跳转的距离 ms
    int64_t time_step = video_duration / sum_count / 1000;
  1. 设置跳转工夫获取不同的视频 Packet
    for (int i = 0; i < sum_count ; ++i) {
        cv::Mat tempImage;
        // 每次读取雷同工夫距离的图像并存入 vImage 中
        while (av_read_frame(pFormatContext, pPacket) >= 0) {if (pPacket->stream_index == video_stream_index) {response = decode_packet_2mat(pPacket, pCodecContext, pFrame, tempImage);// 返回
            }
            if (response == 0)// 胜利读取一帧
                break;
            if (response < 0)
                continue;
        }
        vImage.push_back(tempImage);
        // 跳转视频
        av_seek_frame(pFormatContext, -1, ((double)time_step / (double)1000)* AV_TIME_BASE*(double)(i+1) + (double)pFormatContext->start_time, AVSEEK_FLAG_BACKWARD);
    }

3. 获取 Frame
在固定的工夫点可能无奈获取从以后工夫点的 Packet 获取对应的 Frame,所以须要对获取的 Packet 进行判断,如果没有获取到对应的 Frame 应该持续获取下一 Packet 直到获取到对应的 Frame 为止。

static int decode_packet_2mat(AVPacket* pPacket, AVCodecContext* pCodecContext, AVFrame* pFrame, cv::Mat& image) {int response = avcodec_send_packet(pCodecContext, pPacket);

    if (response < 0) {logging("Error while sending a packet to the decoder");
        return response;
    }

    while (response >= 0) {
        // return decoded out data from a decoder
        response = avcodec_receive_frame(pCodecContext, pFrame);
        if (response == AVERROR(EAGAIN) || response == AVERROR_EOF) {logging("averror averror_eof");
            break;
        }
        else if (response < 0) {logging("Error while receiving frame");
            return response;
        }

        if (response >= 0) {
            // 获取到 Frame
            image = frame2Mat(pFrame, pCodecContext->pix_fmt);
        } 
        return 0;
    }
}

帧格局转换

因为从视频流获取的帧是 YUV 格局的 Frame 格局,前面应用 opencv 进行操作所以进行格局转换。

先应用 ffmpeg 中的 SwsContext 将从视频中抽取到的帧从 YUV 转换到 BGR 格局,再从 BGRFrame 中的内存中获取原始数据,并转换到 opencv 的 Mat 类型。

cv::Mat frame2Mat(AVFrame* pFrame, AVPixelFormat pPixFormat)
{
    // image init
    AVFrame* pRGBFrame = av_frame_alloc();
    uint8_t* out_buffer = new uint8_t[avpicture_get_size(AV_PIX_FMT_BGR24, pFrame->width, pFrame->height)];
    avpicture_fill((AVPicture*)pRGBFrame, out_buffer, AV_PIX_FMT_BGR24, pFrame->width, pFrame->height);
    SwsContext* rgbSwsContext = sws_getContext(pFrame->width, pFrame->height, pPixFormat, pFrame->width, pFrame->height, AV_PIX_FMT_BGR24,SWS_BICUBIC, NULL, NULL, NULL);
    if (!rgbSwsContext) {logging("Error could not create frame to rgbframe sws context");
        exit(-1);
    }
    if (sws_scale(rgbSwsContext, pFrame->data, pFrame->linesize, 0, pFrame->height, pRGBFrame->data, pRGBFrame->linesize) < 0) {logging("Error could not sws to rgb frame");
        exit(-1);
    }

    cv::Mat mRGB(cv::Size(pFrame->width, pFrame->height), CV_8UC3);
    mRGB.data = (uchar*)pRGBFrame->data[0];// 留神不能写为:(uchar*)pFrameBGR->data

    av_free(pRGBFrame);
    sws_freeContext(rgbSwsContext);
    return mRGB;
}

应用 opencv 建设画布并生成缩略图

通过画布须要的大小参数,画出红色画布,再对画布进行填充。

cv::Mat makeThumbnail(vector<cv::Mat> vImage, const unsigned int rowNums, const unsigned int colNums)
{
    // 判断图片时候满足条件
    if (vImage.size() != rowNums * colNums) {logging("Error image size not equal input size");
        logging("vImage length: %d, rowNums: %d, col number: %d", vImage.size(), rowNums, colNums);
        exit(-1);
    }
    int interval = 100;
    int height = vImage[0].size().height * rowNums + interval * (rowNums + 1);
    int width = vImage[0].size().width * colNums + interval * (colNums + 1);
    logging("thumbnail size: %d * %d", height, width);
    cv::Mat thumbnail(cv::Size(width, height), CV_8UC3);
    thumbnail.setTo(255);

    // 进行填充
    for (int i = 0; i < rowNums; ++i) {for (int j = 0; j < colNums; ++j) {
            int no = i * rowNums + j;
            int widthOffset = (vImage[0].size().width + interval) * j + interval;
            int heightOffset = (vImage[0].size().height + interval) * i + interval;
            vImage[no].copyTo(thumbnail(cv::Rect(widthOffset, heightOffset, vImage[0].size().width, vImage[0].size().height)));
        }
    }
    return thumbnail;
}

最初的成果

正文完
 0