Problème de synchronisation libavformat / ffmpeg avec x264 et RTP

J’ai travaillé sur un logiciel de diffusion en continu qui utilise des stream en direct de différents types de caméras et de stream sur le réseau en utilisant H.264. Pour ce faire, j’utilise directement l’encodeur x264 (avec le préréglage “zerolatency”) et alimente les NAL car ils sont disponibles pour que libavformat puisse les intégrer à RTP (finalement RTSP). Idéalement, cette application devrait être aussi réelle que possible. Pour la plupart, cela a bien fonctionné.

Malheureusement, il existe un problème de synchronisation: toute lecture vidéo sur les clients semble afficher quelques images lisses, suivies d’une courte pause, puis de plusieurs images; répéter. En outre, il semble y avoir un délai d’environ 4 secondes. Cela se produit avec tous les lecteurs vidéo que j’ai essayés: Totem, VLC et les pipes gstreamer de base.

J’ai tout résumé à un cas de test un peu petit:

#include  #include  #include  #include  #include  #include  #define WIDTH 640 #define HEIGHT 480 #define FPS 30 #define BITRATE 400000 #define RTP_ADDRESS "127.0.0.1" #define RTP_PORT 49990 struct AVFormatContext* avctx; struct x264_t* encoder; struct SwsContext* imgctx; uint8_t test = 0x80; void create_sample_picture(x264_picture_t* picture) { // create a frame to store in x264_picture_alloc(picture, X264_CSP_I420, WIDTH, HEIGHT); // fake image generation // disregard how wrong this is; just writing a quick test int ssortingdes = WIDTH / 8; uint8_t* data = malloc(WIDTH * HEIGHT * 3); memset(data, test, WIDTH * HEIGHT * 3); test = (test <> (8 - 1)); // scale the image sws_scale(imgctx, (const uint8_t* const*) &data, &ssortingdes, 0, HEIGHT, picture->img.plane, picture->img.i_ssortingde); } int encode_frame(x264_picture_t* picture, x264_nal_t** nals) { // encode a frame x264_picture_t pic_out; int num_nals; int frame_size = x264_encoder_encode(encoder, nals, &num_nals, picture, &pic_out); // ignore bad frames if (frame_size oformat = fmt; snprintf(avctx->filename, sizeof(avctx->filename), "rtp://%s:%d", RTP_ADDRESS, RTP_PORT); if (url_fopen(&avctx->pb, avctx->filename, URL_WRONLY) codec; c->codec_id = CODEC_ID_H264; c->codec_type = AVMEDIA_TYPE_VIDEO; c->flags = CODEC_FLAG_GLOBAL_HEADER; c->width = WIDTH; c->height = HEIGHT; c->time_base.den = FPS; c->time_base.num = 1; c->gop_size = FPS; c->bit_rate = BITRATE; avctx->flags = AVFMT_FLAG_RTP_HINT; // write the header av_write_header(avctx); // make some frames for (int frame = 0; frame < 10000; frame++) { // create a sample moving frame x264_picture_t* pic = (x264_picture_t*) malloc(sizeof(x264_picture_t)); create_sample_picture(pic); // encode the frame x264_nal_t* nals; int num_nals = encode_frame(pic, &nals); if (num_nals < 0) printf("invalid frame size: %d\n", num_nals); // send out NALs for (int i = 0; i < num_nals; i++) { stream_frame(nals[i].p_payload, nals[i].i_payload); } // free up resources x264_picture_clean(pic); free(pic); // stream at approx 30 fps printf("frame %d\n", frame); usleep(33333); } return 0; } 

Ce test montre des lignes noires sur un fond blanc qui devraient se déplacer en douceur vers la gauche. Il a été écrit pour ffmpeg 0.6.5 mais le problème peut être reproduit sur 0.8 et 0.10 (d’après ce que j’ai testé jusqu’à présent). J’ai utilisé des raccourcis dans la gestion des erreurs pour rendre cet exemple aussi bref que possible tout en montrant le problème. Veuillez donc excuser le code désagréable. Je dois également noter que même si un SDP n’est pas utilisé ici, j’ai déjà essayé de l’utiliser avec des résultats similaires. Le test peut être compilé avec:

 gcc -g -std=gnu99 streamtest.c -lswscale -lavformat -lx264 -lm -lpthread -o streamtest 

Il peut être joué directement avec gtreamer:

 gst-launch udpsrc port=49990 ! application/x-rtp,payload=96,clock-rate=90000 ! rtph264depay ! decodebin ! xvimagesink 

Vous devriez immédiatement remarquer le bégaiement. Un correctif courant que j’ai vu partout sur Internet consiste à append sync = false au pipeline:

 gst-launch udpsrc port=49990 ! application/x-rtp,payload=96,clock-rate=90000 ! rtph264depay ! decodebin ! xvimagesink sync=false 

Cela rend la lecture fluide (et presque en temps réel), mais ne constitue pas une solution et ne fonctionne qu’avec gstreamer. J’aimerais régler le problème à la source. J’ai été capable de diffuser avec des parameters presque identiques en utilisant raw ffmpeg et je n’ai rencontré aucun problème:

 ffmpeg -re -i sample.mp4 -vcodec libx264 -vpre ultrafast -vpre baseline -b 400000 -an -f rtp rtp://127.0.0.1:49990 -an 

Donc, clairement, je fais quelque chose de mal. Mais qu’est-ce que c’est?

1) Vous n’avez pas défini PTS pour les frameworks que vous envoyez à libx264 (vous devriez probablement voir des avertissements “PTS non ssortingctement monotones”) Il doit être réglé à 100%, mais je suppose que ce serait mieux. D’après le code source, il semble que rtp utilise PTS). 3) IMHO usleep (33333) est mauvais. Cela provoque également le blocage de l’encodeur (augmentation de la latence) pendant que vous pouvez encoder la prochaine image pendant cette période, même si vous n’avez toujours pas besoin de l’envoyer par RTP.

PS, mais vous n’avez pas réglé param.rc.i_rc_method sur X264_RC_ABR, libx264 utilisera donc le CRF 23 et ignorera votre “param.rc.i_bitrate = BITRATE”. En outre, il peut être judicieux d’utiliser VBV lors de l’encodage pour l’envoi réseau.