@21小跑堂#申请原创#
接上回jpeg解码显示,jpeg成功解码显示后我们就可以做个简单的视频播放器啦。而且还是所谓“硬解”。原理很简单,无数张jpeg组成一种mjpeg流,放在一个avi容器中,在代码里解析此AVI即可知道每一帧的jpeg地址和大小(它们分开其实就是一张张jpeg照片),按照avi容器中的码率显示此jpeg序列即可。
做个demo需要解决如下问题:符合要求的视频流、视频放在什么地方、AVI解析、mjpeg播放、如何按照AVI视频文件的码率显示jpeg序列。
符合要求的视频流
视频要求:AVI格式、mjpeg编码、最好800x480分辨率。这个视频可以自己做,但STM32Cube_FW_H7下其实有现成的视频可以做下测试,笔者用的文件位于
- Utilities/Media/Video/video2_800_480.avi
视频放在什么地方
理想位置可以是sd卡或ospi flash,但要集中精力攻打目标关卡,STM32N6这么大的sram,可以放个七、八百KB的视频是没问题的,所以笔者为方便起见把视频放在sram中了,当然最终要做成播放器成品是需要实现下SD卡和fatfs的,这个留在后续TODO中
AVI解析
笔者咨询了下deepseek,AVI格式其实蛮简单的,但是有没有现成的代码可以复用呢?还真让笔者找到了,从STM32Cube_FW_H7中如下位置copy过来即可
- Projects/STM32H743I-EVAL/Examples/JPEG/JPEG_MJPEG_VideoDecodingFromQSPI/Src/AVI_parser.c
- Projects/STM32H743I-EVAL/Examples/JPEG/JPEG_MJPEG_VideoDecodingFromQSPI/Inc/AVI_parser.h
mjpeg播放主流程
获取一帧,如果是视频帧,说明这个帧就是一个jpeg数组,调用上章实现的jpeg解码和显示到LCD的代码即可。如此循环往复,直到AVI中所有的帧遍历完毕。当然这个过程还涉及到双缓冲,一缓冲区用于显示、一缓冲区用于解码。
如何按照AVI视频文件的码率显示jpeg序列
STM32N6性能很强,很有可能一帧解码显示完毕只需很少的时间,所以如果不加延时,最后的效果很有可能像快进一样,所以在帧解码并显示结束后,需要做适量的延时。
主流程代码如下:
- if(AVI_ParserInit(&AVI_Handel, AVI_FILE_ADDRESS) != 0)
- Error_Handler();
- isfirstFrame = 1;
- FrameRate = 0;
- startTime = HAL_GetTick();
- do
- {
- /*##-4- Get a Frame from the AVI file ##################################*/
- FrameType = AVI_GetFrame(&AVI_Handel);
- if(FrameType == AVI_VIDEO_FRAME)
- {
- AVI_Handel.CurrentImage ++;
- /*##-5- Start decoding the current JPEG frame with DMA (Not Blocking ) Method ################*/
- memcpy((void*)jpgBuf, AVI_Handel.pVideoBuffer, AVI_Handel.FrameSize);
- JPEG_Decode_DMA(&hjpeg, jpgBuf, AVI_Handel.FrameSize, jpegOutDataAdreess);
- // JPEG_Decode_DMA(&hjpeg, AVI_FILE_ADDRESS, 117813, jpegOutDataAdreess);
- /*##-6- Wait till end of JPEG decoding ###############################*/
- while(Jpeg_HWDecodingEnd == 0)
- {
- }
- if(isfirstFrame == 1)
- {
- isfirstFrame = 0;
- /*##-7- Get JPEG Info #############################################*/
- HAL_JPEG_GetInfo(&hjpeg, &JPEG_Info);
- /*##-8- Initialize the DMA2D #######################################*/
- DMA2D_Init(JPEG_Info.ImageWidth, JPEG_Info.ImageHeight, JPEG_Info.ChromaSubsampling);
- }
- /*##-9- Copy the Decoded frame to the display frame buffer using the DMA2D #*/
- xPos = (LCD_X_Size - JPEG_Info.ImageWidth)/2;
- yPos = (LCD_Y_Size - JPEG_Info.ImageHeight)/2;
- DMA2D_CopyBuffer((uint32_t *)jpegOutDataAdreess, (uint32_t *)LCD_LAYER_0_ADDRESS, xPos , yPos, JPEG_Info.ImageWidth, JPEG_Info.ImageHeight);
- jpegOutDataAdreess = (jpegOutDataAdreess == (uint32_t)rawBuffer) ? (uint32_t)rawBuffer2: (uint32_t)rawBuffer;
- #ifdef USE_FRAMERATE_REGULATION
- /* Regulate the frame rate to the video native frame rate by inserting delays */
- FrameRate = (HAL_GetTick() - startTime) + 1;
- if(FrameRate < ((AVI_Handel.aviInfo.SecPerFrame/1000) * AVI_Handel.CurrentImage))
- {
- HAL_Delay(((AVI_Handel.aviInfo.SecPerFrame /1000) * AVI_Handel.CurrentImage) - FrameRate);
- }
- #endif /* USE_FRAMERATE_REGULATION */
- }
- }while(AVI_Handel.CurrentImage < AVI_Handel.aviInfo.TotalFrame);
- HAL_DMA2D_PollForTransfer(&hdma2d, 50); /* wait for the Last DMA2D transfer to ends */
- if(AVI_Handel.CurrentImage > 0)
- {
- /*##-10- Calc the average decode frame rate #*/
- FrameRate = (AVI_Handel.CurrentImage * 1000)/(HAL_GetTick() - startTime);
- /* Display decoding info */
- LCD_BriefDisplay();
- }
注:如下这个位置是个坑,就是好像STM32N6的JPEG解码DMA有个对齐要求,如果mjpeg流里jpeg帧的位置不满足对齐就会花屏,笔者这里堵了好久。笔者的解决方案是:copy到jpeg buff中再解码,当然如果能解决这个对齐要求可以少一次copy就更好了,看看后续CubeFW是不是有更新吧,或者读者有更好的方法
麻烦告知哦。
- memcpy((void*)jpgBuf, AVI_Handel.pVideoBuffer, AVI_Handel.FrameSize);
- JPEG_Decode_DMA(&hjpeg, jpgBuf, AVI_Handel.FrameSize, jpegOutDataAdreess);
播放完毕显示的一些参数:
探讨一些潜在的优化方法:
1.如上文所述,JPEG DMA有没有对齐要求,如何解决从而避免一次拷贝
2.现在DMA所涉及的buf,笔者统统都是在MPU中设置成Non-Cacheable的,限制是不是过于强悍了,有没有可能利用上cpu的cache?
3.psram用起来
4.SD卡bring up
5.更多编码格式支持,比如移植ffmpeg解码?
6.能否基于zephyr RTOS LVGL做此播放器,这样一来以后ST家的其它MCU甚至友商的平台直接复用代码
笔者相信,如果这六点做完这个播放器完成度就比较完整了。
视频如下供欣赏:
https://www.bilibili.com/video/BV11wVRz2Exx/
|