<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          IjkPlayer之數(shù)據(jù)讀取線程read_thread

          共 29749字,需瀏覽 60分鐘

           ·

          2022-08-05 01:12

          ??

          9179b8845962fe83df58e7d1a5a0343e.webp
          PS: 控制新技術對你的影響而不是被控制。
          這是 IjkPlayer 系列的第五篇文章了,前四篇依次如下:本文分析下 IjkPlayer 的數(shù)據(jù)讀取線程 read_thread,目的是理清其基本流程以及關鍵函數(shù)的調(diào)用,主要內(nèi)容如下:
          1. IjkPlayer基本使用

          2. read_thread創(chuàng)建

          3. avformat_alloc_context

          4. avformat_open_input

          5. avformat_find_stream_info

          6. avformat_seek_file

          7. av_dump_format

          8. av_find_best_stream

          9. stream_component_open

          10. read_thread主循環(huán)

          IjkPlayer基本使用

          簡單回顧下 IjkPlayer 的基本使用方式如下:
          //?創(chuàng)建IjkMediaPlayer
          IjkMediaPlayer?mMediaPlayer?=?new?IjkMediaPlayer();
          //?設置Log級別
          mMediaPlayer.native_setLogLevel(IjkMediaPlayer.IJK_LOG_DEBUG);
          //?設置Option
          mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER,?"mediacodec",?1);
          //?...
          //?設置事件監(jiān)聽
          mMediaPlayer.setOnPreparedListener(mPreparedListener);
          mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
          mMediaPlayer.setOnCompletionListener(mCompletionListener);
          mMediaPlayer.setOnErrorListener(mErrorListener);
          mMediaPlayer.setOnInfoListener(mInfoListener);
          //?設置Surface
          mMediaPlayer.setSurface(surface)
          //?...
          //?設置url
          mMediaPlayer.setDataSource(dataSource);
          //?準備播放
          mMediaPlayer.prepareAsync();
          當調(diào)用 prepareAsync 之后收到 onPrepared 回調(diào)是調(diào)用 start 開始播放:
          @Override
          public?void?onPrepared(IMediaPlayer?mp)?{
          ????//?開始播放
          ????mMediaPlayer.start();
          }
          到此,一般情況下視頻就能正常播放了,這里只關注調(diào)用流程。

          read_thread創(chuàng)建

          IjkMediaPlayer 的方法 prepareAsync 開始看,其調(diào)用流程如下:
          sequenceDiagram
          Activity->>IjkMediaPlayer.java:?prepareAsync
          IjkMediaPlayer.java->>ijkplayer_jni.c:?_prepareAsync
          ijkplayer_jni.c->>ijkplayer.c:?ijkmp_prepare_async
          ijkplayer.c->>ijkplayer.c:?ijkmp_prepare_async_l
          ijkplayer.c->>ff_ffplay.c:?ffp_prepare_async_l??
          ff_ffplay.c->>ff_ffplay.c:?stream_open
          可知 prepareAsync 最后調(diào)用的是函數(shù) stream_open ,其定義如下:
          static?VideoState?*stream_open(FFPlayer?*ffp,?const?char?*filename,?AVInputFormat?*iformat){
          ????av_log(NULL,?AV_LOG_INFO,?"stream_open\n");
          ????assert(!ffp->is);
          ????//?初始化VideoState及部分參數(shù)。
          ????VideoState?*is;
          ????is?=?av_mallocz(sizeof(VideoState));
          ????if?(!is)
          ????????return?NULL;
          ????is->filename?=?av_strdup(filename);
          ????if?(!is->filename)
          ????????goto?fail;
          ????//?這里iformat還沒被賦值,后面通過探測找到最佳的AVInputFormat
          ????is->iformat?=?iformat;
          ????is->ytop????=?0;
          ????is->xleft???=?0;
          #if?defined(__ANDROID__)
          ????if?(ffp->soundtouch_enable)?{
          ????????is->handle?=?ijk_soundtouch_create();
          ????}
          #endif

          ????/*?start?video?display?*/
          ????//?解碼后幀隊列初始化
          ????if?(frame_queue_init(&is->pictq,?&is->videoq,?ffp->pictq_size,?1)?<?0)
          ????????goto?fail;
          ????if?(frame_queue_init(&is->subpq,?&is->subtitleq,?SUBPICTURE_QUEUE_SIZE,?0)?<?0)
          ????????goto?fail;
          ????if?(frame_queue_init(&is->sampq,?&is->audioq,?SAMPLE_QUEUE_SIZE,?1)?<?0)
          ????????goto?fail;

          ????//?未解碼數(shù)據(jù)隊列初始化
          ????if?(packet_queue_init(&is->videoq)?<?0?||
          ????????packet_queue_init(&is->audioq)?<?0?||
          ????????packet_queue_init(&is->subtitleq)?<?0)
          ????????goto?fail;

          ????//?條件變量(信號量)初始化,包括讀線程、視頻seek、音頻seek相關信號量
          ????if?(!(is->continue_read_thread?=?SDL_CreateCond()))?{
          ????????av_log(NULL,?AV_LOG_FATAL,?"SDL_CreateCond():?%s\n",?SDL_GetError());
          ????????goto?fail;
          ????}

          ????if?(!(is->video_accurate_seek_cond?=?SDL_CreateCond()))?{
          ????????av_log(NULL,?AV_LOG_FATAL,?"SDL_CreateCond():?%s\n",?SDL_GetError());
          ????????ffp->enable_accurate_seek?=?0;
          ????}

          ????if?(!(is->audio_accurate_seek_cond?=?SDL_CreateCond()))?{
          ????????av_log(NULL,?AV_LOG_FATAL,?"SDL_CreateCond():?%s\n",?SDL_GetError());
          ????????ffp->enable_accurate_seek?=?0;
          ????}
          ????//?時鐘初始化
          ????init_clock(&is->vidclk,?&is->videoq.serial);
          ????init_clock(&is->audclk,?&is->audioq.serial);
          ????init_clock(&is->extclk,?&is->extclk.serial);
          ????is->audio_clock_serial?=?-1;
          ????//?初始化音量范圍
          ????if?(ffp->startup_volume?<?0)
          ????????av_log(NULL,?AV_LOG_WARNING,?"-volume=%d?<?0,?setting?to?0\n",?ffp->startup_volume);
          ????if?(ffp->startup_volume?>?100)
          ????????av_log(NULL,?AV_LOG_WARNING,?"-volume=%d?>?100,?setting?to?100\n",?ffp->startup_volume);
          ????ffp->startup_volume?=?av_clip(ffp->startup_volume,?0,?100);
          ????ffp->startup_volume?=?av_clip(SDL_MIX_MAXVOLUME?*?ffp->startup_volume?/?100,?0,?SDL_MIX_MAXVOLUME);
          ????is->audio_volume?=?ffp->startup_volume;
          ????is->muted?=?0;

          ????//?設置音視頻同步方式,默認AV_SYNC_AUDIO_MASTER
          ????is->av_sync_type?=?ffp->av_sync_type;

          ????//?播放互斥鎖
          ????is->play_mutex?=?SDL_CreateMutex();
          ????//?精準seek互斥鎖
          ????is->accurate_seek_mutex?=?SDL_CreateMutex();

          ????ffp->is?=?is;
          ????is->pause_req?=?!ffp->start_on_prepared;

          ????//?視頻渲染線程
          ????is->video_refresh_tid?=?SDL_CreateThreadEx(&is->_video_refresh_tid,?video_refresh_thread,?ffp,?"ff_vout");
          ????if?(!is->video_refresh_tid)?{
          ????????av_freep(&ffp->is);
          ????????return?NULL;
          ????}

          ????is->initialized_decoder?=?0;
          ????//?讀取線程
          ????is->read_tid?=?SDL_CreateThreadEx(&is->_read_tid,?read_thread,?ffp,?"ff_read");
          ????if?(!is->read_tid)?{
          ????????av_log(NULL,?AV_LOG_FATAL,?"SDL_CreateThread():?%s\n",?SDL_GetError());
          ????????goto?fail;
          ????}

          ????//?異步初始化解碼器,和硬解相關,默認未開啟
          ????if?(ffp->async_init_decoder?&&?!ffp->video_disable?&&?ffp->video_mime_type?&&?strlen(ffp->video_mime_type)?>?0
          ????????????????????&&?ffp->mediacodec_default_name?&&?strlen(ffp->mediacodec_default_name)?>?0)?{
          ????????//?mediacodec
          ????????if?(ffp->mediacodec_all_videos?||?ffp->mediacodec_avc?||?ffp->mediacodec_hevc?||?ffp->mediacodec_mpeg2)?{
          ????????????decoder_init(&is->viddec,?NULL,?&is->videoq,?is->continue_read_thread);
          ????????????ffp->node_vdec?=?ffpipeline_init_video_decoder(ffp->pipeline,?ffp);
          ????????}
          ????}
          ????//?允許初始化解碼器
          ????is->initialized_decoder?=?1;

          ????return?is;
          fail:
          ????is->initialized_decoder?=?1;
          ????is->abort_request?=?true;
          ????if?(is->video_refresh_tid)
          ????????SDL_WaitThread(is->video_refresh_tid,?NULL);
          ????stream_close(ffp);
          ????return?NULL;
          }
          可知函數(shù) stream_open 主要做了如下幾件事:
          1. 初始化 VideoState 及部分參數(shù)。

          2. 初始化幀隊列,包括初始化已解碼的視頻幀隊列 pictq、音頻幀隊列 sampq 和字幕幀隊列 subpq和未解碼的視頻數(shù)據(jù)隊列 videoq、音頻數(shù)據(jù)隊列 audioq 和字幕數(shù)據(jù)隊列 subtitleq。

          3. 音視頻同步方式及時鐘初始化,默認 AV_SYNC_AUDIO_MASTER,也就是音頻時鐘作為主時鐘。

          4. 音量初始化范圍。

          5. 創(chuàng)建了線程名為 ff_vout 的視頻渲染線程 video_refresh_thread

          6. 創(chuàng)建了線程名為 ff_read 的視頻渲染線程 read_thread。

          到此開始本文主題數(shù)據(jù)讀取線程 read_thread 函數(shù)的分析, 函數(shù) read_thread 關鍵部分簡化如下:
          static?int?read_thread(void?*arg){
          ????//?...

          ????//?1.?創(chuàng)建AVFormatContext,指定打開流、關閉流的默認函數(shù)等
          ????ic?=?avformat_alloc_context();
          ????if?(!ic)?{
          ????????av_log(NULL,?AV_LOG_FATAL,?"Could?not?allocate?context.\n");
          ????????ret?=?AVERROR(ENOMEM);
          ????????goto?fail;
          ????}
          ????//?...

          ????//?2.?打開碼流獲取header信息
          ????err?=?avformat_open_input(&ic,?is->filename,?is->iformat,?&ffp->format_opts);
          ????if?(err?<?0)?{
          ????????print_error(is->filename,?err);
          ????????ret?=?-1;
          ????????goto?fail;
          ????}
          ????ffp_notify_msg1(ffp,?FFP_MSG_OPEN_INPUT);
          ????//?...

          ????//?3.?獲取碼流信息
          ????if?(ffp->find_stream_info)?{
          ????????err?=?avformat_find_stream_info(ic,?opts);
          ????}?
          ????ffp_notify_msg1(ffp,?FFP_MSG_FIND_STREAM_INFO);
          ????//?...

          ????//?4.?如果有指定播放起始時間則seek到該播放位置
          ????if?(ffp->start_time?!=?AV_NOPTS_VALUE)?{
          ????????int64_t?timestamp;
          ????????timestamp?=?ffp->start_time;
          ????????if?(ic->start_time?!=?AV_NOPTS_VALUE)
          ????????????timestamp?+=?ic->start_time;
          ????????ret?=?avformat_seek_file(ic,?-1,?INT64_MIN,?timestamp,?INT64_MAX,?0);
          ????}
          ????//?...

          ????//?5.?打印格式信息
          ????av_dump_format(ic,?0,?is->filename,?0);
          }
          下面內(nèi)容僅限于 read_thread 函數(shù)的主要流程。

          avformat_alloc_context

          avformat_alloc_context 函數(shù)主要是為 AVFormatContext 分配內(nèi)存、初始化 ic->internal 部分參數(shù),如下:
          AVFormatContext?*avformat_alloc_context(void){
          ????//?為AVFormatContext分配內(nèi)存
          ????AVFormatContext?*ic;
          ????ic?=?av_malloc(sizeof(AVFormatContext));
          ????if?(!ic)?return?ic;
          ????//?初始化打開流、關閉流的默認函數(shù)
          ????avformat_get_context_defaults(ic);
          ????//?為
          ????ic->internal?=?av_mallocz(sizeof(*ic->internal));
          ????if?(!ic->internal)?{
          ????????avformat_free_context(ic);
          ????????return?NULL;
          ????}
          ????ic->internal->offset?=?AV_NOPTS_VALUE;
          ????ic->internal->raw_packet_buffer_remaining_size?=?RAW_PACKET_BUFFER_SIZE;
          ????ic->internal->shortest_end?=?AV_NOPTS_VALUE;
          ????return?ic;
          }
          繼續(xù)查看 avformat_get_context_defaults 函數(shù)如下:
          static?void?avformat_get_context_defaults(AVFormatContext?*s){
          ????memset(s,?0,?sizeof(AVFormatContext));
          ????s->av_class?=?&av_format_context_class;
          ????s->io_open??=?io_open_default;
          ????s->io_close?=?io_close_default;
          ????av_opt_set_defaults(s);
          }
          這里指定了打開流、關閉流的默認函數(shù)分別為 io_open_defaultio_close_default,這里暫不關注后續(xù)流程。

          avformat_open_input

          avformat_open_input 函數(shù)主要用于打開碼流獲取 header 信息,其定義簡化如下:
          int?avformat_open_input(AVFormatContext?**ps,?const?char?*filename,
          ????????????????????????AVInputFormat?*fmt,?AVDictionary?**options)
          {
          ????//?...

          ????//?打開碼流探測碼流輸入格式,返回最佳解復用器的得分
          ????av_log(NULL,?AV_LOG_FATAL,?"avformat_open_input?>?init_input?before?>?nb_streams:%d\n",s->nb_streams);
          ????if?((ret?=?init_input(s,?filename,?&tmp))?<?0)
          ????????goto?fail;
          ????s->probe_score?=?ret;

          ????//?協(xié)議黑白名單檢查、碼流格式白名單檢查等
          ????//?...

          ????//?讀取媒體頭部
          ????//?read_header主要是做某種格式的初始化工作,如填充自己的私有結構
          ????//?根據(jù)流的數(shù)量分配流結構并初始化,把文件指針指向數(shù)據(jù)區(qū)開始處等
          ????//?創(chuàng)建好AVStream,并等待在后續(xù)的流程中,可以取出或?qū)懭胍粢曨l流信息
          ????if?(!(s->flags&AVFMT_FLAG_PRIV_OPT)?&&?s->iformat->read_header)
          ????????if?((ret?=?s->iformat->read_header(s))?<?0)
          ????????????goto?fail;
          ????//?...

          ????//?處理音視頻中附加的圖片,比如專輯里面的圖片
          ????if?((ret?=?avformat_queue_attached_pictures(s))?<?0)
          ????????goto?fail;
          ????//?...

          ????//?更新AVStream解碼器相關信息到AVCodecContext
          ????update_stream_avctx(s);
          ????//?...
          }
          可知 avformat_open_input 函數(shù)主要是打開碼流探測碼流輸入格式、協(xié)議黑白名單和碼流格式白名單檢查、讀取文件 header 信息等,,最后使用函數(shù) update_stream_avctx 更新 AVStream 解碼器相關信息到對應的 AVCodecContext 中,這個操作會在后續(xù)流程中經(jīng)常看到。最重要的是打開碼流探測碼流輸入格式和讀取文件 header 信息,分別調(diào)用了函數(shù) init_inputread_header 函數(shù),read_header 會在讀取 header 信息過程中完成 AVStream 的初始化。init_input 函數(shù)主要是探測碼流格式并返回該碼流格式的得分,最終找到對應該碼流格式的最佳 AVInputFormat,這個結構體是初始化時注冊的解復用器,每個解復用器都對應一個 AVInputFormat 對象,同理復用器對應的是 AVOutputFormat 這里暫且了解一下。init_input 函數(shù)如果執(zhí)行成功,則對應的碼流格式已經(jīng)確定,此時就可以調(diào)用 read_header 函數(shù)了,其對應的是當前碼流格式 AVInputFormat 對應的解復用器 demuxer 中的 xxx_read_header 函數(shù),如果是 hls 格式的碼流,則對應的則是 hls_read_header,其定義如下:
          AVInputFormat?ff_hls_demuxer?=?{
          ????.name???????????=?"hls,applehttp",
          ????.read_header???=?hls_read_header,
          ????//?...
          };

          //?hls_read_header
          static?int?hls_read_header(AVFormatContext?*s,?AVDictionary?**options){
          ????//?...
          }

          avformat_find_stream_info

          avformat_find_stream_info 函數(shù)主要用來獲取碼流信息,用來探測沒有 header 的文件格式比較有用,可以通過該函數(shù)獲取視頻寬高、總時長、碼率、幀率、像素格式等等,其定義簡化如下:
          int?avformat_find_stream_info(AVFormatContext?*ic,?AVDictionary?**options){
          ????//?...

          ????//?1.?遍歷流
          ????for?(i?=?0;?i?<?ic->nb_streams;?i++)?{
          ????????//?初始化流中的解析器,具體是AVCodecParserContext和AVCodecParser初始化
          ????????st->parser?=?av_parser_init(st->codecpar->codec_id);
          ????????//?...

          ????????//?根據(jù)AVStream中的解碼器參數(shù)探測對應的解碼器并返回
          ????????codec?=?find_probe_decoder(ic,?st,?st->codecpar->codec_id);
          ????????//?...

          ????????//?如果解碼器參數(shù)不全,根據(jù)指定的AVCodec初始化AVCodecContext以及調(diào)用解碼器的init函數(shù)以初始化解碼器
          ????????if?(!has_codec_parameters(st,?NULL)?&&?st->request_probe?<=?0)?{
          ????????????if?(codec?&&?!avctx->codec)
          ????????????????if?(avcodec_open2(avctx,?codec,?options???&options[i]?:&thread_opt)?<?0)
          ????????????????????av_log(ic,?AV_LOG_WARNING,
          ???????????????????????????"Failed?to?open?codec?in?%s\n",__FUNCTION__);
          ????????}
          ????}

          ????//?2.?死循環(huán)獲取碼流信息
          ????for?(;;)?{
          ????????//?...
          ????????//?檢查是否有中斷請求,如果有則調(diào)用中斷函數(shù)
          ????????if?(ff_check_interrupt(&ic->interrupt_callback))?{
          ????????????break;
          ????????}

          ????????//?遍歷流,檢查是否還需要處理解碼器相關參數(shù)
          ????????for?(i?=?0;?i?<?ic->nb_streams;?i++)?{
          ????????????int?fps_analyze_framecount?=?20;
          ????????????st?=?ic->streams[i];
          ????????????//?檢查流中的的解碼器參數(shù),如果完整則break,反之則繼續(xù)執(zhí)行,以便進一步分析
          ????????????if?(!has_codec_parameters(st,?NULL))
          ????????????????break;
          ????????????//?...
          ????????}

          ????????if?(i?==?ic->nb_streams)?{
          ????????????//?標識所有的流分析結束
          ????????????analyzed_all_streams?=?1;
          ????????????//?如果當前AVFormatContext設置ctx_flags為AVFMTCTX_NOHEADER則表明當前碼流是沒有header信息的
          ????????????//?此時需要讀取一些數(shù)據(jù)包來獲取流信息,反之則直接break出去,死循環(huán)正常結束
          ????????????if?(!(ic->ctx_flags?&?AVFMTCTX_NOHEADER))?{
          ????????????????/*?If?we?found?the?info?for?all?the?codecs,?we?can?stop.?*/
          ????????????????ret?=?count;
          ????????????????av_log(ic,?AV_LOG_DEBUG,?"All?info?found\n");
          ????????????????flush_codecs?=?0;
          ????????????????break;
          ????????????}
          ????????}

          ????????//?讀取的數(shù)據(jù)已經(jīng)多余允許探測的數(shù)據(jù)大小,但是還未得到所有的編解碼器信息
          ????????if?(read_size?>=?probesize)?{
          ????????????break;
          ????????}

          ????????//?以下針對當前碼流是沒有header信息的處理

          ????????//?讀取一幀壓縮編碼數(shù)據(jù)
          ????????ret?=?read_frame_internal(ic,?&pkt1);
          ????????if?(ret?==?AVERROR(EAGAIN))?continue;
          ????????if?(ret?<?0)?{
          ????????????/*?EOF?or?error*/
          ????????????eof_reached?=?1;
          ????????????break;
          ????????}

          ????????//?讀取的數(shù)據(jù)添加到緩存中,后續(xù)會先從緩存中讀這些數(shù)據(jù)
          ????????ret?=?add_to_pktbuf(&ic->internal->packet_buffer,?pkt,
          ????????????????????????????????&ic->internal->packet_buffer_end,?0);
          ????????//?嘗試解碼一些壓縮編碼數(shù)據(jù)???????????????????????
          ????????try_decode_frame(ic,?st,?pkt,(options?&&?i?<?orig_nb_streams)???&options[i]?:?NULL);

          ????????//?...
          ????}

          ????//?3.?讀取數(shù)據(jù)到流的末尾處理
          ????if?(eof_reached)?{
          ?????????for?(stream_index?=?0;?stream_index?<?ic->nb_streams;?stream_index++)?{
          ?????????????if?(!has_codec_parameters(st,?NULL))?{
          ????????????????const?AVCodec?*codec?=?find_probe_decoder(ic,?st,?st->codecpar->codec_id);
          ????????????????if?(avcodec_open2(avctx,?codec,?(options?&&?stream_index?<?orig_nb_streams)???&options[stream_index]?:?&opts)?<?0)
          ????????????????????????av_log(ic,?AV_LOG_WARNING,
          ?????????}
          ????}
          ????//?4.?解碼器執(zhí)行flushing操作,避免緩存的數(shù)據(jù)未被取出
          ????if?(flush_codecs)?{
          ????????AVPacket?empty_pkt?=?{?0?};
          ????????int?err?=?0;
          ????????av_init_packet(&empty_pkt);
          ????????for?(i?=?0;?i?<?ic->nb_streams;?i++)?{
          ????????????st?=?ic->streams[i];
          ????????????/*?flush?the?decoders?*/
          ????????????if?(st->info->found_decoder?==?1)?{
          ????????????????do?{
          ????????????????????//?
          ????????????????????err?=?try_decode_frame(ic,?st,?&empty_pkt,
          ????????????????????????????????????????????(options?&&?i?<?orig_nb_streams)
          ??????????????????????????????????????????????&options[i]?:?NULL);
          ????????????????}?while?(err?>?0?&&?!has_codec_parameters(st,?NULL));

          ????????????????if?(err?<?0)?{
          ????????????????????av_log(ic,?AV_LOG_INFO,
          ????????????????????????"decoding?for?stream?%d?failed\n",?st->index);
          ????????????????}
          ????????????}
          ????????}
          ????}

          ????//?5.?后續(xù)就是一些碼流信息的計算,比如pix_fmt、橫縱比SAR、實際幀率、平均幀率等等
          ????//?...

          ????//?6.?從流的內(nèi)部AVCodecContext(avctx)更新流對應的解碼器參數(shù)AVCodecParameters
          ????for?(i?=?0;?i?<?ic->nb_streams;?i++)?{
          ????????ret?=?avcodec_parameters_from_context(st->codecpar,?st->internal->avctx);
          ????????//?...
          ????}

          ????//?...
          }
          由于 avformat_find_stream_info 函數(shù)代碼量比較多,上述代碼省略了大部分細節(jié),保留了比較關鍵的部分,這里只看主要流程,從源碼可知,該函數(shù)中會頻繁的使用 has_codec_parameters 函數(shù)來檢查流內(nèi)部解碼器上下問參數(shù)是否合理,如果不合理則要盡可能的采取措施保證流內(nèi)部解碼器上下文參數(shù)合理,當 ret = 0; 標識 avformat_find_stream_info 執(zhí)行成功,其主要流程如下:
          1. 遍歷流,根據(jù)流中的一些參數(shù)初始化 AVCodecParserAVCodecParserContext,通過函數(shù) find_probe_decoder 來探測解碼器,使用函數(shù) avcodec_open2 來初始化 AVCodecContext 并調(diào)用解碼器的 init 函數(shù)來初始化解碼器靜態(tài)數(shù)據(jù)。

          2. for (;;) 死循環(huán)主要是使用 ff_check_interrupt 函數(shù)進行中斷檢測、遍歷流使用 has_codec_parameters 函數(shù)檢測碼流內(nèi)部的解碼器上下文參數(shù)是否合理及數(shù)據(jù)讀取的,如果合理且當前碼流有 header 信息,則標識 analyzed_all_streams = 1;flush_codecs = 0; 直接 break 退出該死循環(huán),如果當前碼流有 header 信息,也就是 ic-&gt;ctx_flags 被設置為 AVFMTCTX_NOHEADER 的時候,此時需要調(diào)用 read_frame_internal 函數(shù)讀取一幀編解碼數(shù)據(jù),并將其添加到緩存中,調(diào)用 try_decode_frame 函數(shù)解碼一幀數(shù)據(jù)進一步填充流中的 AVCodecContext。

          3. eof_reached = 1 表示前面死循環(huán)使用 read_frame_internal 函數(shù)讀取到流的末尾了,遍歷流,再次使用 has_codec_parameters 函數(shù)來檢查流內(nèi)部解碼器上下文參數(shù)是否合理,不合理則再重復上面 2 中的步驟來進行解碼器上下文相關參數(shù)的初始化。

          4. 解碼的過程就是一個不斷放數(shù)據(jù)和取數(shù)據(jù)的過程,分別對應的是 avcodec_send_packetavcodec_receive_frame 函數(shù),為了避免解碼器數(shù)據(jù)殘留,這里通過空的 AVPacket 來 flushing 解碼器,執(zhí)行的條件是 flush_codecs = 1 的時候,也就是需要需要上面 2 中調(diào)用了 try_decode_frame 執(zhí)行了解碼操作。

          5. 后續(xù)就是一些碼流信息的計算,比如pix_fmt、橫縱比SAR、實際幀率、平均幀率等等。

          6. 遍歷流,調(diào)用 avcodec_parameters_from_context 函數(shù)將之前填充的流的內(nèi)部 AVCodecContext 中的解碼器參數(shù)填充到流的解碼器參數(shù) st-&gt;codecpar中,對應結構體 AVCodecParameters,到此 avformat_find_stream_info 函數(shù)主要流程分析完畢。

          avformat_seek_file

          avformat_seek_file 主要用來執(zhí)行 seek 操作的,其定義簡化如下:
          int?avformat_seek_file(AVFormatContext?*s,?int?stream_index,?int64_t?min_ts,
          ???????????????????????int64_t?ts,?int64_t?max_ts,?int?flags)
          {
          ????//?...???????????????????

          ????//?優(yōu)先使用read_seek2
          ????if?(s->iformat->read_seek2)?{
          ????????int?ret;
          ????????ff_read_frame_flush(s);
          ????????ret?=?s->iformat->read_seek2(s,?stream_index,?min_ts,?ts,?max_ts,?flags);
          ????????if?(ret?>=?0)
          ????????????ret?=?avformat_queue_attached_pictures(s);
          ????????return?ret;
          ????}
          ????//?...

          ????//?如果不支持read_seek2則嘗試使用舊版API?seek
          ????if?(s->iformat->read_seek?||?1)?{
          ????????//?...
          ????????int?ret?=?av_seek_frame(s,?stream_index,?ts,?flags?|?dir);
          ????????return?ret;
          ????}
          ????return?-1;?//unreachable???????????????????????????
          }
          可知 avformat_seek_file 函數(shù)執(zhí)行時。如果當前解復用器(AVInputFormat)支持 read_seek2,則使用對應的 read_seek2 函數(shù),否則調(diào)用舊版 API 里面的 av_seek_frame 函數(shù)進行 seek,av_seek_frame 函數(shù)如下:
          int?av_seek_frame(AVFormatContext?*s,?int?stream_index,int64_t?timestamp,?int?flags){
          ????int?ret;
          ????if?(s->iformat->read_seek2?&&?!s->iformat->read_seek)?{
          ????????//?...
          ????????return?avformat_seek_file(s,?stream_index,?min_ts,?timestamp,?max_ts,
          ??????????????????????????????????flags?&?~AVSEEK_FLAG_BACKWARD);
          ????}

          ????ret?=?seek_frame_internal(s,?stream_index,?timestamp,?flags);

          ????//?...
          ????return?ret;
          }
          可知如果當前 AVInputFormat 支持 read_seek2 且不支持 read_seek 則使用 avformat_seek_file 函數(shù)也就是 read_seek2 函數(shù)進行 seek,如果支持 read_seek 則優(yōu)先調(diào)用內(nèi)部 seek 函數(shù) seek_frame_internal 進行 seek,seek_frame_internal 函數(shù)主要提供尋幀的幾種方式:
          1. seek_frame_byte:按照字節(jié)方式尋幀

          2. read_seek:按照當前指定格式的方式尋幀,具體由該格式對應的解復用器提供支持。

          3. ff_seek_frame_binary:按照二分查找的方式尋幀。

          4. seek_frame_generic:按照通用方式尋幀。

          這也是 seek 操作的邏輯,比如 hls 格式的解復用器就不支持 read_seek2,僅支持 read_seek, ff_hls_demuxer 定義如下:
          AVInputFormat?ff_hls_demuxer?=?{
          ????//?...
          ????.read_seek??????=?hls_read_seek,
          };

          av_dump_format

          av_dump_format 函數(shù)用來根據(jù)當前 AVFormatContext 來打印碼流輸入格式的詳細信息,直接看 IjkPlayer 正常播放視頻打印信息如下:
          IJKMEDIA:?Input?#0,?hls,applehttp,?from?'http://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8':
          IJKMEDIA:???Duration:
          IJKMEDIA:?00:30:00.00
          IJKMEDIA:?,?start:
          IJKMEDIA:?19.888800
          IJKMEDIA:?,?bitrate:
          IJKMEDIA:?0?kb/s
          IJKMEDIA:
          IJKMEDIA:???Program?0
          IJKMEDIA:?????Metadata:
          IJKMEDIA:???????variant_bitrate?:
          IJKMEDIA:?0
          IJKMEDIA:
          IJKMEDIA:?????Stream?#0:0
          IJKMEDIA:?,?23,?1/90000
          IJKMEDIA:?:?Video:?h264,?1?reference?frame?([27][0][0][0]?/?0x001B),?yuv420p(tv,?smpte170m/smpte170m/bt709,?topleft),?400x300?(400x304),?0/1
          IJKMEDIA:?,
          IJKMEDIA:?29.92?tbr,
          IJKMEDIA:?90k?tbn,
          IJKMEDIA:?180k?tbc
          IJKMEDIA:
          IJKMEDIA:?????Metadata:
          IJKMEDIA:???????variant_bitrate?:
          IJKMEDIA:?FFP_MSG_FIND_STREAM_INFO:
          IJKMEDIA:?0
          IJKMEDIA:
          IJKMEDIA:?????Stream?#0:1
          IJKMEDIA:?,?9,?1/90000
          IJKMEDIA:?:?Audio:?aac?([15][0][0][0]?/?0x000F),?22050?Hz,?stereo,?fltp
          IJKMEDIA:
          IJKMEDIA:?????Metadata:
          IJKMEDIA:???????variant_bitrate?:
          IJKMEDIA:?0

          av_find_best_stream

          av_find_best_stream 函數(shù)主要用來選擇最合適的音視頻流,其定義簡化如下:
          int?av_find_best_stream(AVFormatContext?*ic,?enum?AVMediaType?type,
          ????????????????????????int?wanted_stream_nb,?int?related_stream,
          ????????????????????????AVCodec?**decoder_ret,?int?flags)
          {
          ????//?...

          ????//?遍歷選擇合適的音視頻流
          ????for?(i?=?0;?i?<?nb_streams;?i++)?{
          ????????int?real_stream_index?=?program???program[i]?:?i;
          ????????AVStream?*st??????????=?ic->streams[real_stream_index];
          ????????AVCodecParameters?*par?=?st->codecpar;
          ????????if?(par->codec_type?!=?type)
          ????????????continue;
          ????????if?(wanted_stream_nb?>=?0?&&?real_stream_index?!=?wanted_stream_nb)
          ????????????continue;
          ????????if?(type?==?AVMEDIA_TYPE_AUDIO?&&?!(par->channels?&&?par->sample_rate))
          ????????????continue;
          ????????if?(decoder_ret)?{
          ????????????decoder?=?find_decoder(ic,?st,?par->codec_id);
          ????????????if?(!decoder)?{
          ????????????????if?(ret?<?0)
          ????????????????????ret?=?AVERROR_DECODER_NOT_FOUND;
          ????????????????continue;
          ????????????}
          ????????}
          ????????disposition?=?!(st->disposition?&?(AV_DISPOSITION_HEARING_IMPAIRED?|?AV_DISPOSITION_VISUAL_IMPAIRED));
          ????????count?=?st->codec_info_nb_frames;
          ????????bitrate?=?par->bit_rate;
          ????????multiframe?=?FFMIN(5,?count);
          ????????if?((best_disposition?>??disposition)?||
          ????????????(best_disposition?==?disposition?&&?best_multiframe?>??multiframe)?||
          ????????????(best_disposition?==?disposition?&&?best_multiframe?==?multiframe?&&?best_bitrate?>??bitrate)?||
          ????????????(best_disposition?==?disposition?&&?best_multiframe?==?multiframe?&&?best_bitrate?==?bitrate?&&?best_count?>=?count))
          ????????????continue;
          ????????best_disposition?=?disposition;
          ????????best_count???=?count;
          ????????best_bitrate?=?bitrate;
          ????????best_multiframe?=?multiframe;
          ????????ret??????????=?real_stream_index;
          ????????best_decoder?=?decoder;
          ????????//?...
          ????}
          ????//?...
          ????return?ret;
          }?
          可知 av_find_best_stream 函數(shù)主要從三個維度進行選擇,比較順序依次是 dispositionmultiframebitrate,在 disposition 相同的時候選擇已解碼幀數(shù)多的,對應 multiframe,最后選擇比特率高的,對應 bitrate。disposition 對應的是 AVStreamdisposition 成員,具體值是 AV_DISPOSITION_ 標識符,比如上面的 AV_DISPOSITION_HEARING_IMPAIRED 表示該流是面向聽障人群的,這個暫時了解一下。對應 read_thread 函數(shù)中 av_find_best_stream 找到了最佳的音頻、視頻、字幕流,接下來就是解碼播放了。

          stream_component_open

          stream_component_open 函數(shù)主要是創(chuàng)建音頻渲染線程,音頻、視頻、字幕解碼線程以及初始化 VideoState,其定義簡化如下:
          static?int?stream_component_open(FFPlayer?*ffp,?int?stream_index){
          ????//?...
          ????//?1.?初始化AVCodecContext
          ????avctx?=?avcodec_alloc_context3(NULL);
          ????if?(!avctx)
          ????????return?AVERROR(ENOMEM);

          ????//?2.?使用流的解碼器參數(shù)更新當前AVCodecContext對應參數(shù)
          ????ret?=?avcodec_parameters_to_context(avctx,?ic->streams[stream_index]->codecpar);
          ????if?(ret?<?0)
          ????????goto?fail;
          ????av_codec_set_pkt_timebase(avctx,?ic->streams[stream_index]->time_base);

          ????//?3.?根據(jù)解碼器ID查找解碼器
          ????codec?=?avcodec_find_decoder(avctx->codec_id);

          ????//?...

          ????//?4.?如果已經(jīng)指定了解碼器名稱則使用解碼器的名字再查找一次解碼器
          ????if?(forced_codec_name)
          ????????codec?=?avcodec_find_decoder_by_name(forced_codec_name);
          ????if?(!codec)?{
          ????????if?(forced_codec_name)?av_log(NULL,?AV_LOG_WARNING,
          ??????????????????????????????????????"No?codec?could?be?found?with?name?'%s'\n",?forced_codec_name);
          ????????else???????????????????av_log(NULL,?AV_LOG_WARNING,
          ??????????????????????????????????????"No?codec?could?be?found?with?id?%d\n",?avctx->codec_id);
          ????????ret?=?AVERROR(EINVAL);
          ????????goto?fail;
          ????}

          ????//?...

          ????//?5.?音頻渲染線程創(chuàng)建,音視、視頻、字幕解碼器初始化,音視、視頻、字幕開始解碼
          ????switch?(avctx->codec_type)?{
          ????case?AVMEDIA_TYPE_AUDIO:
          ????????//?...
          ????????//?打開音頻輸出,創(chuàng)建音頻輸出線程ff_aout_android,對應的音頻線程函數(shù)aout_thread,
          ????????//?最終調(diào)用AudioTrack的write方法寫音頻數(shù)據(jù)
          ????????//?...
          ????????//?音頻解碼器初始化
          ????????decoder_init(&is->auddec,?avctx,?&is->audioq,?is->continue_read_thread);
          ????????if?((is->ic->iformat->flags?&?(AVFMT_NOBINSEARCH?|?AVFMT_NOGENSEARCH?|?AVFMT_NO_BYTE_SEEK))?&&?!is->ic->iformat->read_seek)?{
          ????????????is->auddec.start_pts?=?is->audio_st->start_time;
          ????????????is->auddec.start_pts_tb?=?is->audio_st->time_base;
          ????????}
          ????????//?開始音頻解碼,這里創(chuàng)建音頻解碼線程?ff_audio_dec,對應的音頻解碼線程函數(shù)為audio_thread
          ????????if?((ret?=?decoder_start(&is->auddec,?audio_thread,?ffp,?"ff_audio_dec"))?<?0)
          ????????????goto?out;
          ????????SDL_AoutPauseAudio(ffp->aout,?0);
          ????????break;
          ????case?AVMEDIA_TYPE_VIDEO:
          ????????is->video_stream?=?stream_index;
          ????????is->video_st?=?ic->streams[stream_index];
          ????????//?異步初始化解碼器,和使用MediaCodec有關
          ????????if?(ffp->async_init_decoder)?{
          ????????????//?...
          ????????}?else?{
          ????????????//?視頻解碼器初始化
          ????????????decoder_init(&is->viddec,?avctx,?&is->videoq,?is->continue_read_thread);
          ????????????ffp->node_vdec?=?ffpipeline_open_video_decoder(ffp->pipeline,?ffp);
          ????????????if?(!ffp->node_vdec)
          ????????????????goto?fail;
          ????????}
          ????????????//?開始視頻解碼,這里創(chuàng)建音頻解碼線程?ff_video_dec,對應的音頻解碼線程函數(shù)為video_thread
          ????????if?((ret?=?decoder_start(&is->viddec,?video_thread,?ffp,?"ff_video_dec"))?<?0)
          ????????????goto?out;

          ????????//?...

          ????????break;
          ????case?AVMEDIA_TYPE_SUBTITLE:
          ????????//?...
          ????????//?字幕解碼器初始化
          ????????decoder_init(&is->subdec,?avctx,?&is->subtitleq,?is->continue_read_thread);
          ????????//?開始視頻解碼,這里創(chuàng)建音頻解碼線程?ff_subtitle_dec,對應的音頻解碼線程函數(shù)為subtitle_thread
          ????????if?((ret?=?decoder_start(&is->subdec,?subtitle_thread,?ffp,?"ff_subtitle_dec"))?<?0)
          ????????????goto?out;
          ????????break;
          ????default:
          ????????break;
          ????}
          ????goto?out;

          fail:
          ????avcodec_free_context(&avctx);
          out:
          ????av_dict_free(&opts);

          ????return?ret;
          }
          可知 stream_component_open 函數(shù)已經(jīng)來是了創(chuàng)建了對應的解碼線程了,上述代碼注釋比較詳細,這里不再贅述,在對應 read_thread 中,該函數(shù)之后填充了 IjkMediaMeta 的一些數(shù)據(jù),此時 ffp->prepared = true; 并向應用層發(fā)送播放準備完成的事件消息 FFP_MSG_PREPARED,最終回調(diào)給 OnPreparedListener 中。

          read_thread主循環(huán)

          這里的主循環(huán)是指 read_thread 中讀取數(shù)據(jù)的主循環(huán),關鍵流程如下:
          for?(;;)?{
          ????//?1.?流關閉或者應用層release的時候is->abort_request為1
          ????if?(is->abort_request)
          ????????break;
          ????//?...
          ????//?2.?處理seek操作
          ????if?(is->seek_req)?{
          ????????//?...
          ????????is->seek_req?=?0;
          ????????ffp_notify_msg3(ffp,?FFP_MSG_SEEK_COMPLETE,?(int)fftime_to_milliseconds(seek_target),?ret);
          ????????ffp_toggle_buffering(ffp,?1);
          ????}
          ????//?3.?處理attached_pic
          ????//?如果?個流中含有AV_DISPOSITION_ATTACHED_PIC說明這個流是*.mp3等??件中的?個Video?Stream
          ????//?該流只有?個AVPacket就是attached_pic
          ????if?(is->queue_attachments_req)?{
          ????????if?(is->video_st?&&?(is->video_st->disposition?&?AV_DISPOSITION_ATTACHED_PIC))?{
          ????????????AVPacket?copy?=?{?0?};
          ????????????if?((ret?=?av_packet_ref(&copy,?&is->video_st->attached_pic))?<?0)
          ????????????????goto?fail;
          ????????????packet_queue_put(&is->videoq,?&copy);
          ????????????packet_queue_put_nullpacket(&is->videoq,?is->video_stream);
          ????????}
          ????????is->queue_attachments_req?=?0;
          ????}
          ????//?4.?如果隊列滿了,暫時不能讀取更多數(shù)據(jù)
          ????//?如果是網(wǎng)絡流ffp->infinite_buffer為1
          ????/*?if?the?queue?are?full,?no?need?to?read?more?*/
          ????if?(ffp->infinite_buffer<1?&&?!is->seek_req?&&
          ????????//?...
          ????????SDL_LockMutex(wait_mutex);
          ????????//?等待10ms讓解碼線程有時間消耗
          ????????SDL_CondWaitTimeout(is->continue_read_thread,?wait_mutex,?10);
          ????????SDL_UnlockMutex(wait_mutex);
          ????????continue;
          ????}
          ????//?5.?檢查碼流是否播放完成
          ????if?((!is->paused?||?completed)?&&
          ????????(!is->audio_st?||?(is->auddec.finished?==?is->audioq.serial?&&?frame_queue_nb_remaining(&is->sampq)?==?0))?&&
          ????????(!is->video_st?||?(is->viddec.finished?==?is->videoq.serial?&&?frame_queue_nb_remaining(&is->pictq)?==?0)))?{
          ????????//?是否設置循環(huán)播放
          ????????if?(ffp->loop?!=?1?&&?(!ffp->loop?||?--ffp->loop))?{
          ????????????stream_seek(is,?ffp->start_time?!=?AV_NOPTS_VALUE???ffp->start_time?:?0,?0,?0);
          ????????}?else?if?(ffp->autoexit)?{//?是否自動退出
          ????????????ret?=?AVERROR_EOF;
          ????????????goto?fail;
          ????????}?else?{
          ????????????//?...

          ????????????//?播放出錯...
          ????????????ffp_notify_msg1(ffp,?FFP_MSG_ERROR);

          ????????????//?播放完成...
          ????????????ffp_notify_msg1(ffp,?FFP_MSG_COMPLETED);
          ????????}
          ????}
          ????pkt->flags?=?0;
          ????//?6.?讀取數(shù)據(jù)包
          ????ret?=?av_read_frame(ic,?pkt);
          ????//?7.?檢測數(shù)據(jù)讀取情況
          ????if?(ret?<?0)?{
          ????????//?...

          ????????//?讀取到末尾處理...
          ????????if?(pb_eof)?{
          ????????????if?(is->video_stream?>=?0)
          ????????????????packet_queue_put_nullpacket(&is->videoq,?is->video_stream);
          ????????????if?(is->audio_stream?>=?0)
          ????????????????packet_queue_put_nullpacket(&is->audioq,?is->audio_stream);
          ????????????if?(is->subtitle_stream?>=?0)
          ????????????????packet_queue_put_nullpacket(&is->subtitleq,?is->subtitle_stream);
          ????????????is->eof?=?1;
          ????????}

          ????????//?數(shù)據(jù)讀取處理...
          ????????if?(pb_error)?{
          ????????????if?(is->video_stream?>=?0)
          ????????????????packet_queue_put_nullpacket(&is->videoq,?is->video_stream);
          ????????????if?(is->audio_stream?>=?0)
          ????????????????packet_queue_put_nullpacket(&is->audioq,?is->audio_stream);
          ????????????if?(is->subtitle_stream?>=?0)
          ????????????????packet_queue_put_nullpacket(&is->subtitleq,?is->subtitle_stream);
          ????????????is->eof?=?1;
          ????????????ffp->error?=?pb_error;
          ????????????av_log(ffp,?AV_LOG_ERROR,?"av_read_frame?error:?%s\n",?ffp_get_error_string(ffp->error));
          ????????????//?break;
          ????????}?else?{
          ????????????ffp->error?=?0;
          ????????}
          ????????if?(is->eof)?{
          ????????????ffp_toggle_buffering(ffp,?0);
          ????????????SDL_Delay(100);
          ????????}
          ????????SDL_LockMutex(wait_mutex);
          ????????SDL_CondWaitTimeout(is->continue_read_thread,?wait_mutex,?10);
          ????????SDL_UnlockMutex(wait_mutex);
          ????????ffp_statistic_l(ffp);
          ????????continue;
          ????}?else?{
          ????????is->eof?=?0;
          ????}
          ????//?...
          ????//?8.?填充未解碼的幀隊列
          ????if?(pkt->stream_index?==?is->audio_stream?&&?pkt_in_play_range)?{
          ????????packet_queue_put(&is->audioq,?pkt);
          ????}?else?if?(pkt->stream_index?==?is->video_stream?&&?pkt_in_play_range
          ???????????????&&?!(is->video_st?&&?(is->video_st->disposition?&?AV_DISPOSITION_ATTACHED_PIC)))?{
          ????????packet_queue_put(&is->videoq,?pkt);
          ????}?else?if?(pkt->stream_index?==?is->subtitle_stream?&&?pkt_in_play_range)?{
          ????????packet_queue_put(&is->subtitleq,?pkt);
          ????}?else?{
          ????????av_packet_unref(pkt);
          ????}
          ????//?...
          }
          可知 read_thread 中的死循環(huán)主要用來進行數(shù)據(jù)讀取的,每次讀取的一幀壓縮編碼數(shù)據(jù)都添加到未解碼的幀隊列中,具體如下:
          1. 如果打開流失敗等會調(diào)用 stream_close 函數(shù)或者應用層調(diào)用函數(shù) release 釋放播放器直接 break

          2. 處理播放過程中的 seek 操作。

          3. 處理流中的 attached_pic,如果?個流中含有 AV_DISPOSITION_ATTACHED_PIC 說明這個流是 *.mp3 等?件中的?個 Video Stream,該流只有?個 AVPacket 就是 attached_pic。

          4. 處理隊列滿了的情況,一是當未解碼的隊列,即未解碼的音頻、視頻、字幕對應的隊列大小之和超過 15M,二是使用 stream_has_enough_packets 判斷音頻、視頻、字幕流是否已經(jīng)有了足夠的待解碼的 AVPacket,大于則相當于解復用器緩存滿了,延遲 10ms 供解碼器消耗數(shù)據(jù)。

          5. 檢查碼流是否播放完、是否設置循環(huán)播放、是否自動退出以及播放出錯的處理等。

          6. av_read_frame 可以說是 read_thread 線程的關鍵函數(shù),其作用就是解復用,每次讀取的一幀壓縮編碼數(shù)據(jù)都添加到未解碼的幀隊列中供相應的解碼線程使用。

          7. 檢測數(shù)據(jù)讀取情況,主要是數(shù)據(jù)讀取到流末尾的處理和數(shù)據(jù)讀取出錯的處理。

          8. 如果 av_read_frame 數(shù)據(jù)讀取成功則將其添加到對應的未解碼的幀隊列中。

          到此 IjkPlayer 的數(shù)據(jù)讀取線程 read_thread 線程基本梳理完畢,期待下篇。推薦閱讀:?

          ?

          瀏覽 60
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  丨级视频在线观看 | 日韩v在线| 日屄小视频 | 国产 激情 在线 | 欧美黄色一区二区三区 |