Windows & Esp32基于 libjpeg-9e 编解码库的视频播放器

Posted _npc_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Windows & Esp32基于 libjpeg-9e 编解码库的视频播放器相关的知识,希望对你有一定的参考价值。

目录

🔍 如理解有误,望不吝指正。
🌎 仓库https://gitee.com/npc-gitee/libjpeg_avi_player.git 🚀

一、音视频基础

编码的主要目的是减小文件大小,从而减小存储空间与传输带宽,从某种意义上讲就是一种压缩行为,像通过Zip软件对文件进行打包压缩行为类似。在压缩过程中,分为有损压缩和无损压缩。

  • 有损压缩:丢弃不敏感信息。
  • 无损压缩:保留全部信息。

💡 对于音频数据,按照扬声器的底层工作逻辑,通过改变电压达到改变振动频率,从而发出不同声音。PCM 格式作为一种非编码格式,可通过将 PCM 的数据经过DAC 即可输出音频信号。对于 MP3 等经过编码的音频文件,需要通过解码后,将其转换为PCM数据后,然后输出音频信号。

💡 对于图像数据,按照显示器的底层工作逻辑,显示器由多个像素组成,一个像素由三个发光源(RGB)组合成不同的颜色。RGB 数据格式写入 GRAM 中即可显示图像。RGB 只是图像显示的一种颜色空间,还有YUVHSV等颜色空间,颜色空间对应的是一个数学概念叫做向量空间,一个非线性相关的向量组,就可以当成线性空间一个基,可通过基来描述空间中任何一个向量,颜色空间与向量空间是一个与之类似的场景。 🚀 这些颜色空间之间可以相互转换,比如 YUVRGBRGBYUV
对于 jpeg 等经过编码的图像文件,需要经过解码后,转换为相应的颜色空间的数据,然后依据显示器的显示原理,将其转换为 RGB 颜色空间,完成显示任务。

💡 对于视频数据,本质上是多帧图像和音频数据的集合,图像的颜色空间的概念与图像中一样。多张图片在短时间内切换,给人呈现出一种动画的效果。图像的编码针对于帧内的数据进行,而视频的编码除了在帧内完成编码外,还会在帧间完成编码工作。图像所做的是 空间冗余,视频所做的是 空间冗余时间冗余

参考:
[1]: 音视频基础知识、YUV、H264 🚀

1.1、图像编码

  • bmp 格式没有压缩像素格式,存储在文件中时先有文件头、再图像头、后面就都是像素数据了,上下颠倒存储。bmp格式也是可以压缩,bmp格式也可以有颜色板。
  • JPEG 是一种静止图像的压缩标准,属于有损压缩,它是一种标准的帧内压缩编码方式。jpeg 编码是通过将 RGB 数据转换为 YUV 的色彩空间,然后进行 压缩 的。
  • M-JPEG 源于JPEG压缩技术,是一种简单的帧内JPEG压缩,但是由于这种压缩本身技术限制, 无法做到大比例压缩。
  • png 是一种无损压缩格式。
  • gif 可以保存多帧图像。gif中有个参数可以控制图片变化的快慢。

所谓颜色看板,就是在文件中创建一个颜色索引,图中像素用到的某个颜色则文件中存储的为索引值,而不是之前的 RGB 数值。

1.2、视频编码

  • MPEG是压缩运动图像及其伴音的视音频编码标准,它采用了帧间压缩,仅存储连续帧之间有差别的地方 ,从而达到较大的压缩比。
  • H264 视频帧编码和jpeg的编码逻辑一样,空间冗余,在其基础上添加了 IPB 帧的逻辑,在帧与帧之间做差运算,时间冗余。

视频文件格式: windows设置后缀名的目的是让相应的应用程序来打开相应的文件。可以随意更改后缀名,不会更改文件的内部数据格式。

视频封装格式: 一种存储视频信息的容器。视频封装格式不同,也不会影响视频数据,主要是一种对视频数据的组合。视频封装格式与视频文件格式一一对应

视频编码方式: 对多帧图像数据进行压缩。

参考:
[1]: 图像和视频的主要格式与编码格式 🚀
[2]: H264系列(7):H.264与MPEG4区别 🚀
[3]: JPEG编码和H264 🚀
[4]: 视频编码与封装方式(整合) 🚀

1.3、AVI 文件结构

AVI 其音频数据采用 16 位线性 PCM 格式(未压缩),而视频数据,则采用MJPG 编码方式。

MJPGMJPEG 的缩写,还可以文件格式扩展名。

MJPEG 不像 MPEG,不使用帧间编码。

MJPEG 的工作是将 RGB 格式的影像转换成 YCrCB 格式,目的是为了减少档案大小,一般约可减少 1/3 ~ 1/2 左右。

MJPEG 是视频,就是由系列 jpg 图片组成的视频。

AVI 视频封装格式,采用的是 RIFF 文件结构方式。构造 RIFF 文件的基本单元叫做数据块(Chunk),每个数据块至少包含 3 个部分,

  • 4字节的数据块标记(又称数据块的ID,Chunk ID)
  • 数据块大小
  • 4 字节的形式类型或者列表类型( ID)(RIFF块和LIST块独有)
  • 数据

整个 RIFF 文件可以看成一个数据块,其数据 ID 为 RIFF,称为 RIFF 块。一个 RIFF 文件中只允许存在一个 RIFF 块。RIFF 块中包含一系列的子块,其中有一种子块的 ID 为 “LIST”,称为 LIST 块LIST 块包含一系列的子块,但是 LIST 块外的其他所有子块都不能再包含子块

RIFF 块和 LIST 块比普通的数据块多了一个 形式类型(Form Type)列表类型(List Type) 的数据域。 AVI 的 RIFF 块的形式类型是 AVI 注意:‘AVI ’,是带了一个空格的),它一般包含 3 个子块:

  • 信息块(HeaderList):一个 ID 为 “hdrl” 的 LIST 块,定义 AVI 文件的数据格式
  • 数据块(MovieList): 一个 ID 为 “movi” 的 LIST 块,定义 AVI 文件的音视频序列数据格式
  • 索引块(Index Chunk):一个 ID 为 “idxl” 的子块,定义 “movi” LIST 块的索引数据

AVI 音视频文件的二进制内容可通过 WinHex 软件查看:

将上述数据按 AVI 文件结构划分:


AVI 文件结构图:

  • avih 块,用于记录 AVI 的全局信息,比如数据流的数量,视频图像的宽度和高度等信息,AVIH_HEADER为数据保存结构。
  • strl 子列表,文件中有多少种数据流(即前面的 Streams),就有多少个 strl 子列表。
    • 每个 strl 中,至少包含一个strh(Stream Header)和一个strf(Stream Formate),可选strn(Stream Name)。
    • strf 的数据类型与strh相关,如果strh为视频流,那么 strf 对应视频流的结构保存信息。
    • strl 子列表出现的顺序与后面数据块中的编号对应,假设 strl 子列表第一个是视频流,那么后面数据块视频帧的编号为00dc,第二个是音频流,那么编号为01wb
  • 数据块,图像数据和音频数据交错存储在数据块中,##dc/##wb 后面接当前一帧数据的大小值(不包含标准类型值和数据大小值,所以偏移需要加8),如果这个值为奇数,则加1,在读数据的时候一般一次性读完一帧,方便解码。
  • 索引块, 为 AVI 文件中每一个媒体数据块进行索引,并且记录它们在文件中的偏移(可能相对于‘movi’列表,也可能相对于 AVI 文件开头)。

二、TF卡基础

控制器对 SD 卡进行读写通信操作一般有两种通信接口可选,一种是 SPI 接口,另外一种就是 SDIO 接口

根据容量,SD 卡可划分为SDSC(<2GB)SDHC(2~32GB)SDXC(32GB~2TB)

当前 SD 协议提供的 SD 卡规范版本最新是 8.0 版本(2020年),但是有些芯片(如stm32f4xx系列)控制器只支持 SD 卡规范版本 2.0,即只支持标准容量 SD 和高容量 SDHC 标准卡,不支持超大容量 SDXC 标准卡。

一张 SD 卡包括有存储单元存储单元接口电源检测卡及接口控制器接口驱动器 5 个部分。

  • 存储单元是存储数据部件,存储单元通过存储单元接口与卡控制单元进行数据传输;
  • 电源检测单元保证 SD 卡工作在合适的电压下,如出现掉电或上状态时,它会使控制单元和存储单元接口复位;
  • 卡及接口控制单元控制 SD 卡的运行状态,它包括有 8 个寄存器;
  • 接口驱动器控制 SD 卡引脚的输入输出。

转载自:[野火]STM32 库开发实战指南

三、Windows上播放音视频

libjpeg 是一个完全用 C 语言编写的库,包含功能有 JPEG 解码JPEG 编码 和其它的JPEG功能的实现,这里使用的版本为 jpeg-9e

开发平台:vs2019

3.1、在 Windows 下使用 vs2019 编译 libjpeg 库

win10 系统下没有这个文件,win7 系统可能会有。这里是在网站上下的文件。
win32.mdkhttps://gitee.com/guangpengz/ms-sdk?_from=gitee_search 🚀

cmd 切换目录:Windows-cmd切换目录

1、下载 libjpeg,这里使用 jpegsr9e.zip,官网:http://www.ijg.org/ 🚀

2、解压源码

3、进入解压后的目录,找到 makefile.vc 文件,修改下面语句:

!include <win32.mak>

所在行,并将 win32.mak 替换为实际位置, 我这边修改后的值为,修改为:

!include <C:\\Users\\user\\Desktop\\X\\ms-sdk-master\\win32.mak>

5、生成 sln 工程文件:在电脑的 “开始” 菜单中,找到 vs2019 的命令行工具,并打开,这里是:

x86 Native Tools Command Prompt for VS 2019

6、切换到 jpegsr9e 目录下,然后输入下面命令:

NMAKE /f makefile.vc setup-v16

7、打开 jpeg.sln 文件,然后在 vs2019 中,选择 Win32,之后点击生成解决方案,最后就会在 jpegsr9e 文件夹下多一个 Release 文件夹,里面有 jpeg.lib 静态库。

参考:
[1]: 在Windows下使用vs2019编译libjpeg库(静态库与动态库) 🚀
[2]: Windows10下利用VS2022编译JpegLib 🚀

3.2、创建 libjpeg 解码项目

创建项目过程同上一篇博客 🚀 的创建方式相似,这里创建项目的名称为:libjpeg_for_windows

1、头文件路径添加

libjpeg_for_windows 右键 ——> 属性 ——> C/C++ ——> 常规 ——> 附加库目录中输入.\\lib\\jpeg-9e ——> 确定

2、库文件路径添加

libjpeg_for_windows 右键 ——> 属性 ——> 链接器 ——> 常规 ——> 附加包含目录中输入.\\lib\\jpeg-9e\\Release\\Win32——> 确定

3、附加依赖项添加

libjpeg_for_windows 右键 ——> 属性 ——> 链接器 ——> 输入 ——> 附加依赖项:点击右边向下箭头’ v ’ ——> 编辑 ——> 输入:jpeg.lib (之前编译的静态库) ——> 确定 ——> 应用 ——> 确定。

jpegsr9e 文件夹下的 example.c 添加到项目中,生成解决方案。这时候就会出现这些报错:

LNK2001 无法解析的外部符号 _image_buffer
LNK2001 无法解析的外部符号 _image_height
LNK2001 无法解析的外部符号 _image_width
LNK2001 无法解析的外部符号 _main
LNK2001 无法解析的外部符号 _put_scanline_someplace

这些符号都是没有定义的,需要通过外部链接,所以这里直接在 example.c 文件中定义就可以了。添加代码如下:

JSAMPLE* image_buffer;
int image_height;
int image_width;

int put_scanline_someplace(JSAMPROW buffer, int row_stride)

/* 根据需要自己完善 */


int main()

/* 根据需要自己完善 */

jpeglib.h 头文件依赖于 stdio.hstring.h 头文件

错误:C4996 fopen(‘fscanf’、strcmp):This function or variable may be unsafe

解决:在程序最前面加 #define _CRT_SECURE_NO_WARNINGS,编译还是报错,所以这里通过 右键项目 —> 预处理器 —> 预处理器定义 —> 点击 ‘v’ —> 编辑 —> 添加 _CRT_SECURE_NO_WARNINGS —> 确定,应用。

参考:
[1]: LIBJPEG 安装编译,读取jpeg图像数据 🚀

3.3、libjpeg 中 example.c 功能解析

/* example1.c */
#include <stdio.h>
#include "jpeglib.h"
#include <setjmp.h>

JSAMPLE* image_buffer;	/* Points to large array of R,G,B-order data */
int image_height;	/* Number of rows in image */
int image_width;		/* Number of columns in image */

/* 这里主要测试解码,所以这段代码注释了 */
//GLOBAL(void) write_JPEG_file(char *filename, int quality)
//...

struct my_error_mgr 
    struct jpeg_error_mgr pub;	/* "public" fields */

    jmp_buf setjmp_buffer;	/* for return to caller */
;

typedef struct my_error_mgr* my_error_ptr;

METHODDEF(void)
my_error_exit(j_common_ptr cinfo)

    /* cinfo->err really points to a my_error_mgr struct, so coerce pointer */
    my_error_ptr myerr = (my_error_ptr)cinfo->err;

    /* Always display the message. */
    /* We could postpone this until after returning, if we chose. */
    (*cinfo->err->output_message) (cinfo);

    /* Return control to the setjmp point */
    longjmp(myerr->setjmp_buffer, 1);




GLOBAL(int) read_JPEG_file(char* filename)

    struct jpeg_decompress_struct cinfo;
    struct my_error_mgr jerr;
    FILE* infile;		    /* source file */
    JSAMPARRAY buffer;		/* Output row buffer */ // unsigned char ** buffer;
    int row_stride;		/* physical row width in output buffer */

    if ((infile = fopen(filename, "rb")) == NULL) 
        fprintf(stderr, "can't open %s\\n", filename);
        return 0;
    

    cinfo.err = jpeg_std_error(&jerr.pub);
    jerr.pub.error_exit = my_error_exit;
   
    if (setjmp(jerr.setjmp_buffer)) 
        jpeg_destroy_decompress(&cinfo);
        fclose(infile);
        return 0;
    
    /* Now we can initialize the JPEG decompression object. */
    jpeg_create_decompress(&cinfo);

    jpeg_stdio_src(&cinfo, infile);

    (void)jpeg_read_header(&cinfo, TRUE);

    printf("image_width = %d\\n", cinfo.image_width);
    printf("image_height = %d\\n", cinfo.image_height);
    printf("num_components = %d\\n", cinfo.num_components);

    printf("output_width = %d\\n", cinfo.output_width);
    printf("output_components = %d\\n", cinfo.output_components);

    cinfo.out_color_space = JCS_RGB;   // 以 RGB 为结果输出


   (void)jpeg_start_decompress(&cinfo);
  
    row_stride = cinfo.output_width * cinfo.output_components;
    printf("output_width2 = %d\\n", cinfo.output_width);
    printf("output_components2 = %d\\n", cinfo.output_components);
    
    // 计算buffer大小并申请相应空间
    buffer = (*cinfo.mem->alloc_sarray)
        ((j_common_ptr)&cinfo, JPOOL_IMAGE, row_stride, 1);
    
   
    while (cinfo.output_scanline < cinfo.output_height) 
       (void)jpeg_read_scanlines(&cinfo, buffer, 1);
        put_scanline_someplace(buffer[0], row_stride);
    

    (void)jpeg_finish_decompress(&cinfo);
  
    jpeg_destroy_decompress(&cinfo);

    fclose(infile);

    return 1;


int put_scanline_someplace(JSAMPROW buffer, int row_stride)

    int i = 0;
    for (; i < row_stride; i++) 
        printf("0x%x ", buffer[i]);
    

    printf("\\nend\\n");



int main()

    read_JPEG_file("C:\\\\Users\\\\xxx\\\\Desktop\\\\pic.jpg");
    return 0;


这里对 example.c 文件的框架没有改动,只是在程序中加入一些打印函数,测试输出结果。

这里加入了一条语句:
cinfo.out_color_space = JCS_RGB; // 以 RGB 为结果输出

程序的目的:

  • 1、查看程序是否能正确获得图片信息。
  • 2、row_stride 变量 = 图像宽度 x RGB(3字节)。
  • 3、jpeg_read_scanlines 函数是一行一行解码,且解码后的 RGB 数据放到 buffer 中(第三个参数为1,设定一次解码行数)。

这里通过 PS 软件构造了一张 6x6 的红色图片。(如何构造请看下文 👀)


从打印的结果可知,正好是 6 行 6 列,且每个像素的 RGB 为(0xFE, 0x0,0x0),如果将数据发送给屏幕,那么就能显示一张完整的图片了。

参考:
[1]: libjpeg库的简单使用,rgb565与rgb888互转,以及色块的寻找 🚀

3.4、SDL2 库配置与使用

对于 RGB 数据在 Windows 系统显示方面,这里采用 SDL2 开源库。

SDL2 安装教程参考:VS2019配置SDL2库 🚀

开发基本流程:

  1. 【初始化SDL系统】SDL_int()
  2. 【创建窗口】SDL_CreateWindows()
  3. 【创建渲染器】SDL_CreateRenderer()
  4. 【渲染器中创建一个材质】SDL_CreateTexture()
  5. 【申请待显示RGB图像空间】
  6. 【为图像数据赋值】
  7. 【将内存中的RGB数据写入材质】SDL_UpdateTexture()
  8. 【清理渲染区(清理屏幕)】SDL_RenderClear()
  9. 【设定渲染的目标区域】
  10. 【复制材质到渲染器对象】SDL_RenderCopy()
  11. 【执行渲染操作】SDL_RenderPresent()
  12. 【销毁渲染器】SDL_DestroyRenderer()
  13. 【销毁窗口】SDL_DestroyWindow()
  14. 【退出SDL系统】SDL_Quit()
#include <stdio.h>
#include "jpeglib.h"
#include <setjmp.h>

#include <iostream>
#include <SDL.h>

using namespace std;

#pragma comment(lib, "SDL2.lib")

// SDL 库里面定义了一个宏 main, 这里给取消掉,否则会与main 函数名冲突,导致编译错误
#undef main

#define __DEBUG__ 0

JSAMPLE* image_buffer;	/* Points to large array of R,G,B-order data */
int image_height;	/* Number of rows in image */
int image_width;		/* Number of columns in image */


int Windows_Width = 0;
int Windows_Height = 0;

SDL_Window* screen = NULL;
SDL_Renderer* render = NULL;
SDL_Texture* texture = NULL;
unsigned char* r = NULL;

int put_scanline_someplace(JSAMPROW buffer, int row_stride);
int Image_Init(void);
int Image_Display(void);

//GLOBAL(void) write_JPEG_file(char* filename, int quality)
//...


struct my_error_mgr 
    struct jpeg_error_mgr pub;	/* "public" fields */

    jmp_buf setjmp_buffer;	/* for return to caller */
;

typedef struct my_error_mgr* my_error_ptr;


METHODDEF(void)
my_error_exit(j_common_ptr cinfo)

    my_error_ptr myerr = (my_error_ptr)cinfo->err;

    (*cinfo->err->output_message) (cinfo);

    longjmp(myerr->setjmp_buffer, 1);



GLOBAL(int)
read_JPEG_file(char* filename)

    struct jpeg_decompress_struct cinfo;

    struct my_error_mgr jerr;

    FILE* infile;		/* source file */
    JSAMPARRAY buffer;		/* Output row buffer */ // unsigned char ** buffer;
    int row_stride;		/* physical row width in output buffer */

    if ((infile = fopen(filename, "rb")) == NULL) 
        fprintf(stderr, "can't open %s\\n", filename);
        return 0;
    

    cinfo.err = jpeg_std_error(&jerr.pub);
    jerr.pub.error_exit = my_error_exit;
    /* Establish the setjmp return context for my_error_exit to use. */
    if (setjmp(jerr.setjmp_buffer)) 

        jpeg_destroy_decompress(&cinfo);
        fclose(infile);
        return 0;
    
    /* Now we can initialize the JPEG decompression object. */
    jpeg_create_decompress(&cinfo);

    jpeg_stdio_src(&cinfo, infile);

    (void)jpeg_read_header(&cinfo, TRUE);

#if __DEBUG__
    printf("image_width = %d\\n", cinfo.image_width);
    printf("image_height = %d\\n", cinfo.image_height);
    printf("num_components = %d\\n", cinfo.num_components);

    printf("output_width = %d\\n", cinfo.output_width);
    printf("output_components = %d\\n", cinfo.output_components);
#endif

    cinfo.out_color_space = JCS_RGB;


    // 显示初始化
    // 定义图像的宽高
    Windows_Width = cinfo.image_width;
    Windows_Height = cinfo.image_height;

    // 显示初始化
    if (Image_Init() != 0) 
        printf("Image Init error\\n");
        return -1;
    

    // 准备一幅w*h的红色RGB图像数据
    shared_ptr<unsigned char> rgb(new unsigned char[Windows_Width * Windows_Height * 4]);        // 乘以4是因为像素格式已指定为ARGB888,单个像素点占4字节
    r = rgb.get();
    
    (void)jpeg_start_decompress(&cinfo);

    row_stride = cinfo.output_width * cinfo.output_components;

#if __DEBUG__
    printf("output_width2 = %d\\n", cinfo.output_width);
    printf("output_components2 = %d\\n", cinfo.output_components);
#endif

    // 计算buffer大小并申请相应空间
    buffer = (*cinfo.mem->alloc_sarray)
        ((j_common_ptr)&cinfo, JPOOL_IMAGE, row_stride, 1);

    int j = 0;
    int lineR = 0;   // 每一行R分量的起始位置
    while (cinfo.output_scanline < cinfo.output_height) 
        int i = 0;
        (void)jpeg_read_scanlines(&cinfo, buffer, 1);
        /* Assume put_scanline_someplace wants a pointer and sample count. */
        //put_scanline_someplace(buffer[0], row_stride);

        
        // 为上述图像数据赋值
        for (int k = 0; k < Windows_Width * 4; k += 4)
        
           r[lineR + k] = buffer[0][i+2];                                      // B
           r[lineR + k + 1] = buffer[0][i+1];                                  // G
           r[lineR + k + 2] = buffer[0][i];                                    // R
           r[

ESP32基于Arduino框架下U8g2驱动I2C OLED 时间显示

ESP32基于Arduino框架下U8g2驱动I2C OLED时间显示


  • 📺演示:

  • ✨本案例采用1.3寸OLED屏幕,同时保留0.96屏幕接口函数,只要u8g2库支持的屏幕都可以,需要到\\U8g2\\src\\U8g2lib.h中匹配自己的屏幕函数接口。

📑I2C引脚接线说明

SDA-----> 21
SCL----->22

🛠所需库

🎉 所需库都可以在IDE管理库当中下载到。

U8g2库
Ticker库

📝驱动代码

#include <Arduino.h>
#include <U8g2lib.h>//包含u8g2头文件,用于OLED显示
//#include <ESP8266WiFi.h>//包含ESP8266头文件
#include <WiFi.h>

#include <Ticker.h> //调用Ticker.h库

#define NTP1  "ntp1.aliyun.com"
#define NTP2  "ntp2.aliyun.com"
#define NTP3  "ntp3.aliyun.com"

#ifndef STASSID
#define STASSID "MERCURY_D268G"
#define STAPSK  "pba5ayzk"
#endif

const char* ssid = STASSID;
const char* password = STAPSK;

#define U8X8_HAVE_HW_I2C

//U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); // 1.3寸,默认引脚
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/U8X8_PIN_NONE, /* clock=*/22, /* data=*/21); // 1.3寸,指定引脚
//U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);//0.96寸,默认引脚
//U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/U8X8_PIN_NONE, /* clock=*/22, /* data=*/21); //0.96寸,指定引脚

#ifdef U8X8_HAVE_HW_I2C          //定义OLED连接方式为I2C
#include <Wire.h>
#endif

const String WDAY_NAMES[] = "Sun","Mon", "Tue",  "Wed", "Thu", "Fri", "Sat";  //星期


void printLocalTime()
 struct tm timeInfo;//声明一个结构体
 char buff[16];
  if (!getLocalTime(&timeInfo))
  
      Serial.println("Failed to obtain time");
      u8g2.setCursor(0,9);
      u8g2.print("NTP time failure");
      return;
  
 // Serial.println(&timeinfo, "%F %T %a"); // 格式化输出,串口显示
 String date = WDAY_NAMES[timeInfo.tm_wday];
  u8g2.clearBuffer();
  sprintf_P(buff, PSTR("%04d-%02d-%02d %s"), timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday, WDAY_NAMES[timeInfo.tm_wday].c_str());
  u8g2.setFont(u8g2_font_wqy14_t_gb2312a);
  u8g2.setCursor(9 , 20 ); //年份居中显示
  u8g2.println(buff);//显示出年份
   //Serial.println(buff);
  sprintf_P(buff, PSTR("%02d:%02d:%02d"), timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec);//时间
  u8g2.setFont(u8g2_font_fur17_tn);
  u8g2.drawStr(19 , 45 , buff);
  u8g2.sendBuffer();



Ticker timer1(printLocalTime, 1000);


void setup() 
  // put your setup code here, to run once:
  Serial.begin(115200);
  u8g2.begin();          //初始化OLED
  WiFi.mode(WIFI_STA); //设置ESP32工作模式为无线终端模式
  WiFi.begin(ssid, password);
    // Wait for connection
  while (WiFi.status() != WL_CONNECTED) 
    delay(500);
    Serial.print(".");
  
    Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
  
  configTime(8 * 3600, 0, NTP1, NTP2,NTP3);   
  u8g2.setPowerSave(0);// 关省电模式
  timer1.start();


void loop() 
timer1.update();

以上是关于Windows & Esp32基于 libjpeg-9e 编解码库的视频播放器的主要内容,如果未能解决你的问题,请参考以下文章

ESP32之 ESP-IDF + Clion 开发环境搭建—— Windows版

ESP8266/ESP32 网络温控器监控 Web服务器-基于温度控制输出

基于Windows下离线安装当前最新Arduino ESP32 SDK(2.0.7)固件开发包

ESP32-C3学习笔记:ESP32 C3 IIC总线驱动光照强度传感器(基于ESP-IDF Eclipse)

ESP32基于Arduino框架下U8g2驱动I2C OLED 时间显示

自行编译micropython固件刷入ESP32 cam,并测试拍照及图传