H264视频通过RTMP直播

16 篇文章 1 订阅

转自https://blog.csdn.net/firehood_/article/details/8783589

https://blog.csdn.net/STN_LCD/article/details/68930891

 

       前面的文章中提到了通过RTSP(Real Time Streaming Protocol)的方式来实现视频的直播,但RTSP方式的一个弊端是如果需要支持客户端通过网页来访问,就需要在在页面中嵌入一个ActiveX控件,而ActiveX一般都需要签名才能正常使用,否则用户在使用时还需要更改浏览器设置,并且ActiveX还只支持IE内核的浏览器,Chrome、FireFox需要IE插件才能运行,因此会特别影响用户体验。而RTMP(Real Time Messaging Protocol)很好的解决了这一个问题。由于RTMP是针对FLASH的流媒体协议,视频通过RTMP直播后,只需要在WEB上嵌入一个Web Player(如Jwplayer)即可观看,而且对平台也没什么限制,还可以方便的通过手机观看。

       视频通过RTMP方式发布需要一个RTMP Server(常见的有FMS、Wowza Media Server, 开源的有CRtmpServer、Red5等),原始视频只要按照RTMP协议发送给RTMP Server就可以RTMP视频流的发布了。为了便于视频的打包发布,封装了一个RTMPStream,目前只支持发送H264的视频文件。可以直接发送H264数据帧或H264文件,RTMPStream提供的接口如下。

 

 
  1. class CRTMPStream

  2. {

  3. public:

  4. CRTMPStream(void);

  5. ~CRTMPStream(void);

  6. public:

  7. // 连接到RTMP Server

  8. bool Connect(const char* url);

  9. // 断开连接

  10. void Close();

  11. // 发送MetaData

  12. bool SendMetadata(LPRTMPMetadata lpMetaData);

  13. // 发送H264数据帧

  14. bool SendH264Packet(unsigned char *data,unsigned int size,bool bIsKeyFrame,unsigned int nTimeStamp);

  15. // 发送H264文件

  16. bool SendH264File(const char *pFileName);

  17. //...

  18. }

调用示例:

 
  1. #include <stdio.h>

  2. #include "RTMPStream\RTMPStream.h"

  3.  
  4. int main(int argc,char* argv[])

  5. {

  6. CRTMPStream rtmpSender;

  7.  
  8. bool bRet = rtmpSender.Connect("rtmp://192.168.1.104/live/test");

  9.  
  10. rtmpSender.SendH264File("E:\\video\\test.264");

  11.  
  12. rtmpSender.Close();

  13. }


通过JwPlayer播放效果如下:

 

最后附上RTMPStream完整的代码:

 

 
  1. /********************************************************************

  2. filename: RTMPStream.h

  3. created: 2013-04-3

  4. author: firehood

  5. purpose: 发送H264视频到RTMP Server,使用libRtmp库

  6. *********************************************************************/

  7. #pragma once

  8. #include "rtmp.h"

  9. #include "rtmp_sys.h"

  10. #include "amf.h"

  11. #include <stdio.h>

  12.  
  13. #define FILEBUFSIZE (1024 * 1024 * 10) // 10M

  14.  
  15. // NALU单元

  16. typedef struct _NaluUnit

  17. {

  18. int type;

  19. int size;

  20. unsigned char *data;

  21. }NaluUnit;

  22.  
  23. typedef struct _RTMPMetadata

  24. {

  25. // video, must be h264 type

  26. unsigned int nWidth;

  27. unsigned int nHeight;

  28. unsigned int nFrameRate; // fps

  29. unsigned int nVideoDataRate; // bps

  30. unsigned int nSpsLen;

  31. unsigned char Sps[1024];

  32. unsigned int nPpsLen;

  33. unsigned char Pps[1024];

  34.  
  35. // audio, must be aac type

  36. bool bHasAudio;

  37. unsigned int nAudioSampleRate;

  38. unsigned int nAudioSampleSize;

  39. unsigned int nAudioChannels;

  40. char pAudioSpecCfg;

  41. unsigned int nAudioSpecCfgLen;

  42.  
  43. } RTMPMetadata,*LPRTMPMetadata;

  44.  
  45.  
  46. class CRTMPStream

  47. {

  48. public:

  49. CRTMPStream(void);

  50. ~CRTMPStream(void);

  51. public:

  52. // 连接到RTMP Server

  53. bool Connect(const char* url);

  54. // 断开连接

  55. void Close();

  56. // 发送MetaData

  57. bool SendMetadata(LPRTMPMetadata lpMetaData);

  58. // 发送H264数据帧

  59. bool SendH264Packet(unsigned char *data,unsigned int size,bool bIsKeyFrame,unsigned int nTimeStamp);

  60. // 发送H264文件

  61. bool SendH264File(const char *pFileName);

  62. private:

  63. // 送缓存中读取一个NALU包

  64. bool ReadOneNaluFromBuf(NaluUnit &nalu);

  65. // 发送数据

  66. int SendPacket(unsigned int nPacketType,unsigned char *data,unsigned int size,unsigned int nTimestamp);

  67. private:

  68. RTMP* m_pRtmp;

  69. unsigned char* m_pFileBuf;

  70. unsigned int m_nFileBufSize;

  71. unsigned int m_nCurPos;

  72. };

 

 
  1. /********************************************************************

  2. filename: RTMPStream.cpp

  3. created: 2013-04-3

  4. author: firehood

  5. purpose: 发送H264视频到RTMP Server,使用libRtmp库

  6. *********************************************************************/

  7. #include "RTMPStream.h"

  8. #include "SpsDecode.h"

  9. #ifdef WIN32

  10. #include <windows.h>

  11. #endif

  12.  
  13. #ifdef WIN32

  14. #pragma comment(lib,"WS2_32.lib")

  15. #pragma comment(lib,"winmm.lib")

  16. #endif

  17.  
  18. enum

  19. {

  20. FLV_CODECID_H264 = 7,

  21. };

  22.  
  23. int InitSockets()

  24. {

  25. #ifdef WIN32

  26. WORD version;

  27. WSADATA wsaData;

  28. version = MAKEWORD(1, 1);

  29. return (WSAStartup(version, &wsaData) == 0);

  30. #else

  31. return TRUE;

  32. #endif

  33. }

  34.  
  35. inline void CleanupSockets()

  36. {

  37. #ifdef WIN32

  38. WSACleanup();

  39. #endif

  40. }

  41.  
  42. char * put_byte( char *output, uint8_t nVal )

  43. {

  44. output[0] = nVal;

  45. return output+1;

  46. }

  47. char * put_be16(char *output, uint16_t nVal )

  48. {

  49. output[1] = nVal & 0xff;

  50. output[0] = nVal >> 8;

  51. return output+2;

  52. }

  53. char * put_be24(char *output,uint32_t nVal )

  54. {

  55. output[2] = nVal & 0xff;

  56. output[1] = nVal >> 8;

  57. output[0] = nVal >> 16;

  58. return output+3;

  59. }

  60. char * put_be32(char *output, uint32_t nVal )

  61. {

  62. output[3] = nVal & 0xff;

  63. output[2] = nVal >> 8;

  64. output[1] = nVal >> 16;

  65. output[0] = nVal >> 24;

  66. return output+4;

  67. }

  68. char * put_be64( char *output, uint64_t nVal )

  69. {

  70. output=put_be32( output, nVal >> 32 );

  71. output=put_be32( output, nVal );

  72. return output;

  73. }

  74. char * put_amf_string( char *c, const char *str )

  75. {

  76. uint16_t len = strlen( str );

  77. c=put_be16( c, len );

  78. memcpy(c,str,len);

  79. return c+len;

  80. }

  81. char * put_amf_double( char *c, double d )

  82. {

  83. *c++ = AMF_NUMBER; /* type: Number */

  84. {

  85. unsigned char *ci, *co;

  86. ci = (unsigned char *)&d;

  87. co = (unsigned char *)c;

  88. co[0] = ci[7];

  89. co[1] = ci[6];

  90. co[2] = ci[5];

  91. co[3] = ci[4];

  92. co[4] = ci[3];

  93. co[5] = ci[2];

  94. co[6] = ci[1];

  95. co[7] = ci[0];

  96. }

  97. return c+8;

  98. }

  99.  
  100. CRTMPStream::CRTMPStream(void):

  101. m_pRtmp(NULL),

  102. m_nFileBufSize(0),

  103. m_nCurPos(0)

  104. {

  105. m_pFileBuf = new unsigned char[FILEBUFSIZE];

  106. memset(m_pFileBuf,0,FILEBUFSIZE);

  107. InitSockets();

  108. m_pRtmp = RTMP_Alloc();

  109. RTMP_Init(m_pRtmp);

  110. }

  111.  
  112. CRTMPStream::~CRTMPStream(void)

  113. {

  114. Close();

  115. WSACleanup();

  116. delete[] m_pFileBuf;

  117. }

  118.  
  119. bool CRTMPStream::Connect(const char* url)

  120. {

  121. if(RTMP_SetupURL(m_pRtmp, (char*)url)<0)

  122. {

  123. return FALSE;

  124. }

  125. RTMP_EnableWrite(m_pRtmp);

  126. if(RTMP_Connect(m_pRtmp, NULL)<0)

  127. {

  128. return FALSE;

  129. }

  130. if(RTMP_ConnectStream(m_pRtmp,0)<0)

  131. {

  132. return FALSE;

  133. }

  134. return TRUE;

  135. }

  136.  
  137. void CRTMPStream::Close()

  138. {

  139. if(m_pRtmp)

  140. {

  141. RTMP_Close(m_pRtmp);

  142. RTMP_Free(m_pRtmp);

  143. m_pRtmp = NULL;

  144. }

  145. }

  146.  
  147. int CRTMPStream::SendPacket(unsigned int nPacketType,unsigned char *data,unsigned int size,unsigned int nTimestamp)

  148. {

  149. if(m_pRtmp == NULL)

  150. {

  151. return FALSE;

  152. }

  153.  
  154. RTMPPacket packet;

  155. RTMPPacket_Reset(&packet);

  156. RTMPPacket_Alloc(&packet,size);

  157.  
  158. packet.m_packetType = nPacketType;

  159. packet.m_nChannel = 0x04;

  160. packet.m_headerType = RTMP_PACKET_SIZE_LARGE;

  161. packet.m_nTimeStamp = nTimestamp;

  162. packet.m_nInfoField2 = m_pRtmp->m_stream_id;

  163. packet.m_nBodySize = size;

  164. memcpy(packet.m_body,data,size);

  165.  
  166. int nRet = RTMP_SendPacket(m_pRtmp,&packet,0);

  167.  
  168. RTMPPacket_Free(&packet);

  169.  
  170. return nRet;

  171. }

  172.  
  173. bool CRTMPStream::SendMetadata(LPRTMPMetadata lpMetaData)

  174. {

  175. if(lpMetaData == NULL)

  176. {

  177. return false;

  178. }

  179. char body[1024] = {0};;

  180.  
  181. char * p = (char *)body;

  182. p = put_byte(p, AMF_STRING );

  183. p = put_amf_string(p , "@setDataFrame" );

  184.  
  185. p = put_byte( p, AMF_STRING );

  186. p = put_amf_string( p, "onMetaData" );

  187.  
  188. p = put_byte(p, AMF_OBJECT );

  189. p = put_amf_string( p, "copyright" );

  190. p = put_byte(p, AMF_STRING );

  191. p = put_amf_string( p, "firehood" );

  192.  
  193. p =put_amf_string( p, "width");

  194. p =put_amf_double( p, lpMetaData->nWidth);

  195.  
  196. p =put_amf_string( p, "height");

  197. p =put_amf_double( p, lpMetaData->nHeight);

  198.  
  199. p =put_amf_string( p, "framerate" );

  200. p =put_amf_double( p, lpMetaData->nFrameRate);

  201.  
  202. p =put_amf_string( p, "videocodecid" );

  203. p =put_amf_double( p, FLV_CODECID_H264 );

  204.  
  205. p =put_amf_string( p, "" );

  206. p =put_byte( p, AMF_OBJECT_END );

  207.  
  208. int index = p-body;

  209.  
  210. SendPacket(RTMP_PACKET_TYPE_INFO,(unsigned char*)body,p-body,0);

  211.  
  212. int i = 0;

  213. body[i++] = 0x17; // 1:keyframe 7:AVC

  214. body[i++] = 0x00; // AVC sequence header

  215.  
  216. body[i++] = 0x00;

  217. body[i++] = 0x00;

  218. body[i++] = 0x00; // fill in 0;

  219.  
  220. // AVCDecoderConfigurationRecord.

  221. body[i++] = 0x01; // configurationVersion

  222. body[i++] = lpMetaData->Sps[1]; // AVCProfileIndication

  223. body[i++] = lpMetaData->Sps[2]; // profile_compatibility

  224. body[i++] = lpMetaData->Sps[3]; // AVCLevelIndication

  225. body[i++] = 0xff; // lengthSizeMinusOne

  226.  
  227. // sps nums

  228. body[i++] = 0xE1; //&0x1f

  229. // sps data length

  230. body[i++] = lpMetaData->nSpsLen>>8;

  231. body[i++] = lpMetaData->nSpsLen&0xff;

  232. // sps data

  233. memcpy(&body[i],lpMetaData->Sps,lpMetaData->nSpsLen);

  234. i= i+lpMetaData->nSpsLen;

  235.  
  236. // pps nums

  237. body[i++] = 0x01; //&0x1f

  238. // pps data length

  239. body[i++] = lpMetaData->nPpsLen>>8;

  240. body[i++] = lpMetaData->nPpsLen&0xff;

  241. // sps data

  242. memcpy(&body[i],lpMetaData->Pps,lpMetaData->nPpsLen);

  243. i= i+lpMetaData->nPpsLen;

  244.  
  245. return SendPacket(RTMP_PACKET_TYPE_VIDEO,(unsigned char*)body,i,0);

  246.  
  247. }

  248.  
  249. bool CRTMPStream::SendH264Packet(unsigned char *data,unsigned int size,bool bIsKeyFrame,unsigned int nTimeStamp)

  250. {

  251. if(data == NULL && size<11)

  252. {

  253. return false;

  254. }

  255.  
  256. unsigned char *body = new unsigned char[size+9];

  257.  
  258. int i = 0;

  259. if(bIsKeyFrame)

  260. {

  261. body[i++] = 0x17;// 1:Iframe 7:AVC

  262. }

  263. else

  264. {

  265. body[i++] = 0x27;// 2:Pframe 7:AVC

  266. }

  267. body[i++] = 0x01;// AVC NALU

  268. body[i++] = 0x00;

  269. body[i++] = 0x00;

  270. body[i++] = 0x00;

  271.  
  272. // NALU size

  273. body[i++] = size>>24;

  274. body[i++] = size>>16;

  275. body[i++] = size>>8;

  276. body[i++] = size&0xff;;

  277.  
  278. // NALU data

  279. memcpy(&body[i],data,size);

  280.  
  281. bool bRet = SendPacket(RTMP_PACKET_TYPE_VIDEO,body,i+size,nTimeStamp);

  282.  
  283. delete[] body;

  284.  
  285. return bRet;

  286. }

  287.  
  288. bool CRTMPStream::SendH264File(const char *pFileName)

  289. {

  290. if(pFileName == NULL)

  291. {

  292. return FALSE;

  293. }

  294. FILE *fp = fopen(pFileName, "rb");

  295. if(!fp)

  296. {

  297. printf("ERROR:open file %s failed!",pFileName);

  298. }

  299. fseek(fp, 0, SEEK_SET);

  300. m_nFileBufSize = fread(m_pFileBuf, sizeof(unsigned char), FILEBUFSIZE, fp);

  301. if(m_nFileBufSize >= FILEBUFSIZE)

  302. {

  303. printf("warning : File size is larger than BUFSIZE\n");

  304. }

  305. fclose(fp);

  306.  
  307. RTMPMetadata metaData;

  308. memset(&metaData,0,sizeof(RTMPMetadata));

  309.  
  310. NaluUnit naluUnit;

  311. // 读取SPS帧

  312. ReadOneNaluFromBuf(naluUnit);

  313. metaData.nSpsLen = naluUnit.size;

  314. memcpy(metaData.Sps,naluUnit.data,naluUnit.size);

  315.  
  316. // 读取PPS帧

  317. ReadOneNaluFromBuf(naluUnit);

  318. metaData.nPpsLen = naluUnit.size;

  319. memcpy(metaData.Pps,naluUnit.data,naluUnit.size);

  320.  
  321. // 解码SPS,获取视频图像宽、高信息

  322. int width = 0,height = 0;

  323. h264_decode_sps(metaData.Sps,metaData.nSpsLen,width,height);

  324. metaData.nWidth = width;

  325. metaData.nHeight = height;

  326. metaData.nFrameRate = 25;

  327.  
  328. // 发送MetaData

  329. SendMetadata(&metaData);

  330.  
  331. unsigned int tick = 0;

  332. while(ReadOneNaluFromBuf(naluUnit))

  333. {

  334. bool bKeyframe = (naluUnit.type == 0x05) ? TRUE : FALSE;

  335. // 发送H264数据帧

  336. SendH264Packet(naluUnit.data,naluUnit.size,bKeyframe,tick);

  337. msleep(40);

  338. tick +=40;

  339. }

  340.  
  341. return TRUE;

  342. }

  343.  
  344. bool CRTMPStream::ReadOneNaluFromBuf(NaluUnit &nalu)

  345. {

  346. int i = m_nCurPos;

  347. while(i<m_nFileBufSize)

  348. {

  349. if(m_pFileBuf[i++] == 0x00 &&

  350. m_pFileBuf[i++] == 0x00 &&

  351. m_pFileBuf[i++] == 0x00 &&

  352. m_pFileBuf[i++] == 0x01

  353. )

  354. {

  355. int pos = i;

  356. while (pos<m_nFileBufSize)

  357. {

  358. if(m_pFileBuf[pos++] == 0x00 &&

  359. m_pFileBuf[pos++] == 0x00 &&

  360. m_pFileBuf[pos++] == 0x00 &&

  361. m_pFileBuf[pos++] == 0x01

  362. )

  363. {

  364. break;

  365. }

  366. }

  367. if(pos == nBufferSize)

  368. {

  369. nalu.size = pos-i;

  370. }

  371. else

  372. {

  373. nalu.size = (pos-4)-i;

  374. }

  375. nalu.type = m_pFileBuf[i]&0x1f;

  376. nalu.data = &m_pFileBuf[i];

  377.  
  378. m_nCurPos = pos-4;

  379. return TRUE;

  380. }

  381. }

  382. return FALSE;

  383. }

 

附上SpsDecode.h文件:

 

 
  1. #include <stdio.h>

  2. #include <math.h>

  3.  
  4. UINT Ue(BYTE *pBuff, UINT nLen, UINT &nStartBit)

  5. {

  6. //计算0bit的个数

  7. UINT nZeroNum = 0;

  8. while (nStartBit < nLen * 8)

  9. {

  10. if (pBuff[nStartBit / 8] & (0x80 >> (nStartBit % 8))) //&:按位与,%取余

  11. {

  12. break;

  13. }

  14. nZeroNum++;

  15. nStartBit++;

  16. }

  17. nStartBit ++;

  18.  
  19.  
  20. //计算结果

  21. DWORD dwRet = 0;

  22. for (UINT i=0; i<nZeroNum; i++)

  23. {

  24. dwRet <<= 1;

  25. if (pBuff[nStartBit / 8] & (0x80 >> (nStartBit % 8)))

  26. {

  27. dwRet += 1;

  28. }

  29. nStartBit++;

  30. }

  31. return (1 << nZeroNum) - 1 + dwRet;

  32. }

  33.  
  34.  
  35. int Se(BYTE *pBuff, UINT nLen, UINT &nStartBit)

  36. {

  37. int UeVal=Ue(pBuff,nLen,nStartBit);

  38. double k=UeVal;

  39. int nValue=ceil(k/2);//ceil函数:ceil函数的作用是求不小于给定实数的最小整数。ceil(2)=ceil(1.2)=cei(1.5)=2.00

  40. if (UeVal % 2==0)

  41. nValue=-nValue;

  42. return nValue;

  43. }

  44.  
  45.  
  46. DWORD u(UINT BitCount,BYTE * buf,UINT &nStartBit)

  47. {

  48. DWORD dwRet = 0;

  49. for (UINT i=0; i<BitCount; i++)

  50. {

  51. dwRet <<= 1;

  52. if (buf[nStartBit / 8] & (0x80 >> (nStartBit % 8)))

  53. {

  54. dwRet += 1;

  55. }

  56. nStartBit++;

  57. }

  58. return dwRet;

  59. }

  60.  
  61.  
  62. bool h264_decode_sps(BYTE * buf,unsigned int nLen,int &width,int &height)

  63. {

  64. UINT StartBit=0;

  65. int forbidden_zero_bit=u(1,buf,StartBit);

  66. int nal_ref_idc=u(2,buf,StartBit);

  67. int nal_unit_type=u(5,buf,StartBit);

  68. if(nal_unit_type==7)

  69. {

  70. int profile_idc=u(8,buf,StartBit);

  71. int constraint_set0_flag=u(1,buf,StartBit);//(buf[1] & 0x80)>>7;

  72. int constraint_set1_flag=u(1,buf,StartBit);//(buf[1] & 0x40)>>6;

  73. int constraint_set2_flag=u(1,buf,StartBit);//(buf[1] & 0x20)>>5;

  74. int constraint_set3_flag=u(1,buf,StartBit);//(buf[1] & 0x10)>>4;

  75. int reserved_zero_4bits=u(4,buf,StartBit);

  76. int level_idc=u(8,buf,StartBit);

  77.  
  78. int seq_parameter_set_id=Ue(buf,nLen,StartBit);

  79.  
  80. if( profile_idc == 100 || profile_idc == 110 ||

  81. profile_idc == 122 || profile_idc == 144 )

  82. {

  83. int chroma_format_idc=Ue(buf,nLen,StartBit);

  84. if( chroma_format_idc == 3 )

  85. int residual_colour_transform_flag=u(1,buf,StartBit);

  86. int bit_depth_luma_minus8=Ue(buf,nLen,StartBit);

  87. int bit_depth_chroma_minus8=Ue(buf,nLen,StartBit);

  88. int qpprime_y_zero_transform_bypass_flag=u(1,buf,StartBit);

  89. int seq_scaling_matrix_present_flag=u(1,buf,StartBit);

  90.  
  91. int seq_scaling_list_present_flag[8];

  92. if( seq_scaling_matrix_present_flag )

  93. {

  94. for( int i = 0; i < 8; i++ ) {

  95. seq_scaling_list_present_flag[i]=u(1,buf,StartBit);

  96. }

  97. }

  98. }

  99. int log2_max_frame_num_minus4=Ue(buf,nLen,StartBit);

  100. int pic_order_cnt_type=Ue(buf,nLen,StartBit);

  101. if( pic_order_cnt_type == 0 )

  102. int log2_max_pic_order_cnt_lsb_minus4=Ue(buf,nLen,StartBit);

  103. else if( pic_order_cnt_type == 1 )

  104. {

  105. int delta_pic_order_always_zero_flag=u(1,buf,StartBit);

  106. int offset_for_non_ref_pic=Se(buf,nLen,StartBit);

  107. int offset_for_top_to_bottom_field=Se(buf,nLen,StartBit);

  108. int num_ref_frames_in_pic_order_cnt_cycle=Ue(buf,nLen,StartBit);

  109.  
  110. int *offset_for_ref_frame=new int[num_ref_frames_in_pic_order_cnt_cycle];

  111. for( int i = 0; i < num_ref_frames_in_pic_order_cnt_cycle; i++ )

  112. offset_for_ref_frame[i]=Se(buf,nLen,StartBit);

  113. delete [] offset_for_ref_frame;

  114. }

  115. int num_ref_frames=Ue(buf,nLen,StartBit);

  116. int gaps_in_frame_num_value_allowed_flag=u(1,buf,StartBit);

  117. int pic_width_in_mbs_minus1=Ue(buf,nLen,StartBit);

  118. int pic_height_in_map_units_minus1=Ue(buf,nLen,StartBit);

  119.  
  120. width=(pic_width_in_mbs_minus1+1)*16;

  121. height=(pic_height_in_map_units_minus1+1)*16;

  122.  
  123. return true;

  124. }

  125. else

  126. return false;

  127. }

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值