Ffmpeg将安卓Camera输出的YUV数据转为Jpeg

141 阅读3分钟

用途

本次是因为项目中有一个需求,要将camera输出的yuv视频帧转为jpeg,用于视频流传输,在使用途中遇到一点问题,特此记录一下。

注意事项

在安卓的camera中输出的YUV视频帧格式大多是NV21,而Ffmpeg中编码jpeg时需要指定视频格式为AV_PIX_FMT_YUVJ420P,这两种格式存在区别,NV21属于YUV420SP,而Ffmpeg需要的是YUV420P格式,因此在输入数据给Ffmpeg编码前,需要对camera的数据进行格式转换,将NV21转为I420(YUV420P),然后再送入Ffmpeg进行编码。

AVPacket *av_packet_alloc(void)
{
    AVPacket *pkt = static_cast<AVPacket *>(av_mallocz(sizeof(AVPacket)));
    if (!pkt)
        return pkt;

    av_init_packet(pkt);

    return pkt;
}

初始化Jpeg编码器

int MJpegEncoder::Open(int width, int height)
{

    AVCodec* cc_encoder;

    m_width = width;
    m_height = height;

    // 指定编码器为JPEG
    cc_encoder = avcodec_find_encoder(CODEC_ID_MJPEG);
    if(cc_encoder == NULL)
    {
        STACK_LOGE("CMJpegEncoder::Init avcodec_find_decoder(CODEC_ID_MJPEG) failed\n");
        return false;
    }

    // 寻找上下文
    codecContext = avcodec_alloc_context3(cc_encoder);
    if(codecContext == NULL)
    {
        STACK_LOGE("CMJpegEncoder::Init avcodec_alloc_context3(cc_encoder) failed\n");
        return false;
    }

    // 设置宽高以及视频格式
    codecContext->coded_width = m_width;
    codecContext->coded_height = m_height;
    codecContext->time_base.den = 10;
    codecContext->time_base.num = 1;
    // 指定编码后的jpeg为YUVJ420P格式
    codecContext->pix_fmt = AV_PIX_FMT_YUVJ420P;


    if (avcodec_open2(codecContext, cc_encoder, NULL) < 0)
    {
        STACK_LOGE("avcodec_open(mjpeg_decoder_, cc_decoder) failed\n");
        return false;
    }

    // 申请AVFrame,存放原始视频数据
    srcFrame = av_frame_alloc();
    if (!srcFrame)
    {
        Close();
        return false;
    }
    srcFrame->width = m_width;
    srcFrame->height = m_height;
    // 原始视频格式,codecContext指定的是J420P,这里选择同格式420P,这里要保证输入的视频也是420P,否则会出错
    srcFrame->format = AV_PIX_FMT_YUV420P;

    if (av_frame_get_buffer(srcFrame, 0) < 0)
    {
        Close();
        return false;
    }

    // 申请存放编码后的jpeg空间
    dstPacket = av_packet_alloc();
    if (!dstPacket)
    {
        Close();
        return false;
    }

    return true;

}

这里需要注意,在安卓中camera一般输出NV21(YUV420SP),需要将NV21转为I420格式,再调用EncodeFrame进行编码,NV21转I420建议使用libyuv进行转换。

编码Jpeg数据, 输入一个包含YUV420P数据的数组,将编码后的JPEG数据保存带dst_data数组中,并返回数据长度。

int MJpegEncoder::EncodeFrame(u_int8_t* data, int length, uint8_t* dst_data)
{

    if (!codecContext)
        return 0;

    // 将data中的数据填充到srcFrame中
    int ret = av_image_fill_arrays(srcFrame->data, srcFrame->linesize, data, AV_PIX_FMT_YUV420P, m_width, m_height, 1);
    if (ret < 0)
    {
        Close();
        return 0;
    }

    int gotPicture = 0;

    // 编码
    if (avcodec_encode_video2(codecContext, dstPacket, srcFrame, &gotPicture) >= 0 && gotPicture)
    {
        int dst_length = dstPacket->size;
        // 将jpeg数据复制到dst_data中,返回后用于网络传输
        memcpy(dst_data, dstPacket->data, dst_length);
        
        av_packet_unref(dstPacket);
        return dst_length;
    }
    else
    {
        LOGD("jpeg encode failed");
        return 0;
    }

}

关闭JPEG编码器,释放相应数据以及资源

void MJpegEncoder::Close()
{

    if (codecContext != NULL)
    {
        avcodec_flush_buffers(codecContext);
        avcodec_close(codecContext);
        av_free(codecContext);
    }

    if (srcFrame)
    {
        av_frame_free(&srcFrame);
    }

    if (dstPacket)
    {
        av_packet_unref(dstPacket);
        av_free_packet(dstPacket);
    }

}