Mp4格式解析

前言

結(jié)合Mp4分析工具,能夠更直觀的學(xué)習(xí)Mp4的各個Box,工具鏈接:
Mp4解析工具Java版

Mp4 Box介紹

多媒體封裝格式(也叫容器格式),是指按照一定的規(guī)則,將視頻數(shù)據(jù)、音頻數(shù)據(jù)等,放到一個文件中。常見的 MKV、AVI 以及本文介紹的 MP4 等,都是封裝格式。

MP4是最常見的封裝格式之一,因為其跨平臺的特性而得到廣泛應(yīng)用。MP4文件的后綴為.mp4,基本上主流的播放器、瀏覽器都支持MP4格式。

MP4文件由多個box組成,每個box存儲不同的信息,且box之間是樹狀結(jié)構(gòu),如下圖所示:


image2021-6-2_15-0-22.png

結(jié)構(gòu)定義:

所有Box,開頭均由8個字節(jié)組成,前4個字節(jié)表示該box的size,后面4個字節(jié)表示該box的類型,ftyp、moov、tkhd等。

class Box { 
  unsigned int(32) size; 
  unsigned int(32) type; 
} 

在Box的基礎(chǔ)上,擴(kuò)展出了FullBox類型。相比Box,F(xiàn)ullBox 多了 version、flags 字段,在moov中,葉子節(jié)點(diǎn)的Box都是FullBox

class FullBox extends Box { 
  unsigned int(8) version; 
  unsigned int(24) flag; 
}

ftyp

ftyp用來指出當(dāng)前文件遵循的規(guī)范,根據(jù)ftyp判斷當(dāng)前文件的格式,詳見格式嗅探。

結(jié)構(gòu)定義:

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

字段含義

major_brand:比如常見的 isom、mp41、mp42、avc1、qt等。它表示“最好”基于哪種格式來解析當(dāng)前的文件。舉例,major_brand 是 A,compatible_brands 是 A1,當(dāng)解碼器同時支持 A、A1 規(guī)范時,最好使用A規(guī)范來解碼當(dāng)前媒體文件,如果不支持A規(guī)范,但支持A1規(guī)范,那么,可以使用A1規(guī)范來解碼;

minor_version:提供 major_brand 的說明信息,比如版本號,不得用來判斷媒體文件是否符合某個標(biāo)準(zhǔn)/規(guī)范;

compatible_brands:文件兼容的brand列表。比如 mp41 的兼容 brand 為 isom。通過兼容列表里的 brand 規(guī)范,可以將文件 部分(或全部)解碼出來;

moov

moov屬于container box,這個box中不包含具體媒體數(shù)據(jù),但包含本文件中所有媒體數(shù)據(jù)的宏觀描述信息,moov box下有mvhd和trak box;mvhd中記錄了創(chuàng)建時間、修改時間、時間度量標(biāo)尺、可播放時長等信息;trak中的一系列子box描述了每個媒體軌道的具體信息。

mvhd

Movie Header Box,mp4文件的整體信息,比如創(chuàng)建時間、文件時長等。

結(jié)構(gòu)定義:

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;
}
int(32) rate = 0x00010000; // typically 1.0
int(16) volume = 0x0100; // typically, full volume const bit(16) reserved = 0;
const unsigned int(32)[2] reserved = 0; // 保留位,可忽略
int(32)[9] matrix =
{ 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 };
  
/**
 * 似乎也可以忽略的值
 * Preview Time
 * Preview Duration
 * Poster Time
 * Selection Time
 * Current Time
 * Next Track ID
 */
 int(32)[6]  pre_defined = 0;
 unsigned int(32)  next_track_ID;
}

字段含義

creation_time:文件創(chuàng)建時間;

modification_time:文件修改時間;

timescale:一秒包含的時間單位(整數(shù))。舉個例子,如果timescale等于1000,那么,一秒包含1000個時間單位(后面track等的時間,都要用這個來換算,比如track的duration為10,000,那么,track的實際時長為10,000/1000=10s);

duration:影片時長(整數(shù)),根據(jù)文件中的track的信息推導(dǎo)出來,等于時間最長的track的duration;

rate:推薦的播放速率,32位整數(shù),高16位、低16位分別代表整數(shù)部分、小數(shù)部分([16.16]),舉例 0x0001 0000 代表1.0,正常播放速度;

volume:播放音量,16位整數(shù),高8位、低8位分別代表整數(shù)部分、小數(shù)部分([8.8]),舉例 0x01 00 表示 1.0,即最大音量;

matrix:視頻的轉(zhuǎn)換矩陣,一般可以忽略不計;

next_track_ID:32位整數(shù),非0,一般可以忽略不計。當(dāng)要添加一個新的track到這個影片時,可以使用的track id,必須比當(dāng)前已經(jīng)使用的track id要大。也就是說,添加新的track時,需要遍歷所有track,確認(rèn)可用的track id;

trak

Track Box,一個mp4可以包含一個或多個軌道(比如視頻軌道、音頻軌道),軌道相關(guān)的信息就在trak里。trak是container box,至少包含兩個box,tkhd、mdia。

tkhd

單個 track 的 metadata,包含時長、音量、寬高等信息。

結(jié)構(gòu)定義:

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;
}

字段含義:

version:tkhd box的版本;

flags:按位或操作獲得,默認(rèn)值是7(0x000001 | 0x000002 | 0x000004),表示這個track是啟用的、用于播放的 且 用于預(yù)覽的。

  • Track_enabled:值為0x000001,表示這個track是啟用的,當(dāng)值為0x000000,表示這個track沒有啟用;
  • Track_in_movie:值為0x000002,表示當(dāng)前track在播放時會用到;
  • Track_in_preview:值為0x000004,表示當(dāng)前track用于預(yù)覽模式;

creation_time:當(dāng)前track的創(chuàng)建時間;

modification_time:當(dāng)前track的最近修改時間;

track_ID:當(dāng)前track的唯一標(biāo)識,不能為0,不能重復(fù);

duration:當(dāng)前track的完整時長(需要除以timescale得到具體秒數(shù));

layer:視頻軌道的疊加順序,數(shù)字越小越靠近觀看者,比如1比2靠上,0比1靠上;

alternate_group:當(dāng)前track的分組ID,alternate_group值相同的track在同一個分組里面。同個分組里的track,同一時間只能有一個track處于播放狀態(tài)。當(dāng)alternate_group為0時,表示當(dāng)前track沒有跟其他track處于同個分組。一個分組里面,也可以只有一個track;

volume:audio track的音量,介于0.0~1.0之間;

matrix:視頻的變換矩陣;

width、height:視頻的寬高;

edts

一個可選的container box,包含了編輯片段的信息

elst

具體的編輯片段信息

結(jié)構(gòu)定義:

class EditListBox extends FullBox(‘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(32) media_rate;
  } 
}

字段含義:

segment_duration: 表示該edit段的時長,以Movie Header Box(mvhd)中的timescale為單位,即 segment_duration/timescale = 實際時長(單位s)

media_time: 表示該edit段的起始時間,以track中Media Header Box(mdhd)中的timescale為單位。如果值為-1(FFFFFF),表示是空edit,一個track中最后一個edit不能為空。

media_rate: edit段的速率為0的話,edit段相當(dāng)于一個”dwell”,即畫面停止。畫面會在media_time點(diǎn)上停止segment_duration時間。否則這個值始終為1。

media

container box,包含track 媒體數(shù)據(jù)信息的Box。

mdhd

包含創(chuàng)建時間、文件時長等信息,mvhd針對整個影片,tkhd針對單個track,mdhd針對媒體,vmhd針對視頻,smhd針對音頻,可以認(rèn)為是從 寬泛 > 具體,前者一般是從后者推導(dǎo)出來的。

結(jié)構(gòu)定義:

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;
  }
  unsigned int(16) language; // ISO-639-2/T language code
  unsigned int(16) quality = 0; // 媒體的回放質(zhì)量
}

字段含義:

creation_time: track中數(shù)據(jù)的創(chuàng)建時間,多同‘tkhd’中creation_time.
modification_time: track中數(shù)據(jù)的修改時間,多同‘tkhd’中modification_time.
timescale: 媒體中時間尺度. 通常和'mvhd'中的timescale不同,精度更高.
duration: 媒體的長度(時間尺度表示).
language:語言,符合ISO-639-2/T標(biāo)準(zhǔn).

hdlr

聲明當(dāng)前track的類型,以及對應(yīng)的處理器(handler)。

結(jié)構(gòu)定義:

class HandlerBox extends FullBox(‘hdlr’, version = 0, 0) {
    unsigned int(32) componentType = 0; // 可忽略
    unsigned int(32) componentSubType;
    unsigned int(32) componentManufacture; // 可忽略
    unsigned int(32) componentFlags; // 可忽略
    unsigned int(32) componentFlagsMask; // 可忽略
    string componentName;
}

字段含義:

componentSubType:
即handler_type,表示track類型,andler_type的取值包括

  • vide(0x76 69 64 65),video track;
  • soun(0x73 6f 75 6e),audio track;
  • hint(0x68 69 6e 74),hint track;

componentName:name為utf8字符串,對handler名稱進(jìn)行描述

minf

該Box是Container Box,下面一般含有三大必須的子Box:

媒體信息頭Box: Vmhd Box或者Smhd Box;
數(shù)據(jù)信息Box:Dinf Box
采樣表Box:Stbl Box

vmhd

針對視頻的box,包含視頻合成模式、合成信息。

結(jié)構(gòu)定義:

class VideoMediaHeaderBox extends FullBox(‘vmhd’, version = 0, 0) {
    unsigned int(16) graphicsMode = 0;
    unsigned int(16) opcolorRed;
    unsigned int(16) opcolorGreen;
    unsigned int(16) opcolorBlue;
}

字段含義:

Graphics mode: 0,直接拷貝原始圖片, 否則與opcolor進(jìn)行合成
Opcolor: 三個16位值,用于指定圖形模式字段中指示的傳輸模式操作的紅色,綠色和藍(lán)色。

smhd

針對音頻的box。

結(jié)構(gòu)定義:

class VideoMediaHeaderBox extends FullBox(‘vmhd’, version = 0, 0) {
    unsigned int(16) balance = 0;
    unsigned int(16) reserved; // 保留位
}

字段含義:

balance: 一個16位整數(shù),指定此聲音媒體的聲音平衡。聲音平衡是控制計算機(jī)兩個揚(yáng)聲器之間混音的設(shè)置。該字段通常設(shè)置為0。平衡值表示為16位定點(diǎn)數(shù),范圍從-1.0到+1.0。
高階8位包含值的整數(shù)部分;低8位包含小數(shù)部分。負(fù)值使平衡器向左揚(yáng)聲器加權(quán);正值強(qiáng)調(diào)正確的渠道。將天平設(shè)置為0對應(yīng)于中性設(shè)置。

dinf

這個Box也是一個container box,一般用來定位媒體信息。一般會包含一個Dref Box即data reference box。

dref

dref下面會有若干個Url Box或者也叫 Urn Box,這些Box組成一個表,用來定位Track的數(shù)據(jù)。
Track可以被分成若干個段,每一段都可以根據(jù)Url或者Urn指向的地址來獲取數(shù)據(jù),sample描述中會用這些片段的序號將這些片段組成一個完整的track,一般情況下當(dāng)數(shù)據(jù)完全包含在文件中,Url和Urn Box的字符串是空的。
這個Box存在的意義就是允許MP4文件的媒體數(shù)據(jù)分開最后還能進(jìn)行恢復(fù)合并操作,但是實際上,Track的數(shù)據(jù)都保存在文件中,所以該字段的重要性還體現(xiàn)不出來。

結(jié)構(gòu)定義:

class DrefListBox extends FullBox(‘dref’, version, 0) {
   unsigned int(32)  entry_count;
   for (i=1; i <= entry_count; i++) {
         unsigned int(32) size;
         unsigned int(32) type;
         unsigned int(8) version;
         unsigned int(24) flag;
         byte* data;
    }
}

字段含義:

entry結(jié)構(gòu)圖:


img

entry type類型說明:


img
stbl

container box, MP4文件的媒體數(shù)據(jù)部分在mdat box里,而stbl則包含了這些媒體數(shù)據(jù)的索引以及時間信息。

stsd

該box給出視頻、音頻的編碼、寬高、音量等信息,以及每個sample中包含多少個frame,stsd給出sample的描述信息,這里面包含了在解碼階段需要用到的任意初始化信息,比如 編碼 等。對于視頻、音頻來說,所需要的初始化信息不同。

結(jié)構(gòu)定義:

class StsdBox extends FullBox(‘dref’, version, 0) {
   unsigned int(32)  entry_count;
   SampleEntry * sampleEntry;
}

根據(jù)不同的track類型,sampleEntry有不同的結(jié)構(gòu)定義

class SampleEntry {
   
  /**
  * Entry大小
  * 4個字節(jié)
  */
 unsigned int(32) size;
 
 /**
  * 解碼器id
  * 4個字節(jié)
  */
  unsigned int(32) format;
 
 
  
   /**
   * 保留位1
   * 4個字節(jié)
   */
  unsigned int(32)  reserved;
 
   /**
   * 保留位2
   * 2個字節(jié)
   */
   unsigned int(16) reserved2;
 
   /**
   * 當(dāng)MP4文件的數(shù)據(jù)部分,可以被分割成多個片段,每一段對應(yīng)一個索引,并分別通過URL地址來獲取,此時,data_reference_index 指向?qū)?yīng)的片段(比較少用到);
   */
   unsigned int(16) data_reference_index;
}

視頻結(jié)構(gòu)定義:

class VideoSampleEntry extends SampleEntry {
    // 可忽略
    unsigned int(16) version;
    // 可忽略
    unsigned int(16) reversionLevel;
    // 可忽略
    String vendor;
    // 可忽略
    unsigned int(32) temporalQuality;  
    // 可忽略
    unsigned int(32) spatialQuality; 
    
    unsigned int(16) width;
    unsigned int(16) height;
    unsigned int(32) horizontalSolution;
    unsigned int(32) verticalResolution;
    const unsigned int(32) reserved = 0;
    template unsigned int(16) frameCount = 1;
    string codecName;
    template unsigned int(16) depth = 0x0018;
    /**
    * 拓展信息,如avc用于存放sps,pps等必要信息
    */
    byte[] extensions;
}

字段含義:

width、height:視頻的寬高,單位是像素;

horizontalSolution、verticalResolution:水平、垂直方向的分辨率(像素/英寸),16.16定點(diǎn)數(shù),默認(rèn)是0x00480000(72dpi);

frameCount:一個sample中包含多少個frame,對video track來說,默認(rèn)是1;

codecName:僅供參考的名字,通常用于展示,占32個字節(jié),比如 AVC Coding。第一個字節(jié),表示這個名字實際要占用N個字節(jié)的長度。第2到第N+1個字節(jié),存儲這個名字。第N+2到32個字節(jié)為填充字節(jié)。
compressorname 可以設(shè)置為0;

depth:位圖的深度信息,比如 0x0018(24),表示不帶alpha通道的圖片;

音頻結(jié)構(gòu)定義:

class AudioSampleEntry extends SampleEntry {
    // 可忽略
    unsigned int(16) version;
    // 可忽略
    unsigned int(16) reversionLevel;
    // 可忽略
    String vendor;
    
    unsigned int(16) channelCount;
    unsigned int(16) sampleSize;
    unsigned int(16) audioCid;
    unsigned int(16) packetSize;
    unsigned int(16) sampleRate;
    /**
    * 拓展信息,如mp4a
    */
    byte[] extensions;
}

字段含義:

channelCount:聲道數(shù)

sampleSize:每一個Sample的大小

sampleRate:采樣率

stts

stts包含了DTS到sample number的映射表,主要用來推導(dǎo)每個幀的時長

結(jié)構(gòu)定義:

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;
    }
}

字段含義:

entry_count:stts 中包含的entry條目數(shù);

sample_count:單個entry中,具有相同時長(duration 或 sample_delta)的連續(xù)sample的個數(shù)。

sample_delta:sample的時長(以timescale為計量)

stss

mp4文件中,關(guān)鍵幀所在的sample序號。如果沒有stss的話,所有的sample中都是關(guān)鍵幀。

結(jié)構(gòu)定義:

aligned(8) class SyncSampleBox
   extends FullBox(‘stss’, version = 0, 0) {
   unsigned int(32)  entry_count;
   int i;
   for (i=0; i < entry_count; i++) {
      unsigned int(32)  sample_number;
   }
}

字段含義:

entry_count:entry的條目數(shù),可以認(rèn)為是關(guān)鍵幀的數(shù)目;

sample_number:關(guān)鍵幀對應(yīng)的sample的序號;(從1開始計算)

ctts

從解碼(dts)到渲染(pts)之間的差值。

對于只有I幀、P幀的視頻來說,解碼順序、渲染順序是一致的,此時,ctts沒必要存在。

對于存在B幀的視頻來說,ctts就需要存在了。當(dāng)PTS、DTS不相等時,就需要ctts了,公式為 CT(n) = DT(n) + CTTS(n) 。

結(jié)構(gòu)定義:

aligned(8) class CompositionOffsetBox extends FullBox(‘ctts’, 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_offset;
   }
}

字段含義:

entry_count:stts 中包含的entry條目數(shù);

sample_count:單個entry中,具有相同offet的連續(xù)sample的個數(shù)。

sample_offset:CT和DT之間的offset。

stsc

每個thunk中包含幾個sample。

sample 以 chunk 為單位分成多個組。chunk的size可以是不同的,chunk里面的sample的size也可以是不同的。

結(jié)構(gòu)定義:

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

字段含義:

entry_count:有多少個表項(每個表項,包含first_chunk、samples_per_chunk、sample_description_index信息);

first_chunk:當(dāng)前表項中,對應(yīng)的第一個chunk的序號;

samples_per_chunk:每個chunk包含的sample數(shù);

sample_description_index:指向 stsd 中 sample description 的索引值(參考stsd小節(jié));

stsz

每個sample的size(單位是字節(jié))。

每個sample的大?。ㄗ止?jié)),根據(jù) sample_size 字段,可以知道當(dāng)前track包含了多少個sample(或幀)。

有兩種不同的box類型,stsz、stz2。

結(jié)構(gòu)定義:

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 u sample_count; i++) {
            unsigned int(32)  entry_size;
        }
    }
}

字段含義:

sample_size:默認(rèn)的sample大小(單位是byte),通常為0。如果sample_size不為0,那么,所有的sample都是同樣的大小。如果sample_size為0,那么,sample的大小可能不一樣。

sample_count:當(dāng)前track里面的sample數(shù)目。如果 sample_size==0,那么,sample_count 等于下面entry的條目;

entry_size:單個sample的大小(如果sample_size==0的話);

stz2

每個sample的size(單位是字節(jié))。

每個sample的大小(字節(jié)),根據(jù) sample_size 字段,可以知道當(dāng)前track包含了多少個sample(或幀)。

有兩種不同的box類型,stsz、stz2。

結(jié)構(gòu)定義:

aligned(8) class CompactSampleSizeBox extends FullBox(‘stz2’, version = 0, 0) {
    unsigned int(24) reserved = 0;
    unisgned int(8) field_size;
    unsigned int(32) sample_count;
    for (i=1; i u sample_count; i++) {
        unsigned int(field_size) entry_size;
    }
}

字段含義:

field_size:entry表中,每個entry_size占據(jù)的位數(shù)(bit),可選的值為4、8、16。4比較特殊,當(dāng)field_size等于4時,一個字節(jié)上包含兩個entry,高4位為entry[i],低4位為entry[i+1];

sample_count:等于下面entry的條目;

entry_size:sample的大小。

stco

thunk在文件中的偏移。

chunk在文件中的偏移量。針對小文件、大文件,有兩種不同的box類型,分別是stco、co64,它們的結(jié)構(gòu)是一樣的,只是字段長度不同。

chunk_offset 指的是在文件本身中的 offset,而不是某個box內(nèi)部的偏移。

結(jié)構(gòu)定義:

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

字段含義:

entry_count:有多少個項;

chunk_offset:在文件本身中的 offset,而不是某個box內(nèi)部的偏移

co64

跟stco一樣,thunk在文件中的偏移。

chunk在文件中的偏移量。針對小文件、大文件,有兩種不同的box類型,分別是stco、co64,它們的結(jié)構(gòu)是一樣的,只是字段長度不同。

chunk_offset 指的是在文件本身中的 offset,而不是某個box內(nèi)部的偏移。

結(jié)構(gòu)定義:

aligned(8) class ChunkLargeOffsetBox
    extends FullBox(‘co64’, version = 0, 0) {
    unsigned int(32) entry_count;
    for (i=1; i u entry_count; i++) {
        unsigned int(64)  chunk_offset;
    }
}

字段含義:

entry_count:有多少個項;

chunk_offset:在文件本身中的 offset,而不是某個box內(nèi)部的偏移

mdat

mdat也是一個containerBox,MP4文件的媒體數(shù)據(jù)在mdat box里,根據(jù)moov中包含的這些媒體數(shù)據(jù)的索引以及時間信息等定位媒體數(shù)據(jù)的具體位置。

free

占坑用,內(nèi)容可被忽略,似乎也沒法用于拓展,ffmpeg最多只讀取了其中的16個字節(jié)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容