LiveRtspClient+Gpac保存MP4文件

测试样例,记录带以后查阅。

LiveRtspClient关键代码:

void DummySink::afterGettingFrame(unsigned frameSize, unsigned numTruncatedBytes,struct timeval presentationTime, unsigned /*durationInMicroseconds*/) 
{
	// We've just received a frame of data.  (Optionally) print out information about it:
#ifdef DEBUG_PRINT_EACH_RECEIVED_FRAME
	if (fStreamId != NULL) 
		envir() << "Stream \"" << fStreamId << "\"; ";

	envir() << fSubsession.mediumName() << "/" << fSubsession.codecName() << ":\tReceived " << frameSize << " bytes";
	if (numTruncatedBytes > 0) 
		envir() << " (with " << numTruncatedBytes << " bytes truncated)";
	char uSecsStr[6+1]; // used to output the 'microseconds' part of the presentation time	
	sprintf(uSecsStr, "%06u", (unsigned)presentationTime.tv_usec);
	envir() << ".\tPresentation time: " << (int)presentationTime.tv_sec << "." << uSecsStr<<"\n";
	
	envir() << "Width:"<<(int)fSubsession.videoWidth()<<"  High:"<<(int)fSubsession.videoHeight()<<"\n";
	envir() << "Scale:"<<(int)fSubsession.scale()<<"    Speed:"<<(int)fSubsession.speed()<<"\n";
	envir() << "FPS:"<<(int)fSubsession.videoFPS()<<"   Channels:"<<(int)fSubsession.numChannels()<<"\n";


	if (fSubsession.rtpSource() != NULL && !fSubsession.rtpSource()->hasBeenSynchronizedUsingRTCP()) 
	{
		envir() << "!"; // mark the debugging output to indicate that this presentation time is not RTCP-synchronized
	}
#ifdef DEBUG_PRINT_NPT
	envir() << "\tNPT: " << fSubsession.getNormalPlayTime(presentationTime);
#endif
	envir() << "\n";
#endif

	CliveRtsp* pThis = ((CliveRtsp*)pUser);

	long long nTimeStamp = presentationTime.tv_sec*1000+presentationTime.tv_usec/1000;
	
	if(strcmp(fSubsession.mediumName(), "video") == 0 &&(strcmp(fSubsession.codecName(), "H264") == 0)) 
	{
		if(firstFrame)
		{
			unsigned int num;
			SPropRecord *sps = parseSPropParameterSets(fSubsession.fmtp_spropparametersets(), num);
			CMP4.CreatMP4File("../test.mp4");
			CMP4.WriteH264_SPSandPPS(sps[0].sPropBytes,sps[0].sPropLength,sps[1].sPropBytes,sps[1].sPropLength);
			firstFrame = false;
		}
		CMP4.WriteH264_NALU(fReceiveBuffer,frameSize,nTimeStamp);

	}
	if(strcmp(fSubsession.mediumName(), "audio") == 0) 
	{
		envir() << "Get Audio "<<fSubsession.mediumName()<<"Size :" <<frameSize <<fSubsession.codecName()<<"/n";
	}

	// Then continue, to request the next frame of data:
	continuePlaying();
}

Gpac封装了个类,其中也实现了SPS信息头的解析用于获取视频宽高,不知道为什么有时候LiveRtspClient中获取到的视频宽高为0,这样Mp4文件的创建就会有问题,所以干脆自己解决了。

头文件:

#ifndef _CMP4ENCODE_H_
#define _CMP4ENCODE_H_
#include "gpac/isomedia.h"
typedef unsigned char UINT8;
//只保存视频数据

class CMP4Encode
{
public:
	CMP4Encode(void);
	~CMP4Encode(void);
public:
	bool CreatMP4File(char*filename);
	bool WriteH264_SPSandPPS(unsigned char*sps,int spslen,unsigned char*pps,int ppslen);
	void WriteH264_NALU(unsigned char*pData,int len,long nTimeStamp);
	void CloseFile();
private: 
	bool WriteH264Frame(unsigned char*data,int len,bool keyframe,long nTimeStamp);
	void ParserSps(unsigned char*pData);	 //解Sps获取宽 视频高
	int get_bit_at_position(UINT8*buf,UINT8 &bytePosition,UINT8 &bitPosition);	//取当前字节 当前位 的值
	int get_u_code_num(UINT8 *buf, UINT8 &bytePosition, UINT8 &bitPosition,UINT8 bitCount);//获取 从左往右 n个bit组成的值
	int get_ue_code_num(UINT8 *buf, UINT8 &bytePosition, UINT8 &bitPosition); //ue(v)无符号指数哥伦布编码
	int get_se_code_num(UINT8 *buf, UINT8 &bytePosition, UINT8 &bitPosition); //se(v)有符号指数哥伦布编码
	

private:
	GF_ISOFile *p_file;//MP4文件
    GF_AVCConfig *p_config;//MP4配置
    GF_ISOSample *p_videosample;//MP4帧

	unsigned int i_videodescidx;
	long m_videostartimestamp;//视频时间戳
	int m_videtrackid;	//视频ID
	int m_wight;	//视频宽
	int m_hight;	//视频高
	int m_Fps;		//视频fps

	long long Total_NALUSize;//总计写入NALU大小

};
#endif

cpp文件:

#include "CMP4Encode.h"
#include <math.h>

CMP4Encode::CMP4Encode(void)
{
	m_videostartimestamp = -1;
    p_file = NULL;
    p_config  = NULL; 
    p_videosample  = NULL;
	m_wight = 0;
    m_hight = 0;
	m_Fps = 0;
	Total_NALUSize = 0;
}


CMP4Encode::~CMP4Encode(void)
{
	CloseFile();
}

bool CMP4Encode::CreatMP4File(char*filename)
{
    if(filename==NULL)
	return false;//打开文件

	p_file=gf_isom_open(filename,GF_ISOM_OPEN_WRITE,NULL);//打开文件

	if (p_file==NULL)
	return false;

	gf_isom_set_brand_info(p_file,GF_ISOM_BRAND_MP42,0); //设置视频类型 mp42
	return true;

}
//写入 sps pps
bool CMP4Encode::WriteH264_SPSandPPS(unsigned char*sps,int spslen,unsigned char*pps,int ppslen)
{
  	if(p_file == NULL)
	return false;
	
	ParserSps(sps);//解析sps 求视频宽高
	if(m_wight==0||m_hight==0)
	return false;

	m_videtrackid = gf_isom_new_track(p_file,0,GF_ISOM_MEDIA_VISUAL,1000);

	if(m_videtrackid == NULL)
	return false;

	gf_isom_set_track_enabled(p_file,m_videtrackid,1);

	p_videosample = gf_isom_sample_new(); //创建一个新的样本
	p_videosample->data=(char*)malloc(1024*1024); //分配内存


	p_config = gf_odf_avc_cfg_new();//MP4配置 构造函数
	
	//创建新的avc配置函数
	gf_isom_avc_config_new(p_file,m_videtrackid,p_config,NULL,NULL,&i_videodescidx);

	//为其指定宽高
	gf_isom_set_visual_info(p_file,m_videtrackid,i_videodescidx,m_wight,m_hight);

	GF_AVCConfigSlot m_slotsps={0};
	GF_AVCConfigSlot m_slotpps={0};
	
	//配置sps信息
	p_config->configurationVersion = 1;
	p_config->AVCProfileIndication = sps[1];
	p_config->profile_compatibility = sps[2];
	p_config->AVCLevelIndication = sps[3];
	
	m_slotsps.size=spslen;
	m_slotsps.data=(char*)malloc(spslen);
	memcpy(m_slotsps.data,sps,spslen);
	gf_list_add(p_config->sequenceParameterSets,&m_slotsps);

	//配置pps信息
	m_slotpps.size=ppslen;
	m_slotpps.data=(char*)malloc(ppslen);
	memcpy(m_slotpps.data,pps,ppslen);
	gf_list_add(p_config->pictureParameterSets,&m_slotpps);

	//更新 AVC 配置
	gf_isom_avc_config_update(p_file,m_videtrackid,1,p_config);

	free(m_slotsps.data);
	free(m_slotpps.data);
	return true;

}

/*
 解析NAL第一个字符的标志位 确定其类型 只判断 profile_idc(5 bit)
 sps -- 7
 pps -- 8
 IDR帧 -- 5
 非IDR帧 -- 1
 SEI -- 6 补充增强信息单元
*/
void CMP4Encode::WriteH264_NALU(unsigned char*pData,int len,long nTimeStamp)
{
	if((pData == NULL)||(len == 0))
	return;
	unsigned char* RealData = new unsigned char[len+4];//填充长度 高在前
	RealData[0] = (len>>24)&0xFF;
	RealData[1] = (len>>16)&0xFF;
	RealData[2] = (len>>8)&0xFF;
	RealData[3] = len&0xFF;
	memcpy(RealData+4,pData,len);

	//写入数据到MP4文件
    unsigned int Type = pData[0]&0x1f;
	switch(Type)
	{
	case 1: 
	   WriteH264Frame(RealData,len+4,false,nTimeStamp);
	   break;
	case 5:
	   WriteH264Frame(RealData,len+4,true,nTimeStamp);
	   break;
	case 6:
	   WriteH264Frame(RealData,len+4,false,nTimeStamp);
	   break;
	case 7:
	   WriteH264Frame(RealData,len+4,false,nTimeStamp);
	   break;
	case 8:
	   WriteH264Frame(RealData,len+4,false,nTimeStamp);
	   break;
	default:break;
	}
	delete []RealData;
    return ;
}

//写入一帧,前四字节为该帧NAL长度
bool CMP4Encode::WriteH264Frame(unsigned char*data,int len,bool keyframe, long nTimeStamp)
{		
	if (!p_videosample)
	return false;
	获取系统时间
	//win_time_val_t wintv;
 //   win_gettimeofday(&wintv);
 //   long long timestamp = wintv.sec * 1000 + wintv.msec / 1000;
	if (m_videostartimestamp == -1)	//判断IDR帧 easyplayer中应该是一整帧 
	{
		m_videostartimestamp = nTimeStamp;
	}
	if (m_videostartimestamp!=-1)
	{
		p_videosample->IsRAP = keyframe;
		p_videosample->dataLength = len;
		p_videosample->data = (char*)data;
		memcpy(p_videosample->data,data,len);

		p_videosample->DTS= nTimeStamp - m_videostartimestamp;
		p_videosample->CTS_Offset = 0;	

		GF_Err gferr=gf_isom_add_sample(p_file,m_videtrackid,i_videodescidx,p_videosample);			
		if (gferr == -1)
		{
			p_videosample->DTS = nTimeStamp - m_videostartimestamp + 15;
			gf_isom_add_sample(p_file,m_videtrackid,i_videodescidx,p_videosample);
		}
	}
	Total_NALUSize += len;
	return true;
}
void CMP4Encode::CloseFile()
{
	m_videostartimestamp = -1;

	if (p_file)	 //清理文件
	{
		gf_isom_close(p_file);
		p_file=NULL;
	}
	if(p_config) //清理文件配置
	{
		p_config->pictureParameterSets=NULL;
		p_config->sequenceParameterSets=NULL;
		gf_odf_avc_cfg_del(p_config);
		p_config=NULL;
	}

	if(p_videosample) //清理数据帧
	{
		if(p_videosample->data)
		{
			free(p_videosample->data);
			p_videosample->data=NULL;
		}
		gf_isom_sample_del(&p_videosample);
		p_videosample = NULL;
	}

	//清理视频参数
	m_wight  =  0;
    m_hight  =  0;
	m_Fps  =  0;
	Total_NALUSize = 0;
    return;
}
//解Sps获取宽 视频高
void CMP4Encode::ParserSps(unsigned char*strArray)	
{
	UINT8 bytePosition = 0, bitPosition = 0;

	//NALU 头
	int forbidden_bit = get_u_code_num(strArray,bytePosition,bitPosition,1);
	int nal_ref_idc = get_u_code_num(strArray,bytePosition,bitPosition,2);
    int nal_unit_type = get_u_code_num(strArray,bytePosition,bitPosition,5);  //0x07

	//这里可能包含防竞争码 需做处理海康视频流中并未见此字段

	//sps信息	 
	int profile_idc = get_u_code_num(strArray,bytePosition,bitPosition,8);
    int constraint_set0_flag = get_u_code_num(strArray,bytePosition,bitPosition,1);
    int constraint_set1_flag = get_u_code_num(strArray,bytePosition,bitPosition,1);
    int constraint_set2_flag = get_u_code_num(strArray,bytePosition,bitPosition,1);
    int constraint_set3_flag = get_u_code_num(strArray,bytePosition,bitPosition,1);
	int constraint_set4_flag = get_u_code_num(strArray,bytePosition,bitPosition,1);
    int constraint_set5_flag = get_u_code_num(strArray,bytePosition,bitPosition,1);
	int reserve_zero_2bit = get_u_code_num(strArray,bytePosition,bitPosition,2);


	int level_idc = get_u_code_num(strArray,bytePosition,bitPosition,8);

	int seq_parameter_set_id = get_ue_code_num(strArray,bytePosition,bitPosition);
    int chroma_format_idc = 0;
    if (profile_idc == 100 || profile_idc == 110 ||profile_idc == 122 || profile_idc == 244 ||
		profile_idc == 44 || profile_idc == 83 ||profile_idc == 86 || profile_idc == 118 ||profile_idc == 128||
		profile_idc == 138 || profile_idc == 139 || profile_idc == 134 || profile_idc == 135)
	{
		 chroma_format_idc = get_ue_code_num(strArray,bytePosition,bitPosition);
		 if(chroma_format_idc == 3)
		 {
		 	int separte_colour_plane_flag = get_u_code_num(strArray,bytePosition,bitPosition,1);
		 }
		 int bit_depth_luma_minus8 = get_ue_code_num(strArray,bytePosition,bitPosition);
		 int bit_depth_chroma_minus8 = get_ue_code_num(strArray,bytePosition,bitPosition);
		 int qpprime_y_zero_transfrom_bypass_flag = get_u_code_num(strArray,bytePosition,bitPosition,1);
		 int seq_scaling_matrix_preasent_flag = get_u_code_num(strArray,bytePosition,bitPosition,1);
		 if(seq_scaling_matrix_preasent_flag)
		 {	 
			 int seq_scaling_list_present_flag[12] = {0};
			 for(int i = 0;i<((chroma_format_idc!=3)?8:12);i++)
			 {
			 	seq_scaling_list_present_flag[i] = get_u_code_num(strArray,bytePosition,bitPosition,1);
				//余下不解析
			 }
		 }
	}
 	int log2_max_frame_num_minus4 =  get_ue_code_num(strArray,bytePosition,bitPosition);
	int pic_order_cnt_type =  get_ue_code_num(strArray,bytePosition,bitPosition);
	if(pic_order_cnt_type == 0)
	{
	   int log2_max_pic_order_cnt_lsb_minus4 =  get_ue_code_num(strArray,bytePosition,bitPosition);
	}
	else if(pic_order_cnt_type == 1)
	{
	   int delta_pic_order_always_zero_flag =  get_u_code_num(strArray,bytePosition,bitPosition,1);
	   int offset_for_non_ref_pic = get_se_code_num(strArray,bytePosition,bitPosition);
	   int offset_for_top_to_bottom_field = get_se_code_num(strArray,bytePosition,bitPosition);
	   int num_ref_frames_in_pic_order_cnt_cycle = get_ue_code_num(strArray,bytePosition,bitPosition);
	   int* offset_for_ref_frame = new int[num_ref_frames_in_pic_order_cnt_cycle];
	   for(int i = 0;i<num_ref_frames_in_pic_order_cnt_cycle;i++)
	   {
	   	   offset_for_ref_frame[i] = get_se_code_num(strArray,bytePosition,bitPosition);
	   }
	   delete []offset_for_ref_frame;
	}
	int max_num_ref_frames = get_ue_code_num(strArray,bytePosition,bitPosition);
	int gaps_in_frame_num_value_allowed_flag = get_u_code_num(strArray,bytePosition,bitPosition,1);
	
	//视频宽高参数
	int pic_width_in_mbs_minusl = get_ue_code_num(strArray,bytePosition,bitPosition);
	int pic_height_in_map_units_minusl = get_ue_code_num(strArray,bytePosition,bitPosition);
	int frame_mbs_only_flag = get_u_code_num(strArray,bytePosition,bitPosition,1);
	if(!frame_mbs_only_flag)
	{
		int mb_adaptive_frame_field_flag = get_u_code_num(strArray,bytePosition,bitPosition,1);
	}
	int direct_8x8_inference_flag = get_u_code_num(strArray,bytePosition,bitPosition,1);
	int frame_cropping_flag = get_u_code_num(strArray,bytePosition,bitPosition,1);
	//计算宽高
	m_wight = ((int)pic_width_in_mbs_minusl + 1) * 16;
	m_hight = (2 - (int)frame_mbs_only_flag) * ((int)pic_height_in_map_units_minusl + 1) * 16;
	
	//视频需要裁剪
	if(frame_cropping_flag)
	{
		 int frame_crop_left_offset =  get_ue_code_num(strArray,bytePosition,bitPosition);
		 int frame_crop_right_offset =  get_ue_code_num(strArray,bytePosition,bitPosition);
		 int frame_crop_top_offset =  get_ue_code_num(strArray,bytePosition,bitPosition);
		 int frame_crop_bottom_offset =  get_ue_code_num(strArray,bytePosition,bitPosition);
		 int crop_unit_x;
		 int crop_unit_y;
		 if (0 == chroma_format_idc) // monochrome
		 {
			 crop_unit_x = 1;
			 crop_unit_y = 2 - frame_mbs_only_flag;
		 }
		 else if (1 == chroma_format_idc) // 4:2:0
		 {
			 crop_unit_x = 2;
			 crop_unit_y = 2 * (2 - frame_mbs_only_flag);
		 }
		 else if (2 == chroma_format_idc) // 4:2:2
		 {
			 crop_unit_x = 2;
			 crop_unit_y = 2 - frame_mbs_only_flag;
		 }
		 else              // 4:4:4
		 {
			 crop_unit_x = 1;
			 crop_unit_y = 2 - frame_mbs_only_flag;
		 }
		m_wight -= crop_unit_x * ((int)frame_crop_left_offset + (int)frame_crop_right_offset);
	    m_hight -= crop_unit_y * ((int)frame_crop_top_offset + (int)frame_crop_bottom_offset);

	}
    //VUI相关,这里只解析 FPS
    int vui_parameter_present_flag = get_u_code_num(strArray,bytePosition,bitPosition,1); 
	if(vui_parameter_present_flag)
	{
	   //fps相关不解析
		int aspect_ratio_info_present_flag = get_u_code_num(strArray,bytePosition,bitPosition,1);
		if (aspect_ratio_info_present_flag)
		{
			int aspect_ratio_idc = get_u_code_num(strArray,bytePosition,bitPosition,8);
			if (aspect_ratio_idc == 255) 
			{    
				int sar_width = get_u_code_num(strArray,bytePosition,bitPosition,16);     
				int sar_height = get_u_code_num(strArray,bytePosition,bitPosition,16);  
			}
		}
		int overscan_info_present_flag = get_u_code_num(strArray,bytePosition,bitPosition,1);
		if (overscan_info_present_flag) 
		{
			int overscan_appropriate_flag = get_u_code_num(strArray,bytePosition,bitPosition,1);    
		}
		int video_signal_type_present_flag = get_u_code_num(strArray,bytePosition,bitPosition,1);
		if (video_signal_type_present_flag) 
		{
			int video_format = get_u_code_num(strArray,bytePosition,bitPosition,3);
			int video_full_range_flag = get_u_code_num(strArray,bytePosition,bitPosition,1);
			int colour_description_present_flag = get_u_code_num(strArray,bytePosition,bitPosition,1);
			if (colour_description_present_flag) 
			{
				int colour_primaries = get_u_code_num(strArray,bytePosition,bitPosition,8);
				int transfer_characteristics = get_u_code_num(strArray,bytePosition,bitPosition,8);
				int matrix_coefficients = get_u_code_num(strArray,bytePosition,bitPosition,8);
			}
		}
		  
		int chroma_loc_info_present_flag = get_u_code_num(strArray,bytePosition,bitPosition,1);
		if (chroma_loc_info_present_flag) 
		{
			int chroma_sample_loc_type_top_field = get_ue_code_num(strArray,bytePosition,bitPosition);    
			int chroma_sample_loc_type_bottom_field = get_ue_code_num(strArray,bytePosition,bitPosition);    
		}

		int timing_info_present_flag = get_u_code_num(strArray,bytePosition,bitPosition,1);
		if (timing_info_present_flag) 
		{
			int num_units_in_tick = get_u_code_num(strArray,bytePosition,bitPosition,32);
			int time_scale = get_u_code_num(strArray,bytePosition,bitPosition,32);
			 //海康此参数 好像有问题 采用x264编码未设置此值,结果不能参考fixed_frame_rate_flag 直接除以2
			int fixed_frame_rate_flag = get_u_code_num(strArray,bytePosition,bitPosition,1); 
			m_Fps = (int)((float)time_scale / (float)num_units_in_tick)/2;
		}
		//nal_hrd_parameters_present_flag 等参数信息 不解析
	}
	return ;
}

//获取当前字节 当前位的值
int CMP4Encode::get_bit_at_position(UINT8*buf,UINT8 &bytePosition,UINT8 &bitPosition)
{
	 UINT8 mask = 0,val = 0;
	 mask = 1<<(7-bitPosition) ;	//由高往低
	 val = (buf[bytePosition]&mask)?1:0;
	 if(++bitPosition >7)
	 {
	   bytePosition++;
	   bitPosition = 0;
	 }
	 return val;
}

//获取 从左往右 n个bit组成的值
int CMP4Encode::get_u_code_num(UINT8 *buf, UINT8 &bytePosition, UINT8 &bitPosition,UINT8 bitCount)
{
	 UINT8  bitVal = 0;
	 int  code_num = 0;
	 for (int bit_pos = bitCount-1; bit_pos >-1; bit_pos--)
	 {
	 	  bitVal = get_bit_at_position(buf, bytePosition, bitPosition);
	 	  code_num += (1 << bit_pos)*bitVal;
	 } 
	 return code_num; 
}

//ue(v)无符号指数哥伦布编码
int CMP4Encode::get_ue_code_num(UINT8 *buf, UINT8 &bytePosition, UINT8 &bitPosition)
{
	if(bitPosition > 0x08)
	 return 0;
	UINT8 bitVal = 0;
	int codeNum = 0,prefix = 0, surfix = 0,leadingZeroBits = 0;
	
	//获取前导0的个数
	while(true)
	{
	    bitVal = get_bit_at_position(buf, bytePosition, bitPosition);
		if (0 == bitVal)
		{
		   leadingZeroBits++; //前导 0 的统计
		}
		else
		{
		   break;
		}
	}
	//计算前缀
     prefix = (1 << leadingZeroBits) - 1;
	//计算后缀
	 for (int bit_pos = leadingZeroBits-1; bit_pos >-1; bit_pos--)
	 {
	 	  bitVal = get_bit_at_position(buf, bytePosition, bitPosition);
	 	  surfix += (1 << bit_pos)*bitVal;
	 }
	
	 codeNum = prefix + surfix;
	 return codeNum;
}

//se(v)有符号指数哥伦布编码
int CMP4Encode::get_se_code_num(UINT8 *buf, UINT8 &bytePosition, UINT8 &bitPosition)
{
	if(bitPosition > 0x08)
	return 0;
	UINT8 bitVal = 0;
	int SeNum = 0,codeNum = 0,prefix = 0, surfix = 0,leadingZeroBits = 0;

	//获取前导0的个数
	while(true)
	{
	    bitVal = get_bit_at_position(buf, bytePosition, bitPosition);
		if (0 == bitVal)
		{
		   leadingZeroBits++; //前导 0 的统计
		}
		else
		{
		   break;
		}
	}

  /*如 0 0 0     1     0 1 1 
    前缀prefix 为  2^(leadingZeroBits = 3) -1 = 7
    后缀surfix  0 1 1  = 0*2^2 + 1*2^1 +1*2^0 = 3
	codeNum = prefix + surfix = 10
   */
     prefix = (1 << leadingZeroBits) - 1;

	 for (int bit_pos = leadingZeroBits-1; bit_pos >-1; bit_pos--)
	 {
	 	  bitVal = get_bit_at_position(buf, bytePosition, bitPosition);
	 	  surfix += (1 << bit_pos)*bitVal;
	 }
	 codeNum = prefix + surfix;

	 //计算Se 公式 (−1)codeNum+1 Ceil(codeNum÷2 )
	 SeNum = ceil(codeNum/2.0);
	 if((codeNum+1)%2) //奇
		return -SeNum;
	 return SeNum;

}

粗糙的demo代码,细节部分可自行处理设计。

资源库链接:https://download.csdn.net/my

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值