一文读懂播放内核解码音视频编码数据并播放的实现原理
csdh11 2025-04-23 23:18 7 浏览
一、封装格式与编码格式的区别与联系
1.1 什么是封装格式?
封装格式(也叫容器)就是将已经编码压缩好的视频流、音频流及字幕按照一定的方案放到一个文件中,便于播放软件播放。一般来说,视频文件的后缀名就是它的封装格式。封装的格式不一样,后缀名也就不一样。比如:同样的陷可以做成饺子也可以做成包子。对于视频也是一个道理,同样的音视频流可以用不同容器来承载。常见的视频封装格式有:
1.2 编码格式
视频编码格式是用于存储或传输数字视频内容的格式,它通常使用标准化的视频压缩算法,将某个视频格式的文件转换成另一种视频格式的文件。视频压缩技术是计算机处理视频的前提,视频信号数字化后数据带宽很高,因此计算机很难对之进行保存和处理,采用压缩技术降低数据带宽,就可以将视频信号保存在计算机中并作相应的处理。
音视频编码的标准,目前视频主流的是 H.264/H.265,音频则为AAC。2003 年 H.264 的标准就已经制定完成,H.265 是 2013 年定稿的,但是由于专利费用太高,所以应用还是以 H.264 为主。此外以谷歌为代表的 AOM ,从 2008 年到 2013 年的 VP8、VP9。因为在芯片这块的推广效果不佳,所以也只是在谷歌自家应用。当直接将编码后的数据在网络上传播或者编码后的数据未使用封装格式承载的通常称之为裸流。常见的编码格式如下:
1.3 封装格式与编码格式的联系
这两者的关系好比酒与酒瓶的关系,编码格式好比酒瓶里的酒,是视频的核心内容,封装格式好比酒瓶,它只是负责把内部的视频轨、音频轨、字幕轨集成在一起。播放的时候需要从封装格式里面将编码格式数据解析出来然后送到解码器解码播放。弄清楚它们之间的关系后,就很清楚它们的区别了,视频编码格式是视频的内核,视频封装格式是视频的外壳。
二、播放器播放多媒体文件原理
视频播放器播放一个互联网上的视频文件,需要经过以下几个步骤:解协议,解封装,找流,搜索解码器,解码视音频,视音频同步。如果播放本地文件则不需要解协议,分为以下几个步骤:解封装,解码视音频,视音频同步。它们的过程如图所示。(图片来源于网络)
- 协议层(Protocol Layer):该层处理的数据为符合特定流媒体协议规范的数据,例如http,rtmp,file等。
- 封装格式层(Format Layer):该层处理的数据为符合特定封装格式规范的数据,例如mkv,mp4,flv,mpegts,avi等。
- 编解码层(Codec Layer):该层处理的数据为符合特定编码标准规范的数据,例如h264,h265,mpeg2,mpeg4等。
- 像素层(Pixel Layer):该层处理的数据为符合特定像素格式规范的数据,例如yuv420p,yuv422p,yuv444p,rgb24等。
- 解协议的作用,就是将流媒体协议的数据,解析为标准的相应的封装格式数据。视音频在网络上传播的时候,常常采用各种流媒体协议,例如HTTP,RTMP,RTMP ,RTP或是私有协议等等。这些协议在传输视音频数据的同时,也会传输一些信令数据。这些信令数据包括对播放的控制(播放,暂停,停止),或者对网络状态的描述等。解协议的过程中会去除掉信令数据而只保留视音频数据。例如,采用RTMP协议传输的数据,经过解协议操作后,输出FLV格式的数据。
- 解封装的作用,就是将输入的封装格式的数据,分离成为音频流压缩编码数据和视频流压缩编码数据。封装格式种类很多,例如MP4,MKV,RMVB,TS,FLV,AVI等等,它的作用就是将已经压缩编码的视频数据和音频数据按照一定的格式放到一起。例如,FLV格式的数据,经过解封装操作后,输出H.264编码的视频码流和AAC编码的音频码流。
- 解码的作用,就是将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。音频的压缩编码标准包含AAC,MP3,AC-3等等,视频的压缩编码标准则包含H.264,MPEG2,VC-1等等。解码是整个系统中最重要也是最复杂的一个环节。通过解码,压缩编码的视频数据输出成为非压缩的颜色数据,例如YUV420P,RGB等等;压缩编码的音频数据输出成为非压缩的音频抽样数据,例如PCM数据。
- 音视频同步的目的是为了使播放的声音和显示的画面保持一致。而同步依靠的参数信息则为每帧携带的时间戳,音视频同步的方式有三种:
- 以外部时钟为基准,即将音频时钟与视频时钟都向外部时钟靠齐。
- 以音频时钟为基准,将视频时钟往音频时钟对齐。
- 以视频时钟为基准,将音频时钟往音频时钟对齐。
三、播放器播放音视频编码数据原理
播放器播放裸流数据与播放文件相比省去了解封装,寻找媒体流和寻找编码器的过程。这种形式的数据通常为编码数据外层没有添加封装格式仅加了一层传输协议。为了能够顺利播放这种数据就需要解析裸流文件(如直接播放.h264/.h265 文件)或者直接从传输协议中解析编码数据再送到解码器解码。此过程中因为没有一个解码器嗅探的环节还需要根据解析出来的编码数据设置解码器类型。在代码的表现形式上通常即为直接播放 byte[] 数据。常见的使用场景就是直播了。流程如下:
四、FFMpeg 播放裸流数据
FFMpeg 解码播放裸流数据的方式有两种:
- 一种是扩展ffmpeg 的协议并实现协议接口,如ijkplayer 中使用到的缓存协议:
typedef struct IjkURLProtocol {
const char *name;
int (*url_open2)(IjkURLContext *h, const char *url, int flags, IjkAVDictionary **options);
int (*url_read)( IjkURLContext *h, unsigned char *buf, int size);
int64_t (*url_seek)( IjkURLContext *h, int64_t pos, int whence);
int (*url_close)(IjkURLContext *h);
int (*url_pause)(IjkURLContext *h); // option
int (*url_resume)(IjkURLContext *h); // option
int priv_data_size;
int flags;
} IjkURLProtocol;
实现其中的方法:
IjkURLProtocol ijkio_cache_protocol = {
.name = "ijkiocache",
.url_open2 = ijkio_cache_open,
.url_read = ijkio_cache_read,
.url_seek = ijkio_cache_seek,
.url_close = ijkio_cache_close,
.url_pause = ijkio_cache_pause,
.url_resume = ijkio_cache_resume,
.priv_data_size = sizeof(IjkIOCacheContext),
};
同时将实现的协议注册到协议表中:
static const URLProtocol * const url_protocols[] = {
&ff_async_protocol,
&ff_cache_protocol,
&ff_data_protocol,
&ff_ffrtmphttp_protocol,
&ff_file_protocol,
&ff_ftp_protocol,
&ff_hls_protocol,
&ff_http_protocol,
&ff_httpproxy_protocol,
&ff_https_protocol,
&ff_ijkhttphook_protocol,
&ff_ijklongurl_protocol,
&ff_ijkmediadatasource_protocol,
&ff_ijksegment_protocol,
&ff_ijktcphook_protocol,
&ff_ijkio_protocol,
&ff_pipe_protocol,
&ff_prompeg_protocol,
&ff_rtmp_protocol,
&ff_rtmpt_protocol,
&ff_tee_protocol,
&ff_tcp_protocol,
&ff_tls_openssl_protocol,
&ff_udp_protocol,
&ff_udplite_protocol,
NULL };
第二种为:直接喂裸流数据给解码器
- 裸流嗅探编码数据得到解码器类型
对于一些在业务层直接解析协议得到裸流数据的场景就比较合适这个方式。得到裸流数据之后只需放入待解码队列然后等待解码器解码即可渲染播放。可跳过ffmpeg 找流,解码器嗅探的环节。比如在Android 平台,假设已经得到了裸流数据byte[],接下来的一步需要从byte[] 数据中解析是何种编码格式,再根据编码格式初始化解码器。
- 判断编码是否为H264 编码格式代码如下 :
if (data[0] == 0 &&
data[1] == 0 &&
data[2] == 0 &&
data[3] == 1 &&
((data[4] & 0x1f) == SPS)
//或者
if (data[0] == 0 &&
data[1] == 0 &&
data[2] == 0 &&
((data[3] & 0x1f) == SPS)
- 判断编码是否为H265 编码格式代码如下 :
if (data[0] == 0 &&
data[1] == 0 &&
data[2] == 0 &&
data[3] == 1 &&
((data[0] >> 1) & 0x3f == HEVC_NAL_SPS))
- 解析裸流数据配置解码器信息
对于音频而言这一步需要根据裸流数据解析采样率,声道数,profile 等配置信息,然后再根据这些信息初始化音频解码器,解析音频裸配置信息核心代码如下:
void put_aac_packet(uint8_t *data, int length, int64_t pts, int64_t pos) {
if (!has_config_aac_decoder) {
if (length <= ADTS_HEAD_LENGTH) {
av_log(NULL, AV_LOG_INFO, "read aac head error");
return;
}
int frame_size = 0;
int n = 0;
if ((data[0] == 0xff) && ((data[1] & 0xff) == 0xf1)) {
frame_size |= ((data[3] & 0x03) << 11);
frame_size |= data[4] << 3;
frame_size |= ((data[5] & 0xe0) >> 5);
} else {
av_log(NULL, AV_LOG_INFO, "parse aac file adts head error");
return;
}
size_t aac_len = (size_t) (frame_size - ADTS_HEAD_LENGTH);
uint8_t* aac_buf = malloc(aac_len);
if (!aac_buf) {
av_log(NULL,AV_LOG_FATAL,"malloc fail");
return;
}
memset(aac_buf, 0, aac_len);
memcpy(aac_buf,data + ADTS_HEAD_LENGTH,aac_len);
//n = fread(buf + 7, 1, frame_size - 7, fp);
char profile_str[10] = {0};
char frequence_str[10] = {0};
unsigned char profile = (data[2]) & 0xC0;
profile = profile >> 6;
switch (profile) {
case 0:
sprintf(profile_str, "Main");
break;
case 1:
sprintf(profile_str, "LC");
break;
case 2:
sprintf(profile_str, "SSR");
break;
default:
sprintf(profile_str, "unknown");
break;
}
int channel = (data[2] & 0x01) << 2;
channel |= (data[3] & 0xC0) >> 6;
//parse frequence
unsigned char sampling_frequency_index = data[2] & 0x3C;
sampling_frequency_index = sampling_frequency_index >> 2;
int sampleFreq = 0;
switch (sampling_frequency_index) {
case 0:
sprintf(frequence_str, "96000Hz");
sampleFreq = 96000;
break;
case 1:
sprintf(frequence_str, "88200Hz");
sampleFreq = 88200;
break;
case 2:
sprintf(frequence_str, "64000Hz");
sampleFreq = 64000;
break;
case 3:
sprintf(frequence_str, "48000Hz");
sampleFreq = 48000;
break;
case 4:
sprintf(frequence_str, "44100Hz");
sampleFreq = 44100;
break;
case 5:
sprintf(frequence_str, "32000Hz");
sampleFreq = 32000;
break;
case 6:
sprintf(frequence_str, "24000Hz");
sampleFreq = 24000;
break;
case 7:
sprintf(frequence_str, "22050Hz");
sampleFreq = 22050;
break;
case 8:
sprintf(frequence_str, "16000Hz");
sampleFreq = 16000;
break;
case 9:
sprintf(frequence_str, "12000Hz");
sampleFreq = 12000;
break;
case 10:
sprintf(frequence_str, "11025Hz");
sampleFreq = 11025;
break;
case 11:
sprintf(frequence_str, "8000Hz");
sampleFreq = 8000;
break;
default:
sprintf(frequence_str, "unknown");
break;
}
has_config_aac_decoder = 1;
//av_log(NULL, AV_LOG_INFO, "935--- aac %8s | %8s | %5d | %5d\n", profile_str, frequence_str, channel,frame_size);
set_audio_config(ffp,sampleFreq,channel,profile);
}
}
- 配置音频解码器核心代码如下:
void set_audio_config(FFPlayer *player, int sampleFreq, int channels, int profile) {
ffp = player;
av_init_packet(&flush_pkt);
flush_pkt.data = (uint8_t *) &flush_pkt;
audio_is = av_mallocz(sizeof(VideoState));
if (frame_queue_init(&audio_is->pictq, &audio_is->videoq, player->pictq_size, 1) < 0) {
av_log(NULL, AV_LOG_FATAL, "audio frame_queue_init init failed\n");
return;
}
if (frame_queue_init(&audio_is->sampq, &audio_is->audioq, SAMPLE_QUEUE_SIZE, 1) < 0) {
av_log(NULL, AV_LOG_FATAL, "audio audio_is->sampq init failed\n");
return;
}
if (packet_queue_init(&audio_is->videoq) < 0 || packet_queue_init(&audio_is->audioq) < 0) {
av_log(NULL, AV_LOG_FATAL, "audio packetQueue init failed\n");
return;
}
player->is = audio_is;
// only support aac audio
AVCodec *audio_codec = avcodec_find_decoder(AV_CODEC_ID_AAC);
enum AVSampleFormat sample_fmt = AV_SAMPLE_FMT_FLTP;
int bytesPerSample = av_get_bytes_per_sample(sample_fmt);
audioCodecCtx = avcodec_alloc_context3(NULL);
if (audioCodecCtx == NULL) {
av_log(NULL, AV_LOG_FATAL, "audio AudioAVCodecCtx alloc failed\n");
return;
}
AVCodecParameters *par = avcodec_parameters_alloc();
if (par == NULL) {
av_log(NULL, AV_LOG_FATAL, "audio AVCodecParameters alloc failed\n");
avcodec_free_context(&audioCodecCtx);
return;
}
int64_t channel_layout = av_get_default_channel_layout(channels);
par->codec_type = AVMEDIA_TYPE_AUDIO;
par->sample_rate = sampleFreq;
par->channel_layout = channel_layout;
par->channels = channels;
par->bit_rate = sampleFreq * channels * bytesPerSample;
par->format = sample_fmt;
par->profile = profile;
avcodec_parameters_to_context(audioCodecCtx, par);
avcodec_parameters_free(&par);
int result = avcodec_open2(audioCodecCtx, audio_codec, NULL);
if (result < 0) {
av_log(NULL, AV_LOG_FATAL, "audio decodec open fail\n");
return;
}
audio_is->auddec.avctx = audioCodecCtx;
// set audio time_base
time_base = av_make_q(1, sampleFreq);
m_channel_layout = channel_layout;
m_channels = channels;
m_sampleFreq = sampleFreq;
isPlaying = true;
isStart = true;
// audio_decodec_tid = SDL_CreateThreadEx(&_audio_decodec_tid, audio_decodec, player, "ff_audio_decodec");
// if (!audio_decodec_tid) {
// av_log(NULL, AV_LOG_FATAL, "158---SDL_CreateThread(): %s\n", SDL_GetError());
// return;
// }
audio_player_tid = SDL_CreateThreadEx(&_audio_player_tid, audio_player, player,
"ff_audio_player");
av_log(NULL,AV_LOG_INFO,"[%d-------] init audio decode success",__LINE__);
if (!audio_player_tid) {
av_log(NULL, AV_LOG_FATAL, "[%d-------][%s][%s] create audio_player thread failed\n",
__LINE__, __FILE__, __FUNCTION__);
return;
}
}
- 解码音频的核心代码如下 :
AVPacket *packet = av_packet_alloc();
av_init_packet(packet);
packet->data = data;
packet->size = length;
packet->pts = pts;
packet->pos = pos;
int result_send_packet = avcodec_send_packet(audio_is->auddec.avctx, packet);
if (result_send_packet != 0) {
av_packet_unref(packet);
av_log(audio_is->auddec.avctx, AV_LOG_ERROR,
"[%d-------][%s] avcodec_send_packet failed.\n", __LINE__, __FUNCTION__);
return;
}
av_packet_unref(packet);
free(packet);
//send to decodec
Frame *af;
AVFrame *frame = av_frame_alloc();
avcodec_receive_frame(audio_is->auddec.avctx, frame);
if (!(af = frame_queue_peek_writable(&audio_is->sampq))) {
av_log(audio_is->auddec.avctx, AV_LOG_ERROR,"frame_queue_peek_writable fail.\n");
av_frame_free(&frame);
return;
}
af->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(time_base);
af->pos = frame->pkt_pos;
af->serial = audio_is->auddec.pkt_serial;
af->duration = av_q2d((AVRational){frame->nb_samples, frame->sample_rate});
av_frame_move_ref(af->frame, frame);
frame_queue_push(&audio_is->sampq);
av_frame_free(&frame);
- 对于视频而言需要解析裸流得到分辨率、帧率、编码格式,解析的核心代码如下:
if (data[0] == 0 &&
data[1] == 0 &&
data[2] == 0 &&
data[3] == 1 &&
((data[4] & 0x1f) == SPS)) {
int width = 0;
int height = 0;
int fps = 0;
h264_decode_sps(data, (unsigned int) length, &width, &height, &fps);
set_video_config(ffp, nativeWindow, AV_CODEC_ID_H264, width, height, fps);
av_log(NULL, AV_LOG_INFO,
"[%d---------] parse sps -----width = %d height = %d fps = %d",
__LINE__,
width, height, fps);
has_config_video_decoder = 1;
} else if (data[0] == 0 &&
data[1] == 0 &&
data[2] == 0 &&
data[3] == 1 &&
((data[0] >> 1) & 0x3f == HEVC_NAL_SPS)) {
int width = 0;
int height = 0;
int fps = 0;
h265_parser_sps(data, (unsigned int) length,&width,&height,&fps);
set_video_config(ffp, nativeWindow, AV_CODEC_ID_HEVC, width, height, fps);
has_config_video_decoder = 1;
}
}
int h264_decode_sps(BYTE *buf, unsigned int nLen, int *width, int *height, int *fps) {
UINT startss = 32;
UINT* StartBit = &startss;
*fps = 0;
de_emulation_prevention(buf, &nLen);
int forbidden_zero_bit = u(1, buf, StartBit);
int nal_ref_idc = u(2, buf, StartBit);
int nal_unit_type = u(5, buf, StartBit);
if (nal_unit_type == 7) {
int profile_idc = u(8, buf, StartBit);
int constraint_set0_flag = u(1, buf, StartBit);//(buf[1] & 0x80)>>7;
int constraint_set1_flag = u(1, buf, StartBit);//(buf[1] & 0x40)>>6;
int constraint_set2_flag = u(1, buf, StartBit);//(buf[1] & 0x20)>>5;
int constraint_set3_flag = u(1, buf, StartBit);//(buf[1] & 0x10)>>4;
int reserved_zero_4bits = u(4, buf, StartBit);
int level_idc = u(8, buf, StartBit);
int seq_parameter_set_id = Ue(buf, nLen, StartBit);
if (profile_idc == 100 || profile_idc == 110 ||
profile_idc == 122 || profile_idc == 144) {
int chroma_format_idc = Ue(buf, nLen, StartBit);
if (chroma_format_idc == 3) {
int residual_colour_transform_flag = u(1, buf, StartBit);
}
int bit_depth_luma_minus8 = Ue(buf, nLen, StartBit);
int bit_depth_chroma_minus8 = Ue(buf, nLen, StartBit);
int qpprime_y_zero_transform_bypass_flag = u(1, buf, StartBit);
int seq_scaling_matrix_present_flag = u(1, buf, StartBit);
int seq_scaling_list_present_flag[8];
if (seq_scaling_matrix_present_flag) {
for (int i = 0; i < 8; i++) {
seq_scaling_list_present_flag[i] = u(1, buf, StartBit);
}
}
}
int log2_max_frame_num_minus4 = Ue(buf, nLen, StartBit);
int pic_order_cnt_type = Ue(buf, nLen, StartBit);
if (pic_order_cnt_type == 0) {
int log2_max_pic_order_cnt_lsb_minus4 = Ue(buf, nLen, StartBit);
}
else if (pic_order_cnt_type == 1) {
int delta_pic_order_always_zero_flag = u(1, buf, StartBit);
int offset_for_non_ref_pic = Se(buf, nLen, StartBit);
int offset_for_top_to_bottom_field = Se(buf, nLen, StartBit);
int num_ref_frames_in_pic_order_cnt_cycle = Ue(buf, nLen, StartBit);
//int *offset_for_ref_frame = new int[num_ref_frames_in_pic_order_cnt_cycle];
int* offset_for_ref_frame = malloc(num_ref_frames_in_pic_order_cnt_cycle * sizeof(int*));
memset(offset_for_ref_frame,num_ref_frames_in_pic_order_cnt_cycle * sizeof(int*),0);
for (int i = 0; i < num_ref_frames_in_pic_order_cnt_cycle; i++)
offset_for_ref_frame[i] = Se(buf, nLen, StartBit);
free(offset_for_ref_frame);
}
int num_ref_frames = Ue(buf, nLen, StartBit);
int gaps_in_frame_num_value_allowed_flag = u(1, buf, StartBit);
int pic_width_in_mbs_minus1 = Ue(buf, nLen, StartBit);
int pic_height_in_map_units_minus1 = Ue(buf, nLen, StartBit);
*width = (pic_width_in_mbs_minus1 + 1) * 16;
*height = (pic_height_in_map_units_minus1 + 1) * 16;
av_log(NULL,AV_LOG_INFO,"155------width = %d height = %d",*width,*height);
int frame_mbs_only_flag = u(1, buf, StartBit);
if (!frame_mbs_only_flag) {
int mb_adaptive_frame_field_flag = u(1, buf, StartBit);
}
int direct_8x8_inference_flag = u(1, buf, StartBit);
int frame_cropping_flag = u(1, buf, StartBit);
if (frame_cropping_flag) {
int frame_crop_left_offset = Ue(buf, nLen, StartBit);
int frame_crop_right_offset = Ue(buf, nLen, StartBit);
int frame_crop_top_offset = Ue(buf, nLen, StartBit);
int frame_crop_bottom_offset = Ue(buf, nLen, StartBit);
}
int vui_parameter_present_flag = u(1, buf, StartBit);
if (vui_parameter_present_flag) {
int aspect_ratio_info_present_flag = u(1, buf, StartBit);
if (aspect_ratio_info_present_flag) {
int aspect_ratio_idc = u(8, buf, StartBit);
if (aspect_ratio_idc == 255) {
int sar_width = u(16, buf, StartBit);
int sar_height = u(16, buf, StartBit);
}
}
int overscan_info_present_flag = u(1, buf, StartBit);
if (overscan_info_present_flag) {
int overscan_appropriate_flagu = u(1, buf, StartBit);
}
int video_signal_type_present_flag = u(1, buf, StartBit);
if (video_signal_type_present_flag) {
int video_format = u(3, buf, StartBit);
int video_full_range_flag = u(1, buf, StartBit);
int colour_description_present_flag = u(1, buf, StartBit);
if (colour_description_present_flag) {
int colour_primaries = u(8, buf, StartBit);
int transfer_characteristics = u(8, buf, StartBit);
int matrix_coefficients = u(8, buf, StartBit);
}
}
int chroma_loc_info_present_flag = u(1, buf, StartBit);
if (chroma_loc_info_present_flag) {
int chroma_sample_loc_type_top_field = Ue(buf, nLen, StartBit);
int chroma_sample_loc_type_bottom_field = Ue(buf, nLen, StartBit);
}
int timing_info_present_flag = u(1, buf, StartBit);
if (timing_info_present_flag) {
int num_units_in_tick = u(32, buf, StartBit);
int time_scale = u(32, buf, StartBit);
*fps = time_scale / num_units_in_tick;
int fixed_frame_rate_flag = u(1, buf, StartBit);
if (fixed_frame_rate_flag) {
*fps = *fps / 2;
}
av_log(NULL,AV_LOG_INFO,"210--------fps = %d",*fps);
}
}
return 1;
} else
return 2;
}
int h265_parser_sps(unsigned char *buffer, unsigned int bufferlen,int *width,int *height,int *fps) {
UINT startss = 0;
unsigned int* StartBit=&startss;
de_emulation_prevention(buffer,&bufferlen);
uint32_t sps_video_parameter_set_id = 0;
uint32_t sps_max_sub_layers_minus1 = 0;
int sps_temporal_id_nesting_flag;
uint32_t sps_seq_parameter_set_id = 0;
uint32_t chroma_format_idc;
int separate_colour_plane_flag = 0;
uint32_t pic_width_in_luma_samples;
uint32_t pic_height_in_luma_samples;
int conformance_window_flag;
uint32_t conf_win_left_offset;
uint32_t conf_win_right_offset;
uint32_t conf_win_top_offset;
uint32_t conf_win_bottom_offset;
uint32_t bit_depth_luma_minus8;
uint32_t bit_depth_chroma_minus8;
uint32_t log2_max_pic_order_cnt_lsb_minus4;
int sps_sub_layer_ordering_info_present_flag;
int rbsp_stop_one_bit;
u(16,buffer,StartBit);//nal_unit_header
sps_video_parameter_set_id = u(4,buffer,StartBit);
sps_max_sub_layers_minus1 = u(3,buffer,StartBit);
sps_temporal_id_nesting_flag = u(1,buffer,StartBit);
h265_parse_ptl(sps_max_sub_layers_minus1,buffer,StartBit,bufferlen);
sps_seq_parameter_set_id = Ue(buffer,bufferlen,StartBit);
//p_sps = &sps[sps_seq_parameter_set_id];
chroma_format_idc = Ue(buffer,bufferlen,StartBit);
if (3 == chroma_format_idc)
{
separate_colour_plane_flag = u(1,buffer,StartBit);
}
pic_width_in_luma_samples = Ue(buffer,bufferlen,StartBit);
pic_height_in_luma_samples = Ue(buffer,bufferlen,StartBit);
*width = pic_width_in_luma_samples;
*height = pic_height_in_luma_samples;
av_log(NULL,AV_LOG_INFO,"352----------hevc width = %d height = %d ", pic_width_in_luma_samples,pic_height_in_luma_samples);
// conformance_window_flag = u(1,buffer,StartBit);
//
// if (conformance_window_flag)
// {
// conf_win_left_offset = Ue(buffer,bufferlen,StartBit);
// conf_win_right_offset = Ue(buffer,bufferlen,StartBit);
// conf_win_top_offset = Ue(buffer,bufferlen,StartBit);
// conf_win_bottom_offset = Ue(buffer,bufferlen,StartBit);
// }
//
// bit_depth_luma_minus8 = Ue(buffer,bufferlen,StartBit);
// bit_depth_chroma_minus8 = Ue(buffer,bufferlen,StartBit);
// log2_max_pic_order_cnt_lsb_minus4 = Ue(buffer,bufferlen,StartBit);
//
// sps_sub_layer_ordering_info_present_flag = u(1,buffer,StartBit);
//
// int i;
// uint32_t *sps_max_dec_pic_buffering_minus1 = new uint32_t[sps_max_sub_layers_minus1 + 1];
// uint32_t *sps_max_num_reorder_pics = new uint32_t[sps_max_sub_layers_minus1 + 1];
// uint32_t *sps_max_latency_increase_plus1 = new uint32_t[sps_max_sub_layers_minus1 + 1];
//
// for (i = (sps_sub_layer_ordering_info_present_flag ? 0 : sps_max_sub_layers_minus1); i <= sps_max_sub_layers_minus1; i++ )
// {
// sps_max_dec_pic_buffering_minus1[i] = Ue(buffer,bufferlen,StartBit);
// sps_max_num_reorder_pics[i] = Ue(buffer,bufferlen,StartBit);
// sps_max_latency_increase_plus1[i] = Ue(buffer,bufferlen,StartBit);
// }
//
// uint32_t log2_min_luma_coding_block_size_minus3;
// uint32_t log2_diff_max_min_luma_coding_block_size;
// uint32_t log2_min_transform_block_size_minus2;
// uint32_t log2_diff_max_min_transform_block_size;
// uint32_t max_transform_hierarchy_depth_inter;
// uint32_t max_transform_hierarchy_depth_intra;
// bool scaling_list_enabled_flag;
//
// log2_min_luma_coding_block_size_minus3 = Ue(buffer,bufferlen,StartBit);
// log2_diff_max_min_luma_coding_block_size = Ue(buffer,bufferlen,StartBit);
// log2_min_transform_block_size_minus2 = Ue(buffer,bufferlen,StartBit);
// log2_diff_max_min_transform_block_size = Ue(buffer,bufferlen,StartBit);
// max_transform_hierarchy_depth_inter = Ue(buffer,bufferlen,StartBit);
// max_transform_hierarchy_depth_intra = Ue(buffer,bufferlen,StartBit);
// scaling_list_enabled_flag = u(1,buffer,StartBit);
//
// if (scaling_list_enabled_flag)
// {
// bool sps_scaling_list_data_present_flag;
//
// sps_scaling_list_data_present_flag = u(1,buffer,StartBit);
//
// if (sps_scaling_list_data_present_flag)
// {
// h265_parse_scaling_list(buffer,StartBit,bufferlen);
// }
// }
//
// bool amp_enabled_flag;
// bool sample_adaptive_offset_enabled_flag;
// bool pcm_enabled_flag;
// uint32_t pcm_sample_bit_depth_luma_minus1;
// uint32_t pcm_sample_bit_depth_chroma_minus1;
// uint32_t log2_min_pcm_luma_coding_block_size_minus3;
// uint32_t log2_diff_max_min_pcm_luma_coding_block_size;
// bool pcm_loop_filter_disabled_flag;
//
// amp_enabled_flag = u(1,buffer,StartBit);
// sample_adaptive_offset_enabled_flag = u(1,buffer,StartBit);
// pcm_enabled_flag = u(1,buffer,StartBit);
//
// if (pcm_enabled_flag)
// {
// pcm_sample_bit_depth_luma_minus1 = u(4,buffer,StartBit);
// pcm_sample_bit_depth_chroma_minus1 = u(4,buffer,StartBit);
// log2_min_pcm_luma_coding_block_size_minus3 = Ue(buffer,bufferlen,StartBit);
// log2_diff_max_min_pcm_luma_coding_block_size = Ue(buffer,bufferlen,StartBit);
// pcm_loop_filter_disabled_flag = u(1,buffer,StartBit);
// }
//
// uint32_t num_short_term_ref_pic_sets = 0;
//
// num_short_term_ref_pic_sets = Ue(buffer,bufferlen,StartBit);
// createRPSList(p_sps, num_short_term_ref_pic_sets);
// for (i = 0; i < num_short_term_ref_pic_sets; i++)
// {
// //parse_short_term_ref_pic_set(i)
// int inter_ref_pic_set_prediction_flag = 0;
// int delta_idx_minus1 = 0;
// int delta_rps_sign = 0;
// int abs_delta_rps_minus1 = 0;
// int delta_rps = 0;
// if(i){
// inter_ref_pic_set_prediction_flag = u(1,buffer,StartBit);
// }
// if(inter_ref_pic_set_prediction_flag){
// if(i == num_short_term_ref_pic_sets){
// delta_idx_minus1 = Ue(buffer,bufferlen,StartBit);
// }
// delta_rps_sign = u(1,buffer,StartBit);
// abs_delta_rps_minus1 = Ue(buffer,bufferlen,StartBit);
// delta_rps = (1 - (delta_rps_sign << 1)) * (abs_delta_rps_minus1+1);
//// for (i = 0; i <= rps_ridx->num_delta_pocs; i++) {
////
//// }
// }
//
// }
//
// bool long_term_ref_pics_present_flag = false;
//
// long_term_ref_pics_present_flag = u(1,buffer,StartBit);
//
// if (long_term_ref_pics_present_flag)
// {
// uint32_t num_long_term_ref_pics_sps;
//
// num_long_term_ref_pics_sps = Ue(buffer,bufferlen,StartBit);
//
// uint32_t *lt_ref_pic_poc_lsb_sps = new uint32_t[num_long_term_ref_pics_sps];
// uint32_t *used_by_curr_pic_lt_sps_flag = new uint32_t[num_long_term_ref_pics_sps];
//
// for (i = 0; i < num_long_term_ref_pics_sps; i++)
// {
// int varible = (log2_max_pic_order_cnt_lsb_minus4+4)>16?16:(log2_max_pic_order_cnt_lsb_minus4+4);
// lt_ref_pic_poc_lsb_sps[i] = u(varible,buffer,StartBit);
// used_by_curr_pic_lt_sps_flag[i] = u(1,buffer,StartBit);
// }
// }
//
// bool sps_temporal_mvp_enabled_flag;
// bool strong_intra_smoothing_enabled_flag;
// bool vui_parameters_present_flag;
//
// sps_temporal_mvp_enabled_flag = u(1,buffer,StartBit);
// strong_intra_smoothing_enabled_flag = u(1,buffer,StartBit);
// vui_parameters_present_flag = u(1,buffer,StartBit);
//
// if (vui_parameters_present_flag)
// {
// //parse_vui(sps_max_sub_layers_minus1);
// h265_parse_vui(buffer,StartBit,bufferlen);
// }
// bool sps_extension_flag = false;
//
// sps_extension_flag = READ_FLAG("sps_extension_flag");
//
// if (sps_extension_flag)
// {
// while (MORE_RBSP_DATA())
// {
// bool sps_extension_data_flag;
//
// sps_extension_data_flag = READ_FLAG("sps_extension_data_flag");
// }
// }
//
// rbsp_stop_one_bit = READ_FLAG("rbsp_stop_one_bit");
//
// p_sps->m_VPSId = sps_video_parameter_set_id;
// p_sps->m_SPSId = sps_seq_parameter_set_id;
// p_sps->m_chromaFormatIdc = chroma_format_idc;
//
// p_sps->m_uiBitsForPOC = log2_max_pic_order_cnt_lsb_minus4 + 4;
//
// p_sps->m_separateColourPlaneFlag = separate_colour_plane_flag;
//
// p_sps->m_log2MinCodingBlockSize = log2_min_luma_coding_block_size_minus3 + 3;
// p_sps->m_log2DiffMaxMinCodingBlockSize = log2_diff_max_min_luma_coding_block_size;
// p_sps->m_uiMaxCUWidth = 1 << (p_sps->m_log2MinCodingBlockSize + p_sps->m_log2DiffMaxMinCodingBlockSize);
// p_sps->m_uiMaxCUHeight = 1 << (p_sps->m_log2MinCodingBlockSize + p_sps->m_log2DiffMaxMinCodingBlockSize);
//
// p_sps->m_bLongTermRefsPresent = long_term_ref_pics_present_flag;
// p_sps->m_TMVPFlagsPresent = sps_temporal_mvp_enabled_flag;
// p_sps->m_bUseSAO = sample_adaptive_offset_enabled_flag;
// //printf("CtbSizeY=%u\n", p_sps->m_uiMaxCUWidth);
//
// if(conformance_window_flag){
// int sub_width_c = ((1==chroma_format_idc)||(2 == chroma_format_idc))&&(0==separate_colour_plane_flag)?2:1;
// int sub_height_c = (1==chroma_format_idc)&& (0 == separate_colour_plane_flag)?2:1;
// g_HevcInfo.Width -= (sub_width_c*conf_win_right_offset + sub_width_c*conf_win_left_offset);
// g_HevcInfo.Height -= (sub_height_c*conf_win_bottom_offset + sub_height_c*conf_win_top_offset);
// }else{
// g_HevcInfo.Width = pic_width_in_luma_samples;
// g_HevcInfo.Height = pic_height_in_luma_samples;
// }
return 1;
}
- 配置视频解码器的核心代码如下:
void set_video_config(FFPlayer *opaque, ANativeWindow *native_window, enum AVCodecID codecId, int width,
int height, int fps) {
has_config_video_decoder = 0;
nativeWindow = native_window;
ffp = opaque;
avcodec_register_all();
AVCodec *video_codec = avcodec_find_decoder(codecId);
av_init_packet(&flush_pkt);
flush_pkt.data = (uint8_t *) &flush_pkt;
video_is = av_mallocz(sizeof(VideoState));
if (frame_queue_init(&video_is->pictq, &video_is->videoq, ffp->pictq_size, 1) < 0) {
av_log(NULL, AV_LOG_FATAL, "audio frame_queue_init init failed\n");
return;
}
videoCodecCtx = avcodec_alloc_context3(video_codec);
videoCodecCtx->width = width;
videoCodecCtx->height = height;
videoCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
videoCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
AVRational framerate = { 1, fps ? fps : 25};
videoCodecCtx->framerate = framerate;
int result = avcodec_open2(videoCodecCtx, video_codec, NULL);
if (result < 0) {
av_log(NULL,AV_LOG_INFO,"Player Error : video_open fail");
return;
}
videoWidth = videoCodecCtx->width;
videoHeight = videoCodecCtx->height;
result = ANativeWindow_setBuffersGeometry(native_window,
videoCodecCtx->width,
videoCodecCtx->height,
WINDOW_FORMAT_RGBA_8888);
if (result < 0) {
av_log(NULL,AV_LOG_INFO,"Player Error : Can not set native window buffer");
ANativeWindow_release(nativeWindow);
return;
}
video_is->paused = 0;
video_is->pause_req = 0;
video_player_tid = SDL_CreateThreadEx(&_video_player_tid, video_player_task, ffp,
"ff_video_player");
av_log(NULL,AV_LOG_INFO,"[%d-------] init video decode success",__LINE__);
if (!video_player_tid) {
av_log(NULL, AV_LOG_FATAL, "[%d-------][%s][%s] create video_player thread failed\n",
__LINE__, __FILE__, __FUNCTION__);
return;
}
if (nativeWindow == NULL) {
av_log(NULL,AV_LOG_INFO,"Player Error : Can not create native window");
return;
}
}
- 视频解码核心代码如下:
AVPacket *packet = av_packet_alloc();
av_init_packet(packet);
packet->data = data;
packet->size = length;
packet->pts = pts;
//packet->pos = pos;
int result_send_packet = avcodec_send_packet(videoCodecCtx, packet);
if (result_send_packet != 0) {
av_packet_unref(packet);
av_log(video_is->viddec.avctx, AV_LOG_ERROR,
"[%d-------][%s] avcodec_send_packet failed.\n", __LINE__, __FUNCTION__);
return;
}
av_packet_unref(packet);
free(packet);
free(data);
//send to decodec
Frame *af;
AVFrame *frame = av_frame_alloc();
if (!(af = frame_queue_peek_writable(&video_is->pictq))) {
av_log(videoCodecCtx, AV_LOG_ERROR,"frame_queue_peek_writable fail.\n");
av_frame_free(&frame);
return;
}
avcodec_receive_frame(videoCodecCtx, frame);
af->width = frame->width;
af->height = frame->height;
af->format = frame->format;
af->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(time_base);
af->pos = frame->pkt_pos;
af->serial = video_is->viddec.pkt_serial;
av_frame_move_ref(af->frame, frame);
frame_queue_push(&video_is->pictq);
av_frame_free(&frame);
五、音视频频同步介绍
一般来说,音视频同步指的是视频和音频同步播放,它的目的是为了使播放的声音和显示的画面保持一致。想象一下,看一部电影的时候只看到人物嘴动没有声音传出;或者画面是激烈的战斗场景,而声音不是枪炮声却是人物说话的声音,这是非常差的一种体验。 在视频流和音频流中已包含了其以怎样的速度播放的相关数据,视频的帧率(Frame Rate)指示视频一秒显示的帧数(图像数);音频的采样率(Sample Rate)表示音频一秒播放的样本(Sample)的个数。可以使用以上数据通过简单的计算得到其在某一Frame(Sample)的播放时间,以这样的速度音频和视频各自播放互不影响,在理想条件下,其应该是同步的,不会出现偏差。但是在网络环境中一旦造成丢包就可能会出现音频报文与视频报文对不齐了。如果这时候对这种情况不做处理,慢慢的就会出现音视频不同步的情况。要不是视频播放快了,要么是音频播放快了,很难准确的同步。这就需要一种随着时间会线性增长的量,视频和音频的播放速度都以该量为标准,播放快了就减慢播放速度;播放快了就加快播放的速度。所以呢,视频和音频的同步实际上是一个动态的过程,同步是暂时的,不同步则是常态。以选择的播放速度量为标准,快的等待慢的,慢的则加快速度,是一个你等我赶的过程。
我们以一个44.1KHz的AAC音频流和25FPS的H264视频流为例,来看一下理想情况下音视频的同步过程: 一个AAC音频frame每个声道包含1024个采样点(也可能是2048,参“FFmpeg关于nb_smples,frame_size以及profile的解释”),则一个frame的播放时长(duration)为:(1024/44100)×1000ms = 23.22ms;一个H264视频frame播放时长(duration)为:1000ms/25 = 40ms。声卡虽然是以音频采样点为播放单位,但通常我们每次往声卡缓冲区送一个音频frame,每送一个音频frame更新一下音频的播放时刻,即每隔一个音频frame时长更新一下音频时钟,实际上ffplay就是这么做的。我们暂且把一个音频时钟更新点记作其播放点,理想情况下,音视频完全同步,音视频播放过程如下图所示:(图片来源于网络)
音视频同步的方式基本是确定一个时钟(音频时钟、视频时钟、外部时钟)作为主时钟,非主时钟的音频或视频时钟为从时钟。在播放过程中,主时钟作为同步基准,不断判断从时钟与主时钟的差异,调节从时钟,使从时钟追赶(落后时)或等待(超前时)主时钟。按照主时钟的不同种类,可以将音视频同步模式分为如下三种:
- 将视频同步到音频上,就是以音频的播放速度为基准来同步视频。视频比音频播放慢了,加快其播放速度;快了,则延迟播放。
- 将音频同步到视频上,就是以视频的播放速度为基准来同步音频。
- 将视频和音频同步外部的时钟上,选择一个外部时钟为基准,视频和音频的播放速度都以该时钟为标准。
例:以音频时钟为参考时钟实现音视频同步
以音频时钟为参考做音画同步,音频的解码播放的过程中认为是正常播放的不做处理。在播放的过程中,需要不断地将视频的播放时间戳和音频时间戳作比较,如果两者的差值超过了某个阈值,则认为视频播放太快或者太慢了,需要将视频播放调慢或者调快甚至丢帧,如果它们的差值在允许的阈值范围内,则代表播放正常,不用调整。如下图所示(图片来源于网络)
需要同步的处理的情况通常有两种:
- 如果视频出现播放早于音频,那么这种情况就需要进行丢帧处理将当前未播放的帧全部丢弃直到与音频时间戳在合理的阈值内。同时要增加播放视频线程的休眠时间,让它工作的慢点。
- 如果视频播放滞后音频,就需要加快视频的播放,这种加快的处理其实就是减少视频播放线程的休眠时间。
主要是准备的计算它两的时间戳之间的差值,然后根据差值再进行上述两种情况的业务处理。核心代码如下:
double MediaSync::calculateDelay(double delay) {
double sync_threshold, diff = 0;
// 如果不是同步到视频流,则需要计算延时时间
if (playerState->syncType != AV_SYNC_VIDEO) {
// 计算差值,videoClock->getClock()是当前播放帧的时间戳,这里计算当前播放视频帧的时间戳和音频时间戳的差值
diff = videoClock->getClock() - getMasterClock(); // 这里是同步到音频时钟,所以主时钟就是音频时钟
// 计算阈值
sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
if (!isnan(diff) && fabs(diff) < maxFrameDuration) {
if (diff <= -sync_threshold) { // 视频慢了,需要加快视频的播放
delay = FFMAX(0, delay + diff);
}
// 视频快了并且上一帧视频的播放时长超过了阈值
else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD) {
delay = delay + diff;
}
else if (diff >= sync_threshold) { // 视频快了
delay = 2 * delay;
}
}
}
av_log(NULL, AV_LOG_TRACE, "video: delay=%0.3f A-V=%f\n", delay, -diff);
return delay;
}
- 上述函数中 delay校正策略如下:
- 视频时钟落后于同步时钟且落后值超过同步域值:
- 若当前帧播放时刻落后于同步时钟(delay+diff<0)则delay=0(视频追赶,立即播放);
- 否则delay=duration+diff
- 视频时钟超前于同步时钟且超过同步域值:
- 上一帧播放时长过长(超过最大值),仅校正为delay=duration+diff;
- 否则delay=duration×2,视频播放放慢脚步,等待音频
- 视频时钟与音频时钟的差异在同步域值内,表明音视频处于同步状态,不校正delay,则delay=duration
对上述视频同步到音频的过程作一个总结,参考下图:
图中,小黑圆圈是代表帧的实际播放时刻,小红圆圈代表帧的理论播放时刻,小绿方块表示当前系统时间(当前时刻),小红方块表示位于不同区间的时间点,则当前时刻处于不同区间时,视频同步策略为:
- 当前时刻在T0位置,则重复播放上一帧,延时remaining_time后再播放当前帧
- 当前时刻在T1位置,则立即播放当前帧
- 当前时刻在T2位置,则忽略当前帧,立即显示下一帧,加速视频追赶 上述内容是为了方便理解进行的简单而形象的描述。实际过程要计算相关值,根据compute_target_delay()和video_refresh()中的策略来控制播放过程。
相关推荐
- Centos离线静默安装 oracle11g,步骤细验证成功
-
一、环境要求1.1.涉及工具及环境1)CentOS764位系统2)oracle安装包文件a)linux.x64_11gR2_database_1of2.zip...
- zabbix 5.0 ODBC监控Oracle
-
以下配置基于zabbix5.0.24完成,基于5.4测试无法提示找到libsqora.so.21.1类库文件,网上搜索了很多方法都无法解决。本次ODBC数据采集服务不在zabbix中部署,而是由单独...
- Windows环境中Oracle数据库ORA-01034错误的处理过程
-
摘要:今天一早发现公司的一台Oracle数据库无法访问,使用PL/SQL登录Oracle数据库,提示【ORA-12514:TNS:监听程序当前无法识别连接描述符中请求的服务】,后来经过修复后,并在服务...
- 部署单机oracle数据库,干货太多,我编辑都累
-
一、前言本次实施内容是,oracle单实例系统文件安装,操作系统为CentOS6.9,数据库版本11.2.0.4。二、oracle软件安装...
- 带你部署单机oracle数据库,超详细带图解说
-
一、前言本次实施内容是,oracle单实例系统文件安装,操作系统为CentOS6.9,数据库版本11.2.0.4。二、oracle软件安装...
- oracle用户创建及权限设置
-
权限:createsessioncreatetableunlimitedtablespaceconnectresource...
- 简单且优雅数据库操作-测试向
-
cmd打开命令行输入框sqlplus/nolog连接无用户数据库connectsys/zhwylanassysdba;连接sys用户的管理员身份showuser;显示当前用户目录...
- 手撸一个Oracle Rac(3)
-
(文章太长,只能分段发表,哎~)四、RAC维护一般命令1.查看集群状态grid用户执行crs_stat-t...
- 「运维经」第23章——忘记oracle密码
-
忘记oracle密码前提条件:你确保安装oracleserver主机的oracle用户密码你还记得。...
- 详解BarTender超过文件尾访问
-
Bartender标签打印机软件出现不能正常运行,在打开这个软件会出来一个窗口“尝试超过文件尾访问文件…….btw”的字样。解决方法:彻底卸载软件,删除软件所有的注册信息。方法一:可以采用360安全助...
- BarTender只打印口令密码去除教程
-
BarTender条码打印软件具有“只打印口令设置”功能,通过设置该口令密码可以防止操作人员随意变动模板内容。但是,如果您将BarTender只打印口令设置密码忘了怎么办?也许你会觉得卸载软件重装就O...
- Bartender怎么破解?Bartender如何安装图文教程讲解
-
什么是bartender?bartender是美国海鸥科技推出的一款条码打印软件bartender在150多个国家与地区已拥有上千万的用户,该软件在条码行业中得到众多群体使用和行业设备巨头认可并与...
- FFmpeg+SDL视频播放器-图形界面版
-
MFC知识创建MFC工程的方法...
- 技术|开源空间音频格式Eclipsa Audio登场,继HDR10+后Samsung再次硬杠Dolby
-
说到空间音频/沉浸式3D音效,你首先想到谁?DolbyAtmos、DTS:X还是Auro3D?如今新的“搅局者”出现了!韩系电视大厂Samsung与Google合作,推出空间音频格式Eclipsa...
- 视频怎么进行格式转换?6款视频转换MP4格式的免费软件!
-
在数字时代,视频格式的多样性为我们提供了丰富的观看和编辑选择,但同时也带来了格式不兼容的困扰:MOV、AVI、WMV、MKV……这些格式多多少少都会遇到因不兼容而无法播放或下载分享的场景。当你想要将视...
- 一周热门
- 最近发表
- 标签列表
-
- mydisktest_v298 (34)
- document.appendchild (35)
- 头像打包下载 (61)
- acmecadconverter_8.52绿色版 (39)
- word文档批量处理大师破解版 (36)
- server2016安装密钥 (33)
- mysql 昨天的日期 (37)
- parsevideo (33)
- 个人网站源码 (37)
- centos7.4下载 (33)
- mysql 查询今天的数据 (34)
- intouch2014r2sp1永久授权 (36)
- 先锋影音源资2019 (35)
- jdk1.8.0_191下载 (33)
- axure9注册码 (33)
- pts/1 (33)
- spire.pdf 破解版 (35)
- shiro jwt (35)
- sklearn中文手册pdf (35)
- itextsharp使用手册 (33)
- 凯立德2012夏季版懒人包 (34)
- 反恐24小时电话铃声 (33)
- 冒险岛代码查询器 (34)
- 128*128png图片 (34)
- jdk1.8.0_131下载 (34)