开发RTSP 直播软件 H264 AAC 编码

上一篇对摄像头预览,拍照做了大概的介绍,现在已经可以拿到视频帧了,在加上 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 
  6 #include 
  7 #include 
  8 #include 
  9 #include 
 10 #include 
 11 
 12 #include 
 13 #include 
 14 #include 
 15 
 16 #include 
 17 #include 
 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 %s\n", 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 encoding\n");
 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 encoding\n");
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, ℑ_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  \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 failed\n");
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 found\n", codec_name);
172         exit(1);
173     }
174 
175     c = avcodec_alloc_context3(codec);
176     if (!c) {
177         fprintf(stderr, "Could not allocate video codec context\n");
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: %s\n", av_err2str(ret));
211         exit(1);
212     }
213 
214     f = fopen(filename, "wb");
215     if (!f) {
216         fprintf(stderr, "Could not open %s\n", filename);
217         exit(1);
218     }
219 
220     frame = av_frame_alloc();
221     if (!frame) {
222         fprintf(stderr, "Could not allocate video frame\n");
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 data\n");
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:%s\n", file_image);
251             read_jpeg(file_image, &video_width, &video_height, ℑ_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 以减少服务器压力。