【正点原子K210连载】第二十六章 视频播放实验《DNK210使用指南-SDK版》

[复制链接]
1911|1
第二十六章 视频播放实验

本章将介绍Kendryte K210如何使用软件JPEG解码器来实现视频播放的功能。通过学习本章内容,读者将学习到Kendryte K210利用软件JPEG解码器实现视频播放的方法。
本章分为如下几个小节:
26.1 AVI&libjpeg简介
26.2 硬件设计
26.3 程序设计
26.4 运行验证


26.1 AVI&libjpeg简介
本章,我们将使用libjepg(由IJG提供),来实现MJPEG编码的AVI格式视频播放,我们先来简单介绍一下AVI和libjpeg。
26.1.1 AVI简介
AVI是音频视频交错(Audio Video Interleaved)的英文缩写,它是微软开发的一种符合RIFF文件规范的数字音频与视频文件格式,原先用于Microsoft Video for Windows (简称VFW)环境,现在已被多数操作系统直接支持。
AVI格式允许视频和音频交错在一起同步播放,支持256色和RLE压缩,但AVI文件并未限定压缩标准,AVI仅仅是一个容器,用不同压缩算法生成的AVI文件,必须使用相应的解压缩算法才能播放出来。比如本章,我们使用的AVI,其音频数据采用16位线性PCM格式(未压缩),而视频数据,则采用MJPG编码方式。
在介绍AVI文件前,我们要先来看看RIFF文件结构。AVI文件采用的是RIFF文件结构方式,RIFFResource Interchange File Format,资源互换文件格式)是微软定义的一种用于管理WINDOWS环境中多媒体数据的文件格式,波形音频WAVEMIDI和数字视频AVI都采用这种格式存储。构造RIFF文件的基本单元叫做数据块(Chunk),每个数据块包含3个部分:
14字节的数据块标记(或者叫做数据块的ID
2、数据块的大小
3、数据
整个RIFF文件可以看成一个数据块,其数据块IDRIFF,称为RIFF块。一个RIFF文件中只允许存在一个RIFF块。RIFF块中包含一系列的子块,其中有一种子块的ID"LIST",称为LIST块,LIST块中可以再包含一系列的子块,但除了LIST块外的其他所有的子块都不能再包含子块。
RIFFLIST块分别比普通的数据块多一个被称为形式类型(Form Type)和列表类型(List Type)的数据域,其组成如下:
14字节的数据块标记(Chunk ID
2、数据块的大小
34字节的形式类型或者列表类型(ID
4、数据
下面我们看看AVI文件的结构。AVI文件是目前使用的最复杂的RIFF文件,它能同时存储同步表现的音频视频数据。AVIRIFF块的形式类型(Form Type)是AVI,它一般包含3个子块,如下所述:
1、信息块,一个ID"hdrl"LIST块,定义AVI文件的数据格式。
2、数据块,一个ID"movi"LIST块,包含AVI的音视频序列数据。
3、索引块,ID"idxl"的子块,定义"movi"LIST块的索引数据,是可选块(不一定有)。
接下来,我们详细介绍下AVI文件的各子块构造,AVI文件的结构如图26.1.1所示:

图26.1.1 AVI文件结构图
从上图可以看出(注意AVI ’,是带了一个空格的),AVI文件,由:信息块(HeaderList)、数据块(MovieList)和索引块(Index Chunk)等三部分组成,下面,我们分别介绍这几个部分。
1、信息块(HeaderList
信息块,即ID为“hdrlLIST块,它包含文件的通用信息,定义数据格式,所用的压缩算法等参数等。hdrl块还包括了一系列的字块,首先是:avih块,用于记录AVI的全局信息,比如数据流的数量,视频图像的宽度和高度等信息,avih块(结构体都有把BlockIDBlockSize包含进来,下同)的定义如下:
/* avih 子块信息 */
typedef struct
{
    uint32_t BlockID;               /* 块标志:avih==0X61766968 */
    uint32_t BlockSize;/*块大小(不包含最初8字节,也就是BlockID和BlockSize不计算在内*/
    uint32_t SecPerFrame;          /* 视频帧间隔时间(单位为us) */
    uint32_t MaxByteSec;           /* 最大数据传输率,字节/秒 */
    uint32_t PaddingGranularity; /* 数据填充的粒度 */
    uint32_t Flags;                 /* AVI文件的全局标记,比如是否含有索引块等 */
    uint32_t TotalFrame;           /* 文件总帧数 */
    uint32_t InitFrames;           /* 为交互格式指定初始帧数(非交互格式应该指定为0)*/
    uint32_t Streams;               /* 包含的数据流种类个数,通常为2 */
    uint32_t RefBufSize;/* 建议读取本文件的缓存大小(应能容纳最大的块)默认可能是1M字节*/
    uint32_t Width;                 /* 图像宽 */
    uint32_t Height;                /* 图像高 */
    uint32_t Reserved[4];          /* 保留 */
} AVIH_HEADER;
这里有很多我们要用到的信息,比如SecPerFrame,通过该参数,我们可以知道每秒钟的帧率,也就知道了每秒钟需要解码多少帧图片,才能正常播放。TotalFrame告诉我们整个视频有多少帧,结合SecPerFrame参数,就可以很方便计算整个视频的时间了。Streams告诉我们数据流的种类数,一般是2,即包含视频数据流和音频数据流。
avih块之后,是一个或者多个strl子列表,文件中有多少种数据流(即前面的Streams),就有多少个strl子列表。每个strl子列表,至少包括一个strhStream Header)块和一个strfStream Format)块,还有一个可选的strnStream Name)块(未列出)。注意:strl子列表出现的顺序与媒体流的编号(比如:00dc,前面的00,即媒体流编号00)是对应的,比如第一个strl子列表说明的是第一个流(Stream 0),假设是视频流,则表征视频数据块的四字符码00dc”,第二个strl子列表说明的是第二个流(Stream 1),假设是音频流,则表征音频数据块的四字符码01dw”,以此类推。
先看strh子块,该块用于说明这个流的头信息,定义如下:
/* strh 流头子块信息(strh∈strl) */
typedef struct
{
uint32_t BlockID;        /* 块标志:strh==0X73747268 */
/* 块大小(不包含最初的8字节,也就是BlockID和BlockSize不计算在内) */
uint32_t BlockSize;
    uint32_t StreamType;/*数据流种类,vids(0X73646976):视频;auds(0X73647561):音频*/
    uint32_t Handler;   /*指定流的处理者,对于音视频来说就是解码器,比如MJPG/H264之类的*/
    uint32_t Flags;          /* 标记:是否允许这个流输出?调色板是否变化? */
    uint16_t Priority;      /* 流的优先级(当有多个相同类型的流时优先级最高的为默认流) */
    uint16_t Language;      /* 音频的语言代号 */
    uint32_t InitFrames;    /* 为交互格式指定初始帧数 */
    uint32_t Scale;          /* 数据量, 视频每帧的大小或者音频的采样大小 */
    uint32_t Rate;           /* Scale/Rate=每秒采样数 */
    uint32_t Start;          /* 数据流开始播放的位置,单位为Scale */
    uint32_t Length;         /* 数据流的数据量,单位为Scale */
    uint32_t RefBufSize;    /* 建议使用的缓冲区大小 */
    uint32_t Quality;       /* 解压缩质量参数,值越大,质量越好 */
    uint32_t SampleSize;   /* 音频的样本大小 */
    struct                    /* 视频帧所占的矩形 */
    {
        short Left;
        short Top;
        short Right;
        short Bottom;
    } Frame;
} STRH_HEADER;
这里面,对我们最有用的即StreamType Handler这两个参数了,StreamType用于告诉我们此strl描述的是音频流(“auds”),还是视频流(“vids”)。而Handler则告诉我们所使用的解码器,比如MJPG/H264等(实际以strf块为准)。
然后是strf子块,不过strf字块,需要根据strh字块的类型而定。
如果strh子块是视频数据流(StreamType=vids),则strf子块的内容定义如下:
/* BMP结构体 */
typedef struct
{
    uint32_t BmpSize;         /* bmp结构体大小,包含(BmpSize在内) */
    long Width;                /* 图像宽 */
    long Height;               /* 图像高 */
    uint16_t  Planes;         /* 平面数,必须为1 */
    uint16_t  BitCount;       /* 像素位数,0X0018表示24位 */
    uint32_t  Compression;   /* 压缩类型,比如:MJPG/H264等 */
    uint32_t  SizeImage;     /* 图像大小 */
    long XpixPerMeter;       /* 水平分辨率 */
    long YpixPerMeter;       /* 垂直分辨率 */
    uint32_t  ClrUsed;       /* 实际使用了调色板中的颜色数,压缩格式中不使用 */
    uint32_t  ClrImportant;     /* 重要的颜色 */
} BMP_HEADER;
/* 颜色表 */
typedef struct
{
    uint8_t  rgbBlue;        /* 蓝色的亮度(值范围为0-255) */
    uint8_t  rgbGreen;       /* 绿色的亮度(值范围为0-255) */
    uint8_t  rgbRed;         /* 红色的亮度(值范围为0-255) */
    uint8_t  rgbReserved;     /* 保留,必须为0 */
} AVIRGBQUAD;
/* 对于strh,如果是视频流,strf(流格式)使STRH_BMPHEADER块 */
typedef struct
{
    uint32_t BlockID;         /* 块标志,strf==0X73747266 */
uint32_t BlockSize;      /* 块大小(不包含最初的8字节,也就是BlockID
和本BlockSize不计算在内) */
    BMP_HEADER bmiHeader;    /* 位图信息头 */
    AVIRGBQUAD bmColors[1]; /* 颜色表 */
} STRF_BMPHEADER;
这里有3个结构体,strf子块完整内容即:STRF_BMPHEADER结构体,不过对我们有用的信息,都存放在BMP_HEADER结构体里面,本结构体对视频数据的解码起决定性的作用,它告诉我们视频的分辨率(WidthHeight),以及视频所用的编码器(Compression),因此它决定了视频的解码。本章例程仅支持解码视频分辨率小于屏幕分辨率,且编解码器必须是MJPG的视频格式。
如果strh子块是音频数据流(StreamType=auds”),则strf子块的内容定义如下:
/* 对于strh,如果是音频流,strf(流格式)使STRH_WAVHEADER块 */
typedef struct
{
    uint32_t BlockID;        /* 块标志,strf==0X73747266 */
uint32_t BlockSize;      /* 块大小(不包含最初的8字节,也就是BlockID
和本BlockSize不计算在内) */
    uint16_t FormatTag;      /* 格式标志:0X0001=PCM,0X0055=MP3 */
    uint16_t Channels;       /* 声道数,一般为2,表示立体声 */
    uint32_t SampleRate;     /* 音频采样率 */
    uint32_t BaudRate;       /* 波特率 */
    uint16_t BlockAlign;        /* 数据块对齐标志 */
    uint16_t Size;           /* 该结构大小 */
} STRF_WAVHEADER;
本结构体对音频数据解码起决定性的作用,他告诉我们音频信号的编码方式(FormatTag)、声道数(Channels)和采样率(SampleRate)等重要信息。本章例程仅支持PCM格式(FormatTag=0X0001)的音频数据解码。
2、数据块(MovieList
信息块,即ID为“moviLIST块,它包含AVI的音视频序列数据,是这个AVI文件的主体部分。音视频数据块交错的嵌入在“moviLIST块里面,通过标准类型码进行区分,标准类型码有如下4种:
1)“##db”(非压缩视频帧)
2)“##dc”(压缩视频帧)
3)“##pc”(改用新的调色板、
4)“##wb”(音频帧)
其中##是编号,得根据我们的数据流顺序来确定,也就是前面的strl块。比如,如果第一个strl块是视频数据,那么对于压缩的视频帧,标准类型码就是:00dc。第二个strl块是音频数据,那么对于音频帧,标准类型码就是:01wb
紧跟着标准类型码的是4个字节的数据长度(不包含类型码和长度参数本身,也就是总长度必须要加8才对),该长度必须是偶数,如果读到为奇数,则加1即可。我们读数据的时候,一般一次性要读完一个标准类型码所表征的数据,方便解码。
3、索引块(Index Chunk
最后,紧跟在hdrl’列表和‘movi’列表之后的,就是AVI文件可选的索引块。这个索引块为AVI文件中每一个媒体数据块进行索引,并且记录它们在文件中的偏移(可能相对于‘movi’列表,也可能相对于AVI文件开头)。本章我们用不到索引块,这里就不详细介绍了。
关于AVI文件,我们就介绍到这,有兴趣的朋友,可以再看看光盘:软件资料àAVI学习资料 里面的相关文档。
26.1.2 libjpeg简介
libjpeg是一个完全用C语言编写的库,包含了广泛使用的JPEG解码、JPEG编码和其他的JPEG功能的实现。这个IJG库由组织Independent JPEG Group(独立JPEG小组))提供并维护。libjepg,目前最新版本为v9d,可以https://www.ijg.org/files/这个网站下载。libjpeg具有稳定、兼容性强和解码速度较快等优点。
本章,我们使用libjpeg来实现MJPG数据流的解码,MJPG数据流,其实就是一张张的JPEG图片拼起来的图片视频流,只要能快速解码JPEG图片,就可以实现视频播放。
相比较另外一个常用的JPEG软件解码器,TJPG的特点是:占用资源少、但解码速度慢。而libjpeg的特点是:解码速度较快,但是占用资源较多对于Kendryte K210来说,8MibSRAM可以无需过多考虑资源占用问题,在实际测试中,经过优化后的libjpeg,使用Kendryte K210400MHz不超频的情况下,可以流畅播放320*240@ 10MJPG视频(带音频),超频到600MHz解码一张320*240JPG图片仅需要30ms,能够流畅播放320*240@ 30MJPG视频(带音频)。
关于libjpeg的移植和使用,在下载的libjpeg源码里面还有很多介绍,可以重点看:readme.txtfilelist.txtinstall.txtlibjpeg.txt等,也可以参考光盘源码进行移植与使用。
我们在移植过程中对部分的源码进行了删除和修改,修改的文件有:wrppm.cjdapistd.cjdmerge.cjerror.cjmemnobs.c文件,大家可以自己去对比和理解,这里不再叙述。
最后,我们看看要实现avi视频文件的播放,主要有哪些步骤,如下:
1)初始化各外设
要解码视频,相关外设肯定要先初始化好,比如:SD卡、I2SDMALCD和按键等。这些具体初始化过程,在前面的例程都有介绍,大同小异,这里就不再细说了。
2)读取AVI文件,并解析
要解码,得先读取avi文件,按26.1.1节的介绍,读取出音视频关键信息,音频参数:编码方式、采样率、位数和音频流类型码(01wb/00wb)等;视频参数:编码方式、帧间隔、图片尺寸和视频流类型码(00dc/01dc)等;共同的:数据流起始地址。有了这些参数,我们便可以初始化音视频解码,为后续解码做好准备。
3)根据解析结果,设置相关参数
根据第2步解析的结果,设置I2S的音频采样率和位数,同时要让视频显示在LCD上,得根据图片尺寸,设置LCD开窗时xy方向的偏移量。
4)读取数据流,开始解码
前面三步完成,就可以正式开始播放视频了。读取视频流数据(movi块),根据类型码,执行音频/视频解码。对于音频数据(01wb/00wb),本例程只支持未压缩的PCM数据,所以,直接填充到DMA缓冲区即可,由DMA循环发送给数字功放驱动扬声器播放音频。对于视频数据(00dc/01dc),本例程只支持MJPG,通过libjpeg软件解码器解码。然后,利用定时器来控制帧间隔,以正常速度播放视频,从而实现音视频解码。
5)解码完成,释放资源
最后在文件读取完后(或者出错了),需要释放申请的内存、关闭定时器、停止I2S播放音乐和关闭文件等一系列操作,等待下一次解码。
26.2 硬件设计
26.2.1 例程功能
1.开发板开机上电后自动播放SD卡根目录的play.wav音频文件(提前将音频文件拷贝到SD卡根目录), LCD模块显示播放总时间和当前播放时间,播放完成后程序退出。
26.2.2 硬件资源
1. LCD
LCD_RD - IO34
LCD_BL - IO35
LCD_CS - IO36
LCD_RST - IO37
LCD_RS - IO38
LCD_WR - IO39
LCD_D0~LCD_D7 - SPI0_D0~SPI0_D7
2. 数字功放NS4168
SPK_CTRL - IO21
IIS_SDOUT - IO31
IIS_BCK - IO32
IIS_LRCK - IO33
26.2.3 原理图
本章实验内容,主要讲解libjpeg库的使用,无需关注原理图。
26.3 程序设计
26.3.1 MJPEG驱动
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。MJPEG驱动源码包括四个文件:avi.c、avi.h、mjpeg.c和mjpeg.h。
avi.h头文件在26.1小节部分讲过,具体请看源码。下面来看到avi.c文件,这里总共有三个函数都很重要,首先介绍AVI解码初始化函数,该函数定义如下:
/* avi文件相关信息 */
AVI_INFO g_avix;
/* 视频编码标志字符串,00dc/01dc */
char *const AVI_VIDS_FLAG_TBL[2] = {"00dc", "01dc"};
/* 音频编码标志字符串,00wb/01wb */
char *const AVI_AUDS_FLAG_TBL[2] = {"00wb", "01wb"};
/**
* @brief       avi解码初始化
* @param       buf  : 输入缓冲区
* @param       size : 缓冲区大小
* @retval      res
* @ARG          OK,avi文件解析成功
* @arg         其他,错误代码
*/
AVISTATUS avi_init(uint8_t *buf, uint32_t size)
{
    uint16_t offset;
    uint8_t *tbuf;
    AVISTATUS res = AVI_OK;
    AVI_HEADER *aviheader;
    LIST_HEADER *listheader;
    AVIH_HEADER *avihheader;
    STRH_HEADER *strhheader;
    STRF_BMPHEADER *bmpheader;
    STRF_WAVHEADER *wavheader;
    tbuf = buf;
    aviheader = (AVI_HEADER *)buf;
    if (aviheader->RiffID != AVI_RIFF_ID)
    {
        return AVI_RIFF_ERR;             /* RIFF ID错误 */
    }
    if (aviheader->AviID != AVI_AVI_ID)
    {
        return AVI_AVI_ERR;              /* AVI ID错误 */
    }
    buf += sizeof(AVI_HEADER);           /* 偏移 */
    listheader = (LIST_HEADER *)(buf);
    if (listheader->ListID != AVI_LIST_ID)
    {
        return AVI_LIST_ERR;             /* LIST ID错误 */
    }
    if (listheader->ListType != AVI_HDRL_ID)
    {
        return AVI_HDRL_ERR;             /* HDRL ID错误 */
    }
    buf += sizeof(LIST_HEADER);          /* 偏移 */
    avihheader = (AVIH_HEADER *)(buf);
    if (avihheader->BlockID != AVI_AVIH_ID)
    {
        return AVI_AVIH_ERR;             /* AVIH ID错误 */
    }
    g_avix.SecPerFrame = avihheader->SecPerFrame;  /* 得到帧间隔时间 */
    g_avix.TotalFrame = avihheader->TotalFrame;     /* 得到总帧数 */
    buf += avihheader->BlockSize + 8;                /* 偏移 */
    listheader = (LIST_HEADER *)(buf);
    if (listheader->ListID != AVI_LIST_ID)
    {
        return AVI_LIST_ERR;             /* LIST ID错误 */
    }
    if (listheader->ListType != AVI_STRL_ID)
    {
        return AVI_STRL_ERR;             /* STRL ID错误 */
    }
    strhheader = (STRH_HEADER *)(buf + 12);
    if (strhheader->BlockID != AVI_STRH_ID)
    {
        return AVI_STRH_ERR;                /* STRH ID错误 */
    }
    if (strhheader->StreamType == AVI_VIDS_STREAM)  /* 视频帧在前 */
    {
        if (strhheader->Handler != AVI_FORMAT_MJPG)
        {
            return AVI_FORMAT_ERR;           /* MJPG视频流,不支持 */
        }
        g_avix.VideoFLAG = AVI_VIDS_FLAG_TBL[0];     /* 视频流标记  "00dc" */
        g_avix.AudioFLAG = AVI_AUDS_FLAG_TBL[1];     /* 音频流标记  "01wb" */
        bmpheader = (STRF_BMPHEADER *)(buf + 12 + strhheader->BlockSize + 8);   
        if (bmpheader->BlockID != AVI_STRF_ID)
        {
            return AVI_STRF_ERR;         /* STRF ID错误 */
        }
        g_avix.Width = bmpheader->bmiHeader.Width;
        g_avix.Height = bmpheader->bmiHeader.Height;
        buf += listheader->BlockSize + 8;         /* 偏移 */
        listheader = (LIST_HEADER *)(buf);
        if (listheader->ListID != AVI_LIST_ID)    /* 是不含有音频帧的视频文件 */
        {
            g_avix.SampleRate = 0;                /* 音频采样率 */
            g_avix.Channels = 0;                  /* 音频通道数 */
            g_avix.AudioType = 0;                 /* 音频格式 */
        }
        else
        {
            if (listheader->ListType != AVI_STRL_ID)
            {
                return AVI_STRL_ERR;    /* STRL ID错误 */
            }
            strhheader = (STRH_HEADER *)(buf + 12);
            if (strhheader->BlockID != AVI_STRH_ID)
            {
                return AVI_STRH_ERR;    /* STRH ID错误 */
            }
            if (strhheader->StreamType != AVI_AUDS_STREAM)
            {
                return AVI_FORMAT_ERR;  /* 格式错误 */
            }
            wavheader = (STRF_WAVHEADER *)(buf + 12 + strhheader->BlockSize + 8);   
            if (wavheader->BlockID != AVI_STRF_ID)
            {
                return AVI_STRF_ERR;    /* STRF ID错误 */
            }
            g_avix.SampleRate = wavheader->SampleRate;   /* 音频采样率 */
            g_avix.Channels = wavheader->Channels;    /* 音频通道数 */
            g_avix.AudioType = wavheader->FormatTag;      /* 音频格式 */
        }
    }
    else if (strhheader->StreamType == AVI_AUDS_STREAM)/* 音频帧在前 */
    {
        g_avix.VideoFLAG = AVI_VIDS_FLAG_TBL[1];         /* 视频流标记  "01dc" */
        g_avix.AudioFLAG = AVI_AUDS_FLAG_TBL[0];         /* 音频流标记  "00wb" */
        wavheader = (STRF_WAVHEADER *)(buf + 12 + strhheader->BlockSize + 8);
        if (wavheader->BlockID != AVI_STRF_ID)
        {
            return AVI_STRF_ERR;                             /* STRF ID错误 */
        }
        g_avix.SampleRate = wavheader->SampleRate;       /* 音频采样率 */
        g_avix.Channels = wavheader->Channels;            /* 音频通道数 */
        g_avix.AudioType = wavheader->FormatTag;         /* 音频格式 */
        buf += listheader->BlockSize + 8;                  /* 偏移 */
        listheader = (LIST_HEADER *)(buf);
        if (listheader->ListID != AVI_LIST_ID)
        {
            return AVI_LIST_ERR;    /* LIST ID错误 */
        }
        if (listheader->ListType != AVI_STRL_ID)
        {
            return AVI_STRL_ERR;    /* STRL ID错误 */
        }
        strhheader = (STRH_HEADER *)(buf + 12);
        if (strhheader->BlockID != AVI_STRH_ID)
        {
            return AVI_STRH_ERR;    /* STRH ID错误 */
        }
        if (strhheader->StreamType != AVI_VIDS_STREAM)
        {
            return AVI_FORMAT_ERR;  /* 格式错误 */
        }
        bmpheader = (STRF_BMPHEADER *)(buf + 12 + strhheader->BlockSize + 8);   
        if (bmpheader->BlockID != AVI_STRF_ID)
        {
            return AVI_STRF_ERR;    /* STRF ID错误 */
        }
        if (bmpheader->bmiHeader.Compression != AVI_FORMAT_MJPG)
        {
            return AVI_FORMAT_ERR;  /* 格式错误 */
        }
        g_avix.Width = bmpheader->bmiHeader.Width;
        g_avix.Height = bmpheader->bmiHeader.Height;
    }
    offset = avi_srarch_id(tbuf, size, "movi");     /* 查找movi ID */
    if (offset == 0)
    {
        return AVI_MOVI_ERR;        /* MOVI ID错误 */
    }
    if (g_avix.SampleRate)          /* 有音频流,才查找 */
    {
        tbuf += offset;
        offset = avi_srarch_id(tbuf, size, g_avix.AudioFLAG);   /* 查找音频流标记 */
        if (offset == 0)
        {
            return AVI_STREAM_ERR;  /* 流错误 */
        }
        tbuf += offset + 4;
        g_avix.AudioBufSize = *((uint16_t *)tbuf); /* 得到音频流buf大小 */
    }
    printf("avi init ok\r\n");
    printf("g_avix.SecPerFrame:%ld\r\n", g_avix.SecPerFrame);
    printf("g_avix.TotalFrame:%ld\r\n", g_avix.TotalFrame);
    printf("g_avix.Width:%ld\r\n", g_avix.Width);
    printf("g_avix.Height:%ld\r\n", g_avix.Height);
    printf("g_avix.AudioType:%d\r\n", g_avix.AudioType);
    printf("g_avix.SampleRate:%ld\r\n", g_avix.SampleRate);
    printf("g_avix.Channels:%d\r\n", g_avix.Channels);
    printf("g_avix.AudioBufSize:%d\r\n", g_avix.AudioBufSize);
    printf("g_avix.VideoFLAG:%s\r\n", g_avix.VideoFLAG);
    printf("g_avix.AudioFLAG:%s\r\n", g_avix.AudioFLAG);
    return res;
}
该函数用于解析AVI文件,获取音视频流数据的详细信息,为后续解码做准备。
接下来介绍的是查找 ID函数,其定义如下:
/**
* @brief       查找 ID
* @param       buf  : 待查缓存区
* @param       size : 缓存大小
* @param       id   : 要查找的id,必须是4字节长度
* @retval      0,接收应答失败
*               其他:movi ID偏移量
*/
uint16_t avi_srarch_id(uint8_t *buf, uint32_t size, char *id)
{
uint32_t i;
uint32_t idsize = 0;
    size -= 4;
    for (i = 0; i < size; i++)
    {
        if ((buf == id[0]) &&
            (buf[i + 1] == id[1]) &&
            (buf[i + 2] == id[2]) &&
            (buf[i + 3] == id[3]))   
                {
                    idsize = MAKEDWORD(buf + i + 4);   
/* 得到帧大小,必须大于16字节,才返回,否则不是有效数据 */
                    if (idsize > 0X10)return i; /* 找到"id"所在的位置 */
              }
            }
         }
    }
    return 0;
}
该函数用于查找某个ID,可以是4个字节长度的ID,比如00dc,01wb,movi之类的,在解析数据以及快进快退的时候,有用到。
接下来介绍的是得到stream流信息函数,其定义如下:
/**
* @brief       得到stream流信息
* @param       buf  : 流开始地址(必须是01wb/00wb/01dc/00dc开头)
* @retval      执行结果
*   @arg       AVI_OK, AVI文件解析成功
*   @arg       其他  , 错误代码
*/
AVISTATUS avi_get_streaminfo(uint8_t *buf)
{
    g_avix.StreamID = MAKEWORD(buf + 2);    /* 得到流类型 */
    g_avix.StreamSize = MAKEDWORD(buf + 4); /* 得到流大小 */
if (g_avix.StreamSize > AVI_MAX_FRAME_SIZE)   /* 帧大小太大了,直接返回错误 */
    {
        printf("FRAME SIZE OVER:%d\r\n", g_avix.StreamSize);
        g_avix.StreamSize = 0;
        return AVI_STREAM_ERR;
    }
    if (g_avix.StreamSize % 2)
    {
        g_avix.StreamSize++;    /* 奇数加1(g_avix.StreamSize,必须是偶数) */
    }
    if (g_avix.StreamID == AVI_VIDS_FLAG || g_avix.StreamID == AVI_AUDS_FLAG)
    {
        return AVI_OK;
    }
    return AVI_STREAM_ERR;
}
该函数用来获取当前数据流信息,重点是取得流类型和流大小,方便解码和读取下一个数据流。
mjpeg.h文件只有一些函数和变量声明,接下来,介绍mjpeg.c里面的几个函数,首先是初始化MJPEG解码数据源的函数,其定义如下:
/**
* @brief       mjpeg 解码初始化
* @param       offx,offy:x,y方向的偏移
* @retval      0,成功; 1,失败
*/
char mjpegdec_init(uint16_t offx, uint16_t offy)
{
cinfo = (struct jpeg_decompress_struct *)
malloc(sizeof(struct jpeg_decompress_struct));
    jerr = (struct my_error_mgr *)malloc(sizeof(struct my_error_mgr));
    if (cinfo == NULL || jerr == NULL)
    {
        printf("[E][mjpeg.cpp] mjpegdec_init():
malloc failed to apply for memory\r\n");
        mjpegdec_free();
        return -1;
    }
    /* 保存图像在x,y方向的偏移量 */
    imgoffx = offx;
    imgoffy = offy;
    return 0;
}
该函数用于初始化jpeg解码,主要是申请内存,然后确定视频在液晶上面的偏移(让视频显示在SPILCD中央)。
下面介绍的是MJPEG释放所有申请的内存函数,其定义如下:
/**
* @brief       mjpeg结束,释放内存
* @param      
* @retval      
*/
void mjpegdec_free(void)
{
    iomem_free(p_cinfo);
    iomem_free(p_jerr);
    iomem_free(p_jmembuf);
}
该函数用于释放内存,解码结束后调用。
下面介绍的是解码一副JPEG图片函数,其定义如下:
/**
* @brief       解码一副JPEG图片
* @param       buf     : jpeg数据流数组
* @param       bsize   : 数组大小
* @retval      0,成功;
*              其他,错误
*/
uint8_t mjpegdec_decode(uint8_t *buf, uint32_t bsize)
{
    JSAMPARRAY buffer = 0;
    if (bsize == 0)return 1;
    p_jpegbuf = buf;
    g_jbufsize = bsize;
    g_jmempos = 0; /* MJEPG解码,重新从0开始分配内存 */
    p_cinfo->err = jpeg_std_error(&p_jerr->pub);
    p_jerr->pub.error_exit = my_error_exit;
    p_jerr->pub.emit_message = my_emit_message;
    //if(bsize>20*1024)printf("s:%d\r\n",bsize);
    if (setjmp(p_jerr->setjmp_buffer))    /* 错误处理 */
    {
        jpeg_abort_decompress(p_cinfo);
        jpeg_destroy_decompress(p_cinfo);
        return 2;
    }
    jpeg_create_decompress(p_cinfo);
    jpeg_filerw_src_init(p_cinfo);
    jpeg_read_header(p_cinfo, TRUE);
    p_cinfo->dct_method = JDCT_IFAST;
    p_cinfo->do_fancy_upsampling = 0;
    jpeg_start_decompress(p_cinfo);   /*JPEG图片转化为RGB565格式*/
    // lcd_set_area(g_imgoffx, g_imgoffy, p_cinfo->output_width, p_cinfo->output_height);                    /* 设置窗口 */   
    // lcd_write_ram_prepare();                                                                                /* 开始写入GRAM */
    while (p_cinfo->output_scanline < p_cinfo->output_height)
    {
        jpeg_read_scanlines(p_cinfo, buffer, 1);   /*实现JPEG图片传输到摄像头*/
    }
    // lcd_set_area(0, 0, 320, 240);                                                                          /* 恢复窗口 */   
    jpeg_finish_decompress(p_cinfo);
    jpeg_destroy_decompress(p_cinfo);  
    return 0;
}
该函数是解码jpeg的主要函数,函数的参数buf指向内存里面的一帧jpeg数据,bsize是数据大小。
26.3.2 视频播放驱动
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。视频播放驱动源码包括两个文件:videoplayer.c和videoplayer.h。
videoplayer.h头文件有两个宏定义和函数声明,具体请看源码。下面来看到videoplayer.c文件中,播放一个MJPEG文件函数,其定义如下:
/**
* @brief       播放一个mjpeg文件
* @param       pname : 文件名
* @retval      res
*   @arg       KEY0_PRES , 下一曲.
*   @arg       KEY1_PRES , 上一曲.
*   @arg       其他 , 错误
*/
uint8_t video_play_mjpeg(uint8_t *pname)
{
    uint8_t *framebuf;  /* 视频解码buf */
    uint8_t *pbuf;      /* buf指针 */
    FIL *favi;
    uint8_t  res = 0;
    uint8_t i = 0;
    uint16_t offset = 0;
    uint32_t nr;
    uint8_t i2ssavebuf;
    for (i = 0; i < AVI_AUDIO_BUF_NUM; i++)
    {
        p_avi_i2s_buf = iomem_malloc(AVI_AUDIO_BUF_SIZE);    /* 申请音频内存 */
        
        if (p_avi_i2s_buf == NULL)                      /* 申请失败, 直接退出 */
        {
            break;
        }
        
        memset(p_avi_i2s_buf, 0, AVI_AUDIO_BUF_SIZE);            /* 数据清零 */
    }
   
    favi = (FIL *)iomem_malloc(sizeof(FIL));                  /* 申请favi内存 */
    framebuf = iomem_malloc(AVI_VIDEO_BUF_SIZE) ;             /* 申请视频buf */
   
    if (!framebuf)  /* 只要最后这个视频buf申请失败, 前面的申请失不失败都不重要, 总之就是失败了 */
    {
        printf("memory error!\r\n");
        res = 0XFF;
    }
    while (res == 0)
    {
        printf("open file\r\n");
        res = f_open(favi, (char *)pname, FA_READ);
        if (res == 0)
        {
            printf("file open success\n");
            pbuf = framebuf;
            res = f_read(favi, pbuf, AVI_VIDEO_BUF_SIZE, &nr);      /* 开始读取 */
            
            if (res)
            {
                printf("fread error:%d\r\n", res);
                break;
            }
            printf("file read success\n");
            /* 开始avi解析 */
            res = avi_init(pbuf, AVI_VIDEO_BUF_SIZE);             /* avi解析 */
            if (res)
            {
                printf("avi err:%d\r\n", res);
                break;
            }
            video_info_show(&g_avix);
            timer_init(TIMER_DEVICE_0);
            /* 设置定时器超时时间,单位为ns */
            timer_set_interval(TIMER_DEVICE_0, TIMER_CHANNEL_0, g_avix.SecPerFrame / 100 * 1e5);
            /* 设置定时器中断回调 */
            timer_irq_register(TIMER_DEVICE_0, TIMER_CHANNEL_0, 0, 1, timer_timeout_cb, &g_count);
            /* 使能定时器 */
            timer_set_enable(TIMER_DEVICE_0, TIMER_CHANNEL_0, 1);
            offset = avi_srarch_id(pbuf, AVI_VIDEO_BUF_SIZE, "movi");   /* 寻找movi ID */
            avi_get_streaminfo(pbuf + offset + 4);              /* 获取流信息 */
            f_lseek(favi, offset + 12);                                    
            /* 跳过标志ID,读地址偏移到流数据开始处 */
            res = mjpegdec_init((320 - g_avix.Width) / 2, (240 - g_avix.Height));
      /* JPG解码初始化 */
            if (g_avix.SampleRate)                       /* 有音频信息,才初始化 */
            {
                printf("i2s init !\r\n");
                /* 初始化I2S,第三个参数为设置通道掩码,通道0:0x03,通道10x0C,通道20x30,通道3:0xC0 */
                i2s_init(I2S_DEVICE_0, I2S_TRANSMITTER, 0x03);
                /* 设置I2S发送数据的通道参数 */
                i2s_tx_channel_config(
                    I2S_DEVICE_0, /* I2S设备号*/
                    I2S_CHANNEL_0, /* I2S通道 */
                    RESOLUTION_16_BIT, /* 接收数据位数 */
                    SCLK_CYCLES_32, /* 单个数据时钟数 */
                    TRIGGER_LEVEL_4, /* DMA触发时FIFO深度 */
                    STANDARD_MODE); /* 工作模式 */
                           
                i2s_set_sample_rate(I2S_DEVICE_0, g_avix.SampleRate);/* 设置采样率 */
                i2ssavebuf = 0;
            }
            while (1)          /* 播放循环 */
            {
                if (g_avix.StreamID == AVI_VIDS_FLAG)           /* 视频流 */
                {
                    pbuf = framebuf;
                    f_read(favi, pbuf, g_avix.StreamSize + 8, &nr);
                    /* 读入整帧+下一数据流ID信息 */
                    res = mjpegdec_decode(pbuf, g_avix.StreamSize);
                    if (res)
                    {
                        printf("decode error!\r\n");
                    }
                    while (g_avi_frameup == 0);/*待时间到达(TIM的中断里面设置为1)*/
                    g_avi_frameup = 0;            /* 标志清零 */
                    g_avi_frame++;
                }
                else                             /* 音频流 */
                {
                    i2ssavebuf++;
                    if (i2ssavebuf >=  AVI_AUDIO_BUF_NUM)
                    {
                        i2ssavebuf = 0;
                    }
                    f_read(favi, p_avi_i2s_buf[i2ssavebuf], g_avix.StreamSize + 8, &nr);    /* 填充p_avi_sai_buf */
                    pbuf = p_avi_i2s_buf[i2ssavebuf];
                    i2s_play(
                        I2S_DEVICE_0, /* I2S设备号 */
                        DMAC_CHANNEL1, /* DMA通道号 */
                        p_avi_i2s_buf[i2ssavebuf], /* 播放的PCM数据 */
                        g_avix.StreamSize, /* PCM数据的长度 */
                        g_avix.StreamSize / 4, /* 单次发送数量 */
                        16, /* 单次采样位宽 */
                        2); /* 声道数 */
                    video_time_show(favi, &g_avix);/*显示当前播放时间,开启可能会卡*/
                }
                if (avi_get_streaminfo(pbuf + g_avix.StreamSize))  /* 读取下一帧 流标志 */
                {
                    printf("g_frame error \r\n");
                    res = KEY0_PRES;
                    break;
                }
            }
            timer_set_enable(TIMER_DEVICE_0, TIMER_CHANNEL_0, 0);
            // lcd_set_area(0, 0, 320, 240);     /* 恢复窗口 */
            mjpegdec_free();                                       /* 释放内存 */
            f_close(favi);                                         /* 关闭文件 */
        }
    }
   /* 释放内存 */
    for(i = 0; i < AVI_AUDIO_BUF_NUM; i++)
    {
        iomem_free(p_avi_i2s_buf);
    }
    iomem_free(framebuf);
    iomem_free(favi);
    return res;
}
该函数用来播放一个avi视频文件(MJPEG编码)。其他代码,我们就不介绍了,请大家参考本例程源码。
26.3.3 main.c代码
main.c中的代码如下所示:
char *pname = {"0: VIDEO/play.avi"};  
int main(void)
{
    FRESULT res;
    FATFS fs;
   
    sysctl_pll_set_freq(SYSCTL_PLL0, 800000000);
    sysctl_pll_set_freq(SYSCTL_PLL1, 400000000);
    sysctl_pll_set_freq(SYSCTL_PLL2, 45158400);
    sysctl_set_power_mode(SYSCTL_POWER_BANK6, SYSCTL_POWER_V18);
    sysctl_set_power_mode(SYSCTL_POWER_BANK7, SYSCTL_POWER_V18);
    sysctl_set_spi0_dvp_data(1);
    lcd_init();
    lcd_set_direction(DIR_YX_LRUD);
    speaker_i2s_hardware_init(); /* 扬声器接口初始化 */
    /* 初始化中断,使能全局中断,初始化dmac */
    plic_init();
    sysctl_enable_irq();
    dmac_init();
    key_init();
    if (sd_init() != 0)
    {
        printf("SD card initialization failed!\n");
        while (1);
    }
    printf("SD card initialization succeed!\n");
    /* Filesystem mount SD card */
    res = f_mount(&fs, _T("0:"), 1);
    if (res != FR_OK)
    {
        printf("SD card mount failed! Error code: %d\n", res);
        while (1);
    }
    printf("SD card mount succeed!\n");
    video_play_mjpeg((uint8_t *)pname);
    while (1)
    {
        
    }
}
main函数前面,我们定义了pname数组,用于存放音频文件的SD卡路径。main函数代码具体流程大致是:首先完成系统级和用户级初始化工作,挂载SD卡,挂载成功后播放SD卡根目录中文件名为pname中的AVI文件,播放结束后视频播放窗口停止刷新,播放时间进度满。
26.4 运行验证
DNK210开发板连接到电脑主机,通过VSCode将固件烧录到开发板中,可以看到LCD显示视频内容,LCD显示的内容如图26.4.1所示:
图26.4.1 视频播放运行效果图

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
yangjiaxu 发表于 2025-9-28 10:15 | 显示全部楼层
楼主多来点关于RK系列的开发板的开发经验分享呀,哈哈,感觉RK系列更火热一些
您需要登录后才可以回帖 登录 | 注册

本版积分规则

136

主题

137

帖子

3

粉丝
快速回复 在线客服 返回列表 返回顶部