当前位置: 首页 > article >正文

手动将MJPEG图片,转成MP4文件格式

MP4 容器格式

对于MP4,其box类型种类很多,此处只针对使用ffmpeg将MJPEG指定mjpeg编解码器生产的MP4文件内所有的box:

在这里插入图片描述

File Type Box(ftyp)

ftyp box 放在文件的最开始,描述的文件的版本、兼容协议等。

字段大小含义写入内容
box length4 字节box 长度0X0000001C
box type4 字节box 类型,固定值:ftypftyp
major_brand4 字节主版本isom
minor_version4 字节次版本0x00000200
compatible_brands[]n 字节指定兼容的版本,可以包含多个isomiso2mp41
  • major_brand:比如常见的 isom、mp41、mp42、avc1、qt等。它表示“最好”基于哪种格式来解析当前的文件。举例,major_brand 是 A,compatible_brands 是 A1,当解码器同时支持 A、A1 规范时,最好使用A规范来解码当前媒体文件,如果不支持A规范,但支持A1规范,那么,可以使用A1规范来解码;
  • minor_version:提供 major_brand 的说明信息,比如版本号,不得用来判断媒体文件是否符合某个标准/规范;
  • compatible_brands:文件兼容的brand列表。比如 mp41 的兼容 brand 为 isom。通过兼容列表里的 brand 规范,可以将文件 部分(或全部)解码出来;

使用十六进制查看工具打开MP4文件,查看对应内容:

在这里插入图片描述

标准文档定义:

aligned(8) class FileTypeBox extends Box(‘ftyp’) {  
  unsigned int(32) major_brand;  
  unsigned int(32) minor_version;  
  unsigned int(32) compatible_brands[]; // to end of the box  
}

代码实现:

void write_ftyp(FILE* file) {
    uint32_t size = 0X1C;
    write_uint32(file, size);                   // box length
    fwrite("ftyp", 1, 4, file);                 // box type
    fwrite("isom", 1, 4, file);                 // major_brand
    fwrite("\x00\x00\x02\x00", 1, 4, file);     // minor_version
    fwrite("isomiso2mp41", 1, 12, file);        // compatible_brands
}

Free Space Box(free)

free box 可选,用于存储一些不必要的信息,比如版权声明等,不填写该 box 也不影响封装成视频文件。

字段大小含义写入内容
box length4 字节box 长度0X00000008
box type4 字节box 类型,固定值:freefree

使用十六进制查看工具打开MP4文件,查看对应内容:

在这里插入图片描述

代码实现:

void write_free(FILE* file) {
    uint32_t size = 0X08;
    write_uint32(file, size);                  // box length
    fwrite("free", 1, 4, file);                // box type
}

Movie Data Box(mdat)

mdat box 存放媒体数据,mdat box 可选,但一个媒体文件中必须存在一个 mdat box。对于目前项目的实际运用之中,一般只有一个,存储 MJPEG 数据。该 box 可以位于 moov box 前,也可以位于 moov box 后。但必须和 stbl 中的信息保持一致。

字段大小含义写入内容
box length4 字节box 长度不定长
box type4 字节box 类型,固定值:mdatmdat
box datan 字节图像数据MJPEG文件数据内容

mdat box 存放每一帧的图像数据,一般是先填写 box length 为 0,再填写完后再回来填写长度。对于其他不定长的 box,基本上都采取这种方法。

对于MJPEG的图像数据,只用把MJPEG的文件头(FF D8)和文件尾(FF D9)所包含的数据依次全部写入进去即可。

使用十六进制查看工具打开MP4文件,查看对应内容:

在这里插入图片描述

标准文档定义:

aligned(8) class MeidaDataBox extends Box(‘mdat’) {  
  bit(8) data[]; // to end of the box
}

代码实现:

// 将 JPEG 文件写入 mdat 并记录偏移和大小
void write_mdat(FILE* mp4_file, const char** jpeg_files, size_t jpeg_count, uint32_t* frame_offsets, uint32_t* frame_sizes, uint32_t position) {
    // 写入 mdat box 的头部
    uint32_t mdat_size = 8; // 初始化为 header 的大小
    for (size_t i = 0; i < jpeg_count; i++) {
        FILE* jpeg_file = fopen(jpeg_files[i], "rb");
        if (!jpeg_file) {
            perror("Failed to open JPEG file");
            exit(EXIT_FAILURE);
        }
        uint8_t data;
        while (1)
        {
            fread(&data, 1, 1, jpeg_file);
            if (data == 0XFF)
            {
                fread(&data, 1, 1, jpeg_file);
                if (data == 0XD9)
                {
                    break;
                }
            }
        }
        // 获取文件大小
        uint32_t frame_size = ftell(jpeg_file);
        rewind(jpeg_file);
        frame_offsets[i] = mdat_size;  // 记录当前帧偏移
        frame_sizes[i] = frame_size;  // 记录当前帧大小
        mdat_size += frame_size;      // 更新 mdat 总大小

        // 写入 JPEG 数据
        uint8_t* buffer = (uint8_t*)malloc(frame_size);
        fread(buffer, 1, frame_size, jpeg_file);
        fwrite(buffer, 1, frame_size, mp4_file);
        free(buffer);
        fclose(jpeg_file);
    }
    // 回到文件开头写入 mdat box 长度和类型
    fseek(mp4_file, position, SEEK_SET);
    write_uint32(mp4_file, mdat_size); // 写入 mdat 长度
    fwrite("mdat", 1, 4, mp4_file);
    fseek(mp4_file, 0, SEEK_END);
}

Movie Box(moov)

moov box 存放媒体文件的大多数信息,moov box 可选,但一个媒体文件中必须存在一个 moov box。

字段大小含义写入内容
box length4 字节box 长度不定长
box type4 字节box 类型,固定值:moovmoov
box datan 字节box 数据子box内容,包括:mvhd、trak等

使用十六进制查看工具打开MP4文件,查看对应内容:

在这里插入图片描述

代码实现:

uint32_t position_moov_size = ftell(mp4_file);
write_uint32(mp4_file, 0);                      // 占位 moov 长度
fwrite("moov", 1, 4, mp4_file);                 // 写入 moov 类型

对于box data,具体内容如下:

Movie Header Box(mvhd)

mvhd box 存放媒体文件的信息,比如创建时间、修改时间、时间标尺、播放时间、时长等。

字段大小含义写入内容
box length4 字节box 长度0X0000006C
box type4 字节box 类型,固定值:mvhdmvhd
version1 字节版本,取0或者1,一般取00X00
flags3 字节标志0X000000
creation_time8/4 字节创建时间,当version=0时,取4字节0X00000000
modification_time8/4 字节修改时间,当version=0时,取4字节0X00000000
timescale4 字节时间标尺,用于计算时间0X000003E8(十进制1000)
duration4 字节时长,单位为timescaleframe_count * timescale / fps
rate4 字节播放速率,高16位为小数点后2位,低16位为整数0X00010000(1.0)
volume2 字节音量,0为静音,1为最大音量0X0100(1.0)
reserved10 字节保留字段0
matrix36 字节矩阵数据0
pre_defined24 字节预定义字段0
next_track_ID4 字节下一个track的ID,一般为track_count+10X00000002

对于创建时间、修改时间不影响MP4的播放,所以可以都设置为0。视频帧率设置的是30帧。

frame_count :视频帧数,也可以理解成图像个数,一个图像就是一个frame。

timescale :时间标尺/时间基数,在此次封装中默认为1000

fps :视频帧数,在此次封装中默认为30帧。

使用十六进制查看工具打开MP4文件,查看对应内容:

在这里插入图片描述

标准文档定义:

aligned(8) class MovieHeaderBox extends FullBox(‘mvhd’, version, 0) {
    if (version==1) {
        unsigned int(64) creation_time;
        unsigned int(64) modification_time;
        unsigned int(32) timescale;
        unsigned int(64) duration;
    } else { // version==0
        unsigned int(32) creation_time;
        unsigned int(32) modification_time;
        unsigned int(32) timescale;
        unsigned int(32) duration;
    }
    template int(32) rate = 0x00010000; // typically 1.0
    template int(16) volume = 0x0100; // typically, full volume
    const bit(16) reserved = 0;
    const unsigned int(32)[2] reserved = 0;
    template int(32)[9] matrix =
    { 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 };
    // Unity matrix
    bit(32)[6] pre_defined = 0;
    unsigned int(32) next_track_ID;
}

代码实现:

void write_mvhd(FILE* file, uint32_t frame_count, uint32_t timescale) 
{
    uint32_t size = 0X6C;
    write_uint32(file, size);
    fwrite("mvhd", 1, 4, file);
    write_uint32(file, 0);                              //version and flags
    write_uint32(file, 0);                              // Creation time
    write_uint32(file, 0);                              // Modification time
    write_uint32(file, timescale);                      // Timescale
    write_uint32(file, frame_count * timescale / 30);   // Duration
    fwrite("\x00\x01\x00\x00", 1, 4, file);             // Rate
    fwrite("\x01\x00", 1, 2, file);                     // Volume
    fwrite("\x00\x00", 1, 2, file);                     // Reserved

    fwrite("\x00\x00\x00\x00", 1, 4, file);             // Reserved
    fwrite("\x00\x00\x00\x00", 1, 4, file);             // Reserved

    fwrite("\x00\x00\x00\x00", 1, 4, file);             // Matrix
    fwrite("\x00\x00\x00\x00", 1, 4, file);             // Matrix
    fwrite("\x00\x00\x00\x00", 1, 4, file);             // Matrix
    fwrite("\x00\x00\x00\x00", 1, 4, file);             // Matrix
    fwrite("\x00\x00\x00\x00", 1, 4, file);             // Matrix
    fwrite("\x00\x00\x00\x00", 1, 4, file);             // Matrix
    fwrite("\x00\x00\x00\x00", 1, 4, file);             // Matrix
    fwrite("\x00\x00\x00\x00", 1, 4, file);             // Matrix
    fwrite("\x00\x00\x00\x00", 1, 4, file);             // Matrix

    fwrite("\x00\x00\x00\x00", 1, 4, file);             // Pre_defined
    fwrite("\x00\x00\x00\x00", 1, 4, file);             // Pre_defined
    fwrite("\x00\x00\x00\x00", 1, 4, file);             // Pre_defined
    fwrite("\x00\x00\x00\x00", 1, 4, file);             // Pre_defined
    fwrite("\x00\x00\x00\x00", 1, 4, file);             // Pre_defined
    fwrite("\x00\x00\x00\x00", 1, 4, file);             // Pre_defined

    write_uint32(file, 2);                              // Next track ID
}

Track Box(trak)

trak box 记录媒体流信息,文件中可以存在一个或多个 track,它们之间是相互独立的。一般的包含一个视频track和一个音频track。

字段大小含义写入内容
box length4 字节box 长度不定长
box type4 字节box 类型,固定值:traktrak
box datan 字节box 数据子box内容,包括tkhd、edts、mdia等

此次封装只涉及将MJPEG封装为MP4,不涉及音频,所以只有一个trak box。

使用十六进制查看工具打开MP4文件,查看对应内容:

在这里插入图片描述

代码实现:

uint32_t position_trak_size = ftell(mp4_file);
write_uint32(mp4_file, 0);       // 占位 trak 长度
fwrite("trak", 1, 4, mp4_file);

对于box data,具体内容如下:

Track Header Box(tkhd)

tkhd box 存放track的信息,如果是视频会有宽、高信息、 还有文件创建时间、修改时间等。

字段大小含义写入内容
box length4 字节box 长度0X0000005C
box type4 字节box 类型,固定值:tkhdtkhd
version1 字节box 版本,取0或1,一般取00X00
flags3 字节box 标志0X000003
creation_time8/4 字节创建时间,当version=0时,取4字节0X00000000
modification_time8/4 字节修改时间,当version=0时,取4字节0X00000000
track_ID4 字节本track ID0X00000001
reserved4 字节保留字段0
duration4 字节时长frame_count * timescale / fps
reserved8 字节保留字段0
layer2 字节图层0X0000
alternate_group2 字节替代track组0X0000
volume2 字节音量(如果是audio trak,则为0X0100,否则为0)0X0000
reserved2 字节保留字段0X0000
matrix36 字节矩阵0
width4 字节宽度 [16.16]格式width << 10
height4 字节高度 [16.16]格式height << 10

使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:

在这里插入图片描述

标准文档定义:

aligned(8) class TrackHeaderBox extends FullBox(‘tkhd’, version, flags){
    if (version==1) {
        unsigned int(64) creation_time;
        unsigned int(64) modification_time;
        unsigned int(32) track_ID;
        const unsigned int(32) reserved = 0;
        unsigned int(64) duration;
    } else { // version==0
        unsigned int(32) creation_time;
        unsigned int(32) modification_time;
        unsigned int(32) track_ID;
        const unsigned int(32) reserved = 0;
        unsigned int(32) duration;
    }
    const unsigned int(32)[2] reserved = 0;
    template int(16) layer = 0;
    template int(16) alternate_group = 0;
    template int(16) volume = {if track_is_audio 0x0100 else 0};
    const unsigned int(16) reserved = 0;
    template int(32)[9] matrix=
    { 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 };
    // unity matrix
    unsigned int(32) width;
    unsigned int(32) height;
}

代码实现:

void write_tkhd(FILE* file, uint32_t frame_count, uint32_t timescale, int width, int height) {
    uint32_t size = 0X5C;
    write_uint32(file, size);
    fwrite("tkhd", 1, 4, file);
    write_uint32(file, 3);                              //version and flag
    write_uint32(file, 0);                              //creation_time
    write_uint32(file, 0);                              //modification_time
    write_uint32(file, 1);                              //track id
    write_uint32(file, 0);                              //reserved
    write_uint32(file, frame_count * timescale / 30);   //duration

    write_uint32(file, 0);                              //reserved
    write_uint32(file, 0);                              //reserved

    fwrite("\x00\x00", 1, 2, file);                     //layer
    fwrite("\x00\x00", 1, 2, file);                     //alternate_group
    fwrite("\x00\x00", 1, 2, file);                     //volume
    fwrite("\x00\x00", 1, 2, file);                     //reserved

    write_uint32(file, 0);                              //matrix
    write_uint32(file, 0);                              //matrix
    write_uint32(file, 0);                              //matrix
    write_uint32(file, 0);                              //matrix
    write_uint32(file, 0);                              //matrix
    write_uint32(file, 0);                              //matrix
    write_uint32(file, 0);                              //matrix
    write_uint32(file, 0);                              //matrix
    write_uint32(file, 0);                              //matrix

    write_uint32(file, width << 10);                    //width
    write_uint32(file, height << 10);                   //height
}
Edit Box(edts)

edts box 存放track的编辑信息,比如track在媒体文件中的起始时间、时长等。

字段大小含义写入内容
box length4 字节box 长度0X00000024
box type4 字节box 类型edts
box datan 字节box 数据子box内容,包括elst等

使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:

在这里插入图片描述

代码实现:

uint32_t size = 0X24;
write_uint32(file, size);
fwrite("edts", 1, 4, file);

对于box data,具体内容如下:

Edit List Box(elst)

elst box 存放track的编辑信息,比如track在媒体文件中的起始时间、时长等。

字段大小含义写入内容
box length4 字节box 长度0X0000001C
box type4 字节box 类型elst
version1 字节box 版本,取0或1,一般取00X00
flags3 字节box 标志0X000000
entry_count4 字节entry 数量0X00000001
segment_duration4 字节片段时长frame_count * timescale / fps
media_time4 字节媒体开始时间0X00000000
media_rate4 字节媒体播放速率0X00010000

使用十六进制查看工具打开MP4文件,查看对应内容:

在这里插入图片描述

标准文档定义:

aligned(8) class EditListBox extends Ful1Box('elst' , version, 0) {
    unsigned int (32) entry_count;
    for (i=1; i <= entry_count; i++) {
        if (version==1) {
            unsigned int(64) segment_duration;
            int(64) media_ time ;
        } else { // version==0
            unsigned int(32) segment_duration;
            int(32) media_time ;
        }
        int(16) media_rate_integer;
        int(16) media_rate_fraction = 0;
    }
}

代码实现:

write_uint32(file, 0X1C);
fwrite("elst", 1, 4, file);
write_uint32(file, 0);                                  //version and flag
write_uint32(file, 1);                                  //entry_count
write_uint32(file, frame_count * timescale / 30);       //segment_duration
write_uint32(file, 0);                                  //start time
fwrite("\x00\x01\x00\x00", 1, 4, file);                 //rate
Media Box(mdia)

mdia box 描述了这条音视频轨/流(trak)的媒体数据样本的主要信息,对播放器来说是一个很重要的 box。

字段大小含义写入内容
box length4 字节box 长度不定长
box type4 字节box 类型mdia
box datan 字节box 数据子box内容,包括mdhd等

使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:

在这里插入图片描述

代码实现:

uint32_t position_mdia_size = ftell(mp4_file);
write_uint32(mp4_file, 0);                              // 占位 mdia 长度
fwrite("mdia", 1, 4, mp4_file);

对于box data,具体内容如下:

Media Header Box(mdhd)

mdhd box 当前音/视频轨/流(trak)的总体信息。

字段大小含义写入内容
box length4 字节box 长度0X00000020
box type4 字节box 类型mdhd
version1 字节box 版本,取0或1,一般取00X00
flags3 字节box 标志0X000000
creation_time8/4 字节创建时间0X00000000
modification_time8/4 字节修改时间0X00000000
timescale4 字节时间标尺,同mvhd中的timescale0X00000001
duration8/4 字节本track时长0X00000000
pad1 位取00B
language3 * 5 位语言,最高位为0,后面15位为三个字符101010111000100B
pre_defined2 字节保留0X0000

对于language,见ISO 639-2/T,如中文为chi,英文为eng

使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:

在这里插入图片描述

标准文档定义:

aligned(8) class MediaHeaderBox extends FullBox(‘mdhd’, version, 0) {
    if (version==1) {
        unsigned int(64) creation_time;
        unsigned int(64) modification_time;
        unsigned int(32) timescale;
        unsigned int(64) duration;
    } else { // version==0
        unsigned int(32) creation_time;
        unsigned int(32) modification_time;
        unsigned int(32) timescale;
        unsigned int(32) duration;
    }
    bit(1) pad = 0;
    unsigned int(5)[3] language; // ISO-639-2/T language code
    unsigned int(16) pre_defined = 0;
}

代码实现:

void write_mdhd(FILE* file, uint32_t frame_count, uint32_t timescale) {
    uint32_t size = 0X20;
    write_uint32(file, size);
    fwrite("mdhd", 1, 4, file);
    write_uint32(file, 0);                              //version and flag
    write_uint32(file, 0);                              //creation_time
    write_uint32(file, 0);                              //modification_time
    write_uint32(file, timescale);                      //timescale
    write_uint32(file, frame_count * timescale / 30);   //duration
    fwrite("\x55\xC4\x00\x00", 1, 4, file);             //pad and language and pre_defined
}
Media Handler Reference Box(hdlr)

hdlr box 解释了媒体的播放过程信息。

字段大小含义写入内容
box length4 字节box 长度0x0000002D
box type4 字节box 类型hdlr
version1 字节box 版本0X00
flags3 字节box 标志0X000000
pre_defined4 字节保留0X00000000
handler_type4 字节类型0X6D646976
reserved12 字节保留0
namen 字节自定义名称,以’\0’结尾VideoHandler

handler_type :
取值为
vide:Video track,soun:Audio track,
hint:Hint track,
meta:Timed Metadata track,
auxv:Auxiliary Video track
在此处,0X6D646976,将每一个字节转成ASCII字符,根据大端序排序,即vide

使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:

在这里插入图片描述

标准文档定义:

aligned(8) class HandlerBox extends FullBox(‘hdlr’, version = 0, 0) {
    unsigned int(32) pre_defined = 0;
    unsigned int(32) handler_type;
    const unsigned int(32)[3] reserved = 0;
    string name;
}

代码实现:

void write_hdlr(FILE* file) {
    uint32_t size = 0X2D;
    write_uint32(file, size);
    fwrite("hdlr", 1, 4, file);
    write_uint32(file, 0);                              // version and flag
    write_uint32(file, 0);                              // pre_defined
    fwrite("vide", 1, 4, file);                         // handler_type
    write_uint32(file, 0);                              // reserved
    write_uint32(file, 0);                              // reserved
    write_uint32(file, 0);                              // reserved
    fwrite("VideoHandler", 1, 12, file);                // name
    fwrite("\x00", 1, 1, file);
}
Midia Information Box(minf)

minf box 存放track的媒体信息,比如track的duration、timescale等。

字段大小含义写入内容
box length4 字节box 长度不定长
box type4 字节box 类型minf
box datan 字节box 数据子box内容,包括vmhd、dinf等

使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:

在这里插入图片描述

代码实现:

uint32_t position_minf_size = ftell(mp4_file);
write_uint32(mp4_file, 0);                              // 占位 minf 长度
fwrite("minf", 1, 4, mp4_file);

对于box data,具体内容如下:

Video Media Handler Box(vmhd)

vmhd box 存放track的媒体信息,比如track的duration、timescale等。

字段大小含义写入内容
box length4 字节box 长度0x00000014
box type4 字节box 类型vmhd
version1 字节box 版本0X00
flags3 字节box 标志0X000001
graphics mode2 字节视频合成模式,为0时拷贝原始图像,否则与opcolor进行合成0X0000
opcolor6 字节操作颜色{red, green, blue}0X000000000000

使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:

在这里插入图片描述

标准文档定义:

aligned(8) class VideoMediaHeaderBox extends FullBox(‘vmhd’, version = 0, 1) {
    template unsigned int(16) graphicsmode = 0; // copy, see below
    template unsigned int(16)[3] opcolor = {0, 0, 0};
}

代码实现:

void write_vmhd(FILE* file) {
    uint32_t size = 0X14;
    write_uint32(file, size);
    fwrite("vmhd", 1, 4, file);
    write_uint32(file, 1);                              // version and flag
    fwrite("\x00\x00", 1, 2, file);                     // graphicsmode
    fwrite("\x00\x00\x00\x00\x00\x00", 1, 6, file);     // opcolor
}
Data Information Box(dinf)

dinf 解释如何定位媒体信息,是一个 container box。dinf 一般包含一个 dref,即 data reference box; dref 下会包含若干个 url 或 urn ,这些box组成一个表,用来定位track数据。简单的说,track可以被分成若干段,每一段都可以根据 url 或 urn 指向的地址来获取数据,sample描述中会用这些片段的序号将这些片段组成一个完整的track。

字段大小含义写入内容
box length4 字节box 长度不定长(0X00000024)
box type4 字节box 类型dinf
box datan 字节box 数据子box内容,包括dref等

使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:

在这里插入图片描述

代码实现:

uint32_t size = 0X24;
write_uint32(file, size);
fwrite("dinf", 1, 4, file);

对于box data,具体内容如下:

Data Reference Box(dref)
字段大小含义写入内容
box length4 字节box 长度不定长(0X0000001C)
box type4 字节box 类型dref
verion1 字节box 版本0X00
flags3 字节box 标志0X000000
entry count4 字节条目数量0X00000001
url/urnn 字节url/urn 列表url/urn box

url/urn box:

字段大小含义写入内容
box length4 字节box 长度0X0000000C
box type4 字节box 类型url
box flag4 字节box 标志0X00000001

一般情况下,当数据被完全包含在文件中时,url或urn中的定位字符串是空的。

url或urn都是box,url的内容为字符串,urn的内容为一对字符串。当url或urn的box flag为1时,字符串均为空。

使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:

在这里插入图片描述

标准文档定义:

aligned(8) class DataEntryUrlBox (bit(24) flags)
    extends FullBox(‘url ’, version = 0, flags) {
    string location;
}
aligned(8) class DataEntryUrnBox (bit(24) flags)
    extends FullBox(‘urn ’, version = 0, flags) {
    string name;
    string location;
}
aligned(8) class DataReferenceBox
    extends FullBox(‘dref’, version = 0, 0) {
    unsigned int(32) entry_count;
    for (i=1; i <= entry_count; i++) {
        DataEntryBox(entry_version, entry_flags) data_entry;
    }
}

代码实现:

write_uint32(file, 0X1C);                               
fwrite("dref", 1, 4, file);
write_uint32(file, 0);                                      // version and flags
write_uint32(file, 1);                                      // entry count
write_uint32(file, 0X0C);                                   // url box length
fwrite("url ", 1, 4, file);                                 // url box type
write_uint32(file, 1);                                      // url box flag

由绿色部分我们知道包含的 url 或 urn 个数为1,绿色后面为 url box 的内容。紫色为 url 的 box header(根据box type我们知道是个 url ),粉色为 box flag,值为 1,说明 url 中的字符串为空,表示 track 数据已包含在文件中。

注意:虽然写入内容应该是不定长,但是实际写入时,为固定值。

Sample Description Box(stbl)

stbl 解释如何解码媒体信息,是一个 container box。stbl 包含了编码信息、解码参数、时间戳等信息,是媒体文件中最重要的部分。stbl 中包含的各个 box 的含义如下:

字段大小含义写入内容
box length4 字节box 长度不定长
box type4 字节box 类型stbl
box datan 字节box 数据子box内容,包括stsd、stts等

使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:

在这里插入图片描述

代码实现:

uint32_t position_stbl_size = ftell(mp4_file);
write_uint32(mp4_file, 0);                              // 占位 stbl 长度
fwrite("stbl", 1, 4, mp4_file);

对于box data,具体内容如下:

Sample Description Entry(stsd)

stsd 描述了媒体数据的编码信息,是一个 container box。stsd 中包含一个或多个 sample description box,每个 sample description box 描述了一种编码格式。stsd 的 box data 中包含一个 sample description entry,每个 sample description entry 描述了一种编码格式。

字段大小含义写入内容
box length4 字节box 长度0X000000C0
box type4 字节box 类型stsd
version1 字节box 版本0X00
flags3 字节box 标志0X000000
entry count4 字节条目数量0X00000001
box datan 字节box 数据子box内容,包括mp4v box等

使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:

在这里插入图片描述

标准文档定义:

aligned(8) class SampleDescriptionBox (unsigned int(32) handler_type)
    extends FullBox('stsd', 0, 0){
        int i ;
        unsigned int(32) entry_count;
        for (i = 1 ; i <= entry_count ; i++){
            switch (handler_type){
                case ‘soun’: // for audio tracks
                AudioSampleEntry();
                break;
                case ‘vide’: // for video tracks
                VisualSampleEntry();
                break;
                case ‘hint’: // Hint track
                HintSampleEntry();
                break;
                case ‘meta’: // Metadata track
                MetadataSampleEntry();
                break;
            }
        }
    }
}

代码实现:

uint32_t size = 0XC0;
write_uint32(file, size);
fwrite("stsd", 1, 4, file);
write_uint32(file, 0);                                   // version and flags
write_uint32(file, 1);                                   // entry count

对于box data,具体内容如下:

Sample Description Entry(sample entry)

mp4v box:

对于video track,使用“VisualSampleEntry”类型信息。

字段大小含义写入内容
box length4 字节box 类型0X000000B0
box type4 字节box 类型mp4v
reserved6 字节保留0
data_reference_index2 字节数据索引0X0001
pre_defined2 字节保留0
reserved2 字节保留0
pre_defined12 字节保留0
width2 字节像素宽度0X0500
height2 字节像素高度0X02D0
horizresolution4 字节水平,每英尺的像素值0X00480000
vertresolution4 字节垂直,每英尺的像素值0X00480000
reserved4 字节保留0
frame_count2 字节每个sample中的视频帧数,固定为10X0001
compressor32 字节压缩器标识符\x13Lavc61.19.100 mjpeg
depth2 字节深度0X0018
pre_defined2 字节保留,以0XFFFF填充0XFFFF

使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:

在这里插入图片描述

标准文档定义:

aligned(8) class VisualSampleEntry(codingname) extends SampleEntry (codingname){
    unsigned int(16) pre_defined = 0;
    const unsigned int(16) reserved = 0;
    unsigned int(32)[3] pre_defined = 0;
    unsigned int(16) width;
    unsigned int(16) height;
    template unsigned int(32) horizresolution = 0x00480000; // 72 dpi
    template unsigned int(32) vertresolution = 0x00480000; // 72 dpi
    const unsigned int(32) reserved = 0;
    template unsigned int(16) frame_count = 1;
    string[32] compressorname;
    template unsigned int(16) depth = 0x0018;
    int(16) pre_defined = -1;
    // other boxes from derived specifications
    CleanApertureBox clap; // optional
    PixelAspectRatioBox pasp; // optional
}

代码实现:

write_uint32(file, 0XB0);
fwrite("mp4v", 1, 4, file);
fwrite("\x00\x00\x00\x00\x00\x00", 1, 6, file);         // reserved
fwrite("\x00\x01", 1, 2, file);                         // data_reference_index
fwrite("\x00\x00", 1, 2, file);                         // pre_defined
fwrite("\x00\x00", 1, 2, file);                         // reserved
write_uint32(file, 0X0);                                // pre_defined
write_uint32(file, 0X0);                                // pre_defined
write_uint32(file, 0X0);                                // pre_defined
write_uint32(file, 0X0);                                // pre_defined
fwrite("\x05\x00\x02\xD0", 1, 4, file);                 // width and height
fwrite("\x00\x48\x00\xD0", 1, 4, file);                 // horizresolution
fwrite("\x00\x48\x00\xD0", 1, 4, file);                 // vertresolution
write_uint32(file, 0X0);                                // reserved
fwrite("\x00\x01", 1, 2, file);                         // frame_count
fwrite("\x13Lavc61.19.100 mjpeg", 1, 20, file);         // compressorname
write_uint32(file, 0X0);
write_uint32(file, 0X0);
write_uint32(file, 0X0);
fwrite("\x00\x18\xFF\xFF", 1, 4, file);                 // depth and pre_defined

esds box:

字段大小含义写入内容
box length4 字节box 类型0X0000001C
box type4 字节box 类型esds
version1 字节box 版本0X00
flags3 字节box 标志0X000000
ESDescriptorn 字节嵌套的 ESDescriptor 数据,描述流信息子box内容

使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:

在这里插入图片描述

代码实现:

write_uint32(file, 0X2C);
fwrite("esds", 1, 4, file);
write_uint32(file, 0X0);                                // version and flags

ESDescriptor:

字段大小含义写入内容
tag1 字节描述符类型,固定为 0X030X03
length4 字节描述符长度,包含接下来的所有字段0X8080801B
ES_ID2 字节ES 标识符,唯一标识此流0X0001
streamDependenceFlag1 位是否依赖其他流,通常为 00
URL_Flag1 位是否包含 URL 描述符,通常为 00
OCRstreamFlag1 位是否有 OCR 流标识符,通常为 00
streamPriority5 位流的优先级,通常为 000000
DependsOn_ES_ID可选如果 streamDependenceFlag=1,则填写此字段此处不填写
URLstring可选如果 URL_Flag=1,则填写此字段此处不填写
OCR_ES_Id可选如果 OCRstreamFlag=1,则填写此字段此处不填写
DecoderConfigDescriptorn 字节嵌套的 DecoderConfigDescriptor 数据子box内容

使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:

在这里插入图片描述

代码实现:

//该字段内容一般不会变,直接写入即可
fwrite("\x03\x80\x80\x80\x1B\x00\x01\x00", 1, 8, file);

DecoderConfigDescriptor:

字段大小含义写入内容
tag1 字节描述符类型,固定为 0X040X04
length4 字节描述符长度,包含接下来的所有字段0X8080800D
objectTypeIndication1 字节编码对象类型,标识流的编码格式0X6C
streamType6 位流类型,通常为 0x04(视频)或 0x05(音频)000100
upstreamFlag1 位是否为上行流,通常为 00
reserved1 位保留位,固定为 11
bufferSizeDB3 字节解码器的缓冲区大小0X000000
maxBitrate4 字节最大比特率0X00BA0E28
avgBitrate4 字节平均比特率0X00BA0E28
SLConfigDescrTagn 字节嵌套的 SLConfigDescrTag 数据子box内容

使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:

在这里插入图片描述

代码实现:

//该字段内容一般不会变,直接写入即可
fwrite("\x04\x80\x80\x80\x0D\x6C\x11\x00", 1, 8, file);
fwrite("\x00\x00\x00\xBA\x0E\x28\x00\xBA\x0E\x28", 1, 10, file);

objectTypeIndication:0X6C 对应了一种编码:AV_CODEC_ID_MJPEG。FFmpeg isom.c中定义了MP4容器支持的所有编码格式:

/* http://www.mp4ra.org */
/* ordered by muxing preference */
const AVCodecTag ff_mp4_obj_type[] = {
    { AV_CODEC_ID_MOV_TEXT    , 0x08 },
    { AV_CODEC_ID_MPEG4       , 0x20 },
    { AV_CODEC_ID_H264        , 0x21 },
    { AV_CODEC_ID_HEVC        , 0x23 },
    { AV_CODEC_ID_AAC         , 0x40 },
    { AV_CODEC_ID_MP4ALS      , 0x40 }, /* 14496-3 ALS */
    { AV_CODEC_ID_MPEG2VIDEO  , 0x61 }, /* MPEG-2 Main */
    { AV_CODEC_ID_MPEG2VIDEO  , 0x60 }, /* MPEG-2 Simple */
    { AV_CODEC_ID_MPEG2VIDEO  , 0x62 }, /* MPEG-2 SNR */
    { AV_CODEC_ID_MPEG2VIDEO  , 0x63 }, /* MPEG-2 Spatial */
    { AV_CODEC_ID_MPEG2VIDEO  , 0x64 }, /* MPEG-2 High */
    { AV_CODEC_ID_MPEG2VIDEO  , 0x65 }, /* MPEG-2 422 */
    { AV_CODEC_ID_AAC         , 0x66 }, /* MPEG-2 AAC Main */
    { AV_CODEC_ID_AAC         , 0x67 }, /* MPEG-2 AAC Low */
    { AV_CODEC_ID_AAC         , 0x68 }, /* MPEG-2 AAC SSR */
    { AV_CODEC_ID_MP3         , 0x69 }, /* 13818-3 */
    { AV_CODEC_ID_MP2         , 0x69 }, /* 11172-3 */
    { AV_CODEC_ID_MPEG1VIDEO  , 0x6A }, /* 11172-2 */
    { AV_CODEC_ID_MP3         , 0x6B }, /* 11172-3 */
    { AV_CODEC_ID_MJPEG       , 0x6C }, /* 10918-1 */
    { AV_CODEC_ID_PNG         , 0x6D },
    { AV_CODEC_ID_JPEG2000    , 0x6E }, /* 15444-1 */
    { AV_CODEC_ID_VC1         , 0xA3 },
    { AV_CODEC_ID_DIRAC       , 0xA4 },
    { AV_CODEC_ID_AC3         , 0xA5 },
    { AV_CODEC_ID_EAC3        , 0xA6 },
    { AV_CODEC_ID_DTS         , 0xA9 }, /* mp4ra.org */
    { AV_CODEC_ID_VP9         , 0xC0 }, /* nonstandard, update when there is a standard value */
    { AV_CODEC_ID_FLAC        , 0xC1 }, /* nonstandard, update when there is a standard value */
    { AV_CODEC_ID_TSCC2       , 0xD0 }, /* nonstandard, camtasia uses it */
    { AV_CODEC_ID_EVRC        , 0xD1 }, /* nonstandard, pvAuthor uses it */
    { AV_CODEC_ID_VORBIS      , 0xDD }, /* nonstandard, gpac uses it */
    { AV_CODEC_ID_DVD_SUBTITLE, 0xE0 }, /* nonstandard, see unsupported-embedded-subs-2.mp4 */
    { AV_CODEC_ID_QCELP       , 0xE1 },
    { AV_CODEC_ID_MPEG4SYSTEMS, 0x01 },
    { AV_CODEC_ID_MPEG4SYSTEMS, 0x02 },
    { AV_CODEC_ID_NONE        ,    0 },
};

SLConfigDescrTag:

字段大小含义写入内容
tag1 字节描述符类型,固定为 0X060X06
length4 字节描述符长度,包含接下来的所有字段0X80808001
predefined1 字节预定义0X02

使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:

在这里插入图片描述

代码实现:

//该字段内容一般不会变,直接写入即可
fwrite("\x06\x80\x80\x80\x01\x02", 1, 6, file);

对于上述ESDescriptor表、DecoderConfigDescriptorSLConfigDescrTag,其中的 length 采用 VLVC (Variable Length Value Coding) 格式编码。每个字节的最高位是延续位,用来指示是否还有后续字节:

  • 1 表示还有后续字节
  • 0 表示没有后续字节,当前字节是最后一个字节

其余七位表示实际的数值部分。

例如:0X8080800D 是 4 个字节,其中每个字节表示如下:

  • 0x80 的二进制是 10000000:延续位为 1,表示后面还有字节。
  • 0x80 的二进制是 10000000:延续位为 1,表示后面还有字节。
  • 0x80 的二进制是 10000000:延续位为 1,表示后面还有字节。
  • 0x1B 的二进制是 00011011:延续位为 0,表示这是最后一个字节。

移除每个字节的最高位(延续位),只保留数值部分:

  • 第 1 字节:0x80 -> 0x00
  • 第 2 字节:0x80 -> 0x00
  • 第 3 字节:0x80 -> 0x00
  • 第 4 字节:0x1B -> 0x1B

将这些数值部分按照顺序拼接起来:

拼接后得到:0x0000001B(十六进制)或 27(十进制)。

fiel box:

字段大小含义写入内容
box length4 字节box 长度0X0000000A
box type4 字节box 类型fiel
field_count1 字节扫描模式,1 表示逐行扫描,2 表示隔行扫描0X01
field_order1 字节场序,仅在隔行扫描模式时有效0X00

使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:

在这里插入图片描述

代码实现:

write_uint32(file, 0X0A);
fwrite("fiel", 1, 4, file);
fwrite("\x01\x00", 1, 2, file);                         // field_count 和 field_order

对于 field_order 的取值说明如下:

  • 0: 无场序或未定义
  • 1: 上场(Top Field First)
  • 6: 下场(Bottom Field First)

pasp box:

字段大小含义写入内容
box length4 字节box 长度0X00000010
box type4 字节box 类型pasp
hSpacing4 字节水平方向像素的比例因子0X00000001
vSpacing4 字节垂直方向像素的比例因子0X00000001

使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:

在这里插入图片描述

代码实现:

write_uint32(file, 0X10);
fwrite("pasp", 1, 4, file);
write_uint32(file, 1);                                  // hSpacing
write_uint32(file, 1);                                  // vSpacing

像素长宽比 = vSpacing / hSpacing

如果 hSpacing 为 1 且 vSpacing 为 1,则表示像素是正方形。

btrt box:

字段大小含义写入内容
box length4 字节box 长度0X00000014
box type4 字节box 类型btrt
bufferSizeDB4 字节解码器所需的缓冲区大小,单位为字节0X00000000
maxBitrate4 字节流的最大比特率,单位为比特每秒0X00BA0E28
avgBitrate4 字节流的平均比特率,单位为比特每秒0X00BA0E28

使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:

在这里插入图片描述

代码实现:

write_uint32(file, 0X14);
fwrite("btrt", 1, 4, file);
write_uint32(file, 0);
fwrite("\x00\xBA\x0E\x28", 1, 4, file);                 // maxBitrate
fwrite("\x00\xBA\x0E\x28", 1, 4, file);                 // avgBitrate
Decoding Time to Sample Box(stts)

stts 包含了一个压缩版本的表,通过这个表可以从解码时间映射到sample序号。表中的每一项是连续相同的编码时间增量(Decode Delta)的个数和编码时间增量。通过把时间增量累加就可以建立一个完整的time to sample表。

字段大小含义写入内容
box length4 字节box 类型0X00000018
box type4 字节box 类型stts
verion1 字节取 0 或 1,一般取 00X00
flags3 字节box 标志位0X000000
entry_count4 字节条目数量0X00000001
sample_count4 字节sample数量图片数量(0X00000008)
sample_delta4 字节sample间隔0X00000021

在 stts 中,sample_count 和 sample_delta 成对出现的次数为 entry_count。

使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:

在这里插入图片描述

标准文档定义:

aligned(8) class TimeToSampleBox extends FullBox(’stts’, version = 0, 0) {
    unsigned int(32) entry_count;
    int i;
    for (i=0; i < entry_count; i++) {
        unsigned int(32) sample_count;
        unsigned int(32) sample_delta;
    }
}

代码实现:

void write_stts(FILE* file, uint32_t frame_count, uint32_t timescale) {
    uint32_t size = 0X18;
    write_uint32(file, size);
    fwrite("stts", 1, 4, file);
    write_uint32(file, 0);                              //version and flag
    write_uint32(file, 1);                              //entry count
    write_uint32(file, frame_count);                    //sample count
    write_uint32(file, 33);                             //sample delta
}
Sample To Chunk Box(stsc)

media中的sample被分为组成chunk。chunk可以有不同的大小,chunk内的sample可以有不同的大小。

通过stsc中的sample-chunk映射表可以找到包含指定sample的chunk,从而找到这个sample。结构相同的chunk可以聚集在一起形成一个entry,这个entry就是stsc映射表的表项。

字段大小含义写入内容
box length4 字节box 类型0X0000001C
box type4 字节box 类型stsc
verion1 字节取 0 或 1,一般取 00X00
flags3 字节box 标志位0X000000
entry_count4 字节条目数量0X01
first_chunk4 字节一组 chunk 的第一个 chunk 的序号,chunk 的编号从 1 开始0X01
samples_per_chunk4 字节每个 chunk 有多少个 sample图片数量(0X08)
sample_description_index4 字节stsd box 中 sample desc 信息的索引0X01

在 stsc 中 first_chunk 、 samples_per_chunk 和 sample_description_index 成对出现的次数为 entry_count。

使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:

在这里插入图片描述

标准文档定义:

aligned(8) class SampleToChunkBoxextends FullBox(‘stsc’, version = 0, 0) {
    unsigned int(32) entry_count;
    for (i=1; i <= entry_count; i++) {
        unsigned int(32) first_chunk;
        unsigned int(32) samples_per_chunk;
        unsigned int(32) sample_description_index;
    }
}

代码实现:

void write_stsc(FILE* file, uint32_t frame_count, uint32_t timescale) {
    uint32_t size = 0X1C;
    write_uint32(file, size);
    fwrite("stsc", 1, 4, file);
    write_uint32(file, 0);                              // version and flag
    write_uint32(file, 1);                              // entry count
    write_uint32(file, 1);                              // first_chunk
    write_uint32(file, frame_count);                    // samples_per_chunk
    write_uint32(file, 1);                              // sample_description_index 
}
Sample Size Box(stsz)

stsz 包含了每个 sample 的大小,每个 sample 的大小都存储在 stsz 中。

字段大小含义写入内容
box length4 字节box 类型20 + 图片数量 * 4
box type4 字节box 类型stsz
verion1 字节取 0 或 1,一般取 00X00
flags3 字节box 标志位0X000000
sample_size4 字节指定默认的 sample 字节大小,如果所有 sample 的大小不一样,这个字段为 00X00000000
sample_count4 字节trak 中 sample数量图片数量(0X08)
entry_size4 字节每个 sample 的字节大小0X00000000

在 stsz 中,entry_size 出现的次数为 sample_count。

使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:

在这里插入图片描述

标准文档定义:

aligned(8) class SampleSizeBox extends FullBox(‘stsz’, version = 0, 0) {
    unsigned int(32) sample_size;
    unsigned int(32) sample_count;
    if (sample_size==0) {
        for (i=1; i <= sample_count; i++) {
            unsigned int(32) entry_size;
        }
    }
}

代码实现:

void write_stsz(FILE* file, uint32_t frame_count, uint32_t* frame_sizes) {
    uint32_t size = 20 + frame_count * 4;;
    write_uint32(file, size);
    fwrite("stsz", 1, 4, file);
    write_uint32(file, 0);                              //version and flag
    write_uint32(file, 0);                              //sample_size
    write_uint32(file, frame_count);                    //sample_count
    for (int i = 0; i < frame_count; i++)   
    {
        write_uint32(file, frame_sizes[i]);             //entry_size
    }
}
Chunk Offset Box(stco)

Chunk Offset 表存储了每个chunk在文件中的位置,这样就可以直接在文件中找到媒体数据,而不用解析box。

字段大小含义写入内容
box length4 字节box 类型0X00000014
box type4 字节box 类型stco
verion1 字节取 0 或 1,一般取 00X00
flags3 字节box 标志位0X000000
entry_count4 字节条目数量0X00000001
chunk_offset4 字节chunk 在文件中的偏移mdat字符位置

在 stco 中,chunk_offset 出现的次数为 entry_count。

其中,chunk_offset 的值是开始填入 mdat box 的位置。也就是写入 box type 为 mdat 后,获取文件指针位置,然后写入 chunk_offset。

使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:

在这里插入图片描述

标准文档定义:

aligned(8) class ChunkOffsetBox extends FullBox(‘stco’, version = 0, 0) {
    unsigned int(32) entry_count;
    for (i=1; i <= entry_count; i++) {
        unsigned int(32) chunk_offset;
    }
}

代码实现:

void write_stco(FILE* file, uint32_t position) {
    uint32_t size = 0X14;
    write_uint32(file, size);
    fwrite("stco", 1, 4, file);
    write_uint32(file, 0);                              // version and flag
    write_uint32(file, 1);                              // entry_count
    write_uint32(file, position);                       // chunk_offset    
}

提示

1、实际上,在 moov box 中还有一个 udat box,这个 box 是用来存储用户自定义信息的,这个 box 不是必须的,所以这里没有写入。

2、代码目前只支持在代码内更改需要转换的MJPEG图片,如果需要支持不定长图片数量进行转换,需要修改代码,添加文件读取功能。

3、分辨率也是写死的,如果需要支持不同的分辨率,请修改代码,可以通过读取图片文件信息获取分辨率,然后写入到代码中。

4、代码中输出的MP4文件名字是固定的,如果需要支持不同的文件名,请修改代码。

5、目前输出的MP4文件在PC端只能在VLC播放器中播放,经过测试,其他播放器播放该MP4文件会报错,具体原因未知,可能是因为MP4文件格式不标准。

6、在手机端,目前使用了OnePlus 一加手机进行了测试,将文件保存至相册,在相册内打开是可以正常播放的。

7、需要代码请私信我获取。


http://www.kler.cn/a/429128.html

相关文章:

  • HBASE学习(一)
  • 深度学习 Pytorch 张量的线性代数运算
  • 【Unity3D】利用Hinge Joint 2D组件制作绳索效果
  • 重学SpringBoot3-Spring Retry实践
  • PTA L1-039 古风排版
  • 云消息队列 Kafka 版 V3 系列荣获信通院“云原生技术创新标杆案例”
  • centOS7如何配置阿里云或者腾讯云yum源
  • 【Linux】搭建临时HTTP文件传输服务器
  • uniapp支持App横竖屏开发总结
  • iPhone 17 Air基本确认,3个大动作
  • 嵌入式学习——进程间通信方式(5)—— 信号量
  • 22. 五子棋小游戏
  • 阿里云PolarDB 如何进行数据恢复,文档总结
  • 【Qt】QMainWindow、QWidget和QDialog的区别?
  • Oracle 19C RU补丁升级,从19.7to19.25 -单机
  • 5G模组AT命令脚本-关闭模组的IP过滤功能
  • 驱动断链的研究
  • 【C++AVL树】枝叶间的旋律:AVL树的和谐之道
  • H5游戏出海如何获得更多增长机会?
  • 2024年12月9日Github流行趋势
  • Yocto bitbake and codeSonar
  • 【5G】Spectrum 频谱
  • 关于网页自动化工具DrissionPage进行爬虫的使用方法
  • flink终止提交给yarn的任务
  • 什么是CSS盒模型?box-sizing又是什么?
  • 架构09-可靠通信