開發RTSP 直播軟體 H264 AAC 編碼

  • 2020 年 3 月 14 日
  • 筆記

上一篇對攝影機預覽,拍照做了大概的介紹,現在已經可以拿到影片幀了,在加上 RTSP 實現,就是直播的雛形,當然還要加上一些 WEB 管理和手機平台的支援,就是一整套直播軟體。

介紹一些基礎概念:RTP RTSP RTMP

RTP 實時傳輸協議,RTMP 以前  flash 用的影片協議,RTSP 目前比較流行的 直播協議

用到的軟體和第三方庫:ffmpeg live555 VLC

VLC 全平台播放器,win ubuntu mac os android 各個平台都有,功能強大,UI美觀,還沒有廣告。

live555 開源 RTP RTSP 項目

ffmpeg 開源編解碼器,多種格式轉換,加水印,ffplay 更是全能播放器(就是控制做的不行)

ffplay 在 win 上使用時,需要加一個環境變數,否則沒聲音 set SDL_AUDIODRIVER=directsound

1,在本機發布一個 ts 流,用 VLC 和 手機瀏覽器, 進行播放。

ffmpeg -i 1-21.rm -profile:v baseline -level 3.0 -s 640x360 -start_number 0 -hls_time 10 -hls_list_size 0 -f hls 1-12.m3u8

把 ffmpeg 拆分好的文件,複製到 webroot 目錄裡面,然後使用 VLC 播放,也可以通過 html5 的 video 在手機上播放,手機上瀏覽器支援 ts 流比較好

2,RTSP 流 使用 live555 發布

http://live555.com/liveMedia/public/ 下載源碼,編譯安裝,不得不說有的時候在 ubuntu 上開發的確比 win 上簡單。

下載解壓後,執行 ./genMakefiles linux 在 make 會生成 mediaServer/live555MediaServer 運行它,在它的目錄放一些文件,這裡放的是 mkv 的文件,這裡還寫了支援的其它類型的文件。

然後使用 ffplay 進行播放。

使用 wireshark 抓包查看數據包

RTSP 文檔 https://www.rfc-editor.org/rfc/rfc2326.html 自己對照著看吧,如果完全自己從0開發,這些是需要知道的。

3,H264 ACC 編碼

要先找一些原始數據,才能開始編碼。直接從 DirectShow 中,的確是可以拿到數據,每次啟動什麼的還是有點麻煩,所以先生成一些數據,使用 ffmpeg 提取影片為圖片

ffmpeg -i 1.mp4 -r 25 -q:v 2 -f image2 image-%5d.jpg

從 1.mp4 中提取了圖片,幀率是 25 。

win 平台下載編譯好的 lib 比較省心,https://ffmpeg.zeranoe.com/builds/  下載 ffmpeg-4.2.2-win32-dev.zip

linux 可以自行編譯,需要下載很多庫 x264 x265 啥的,這樣看來,還是 win 省心,直接用現成的 lib 就行了。

參考例子 ffmpeg-4.1/doc/examples$

編譯

gcc encode_video.c -lavcodec -lavutil  -o encode_video

gcc muxing.c -lavcodec -lavutil -lswscale -lswresample -lavformat -lm -o muxing

執行 ./encode_video 1.mp4 libx264  ./muxing 2.mp4

使用 ffplay 播放器打開

 

實際上這個是動的,不過 GIF 錄的不好。

H264 中要求是 YUV420P 格式,JPG 默認解碼 RGB  也可以解碼為 JCS_YCbCr 。YCbCr 和 YUV 幾種格式的區別,ffmpeg 中有以下幾種:

AV_PIX_FMT_YUV444P

AV_PIX_FMT_YUV422P

AV_PIX_FMT_YUV420P

其它 RGB 也有幾種 RGB24 – 888 RGB16 – 565 

  1 /**    2  * author:nejidev    3  * date:2020-03-14 12:32    4  */    5 #include <stdio.h>    6 #include <stdlib.h>    7 #include <dirent.h>    8 #include <string.h>    9 #include <jpeglib.h>   10 #include <setjmp.h>   11   12 #include <libavformat/avformat.h>   13 #include <libavcodec/avcodec.h>   14 #include <libswscale/swscale.h>   15   16 #include <libavutil/opt.h>   17 #include <libavutil/imgutils.h>   18 struct my_error_mgr {   19     struct jpeg_error_mgr pub;   20     jmp_buf setjmp_buffer;   21 };   22   23 typedef struct my_error_mgr *my_error_ptr;   24   25 void my_error_exit(j_common_ptr cinfo)   26 {   27     my_error_ptr myerr = (my_error_ptr) cinfo->err;   28     (*cinfo->err->output_message) (cinfo);   29     longjmp(myerr->setjmp_buffer, 1);   30 }   31   32 int read_jpeg(const char *filename, int *out_width, int *out_height, char **out_rgb_buff)   33 {   34     struct jpeg_decompress_struct cinfo;   35     struct my_error_mgr jerr;   36     FILE *infile;   37     char *buffer;   38     char *p;   39     int row_stride;   40   41     if(NULL == (infile = fopen(filename, "rb")))   42     {   43         fprintf(stderr, "can't open %sn", filename);   44         return 0;   45     }   46   47     cinfo.err = jpeg_std_error(&jerr.pub);   48     jerr.pub.error_exit = my_error_exit;   49     if(setjmp(jerr.setjmp_buffer))   50     {   51         jpeg_destroy_decompress(&cinfo);   52         fclose(infile);   53         return 0;   54     }   55     jpeg_create_decompress(&cinfo);   56     jpeg_stdio_src(&cinfo, infile);   57     (void)jpeg_read_header(&cinfo, TRUE);   58   59     (void) jpeg_start_decompress(&cinfo);   60   61     row_stride = cinfo.output_width * cinfo.output_components;   62     buffer = (char *)malloc(row_stride * cinfo.output_height);   63     memset(buffer, 0, row_stride * cinfo.output_height);   64     *out_rgb_buff = buffer;   65   66     *out_width  = cinfo.output_width;   67     *out_height = cinfo.output_height;   68   69     while(cinfo.output_scanline < cinfo.output_height)   70     {   71         p = buffer + cinfo.output_scanline * row_stride;   72         (void) jpeg_read_scanlines(&cinfo, (JSAMPARRAY)&p, 1);   73     }   74   75     (void) jpeg_finish_decompress(&cinfo);   76     jpeg_destroy_decompress(&cinfo);   77     fclose(infile);   78     return 1;   79 }   80   81 static void encode(AVCodecContext *enc_ctx, AVFrame *frame, AVPacket *pkt,   82                    FILE *outfile)   83 {   84     int ret;   85   86     /* send the frame to the encoder */   87     if (frame)   88         printf("Send frame %3"PRId64"n", frame->pts);   89   90     ret = avcodec_send_frame(enc_ctx, frame);   91     if (ret < 0) {   92         fprintf(stderr, "Error sending a frame for encodingn");   93         exit(1);   94     }   95   96     while (ret >= 0) {   97         ret = avcodec_receive_packet(enc_ctx, pkt);   98         if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)   99             return;  100         else if (ret < 0) {  101             fprintf(stderr, "Error during encodingn");  102             exit(1);  103         }  104  105         printf("Write packet %3"PRId64" (size=%5d)n", pkt->pts, pkt->size);  106         fwrite(pkt->data, 1, pkt->size, outfile);  107         av_packet_unref(pkt);  108     }  109 }  110  111 int get_video_size(const char *dir_path, int *out_width, int *out_height)  112 {  113     DIR *dir;  114     struct dirent *file;  115     char filename[256];  116     char *image_buff;  117  118     dir = opendir(dir_path);  119     while(NULL != (file = readdir(dir)))  120     {  121         if(0 != strcmp(".", file->d_name) && 0 != strcmp("..", file->d_name))  122         {  123             snprintf(filename, 256, "%s/%s", dir_path, file->d_name);  124             read_jpeg(filename, out_width, out_height, &image_buff);  125             free(image_buff);  126             break;  127         }  128     }  129  130     closedir(dir);  131     return 0;  132 }  133  134 int main(int argc, char **argv)  135 {  136     const char *filename, *dir_path, *codec_name = "libx264";  137     const AVCodec *codec;  138     AVCodecContext *c= NULL;  139     int i, ret, x, y;  140     FILE *f;  141     AVFrame *frame;  142     AVPacket *pkt;  143     uint8_t endcode[] = { 0, 0, 1, 0xb7 };  144  145     struct SwsContext *sws_ctx = NULL;  146     int video_width;  147     int video_height;  148     int fps = 25;  149  150     DIR *dir;  151     struct dirent *file;  152     char file_image[256];  153     char *image_buff;  154  155     if (argc <= 2) {  156         fprintf(stderr, "Usage: %s <output file> <image dir>n", argv[0]);  157         exit(0);  158     }  159     filename = argv[1];  160     dir_path = argv[2];  161  162     if(get_video_size(dir_path, &video_width, &video_height))  163     {  164         fprintf(stderr, "get video size failedn");  165         exit(1);  166     }  167  168     /* find the mpeg1video encoder */  169     codec = avcodec_find_encoder_by_name(codec_name);  170     if (!codec) {  171         fprintf(stderr, "Codec '%s' not foundn", codec_name);  172         exit(1);  173     }  174  175     c = avcodec_alloc_context3(codec);  176     if (!c) {  177         fprintf(stderr, "Could not allocate video codec contextn");  178         exit(1);  179     }  180  181     pkt = av_packet_alloc();  182     if (!pkt)  183         exit(1);  184  185     /* put sample parameters */  186     c->bit_rate = 400000;  187     /* resolution must be a multiple of two */  188     c->width = video_width;  189     c->height = video_height;  190     /* frames per second */  191     c->time_base = (AVRational){1, fps};  192     c->framerate = (AVRational){fps, 1};  193  194     /* emit one intra frame every ten frames  195      * check frame pict_type before passing frame  196      * to encoder, if frame->pict_type is AV_PICTURE_TYPE_I  197      * then gop_size is ignored and the output of encoder  198      * will always be I frame irrespective to gop_size  199      */  200     c->gop_size = 10;  201     c->max_b_frames = 0;  202     c->pix_fmt = AV_PIX_FMT_YUV420P;  203  204     if (codec->id == AV_CODEC_ID_H264)  205         av_opt_set(c->priv_data, "preset", "slow", 0);  206  207     /* open it */  208     ret = avcodec_open2(c, codec, NULL);  209     if (ret < 0) {  210         fprintf(stderr, "Could not open codec: %sn", av_err2str(ret));  211         exit(1);  212     }  213  214     f = fopen(filename, "wb");  215     if (!f) {  216         fprintf(stderr, "Could not open %sn", filename);  217         exit(1);  218     }  219  220     frame = av_frame_alloc();  221     if (!frame) {  222         fprintf(stderr, "Could not allocate video framen");  223         exit(1);  224     }  225     frame->format = c->pix_fmt;  226     frame->width  = c->width;  227     frame->height = c->height;  228  229     ret = av_frame_get_buffer(frame, 32);  230     if (ret < 0) {  231         fprintf(stderr, "Could not allocate the video frame datan");  232         exit(1);  233     }  234  235     //rgb24 to yun420p  236     sws_ctx = sws_getContext(frame->width, frame->height, AV_PIX_FMT_BGR24,  237                 frame->width, frame->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC,  238                 NULL,NULL,NULL);  239  240     struct dirent **namelist;  241     int n;  242  243     n = scandir(dir_path, &namelist, NULL, alphasort);  244     for(i = 0; i < n; i++)  245     {  246         if(0 != strcmp(".", namelist[i]->d_name) && 0 != strcmp("..", namelist[i]->d_name))  247         {  248             snprintf(file_image, sizeof(file_image), "%s/%s", dir_path, namelist[i]->d_name);  249  250             printf("file_image:%sn", file_image);  251             read_jpeg(file_image, &video_width, &video_height, &image_buff);  252  253             uint8_t *indata[AV_NUM_DATA_POINTERS] = { 0 };  254             indata[0] = image_buff;  255             int inlinesize[AV_NUM_DATA_POINTERS] = { 0 };  256             inlinesize[0] = frame->width * 3;  257  258             ret = sws_scale(sws_ctx, indata, inlinesize, 0, frame->height, frame->data, frame->linesize);  259  260             /* make sure the frame data is writable */  261             ret = av_frame_make_writable(frame);  262             if (ret < 0) exit(1);  263  264             frame->pts = i;  265  266             /* encode the image */  267             encode(c, frame, pkt, f);  268  269             free(image_buff);  270         }  271         free(namelist[i]);  272     }  273     free(namelist);  274     sws_freeContext(sws_ctx);  275  276     closedir(dir);  277  278     /* flush the encoder */  279     encode(c, NULL, pkt, f);  280  281     /* add sequence end code to have a real MPEG file */  282     fwrite(endcode, 1, sizeof(endcode), f);  283     fclose(f);  284  285     avcodec_free_context(&c);  286     av_frame_free(&frame);  287     av_packet_free(&pkt);  288  289     return 0;  290 }

這個是 把 上面拆分的 jpg 圖片合成 264 編碼,編譯方式:gcc encode_video_h264.c -lavcodec -lavutil -lswscale -lswresample -lavformat -ljpeg

這裡使用讀取文件夾內的所有 jpg ,read_jpeg() 是一個用 libjpeg 實現的,得到 jpeg 解碼 RGB 數據的方法,但是 編碼器需要 YUV420P 所以使用 sws_scale 進行轉換。

將轉換好的 xin.264 文件複製到  mediaServer 下面,啟動 live555MediaServer 用 VLC 播放。

 

最終要完成的軟體是 windows 版 攝影機直播軟體,採用技術方案 MFC 、DirectShow、ffmpeg 、live555 。

原理是 DirectShow 採集 RGB 和 PCM 經過 h264 和 aac 編碼以後,送到 live555 通過 RTSP RTP 傳輸,在理想點就是實現 P2P 以減少伺服器壓力。