Cómo hacer una foto con una cámara IP - RTSP - FFMpeg


Última revisión en abril del 2022.
Esta entrada explica cómo hacer una foto desde una cámara IP, primero usando la herramienta ffmpeg desde la línea de comandos y después usando las librerías de FFMpeg desde C.

Puedes encontrar el código de la Raspberry Pi de este post en el repositorio de GitHub: Consolidando.

Desde la línea de comandos

Las cámaras IP disponen del protocolo RTSP que permite recibir su streaming desde otros dispositivos. En nuestro caso vamos a usar FFMpeg para extraer una sóla frame del stream de video y convertirla en una imagen jpeg.

Usando la herramienta ffmpeg desde la línea de comandos sería

	
ffmpeg -rtsp_transport udp (innecesario)
	-i rtsp://IP:PORT/application 
	-f image2 
	-frames:v 1 
	-y test.jpeg
	-hide_banner

Que nos da la siguiente salida por el terminal de la Raspberry donde podemos ver las características de los streams de audio y video de la cámara IP, y el codificador usado para obtener una frame de mpeg desde h264 del stream de video de la cámara IP.

Pero buscamos la flexibilidad que nos da usar directamente las librerías ffmpeg.

Con las librerías de FFMpeg

Hemos creado la estructura RTSPResource que nos permite trabajar con múltiples cámaras en paralelo y la función takeAPicture para hacer una foto donde podemos especificar la calidad (-qscale:v de la herramienta ffmpeg) de la imagen jpeg obtenida.


#include <stddef.h>
#include <unistd.h>
#include "rtsp.h"

#define TMP_FILE_1 "pictureName_1.jpg"
// IP CAM Host
const char *IP_CAM_RESOURCE = "rtsp://192.168.1.24:554/1/h264major";

int main(int argc, char **argv)
{
    RTSPResource resource;
    int ret;

    ret = openIpCam(IP_CAM_RESOURCE, &resource);
    if (ret == 0)
        ret = openVideoCodec(&resource);
    if (ret == 0)
        ret = takeAPicture(&resource, TMP_FILE_1, 9, 0);
    closeVideoCodec(&resource);
    closeIpCam(&resource);
}

Usamos las siguientes librerías de FFMpeg
libavcodec: librería de codecs para codificar y decodificar audio y video.
libavformat: librería que permite muxing y demuxing de audio y video y subtitular.
libavutil: librería con complementos matemáticos, de criptografía ... para ffmpeg.

Abrir el stream de video

La función avformat_find_stream_info tarda unos 2 segundos en detectar el formato del streaming que es lo que determina el retraso a obtener la primera imagen.


int openIpCam(const char *ipCamResource, RTSPResource *pResource)
{
    int ret;

    rtspResourceInit(pResource);

    // Opening the file -------------------------------------------------------
    if (avformat_open_input(&pResource->pFormatContext, ipCamResource, NULL, NULL) != 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Couldn't open file\n");
        return (-1);
    }

    // Retrieve stream information
    if (avformat_find_stream_info(pResource->pFormatContext, NULL) < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Couldn't find stream information\n");
        return (-1);
    }

    // Find the first video stream --------------------------------------------
    pResource->videoStreamIndex = -1;
    for (int i = 0; i < (int)pResource->pFormatContext->nb_streams; ++i)
    {
        if (pResource->pFormatContext->streams[i]->codecpar->codec_type ==
            AVMEDIA_TYPE_VIDEO)
        {
            pResource->videoStreamIndex = i;
            pResource->frameRate = pResource->pFormatContext->streams[i]->avg_frame_rate;
            break;
        }
    }
    if (pResource->videoStreamIndex == -1)
    {
        av_log(NULL, AV_LOG_ERROR, "Didn't find a video stream\n");
        return (-1);
    }

    return (0);
}

Abrir el decodificador

Se abre el decodificador que necesita el stream de video.


int openVideoCodec(RTSPResource *pResource)
{  
    AVCodec *codec = NULL;
    AVDictionary *dict = NULL;
    int ret;

    // Find the decoder for the video stream ----------------------------------
    pResource->pCodecContext = avcodec_alloc_context3(codec);
    avcodec_parameters_to_context(pResource->pCodecContext,
                                  pResource->pFormatContext->streams[pResource->videoStreamIndex]->codecpar);    
    codec = avcodec_find_decoder(pResource->pCodecContext->codec_id);
    if (codec == NULL)
    {
        av_log(NULL, AV_LOG_ERROR, "Codec not found\n");
        return (-1);
    }
    pResource->pCodecContext->codec = codec;

    // Open video decoder ------------------------------------------------------
    ret = avcodec_open2(pResource->pCodecContext, codec, &dict);
    if (ret != 0)
    {
        av_log(NULL, AV_LOG_ERROR, "open codec error\n");
        return (-1);
    }

    pResource->pInputFrame = av_frame_alloc();

    return (0);
}

De packets a frames

Se decodifican los packets en frames


static int decodePacketToFrame(AVCodecContext *avctx, AVFrame *frame, int *pGotFrame, AVPacket *pkt)
{
    int ret;

    *pGotFrame = 0;

    if (pkt)
    {
        ret = avcodec_send_packet(avctx, pkt);
        if (ret < 0 && ret != AVERROR_EOF)
            return ret;
    }

    ret = avcodec_receive_frame(avctx, frame);
    if (ret < 0 && ret != AVERROR(EAGAIN))
        return ret;
    if (ret >= 0)
        *pGotFrame = 1;

    return 0;
}

Leyendo paquetes

La función takeAPicture permite obtener una foto con un retardo en segundos sin cerrar el decodificador.

Tanto cerrar el decodificador como parar de enviarle paquetes implica perder el control sobre la siguiente frame que recibimos del decodificador, dificultando hacer un muestreo.


// takes a picture after a delay in seconds
int takeAPicture(RTSPResource *pResource,
                 const char *pictureName,
                 int jpeqQuality,
                 int delay)
{
    // delay in frames
    int frameDelay = delay * pResource->frameRate.num;

    AVPacket packet;
    av_init_packet(&packet);
    packet.data = NULL;
    packet.size = 0;
    // read a packet from the stream -------------------------------------------
    while ((av_read_frame(pResource->pFormatContext, &packet) >= 0))
    {
        printfTimeDifferenceTag("7");

        if (packet.stream_index == pResource->videoStreamIndex)
        {
            int gotFrame, ret;
            // decode packet to frame ------------------------------------------
            ret = decodePacketToFrame(pResource->pCodecContext, pResource->pInputFrame, &gotFrame, &packet);

            if (gotFrame == 1)
            {
            if ((frameDelay == 0) && (pictureName != NULL))
               {
                   // save video frame as a jpeg image --------------------
                   saveJPEG(pResource->pInputFrame,
                     pResource->pCodecContext->width,
                           pResource->pCodecContext->height,
                           pictureName, jpeqQuality);                   
                }
                av_packet_unref(&packet);

                // flushing the codec ------------------------------------------
                decodePacketToFrame(pResource->pCodecContext, pResource->pInputFrame, &gotFrame, NULL);

                if (frameDelay == 0)
                {
                    break;
                }
                frameDelay--;
            }
            if (ret < 0)
            {
                avcodec_flush_buffers(pResource->pCodecContext);
            }
        }
        av_packet_unref(&packet);
    }

    return (0);
}

De frame a fichero jpeg

El parámetro jpeqQuality equivale a -qscale:v de la herramienta ffmpeg.


static int saveJPEG(AVFrame *frame, int width, int height, const char *out_file, int jpeqQuality)
{
    // Create a new one Output AVFormatContext and allocate memory
    AVFormatContext *output_cxt = NULL;
    avformat_alloc_output_context2(&output_cxt, NULL, "singlejpeg", out_file);

    // Create and initialize an AVIOContext related to the URL
    if (avio_open(&output_cxt->pb, out_file, AVIO_FLAG_READ_WRITE) < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Cannot open file\n");
        return -1;
    }

    // Build a new Stream
    AVStream *stream = avformat_new_stream(output_cxt, NULL);
    if (stream == NULL)
    {
        av_log(NULL, AV_LOG_ERROR, "Failed to create AVStream\n");
        return -1;
    }
    // Initialize AVStream information
    AVCodecContext *codec_cxt = stream->codec;
    codec_cxt->codec_id = output_cxt->oformat->video_codec;
    codec_cxt->codec_type = AVMEDIA_TYPE_VIDEO;
    codec_cxt->pix_fmt = AV_PIX_FMT_YUVJ420P;
    codec_cxt->height = height;
    codec_cxt->width = width;
    codec_cxt->time_base.num = 1;
    codec_cxt->time_base.den = 15;
    codec_cxt->flags |= AV_CODEC_FLAG_QSCALE;
    codec_cxt->global_quality = FF_QP2LAMBDA * jpeqQuality;
    frame->quality = codec_cxt->global_quality;
    //    frame->pict_type = 0;

    avcodec_parameters_from_context(stream->codecpar, codec_cxt);

    AVCodec *codec = avcodec_find_encoder(codec_cxt->codec_id);
    if (!codec)
    {
        av_log(NULL, AV_LOG_ERROR, "Encoder not found\n");
        return -1;
    }

    if (avcodec_open2(codec_cxt, codec, NULL) < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Cannot open the encoder\n");
        return -1;
    }

    // Print output file information
    av_dump_format(output_cxt, 0, out_file, 1);

    // Write the file header, frame and trailer ------------------------------
    avformat_write_header(output_cxt, NULL);
    int size = codec_cxt->width * codec_cxt->height;

    AVPacket packet;
    av_new_packet(&packet, size * 3);

    avcodec_send_frame(codec_cxt, frame);
    if (avcodec_receive_packet(codec_cxt, &packet) == 0)
    {
        av_write_frame(output_cxt, &packet);
    }
    av_write_trailer(output_cxt);

    // release resources ------------------------------------------------------
    av_packet_unref(&packet);
    avio_close(output_cxt->pb);
    avformat_free_context(output_cxt);
    return 0;
}

Para finalizar

La herramienta ffmpeg puede hacer millones de cosas y es fácil de usar. Su código y sus librarías distan mucho de ser triviales debido a su extensión y a la falta de documentación, así que no podemos asegurar que las estemos usando correctamente.

Más información

El código

Puedes encontrar el código la Raspberry Pi de este post en el repositorio de GitHub: Consolidando.


Comentarios