侧边栏壁纸
    • 累计撰写 296 篇文章
    • 累计收到 520 条评论
    嵌入式视频流知识点及代码解析-精简版
    我的学记|刘航宇的博客

    嵌入式视频流知识点及代码解析-精简版

    刘航宇
    2022-01-04 / 0 评论 / 705 阅读 / 正在检测是否收录...

    目录

    背景和意义

    (1)视频的带宽很大,存储,传输不便,故要压缩、解压 、播放。
    (2)应用领域很广 ,交通,在线教育,播放器,自动驾驶。

    框架

    Test
    下面这个图及其重要以及3个ip关系
    abcde代表先后实现顺序注意观看!!!
    TO67M8.png

    代码及相关知识点

    一、知识点篇

    Live 555:
    是一个为流媒体提供解决方案的跨平台的C++开源项目,它实现了标准流媒体传输,对标准流媒体传输协议如RTP/RTCP、RTSP、SIP等的支持。Live555实现了对多种音视频编码格式的音视频数据的流化、接收和处理等支持,包括MPEG、H.263+、DV、JPEG视频和多种音频编码。同时由于良好的设计,Live555非常容易扩展对其他格式的支持。Live555已经被用于多款播放器的流媒体播放功能的实现,如VLC(VideoLan)、MPlayer。
    在本次开发实践中主要用于接收海康威视摄像头的RTP数据包 并通过UDP网络进行转发给PC机。
    FFmpeg:
    Fmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库libavcodec,为了保证高可移植性和编解码质量,libavcodec里很多code都是从头开发的。
    FFmpeg在Linux平台下开发,但它同样也可以在其它操作系统环境中编译运行,包括Windows、Mac OS X等。项目的名称来自MPEG视频编码标准,前面的"FF"代表"Fast Forward"。 在本次开发实践中主要用于H264数据解码。
    SDL:
    SDL(Simple DirectMedia Layer)是一套开放源代码的跨平台多媒体开发库,使用C语言写成。SDL提供了数种控制图像、声音、输出入的函数,让开发者只要用相同或是相似的代码就可以开发出跨多个平台(Linux、Windows、Mac OS X等)的应用软件。目前SDL多用于开发游戏、模拟器、媒体播放器等多媒体应用领域。在本次开发实践中主要用于YUV数据的显示。

    二、问答篇

    1.什么是YUV?与RGB有什么不同?
    YUV 是一种颜色编码方法,FFmpeg 解码后的数据格式。Y 表示明亮度 U 表示色
    度 V 表示浓度。因为通过研究发现,人类对于图像的感知中对明亮最侧重,色彩和
    浓度就相对不那么重要,所以在保存图片时,让明亮度占较多的比重,有效的在不影
    响观看的情况下节约了空间。视频播放器解码出来的格式为 YUV420P,其中明亮度
    占整个数据的 2/3,色度和浓度占 1/3。
    2.RTSP在什么层?答:应用层
    3.你关于IP问题你用到了那些命令?答:ipconfig,ifconfig,ping等
    4.本次课程设计你用到了那些去年学的知识?答:网络通信,文件开关与读写,第一章shell操作命令
    下面部分自行百度:
    5.TCP与UDP特点(TCP的可靠,UDP 的不可靠,UDP快)
    6.简述TCP与UDP
    7.了解HTTP/https
    8.三次握手和四次挥手过程

    三、代码篇

    RTSP程序要不要等待播放器器程序请求?

    答:要

    请你找出上述代码所在位置

    答:如图所示
    Test

    live555(了解)

    BasicTaskScheduler 的父类是BasicTaskScheduler0
    BasicTaskScheduler0是一个用作传递的类,它继承自TaskScheduler,又派生出BasicTaskScheduler。其定义在live555sourcecontrol\UsageEnvironment\include\BasicUsageEnvironment0.hh文件中。
    BasicTaskScheduler0中有
    BasicTaskScheduler 这个类主要实现事件的处理
    BasicUsageEnvironment 涉及调试语句,输出语句
    ourRTSPClient 主要是涉及的数据的发送相关的功能函数,主要的功能继承于父类RTSPClient
    RequestRecord 创建一个请求记录对象,并将回调函数与之关联
    sendRequest
    第一次进入调用:openConnection解析URL,并调用setupStreamSocket和connectToServer,然后
    envir().taskScheduler().setBackgroundHandling(fInputSocketNum, SOCKET_READABLE|SOCKET_EXCEPTION, (TaskScheduler::BackgroundHandlerProc*)&incomingDataHandler, this);
    若不是第一次调用,则打包 RTSP 包协议,并调用
    Send函数发送到服务器。 重点
    RequestRecord入队列 等候读取数据。
    setupStreamSocket调用createSocket ,createSocket调用:
    sock = socket(AF_INET, type, 0);
    connectToServer调用
    connect(socketNum, (struct sockaddr*) &remoteName, sizeof remoteName)
    setBackgroundHandling 主要初始化select 函数的 文件描述符集合
    incomingDataHandler 中包含函数
    readSocket 调用
    int bytesRead = recvfrom(socket, (char*)buffer, bufferSize, 0,

           (struct sockaddr*)&fromAddress,
           &addressSize);

    到此发送描述命令结束 ,等待响应服务器发送过来的数据。
    incomingDataHandler1读取服务器的数据
    incomingDataHandler1先调用readSocket调用,然后调用
    handleResponseBytes 解析RTSP服务器数据
    调用(*foundRequest->handler())(this, resultCode, resultString);
    这个函数就是continueAfterDESCRIBE
    continueAfterDESCRIBE 调用下一步 setup 功能

    SDL

    rb为只读,对于不需要进行更新的文件,可以防止用户的错误的写回操作,防止损毁原有数据。具有较高的安全性。
    rb+为更新二进制文件,可以读取,同时也可以写入,需要用到fseek之类的函数进行配合,以免出错,对于需要不时更新的文件,比如信息管理系统中的数据,可以这样打开。

    1. 初始化SDL
      使用SDL_Init()初始化SDL。该函数可以确定希望激活的子系统。
      int SDLCALL SDL_Init(Uint32 flags)
      SDL_INIT_VIDEO:视频
    2. 创建窗口(Window)
      使用SDL_CreateWindow()创建一个用于视频播放的窗口。
      SDL_Window SDLCALL SDL_CreateWindow(const char title,int x, int y, int w,int h, Uint32 flags);
      SDL_CreatWindow:第一个参数是窗口名字,第二三是窗口的坐标(SDL_winpos_undefined 为采用系统默认)
      title :窗口标题
      x :窗口位置x坐标。也可以设置为SDL_WINDOWPOS_CENTERED或SDL_WINDOWPOS_UNDEFINED。
      y :窗口位置y坐标。同上。
      w :窗口的宽
      h :窗口的高
      flags :支持下列标识。包括了窗口的是否最大化、最小化,能否调整边界等等属性。
      flags:SDL_WINDOW_RESIZABLE 自动调整窗口
    3. 基于窗口创建渲染器(Render)
      使用SDL_CreateRenderer()基于窗口创建渲染器
      SDL_Renderer SDLCALL SDL_CreateRenderer(SDL_Window window,int index, Uint32 flags);
      window: 渲染的目标窗口。
      index:打算初始化的渲染设备的索引。设置“-1”则初始化默认的渲染设备。
      SDL_RENDERER_PRESENTVSYNC:和显示器的刷新率同步
    4. 创建纹理(Texture)
      使用SDL_CreateTexture()基于渲染器创建一个纹理
      SDL_Texture SDLCALL SDL_CreateTexture(SDL_Renderer renderer,Uint32 format,int access, int w,int h);
      renderer:目标渲染器。
      format:纹理的格式。
      access:定义位于SDL_TextureAccess中
      access:SDL_TEXTUREACCESS_STREAMING :变化频繁
      w:纹理的宽
      h:纹理的高
    5. SDL_Thread *refresh_thread = SDL_CreateThread(RefreshVideo,NULL,NULL); //创建线程
      SDL_Event event; //设置触发事件
      6.在SDL中,当事件等待函数监听到事件后,判断事件类型,如果event.type == SDL_KEYDOWN,表明用户按下键盘,保存在event.key.keysym.sym是相应的键值。而根据键值,调用函数SDL_GetKeyName(event.key.keysym.sym)),即可得到按下的按键键名。
      SDLK_RETURN == 13 回车!
      7.读文件fread
      size_t fread(void buffer,size_t size,size_t count,FILE stream)
      buffer 是读取的数据存放的内存的指针(可以是数组,也可以是新开辟的空间,buffer就是一个索引)
      size 是每次读取的字节数
      count 是读取次数
      stream 是要读取的文件的指针
      8.循环显示画面
      (1)设置纹理的数据
      使用SDL_UpdateTexture()设置纹理的像素数据
      int SDLCALL SDL_UpdateTexture(SDL_Texture texture,const SDL_Rect rect,const void *pixels, int pitch);
      texture:目标纹理。
      rect:更新像素的矩形区域。设置为NULL的时候更新整个区域。
      pixels:像素数据。
      pitch:一行像素数据的字节数。
      (2)纹理复制给渲染目标
      使用SDL_RenderCopy()将纹理数据复制给渲染目标。在使用SDL_RenderCopy()之前,可以使用SDL_RenderClear()先使用清空渲染目标。实际上视频播放的时候不使用SDL_RenderClear()也是可以的,因为视频的后一帧会完全覆盖前一帧
      int SDLCALL SDL_RenderCopy(SDL_Renderer renderer,SDL_Texture texture,const SDL_Rect srcrect,const SDL_Rect dstrect);
      renderer:渲染目标。
      texture:输入纹理。
      srcrect:选择输入纹理的一块矩形区域作为输入。设置为NULL的时候整个纹理作为输入。
      dstrect:选择渲染目标的一块矩形区域作为输出。设置为NULL的时候整个渲染目标作为输出。
      (3) 显示
      使用SDL_RenderPresent()显示画面
      void SDLCALL SDL_RenderPresent(SDL_Renderer * renderer);
      参数renderer用于指定渲染目标
      9.退出
      event.type==SDL_QUIT
      fclose(fp); //关闭文件! 防止遗留无用进程
      SDL_DestroyTexture(texture); //关闭纹理
      SDL_DestroyRenderer(renderer); //关闭渲染
      SDL_DestroyWindow(window); //关闭窗口
      SDL_Quit(); //退出线程
      FFMPEG解码deco
      fun_deco_display.c
      从void * deco_thread开始
      play_ubuntu内 功能函数于主函数同时编译
      多个c编译!
      gcc -o demo main.c fun_deco_display.c fun_others.c fun_recv_control.c -I /monchickey/ffmpeg/include -L /monchickey/ffmpeg/lib -lavformat -lavcodec -lavutil -lSDL2 -lpthread
      AVCodec codec; / 解码CODEC*/
      AVCodecContext *cctx;
      AVFrame frame; / 解码后的图像*/
      int byte_buffer_size; //解码器码流长度
      uint8_t *byte_buffer = NULL; //h.264码流
      AVPacket *pkt; //保存媒体流信息
      AVPacket主要保存一些媒体流的基本信息,例如PTS、DTS时间。最重要的当然就是媒体数据的buffer地址了。
      比较重要的有:
      pts:控制显示的pts时间
      dts:控制解码的dts时间
      *data:媒体数据buffer的指针
      duration:AVStream-> time_base单位中此数据包的持续时间,如果未知则为0。 在演示顺序中等于next_pts - this_pts。
      AVFormatContext主要存储视音频封装格式中包含的信息
      解码前数据:AVPacket
      解码后数据:AVFrame
      1.AVFormatContext avformat_alloc_context(void)函数用来申请AVFormatContext类型变量并初始化默认参数。申请的空间通过void avformat_free_context(AVFormatContext s)函数释放。
      2.avformat_find_stream_info()主要用于给每个媒体流(音频/视频)的AVStream结构体赋值,已经实现了解码器的查找,解码器的打开,视音频帧的读取,视音频帧的解码等工作
      3.av_find_best_stream()函数就是要获取音视频对应的stream_index 获取流的索引
      4.解码模块第一步:获取解码器 avcodec_find_decoder()FFmpeg的解码器编码器都存在avcodec的结构体中
      5.avcodec_alloc_context3,avcodec_parameters_to_context,解码器初始化
      6.avcodec_open2打开解码器
      7.av_frame_alloc()首先调用av_mallocz()为AVFrame结构体分配内存
      8.int av_image_get_buffer_size(enum AVPixelFormat pix_fmt, int width, int height, int align);函数的作用是通过指定像素格式、图像宽、图像高来计算所需的内存大小,av_malloc 按需分配空间
      9.av_packet_alloc实际是分配AVPacket以后,调用av_init_packet对AVPacket的成员变量进行初始化赋值
      10.av_read_frame()的作用是读取码流中的音频若干帧或者视频一帧
      11.avcodec_send_packet()以在AVPacket中给出解码器原始的压缩数据
      avcodec_receive_frame()。 成功后,它将返回一个包含未压缩音频或视频数据的 AVFrame
      12.

      void av_image_copy_uc_from     (     uint8_t *      dst_data[4],
            const ptrdiff_t      dst_linesizes[4],
            const uint8_t *      src_data[4],
            const ptrdiff_t      src_linesizes[4],
            enum AVPixelFormat      pix_fmt,
            int      width,
            int      height
      )  

      数据拷贝
      13.av_packet_free(&pkt); //释放数据 关闭进程
      av_frame_free(&frame);
      avformat_close_input(&fctx);
      avcodec_free_context(&cctx);
      avformat_free_context(fctx);
      Client && Server(了解)
      //Client_Upd
      1.int socket(int domain, int type, int protocol);
      函数socket()的参数domain用于设置网络通信的域,函数socket()根据这个参数选择通信协议的族 SOCK_DGRAM udp连接
      socket()函数的原型如下,这个函数建立一个协议族为domain、协议类型为type、协议编号为protocol的套接字文件描述符。
      2.memset可以方便的清空一个结构类型的变量或数组。初始化
      3.family 通信协议的族 AF_INET,PF_INET IPv4 Internet协议
      4.sockaddr_in在头文件#include<netinet/in.h>或#include <arpa/inet.h>中定义,该结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量中。
      htons()作用是将端口号由主机字节序转换为网络字节序的整数值。(host to net)
      inet_addr()作用是将一个IP字符串转化为一个网络字节序的整数值,用于sockaddr_in.sin_addr.s_addr。
      5.在无连接的数据报socket方式下,由于本地socket并没有与远端机器建立连接,所以在发送数据时应指明目的地址,sendto()函数原型为:
        int sendto(int sockfd, const void msg,int len unsigned int flags, const struct sockaddr to, int tolen);
        该函数比send()函数多了两个参数,to表示目地机的IP地址和端口号信息,而tolen常常被赋值为sizeof (struct sockaddr)。
      int PASCAL FAR sendto( SOCKET s, const char FAR* buf, int len, int flags,
      const struct sockaddr FAR* to, int tolen);
      s:一个标识套接口的描述字。
      buf:包含待发送数据的缓冲区。
      len:buf缓冲区中数据的长度。
      flags:调用方式标志位。
      to:(可选)指针,指向目的套接口的地址。
      tolen:to所指地址的长度
      6.recvfrom()函数原型为:
        int recvfrom(int sockfd,void buf,int len,unsigned int lags,struct sockaddr from,int *fromlen);
        from是一个struct sockaddr类型的变量,该变量保存源机的IP地址及端口号。fromlen常置为sizeof (struct sockaddr)。当recvfrom()返回时,fromlen包含实际存入from中的数据字节数
      接收一个数据报并保存源地址。
      int PASCAL FAR recvfrom( SOCKET s, char FAR* buf, int len, int flags,
      struct sockaddr FAR from, int FAR fromlen);
      s:标识一个已连接套接口的描述字。
      buf:接收数据缓冲区。
      len:缓冲区长度。
      flags:调用操作方式。
      from:(可选)指针,指向装有源地址的缓冲区。
      fromlen:(可选)指针,指向from缓冲区长度值。
      https://blog.csdn.net/qq_26399665/article/details/52426529 //sendto/recvfrom
      7.fwrite(const voidbuffer,size_t size,size_t count,FILEstream);
      (1)buffer:是一个指针,对fwrite来说,是要输出数据的地址。
      (2)size:要写入的字节数;
      (3)count:要进行写入size字节的数据项的个数;
      (4)stream:目标文件指针。
      //Server_Udp(了解)
      1.FILE fopen(char path, char * mode);
      path为包含了路径的文件名,mode为文件打开方式
      2.bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
      int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
      第二个参数是一个指向特定协议的地址结构的指针,第三个参数是该地址结构的长度。
      3.size_t fread(void buffer,size_t size,size_t count,FILE stream)
      buffer 是读取的数据存放的内存的指针(可以是数组,也可以是新开辟的空间,buffer就是一个索引)
      size 是每次读取的字节数
      count 是读取次数 ,k
      stream 是要读取的文件的指针

    5
    【1】基于STM32CubeMX-STM32GPIO端口开发
    « 上一篇 2022-01-16
    故事很短,却说透了我们的一生
    下一篇 » 2022-01-02

    评论 (0)

    取消