日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

ffplay.c学习-2-数据读取线程

發布時間:2024/4/11 编程问答 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 ffplay.c学习-2-数据读取线程 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

ffplay.c學習-2-數據讀取線程


目錄

  • 準備?作
  • avformat_alloc_context 創建上下?
  • ic->interrupt_callback
  • avformat_open_input()打開媒體?件
  • avformat_find_stream_info()
  • 檢測是否指定播放起始時間
  • 查找查找AVStream
  • 通過AVCodecParameters和av_guess_sample_aspect_ratio計算出顯示窗?的寬、?
  • stream_component_open()
  • For循環讀取數據
  • 檢測是否退出
  • 檢測是否暫停/繼續
  • 檢測是否需要seek
  • 檢測video是否為attached_pic
  • 檢測隊列是否已經有?夠數據
  • 檢測碼流是否已經播放結束
  • 使?av_read_frame讀取數據包
  • 檢測數據是否讀取完畢
  • 檢測是否在播放范圍內
  • 到這步才將數據插?對應的隊列
  • 退出線程處理

  • 從ffplay框架分析我們可以看到,ffplay有專?的線程read_thread()讀取數據,且在調?av_read_frame讀取數據包之前需要做例如打開?件,查找配置解碼器,初始化?視頻輸出等準備階段,主要包括三?步驟:
  • 準備?作
  • For循環讀取數據
  • 退出線程處理
  • 1. 準備?作

  • avformat_alloc_context 創建上下?
  • ic->interrupt_callback.callback = decode_interrupt_cb;
  • avformat_open_input打開媒體?件
  • avformat_find_stream_info 讀取媒體?件的包獲取更多的stream信息
  • 檢測是否指定播放起始時間,如果指定時間則seek到指定位置avformat_seek_file
  • 查找AVStream,將對應的index值記錄到st_index[AVMEDIA_TYPE_NB];
  • 根據?戶指定來查找流avformat_match_stream_specifier
  • 使?av_find_best_stream查找流
  • 從待處理流中獲取相關參數,設置顯示窗?的寬度、?度及寬??
  • stream_component_open打開?頻、視頻、字幕解碼器,并創建相應的解碼線程以及進?對應輸出參數的初始化
  • 2. For循環讀取數據

  • 檢測是否退出
  • 檢測是否暫停/繼續
  • 檢測是否需要seek
  • 檢測video是否為attached_pic
  • 檢測隊列是否已經有?夠數據
  • 檢測碼流是否已經播放結束
  • 是否循環播放
  • 是否?動退出
  • 使?av_read_frame讀取數據包
  • 檢測數據是否讀取完畢
  • 檢測是否在播放范圍內
  • 到這步才將數據插?對應的隊列
  • 3. 退出線程處理

  • 如果解復?器有打開則關閉avformat_close_input
  • 調?SDL_PushEvent發送退出事件FF_QUIT_EVENT
  • 消耗互斥量wait_mutex

  • 1. 準備?作

  • avformat_alloc_context 創建上下?
  • ic->interrupt_callback.callback = decode_interrupt_cb;
  • avformat_open_input打開媒體?件
  • avformat_find_stream_info 讀取媒體?件的包獲取更多的stream信息
  • 檢測是否指定播放起始時間,如果指定時間則seek到指定位置avformat_seek_file
  • 查找AVStream,將對應的index值記錄到st_index[AVMEDIA_TYPE_NB];
  • 根據?戶指定來查找流avformat_match_stream_specifier
  • 使?av_find_best_stream查找流
  • 從待處理流中獲取相關參數,設置顯示窗?的寬度、?度及寬??
  • stream_component_open打開?頻、視頻、字幕解碼器,并創建相應的解碼線程以及進?對應輸出參數的初始化
  • 1. avformat_alloc_context 創建上下?

  • 調?avformat_alloc_context創建解復?器上下?
  • // 1. 創建上下文結構體,這個結構體是最上層的結構體,表示輸入上下文ic = avformat_alloc_context();
  • 最終該ic 賦值給VideoState的ic變量
  • is->ic = ic; // videoState的ic指向分配的ic

    2. ic->interrupt_callback

    /* 2.設置中斷回調函數,如果出錯或者退出,就根據目前程序設置的狀態選擇繼續check或者直接退出 *//* 當執行耗時操作時(一般是在執行while或者for循環的數據讀取時),會調用interrupt_callback.callback* 回調函數中返回1則代表ffmpeg結束耗時操作退出當前函數的調用* 回調函數中返回0則代表ffmpeg內部繼續執行耗時操作,直到完成既定的任務(比如讀取到既定的數據包)*/ic->interrupt_callback.callback = decode_interrupt_cb;ic->interrupt_callback.opaque = is;
  • interrupt_callback?于ffmpeg內部在執?耗時操作時檢查調?者是否有退出請求,避免?戶退出請求沒有及時響應.
  • /*** @brief 這里是設置給ffmpeg內部,當ffmpeg內部當執行耗時操作時(一般是在執行while或者for循環的數據讀取時)* 就會調用該函數* @param ctx* @return 若直接退出阻塞則返回1,等待讀取則返回0*/static int decode_interrupt_cb(void *ctx) {static int64_t s_pre_time = 0;int64_t cur_time = av_gettime_relative() / 1000;printf("decode_interrupt_cb interval:%lldms\n", cur_time - s_pre_time);s_pre_time = cur_time;VideoState *is = (VideoState *) ctx;return is->abort_request; }

    3. avformat_open_input()打開媒體?件

  • 函數原型:
  • /*** Open an input stream and read the header. The codecs are not opened.* The stream must be closed with avformat_close_input().** @param ps Pointer to user-supplied AVFormatContext (allocated by avformat_alloc_context).* May be a pointer to NULL, in which case an AVFormatContext is allocated by this* function and written into ps.* Note that a user-supplied AVFormatContext will be freed on failure.* @param url URL of the stream to open.* @param fmt If non-NULL, this parameter forces a specific input format.* Otherwise the format is autodetected.* @param options A dictionary filled with AVFormatContext and demuxer-private options.* On return this parameter will be destroyed and replaced with a dict containing* options that were not found. May be NULL.** @return 0 on success, a negative AVERROR on failure.** @note If you want to use custom IO, preallocate the format context and set its pb field.*/ int avformat_open_input(AVFormatContext **ps, const char *url, ff_const59 AVInputFormat *fmt, AVDictionary **options);
  • avformat_open_input?于打開輸??件(對于RTMP/RTSP/HTTP?絡流也是?樣,在ffmpeg內部都抽象為URLProtocol,這?描述為?件是為了?便與后續提到的AVStream的流作區分),讀取視頻?件的基本信息
  • 需要提到的兩個參數是fmt和options。通過fmt可以強制指定視頻?件的封裝,options可以傳遞額外參數給封裝(AVInputFormat)。
  • //特定選項處理if (!av_dict_get(format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE)) {av_dict_set(&format_opts, "scan_all_pmts", "1", AV_DICT_DONT_OVERWRITE);scan_all_pmts_set = 1;}/* 3.打開文件,主要是探測協議類型,如果是網絡文件則創建網絡鏈接等 */err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);if (err < 0) {print_error(is->filename, err);ret = -1;goto fail;}if (scan_all_pmts_set)av_dict_set(&format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE);if ((t = av_dict_get(format_opts, "", NULL, AV_DICT_IGNORE_SUFFIX))) {av_log(NULL, AV_LOG_ERROR, "Option %s not found.\n", t->key);ret = AVERROR_OPTION_NOT_FOUND;goto fail;}
  • scan_all_pmts是mpegts的?個選項,表示掃描全部的ts流的"Program Map Table"表。這?在沒有設定該選項的時候,強制設為1。最后執?avformat_open_input。
  • 參數的設置最終都是設置到對應的解復?器,?如:
  • static const AVOption options[] = {MPEGTS_OPTIONS,{"fix_teletext_pts", "try to fix pts values of dvb teletext streams", offsetof(MpegTSContext, fix_teletext_pts), AV_OPT_TYPE_BOOL,{.i64 = 1}, 0, 1, AV_OPT_FLAG_DECODING_PARAM },{"ts_packetsize", "output option carrying the raw packet size", offsetof(MpegTSContext, raw_packet_size), AV_OPT_TYPE_INT,{.i64 = 0}, 0, 0, AV_OPT_FLAG_DECODING_PARAM | AV_OPT_FLAG_EXPORT | AV_OPT_FLAG_READONLY },{"scan_all_pmts", "scan and combine all PMTs", offsetof(MpegTSContext, scan_all_pmts), AV_OPT_TYPE_BOOL,{.i64 = -1}, -1, 1, AV_OPT_FLAG_DECODING_PARAM },{"skip_unknown_pmt", "skip PMTs for programs not advertised in the PAT", offsetof(MpegTSContext, skip_unknown_pmt), AV_OPT_TYPE_BOOL,{.i64 = 0}, 0, 1, AV_OPT_FLAG_DECODING_PARAM },{"merge_pmt_versions", "re-use streams when PMT's version/pids change", offsetof(MpegTSContext, merge_pmt_versions), AV_OPT_TYPE_BOOL,{.i64 = 0}, 0, 1, AV_OPT_FLAG_DECODING_PARAM },{"skip_changes", "skip changing / adding streams / programs", offsetof(MpegTSContext, skip_changes), AV_OPT_TYPE_BOOL,{.i64 = 0}, 0, 1, 0 },{"skip_clear", "skip clearing programs", offsetof(MpegTSContext, skip_clear), AV_OPT_TYPE_BOOL,{.i64 = 0}, 0, 1, 0 },{ NULL }, };

    4. avformat_find_stream_info()

  • 在打開了?件后,就可以從AVFormatContext中讀取流信息了。?般調?avformat_find_stream_info獲取完整的流信息。為什么在調?了avformat_open_input后,仍然需要調?avformat_find_stream_info才能獲取正確的流信息呢?看下注釋
  • /*** Read packets of a media file to get stream information. This* is useful for file formats with no headers such as MPEG. This* function also computes the real framerate in case of MPEG-2 repeat* frame mode.* The logical file position is not changed by this function;* examined packets may be buffered for later processing.** @param ic media file handle* @param options If non-NULL, an ic.nb_streams long array of pointers to* dictionaries, where i-th member contains options for* codec corresponding to i-th stream.* On return each dictionary will be filled with options that were not found.* @return >=0 if OK, AVERROR_xxx on error** @note this function isn't guaranteed to open all the codecs, so* options being non-empty at return is a perfectly normal behavior.** @todo Let the user decide somehow what information is needed so that* we do not waste time getting stuff the user does not need.*/ int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
  • 該函數是通過讀取媒體?件的部分數據來分析流信息。在?些缺少頭信息的封裝下特別有?,?如說MPEG(?應該說ts更準確)(FLV?件也是需要讀取packet 分析流信息)。?被讀取?以分析流信息的數據可能被緩存,供av_read_frame時使?,在播放時并不會跳過這部分packet的讀取。
  • 5. 檢測是否指定播放起始時間

  • 如果指定時間則seek到指定位置avformat_seek_file。
  • 可以通過 ffplay -ss 設置起始時間,時間格式hh:mm:ss,?如
  • ffplay -ss 00:00:30 test.flv 則是從30秒的起始位置開始播放。
  • {"ss", HAS_ARG, {.func_arg = opt_seek}, "seek to a given position in seconds", "pos"}, {"t", HAS_ARG, {.func_arg = opt_duration}, "play \"duration\" seconds of audio/video", "duration"}, /* if seeking requested, we execute it *//* 5. 檢測是否指定播放起始時間 */if (start_time != AV_NOPTS_VALUE) {int64_t timestamp;timestamp = start_time;/* add the stream start time */if (ic->start_time != AV_NOPTS_VALUE)timestamp += ic->start_time;// seek的指定的位置開始播放ret = avformat_seek_file(ic, -1, INT64_MIN, timestamp, INT64_MAX, 0);if (ret < 0) {av_log(NULL, AV_LOG_WARNING, "%s: could not seek to position %0.3f\n",is->filename, (double) timestamp / AV_TIME_BASE);}}

    6. 查找查找AVStream

  • ?個媒體?件,對應有0到n個?頻流、0到n個視頻流、0~n個字幕流,?如這?我們?了2_audio.mp4是有2個?頻流,1個視頻流

  • 具體現在哪個流進?播放我們有兩種策略:

  • 在播放起始指定對應的流
  • 使?缺省的流進?播放
  • 1 在播放起始指定對應的流
    {"ast", OPT_STRING | HAS_ARG | OPT_EXPERT, {&wanted_stream_spec[AVMEDIA_TYPE_AUDIO]},"select desired audio stream", "stream_specifier"}, {"vst", OPT_STRING | HAS_ARG | OPT_EXPERT, {&wanted_stream_spec[AVMEDIA_TYPE_VIDEO]},"select desired video stream", "stream_specifier"}, {"sst", OPT_STRING | HAS_ARG | OPT_EXPERT, {&wanted_stream_spec[AVMEDIA_TYPE_SUBTITLE]},"select desired subtitle stream", "stream_specifier"},
  • 可以通過

  • -ast n 指定?頻流(?如我們在看電影時,有些電影可以?持普通話和英?切換,此時可以?該命令進?選擇)
  • -vst n 指定視頻流
  • -vst n 指定字幕流
  • 將對應的index值記錄到st_index[AVMEDIA_TYPE_NB];

  • 2. 使?缺省的流進?播放
  • 如果我們沒有指定,則ffplay主要是通過 av_find_best_stream 來選擇,其原型為:
  • /*** Find the "best" stream in the file.* The best stream is determined according to various heuristics as the most* likely to be what the user expects.* If the decoder parameter is non-NULL, av_find_best_stream will find the* default decoder for the stream's codec; streams for which no decoder can* be found are ignored.** @param ic media file handle* @param type stream type: video, audio, subtitles, etc.* @param wanted_stream_nb user-requested stream number,* or -1 for automatic selection* @param related_stream try to find a stream related (eg. in the same* program) to this one, or -1 if none* @param decoder_ret if non-NULL, returns the decoder for the* selected stream* @param flags flags; none are currently defined* @return the non-negative stream number in case of success,* AVERROR_STREAM_NOT_FOUND if no stream with the requested type* could be found,* AVERROR_DECODER_NOT_FOUND if streams were found but no decoder* @note If av_find_best_stream returns successfully and decoder_ret is not* NULL, then *decoder_ret is guaranteed to be set to a valid AVCodec.*/ int av_find_best_stream(AVFormatContext *ic,enum AVMediaType type,int wanted_stream_nb,int related_stream,AVCodec **decoder_ret,int flags); // 6.2 利用av_find_best_stream選擇流,if (!video_disable)st_index[AVMEDIA_TYPE_VIDEO] =av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO,st_index[AVMEDIA_TYPE_VIDEO], -1, NULL, 0);if (!audio_disable)st_index[AVMEDIA_TYPE_AUDIO] =av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO,st_index[AVMEDIA_TYPE_AUDIO],st_index[AVMEDIA_TYPE_VIDEO],NULL, 0);if (!video_disable && !subtitle_disable)st_index[AVMEDIA_TYPE_SUBTITLE] =av_find_best_stream(ic, AVMEDIA_TYPE_SUBTITLE,st_index[AVMEDIA_TYPE_SUBTITLE],(st_index[AVMEDIA_TYPE_AUDIO] >= 0 ?st_index[AVMEDIA_TYPE_AUDIO] :st_index[AVMEDIA_TYPE_VIDEO]),NULL, 0);
  • 如果?戶沒有指定流,或指定部分流,或指定流不存在,則主要由av_find_best_stream發揮作?。
  • 如果指定了正確的wanted_stream_nb,?般情況都是直接返回該指定流,即?戶選擇的流。
  • 如果指定了相關流,且未指定?標流的情況,會在相關流的同?個節?中查找所需類型的流,但?般結果,都是返回該類型第1個流。
  • 7. 通過AVCodecParameters和av_guess_sample_aspect_ratio計算出顯示窗?的寬、?

    //7 從待處理流中獲取相關參數,設置顯示窗口的寬度、高度及寬高比if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {AVStream *st = ic->streams[st_index[AVMEDIA_TYPE_VIDEO]];AVCodecParameters *codecpar = st->codecpar;/*根據流和幀寬高比猜測幀的樣本寬高比。該值只是一個參考*/AVRational sar = av_guess_sample_aspect_ratio(ic, st, NULL);if (codecpar->width) {// 設置顯示窗口的大小和寬高比set_default_window_size(codecpar->width, codecpar->height, sar);}}
  • 具體流程如上所示,這?實質只是設置了default_width、default_height變量的??,沒有真正改變窗?的??。真正調整窗???是在視頻顯示調?video_open()函數進?設置。
  • 8. stream_component_open()

  • 經過以上步驟,?件打開成功,且獲取了流的基本信息,并選擇?頻流、視頻流、字幕流。接下來就可以所選流對應的解碼器了。
  • /* open the streams *//* 8. 打開視頻、音頻解碼器。在此會打開相應解碼器,并創建相應的解碼線程。 */if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {// 如果有音頻流則打開音頻流stream_component_open(is, st_index[AVMEDIA_TYPE_AUDIO]);}ret = -1;if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) { // 如果有視頻流則打開視頻流ret = stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO]);}if (is->show_mode == SHOW_MODE_NONE) {//選擇怎么顯示,如果視頻打開成功,就顯示視頻畫面,否則,顯示音頻對應的頻譜圖is->show_mode = ret >= 0 ? SHOW_MODE_VIDEO : SHOW_MODE_RDFT;}if (st_index[AVMEDIA_TYPE_SUBTITLE] >= 0) { // 如果有字幕流則打開字幕流stream_component_open(is, st_index[AVMEDIA_TYPE_SUBTITLE]);}
  • ?頻、視頻、字幕等流都要調?stream_component_open,他們直接有共同的流程,也有差異化的流程,差異化流程使?switch進?區分。具體原型
  • /* open a given stream. Return 0 if OK */ /*** @brief stream_component_open* @param is* @param stream_index 流索引* @return Return 0 if OK*/ static int stream_component_open(VideoState *is, int stream_index)
  • 看下 stream_component_open .函數也?較?,逐步分析:
  • /* 為解碼器分配一個編解碼器上下文結構體 */avctx = avcodec_alloc_context3(NULL);if (!avctx)return AVERROR(ENOMEM);/* 將碼流中的編解碼器信息拷貝到新分配的編解碼器上下文結構體 */ret = avcodec_parameters_to_context(avctx, ic->streams[stream_index]->codecpar);if (ret < 0)goto fail;// 設置pkt_timebaseavctx->pkt_timebase = ic->streams[stream_index]->time_base;
  • 先是通過 avcodec_alloc_context3 分配了解碼器上下? AVCodecContex ,然后通過avcodec_parameters_to_context 把所選流的解碼參數賦給 avctx ,最后設了 time_base .
  • 補充:avcodec_parameters_to_context 解碼時?,avcodec_parameters_from_context則?于編碼。
  • /* 根據codec_id查找解碼器 */codec = avcodec_find_decoder(avctx->codec_id);switch (avctx->codec_type) {case AVMEDIA_TYPE_AUDIO :is->last_audio_stream = stream_index;forced_codec_name = audio_codec_name;break;case AVMEDIA_TYPE_SUBTITLE:is->last_subtitle_stream = stream_index;forced_codec_name = subtitle_codec_name;break;case AVMEDIA_TYPE_VIDEO :is->last_video_stream = stream_index;forced_codec_name = video_codec_name;break;}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);elseav_log(NULL, AV_LOG_WARNING,"No decoder could be found for codec %s\n", avcodec_get_name(avctx->codec_id));ret = AVERROR(EINVAL);goto fail;}
  • 這段主要是通過 avcodec_find_decoder 找到所需解碼器(AVCodec)。如果?戶有指定解碼器,則設置 forced_codec_name ,并通過 avcodec_find_decoder_by_name 查找解碼器。找到解碼器后,就可以通過 avcodec_open2 打開解碼器了。(forced_codec_name對應到?頻、視頻、字幕不同的傳?的解碼器名字,如果有設置,?如ffplay -acodec aac xx.flv, 此時audio_codec_name被設置為"aac",則相應的forced_codec_name為“aac”)
  • 最后,是?個?的switch-case:
  • switch (avctx->codec_type) {case AVMEDIA_TYPE_AUDIO: #if CONFIG_AVFILTER{AVFilterContext *sink;is->audio_filter_src.freq = avctx->sample_rate;is->audio_filter_src.channels = avctx->channels;is->audio_filter_src.channel_layout = get_valid_channel_layout(avctx->channel_layout, avctx->channels);is->audio_filter_src.fmt = avctx->sample_fmt;if ((ret = configure_audio_filters(is, afilters, 0)) < 0)goto fail;sink = is->out_audio_filter;sample_rate = av_buffersink_get_sample_rate(sink);nb_channels = av_buffersink_get_channels(sink);channel_layout = av_buffersink_get_channel_layout(sink);} #elsesample_rate = avctx->sample_rate;nb_channels = avctx->channels;channel_layout = avctx->channel_layout; #endif/* prepare audio output 準備音頻輸出*/if ((ret = audio_open(is, channel_layout, nb_channels, sample_rate, &is->audio_tgt)) < 0)goto fail;is->audio_hw_buf_size = ret;is->audio_src = is->audio_tgt;is->audio_buf_size = 0;is->audio_buf_index = 0;/* init averaging filter 初始化averaging濾鏡, 非audio master時使用 */is->audio_diff_avg_coef = exp(log(0.01) / AUDIO_DIFF_AVG_NB);is->audio_diff_avg_count = 0;/* 由于我們沒有精確的音頻數據填充FIFO,故只有在大于該閾值時才進行校正音頻同步*/is->audio_diff_threshold = (double) (is->audio_hw_buf_size) / is->audio_tgt.bytes_per_sec;is->audio_stream = stream_index; // 獲取audio的stream索引is->audio_st = ic->streams[stream_index]; // 獲取audio的stream指針// 初始化ffplay封裝的音頻解碼器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;}// 啟動音頻解碼線程if ((ret = decoder_start(&is->auddec, audio_thread, "audio_decoder", is)) < 0)goto out;SDL_PauseAudioDevice(audio_dev, 0);break;case AVMEDIA_TYPE_VIDEO:is->video_stream = stream_index; // 獲取video的stream索引is->video_st = ic->streams[stream_index];// 獲取video的stream指針// 初始化ffplay封裝的視頻解碼器decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread);// 啟動視頻頻解碼線程if ((ret = decoder_start(&is->viddec, video_thread, "video_decoder", is)) < 0)goto out;is->queue_attachments_req = 1; // 使能請求mp3、aac等音頻文件的封面break;case AVMEDIA_TYPE_SUBTITLE: // 視頻是類似邏輯處理is->subtitle_stream = stream_index;is->subtitle_st = ic->streams[stream_index];decoder_init(&is->subdec, avctx, &is->subtitleq, is->continue_read_thread);if ((ret = decoder_start(&is->subdec, subtitle_thread, "subtitle_decoder", is)) < 0)goto out;break;default:break;}
  • 即根據具體的流類型,作特定的初始化。但不論哪種流,基本步驟都包括了ffplay封裝的解碼器的初始化和啟動解碼器線程:

  • decoder_init 初始化解碼器
  • d->avctx = avctx; 綁定對應的解碼器上下? d->queue = queue; 綁定對應的packet隊列 d->empty_queue_cond = empty_queue_cond; 綁定VideoState的continue_read_thread,當解碼線程沒有packet可讀時喚醒read_thread趕緊讀取數據 d->start_pts = AV_NOPTS_VALUE; 初始化start_pts d->pkt_serial = -1; 初始化pkt_serial
  • decoder_start啟動解碼器
  • packet_queue_start 啟?對應的packet 隊列 SDL_CreateThread 創建對應的解碼線程
  • 需要注意的是,對應?頻??,這?還初始化了輸出參數,這塊在講?頻輸出的時候再重點展開

  • 以上是準備的?作,我們再來看for循環。


  • 2. For循環讀取數據

  • 檢測是否退出
  • 檢測是否暫停/繼續
  • 檢測是否需要seek
  • 檢測video是否為attached_pic
  • 檢測隊列是否已經有?夠數據
  • 檢測碼流是否已經播放結束
  • 是否循環播放
  • 是否?動退出
  • 使?av_read_frame讀取數據包
  • 檢測數據是否讀取完畢
  • 檢測是否在播放范圍內
  • 到這步才將數據插?對應的隊列
  • 1. 檢測是否退出

    // 1 檢測是否退出if (is->abort_request)break;
  • 當退出事件發?時,調?do_exit() -> stream_close() -> 將is->abort_request置為1。退出該for循環,并最終退出該線程
  • 2. 檢測是否暫停/繼續

  • 這?的暫停、繼續只是對?絡流有意義
  • // 2 檢測是否暫停/繼續if (is->paused != is->last_paused) {is->last_paused = is->paused;if (is->paused)is->read_pause_return = av_read_pause(ic); // 網絡流的時候有用elseav_read_play(ic);}
  • ?如rtsp
  • 1. av_read_pause
    /* pause the stream */ static int rtsp_read_pause(AVFormatContext *s) {RTSPState *rt = s->priv_data;RTSPMessageHeader reply1, *reply = &reply1;if (rt->state != RTSP_STATE_STREAMING)return 0;else if (!(rt->server_type == RTSP_SERVER_REAL && rt->need_subscription)) {ff_rtsp_send_cmd(s, "PAUSE", rt->control_uri, NULL, reply, NULL);if (reply->status_code != RTSP_STATUS_OK) {return ff_rtsp_averror(reply->status_code, -1);}}rt->state = RTSP_STATE_PAUSED;return 0; }
    2. av_read_play
    static int rtsp_read_play(AVFormatContext *s) {RTSPState *rt = s->priv_data;RTSPMessageHeader reply1, *reply = &reply1;...ff_rtsp_send_cmd(s, "PLAY", rt->control_uri, cmd, reply, NULL);...rt->state = RTSP_STATE_STREAMING;return 0;

    3. 檢測是否需要seek

    // 3 檢測是否seekif (is->seek_req) { // 是否有seek請求int64_t seek_target = is->seek_pos;int64_t seek_min = is->seek_rel > 0 ? seek_target - is->seek_rel + 2 : INT64_MIN;int64_t seek_max = is->seek_rel < 0 ? seek_target - is->seek_rel - 2 : INT64_MAX;// FIXME the +-2 is due to rounding being not done in the correct direction in generation// of the seek_pos/seek_rel variables// 修復由于四舍五入,沒有再seek_pos/seek_rel變量的正確方向上進行ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);if (ret < 0) {av_log(NULL, AV_LOG_ERROR,"%s: error while seeking\n", is->ic->url);} else {/* seek的時候,要把原先的數據情況,并重啟解碼器,put flush_pkt的目的是告知解碼線程需要* reset decoder*/if (is->audio_stream >= 0) { // 如果有音頻流packet_queue_flush(&is->audioq); // 清空packet隊列數據// 放入flush pkt, 用來開起新的一個播放序列, 解碼器讀取到flush_pkt也清空解碼器packet_queue_put(&is->audioq, &flush_pkt);}if (is->subtitle_stream >= 0) { // 如果有字幕流packet_queue_flush(&is->subtitleq); // 和上同理packet_queue_put(&is->subtitleq, &flush_pkt);}if (is->video_stream >= 0) { // 如果有視頻流packet_queue_flush(&is->videoq); // 和上同理packet_queue_put(&is->videoq, &flush_pkt);}if (is->seek_flags & AVSEEK_FLAG_BYTE) {set_clock(&is->extclk, NAN, 0);} else {set_clock(&is->extclk, seek_target / (double) AV_TIME_BASE, 0);}}is->seek_req = 0;is->queue_attachments_req = 1;is->eof = 0;if (is->paused)step_to_next_frame(is);}
  • 主要的seek操作通過avformat_seek_file完成(該函數的具體使?在播放控制seek時做詳解)。根據
    avformat_seek_file的返回值,如果seek成功,需要:

  • 清除PacketQueue的緩存,并放??個flush_pkt。放?的flush_pkt可以讓PacketQueue的serial增1,以區分seek前后的數據(PacketQueue函數的分析0),該flush_pkt也會觸發解碼器重新刷新解碼器緩存avcodec_flush_buffers(),以避免解碼時使?了原來的buffer作為參考?出現?賽克。
  • 同步外部時鐘。在后續?視頻同步的課程中再具體分析。
  • 這?還要注意:如果播放器本身是pause的狀態,則

  • if (is->paused) step_to_next_frame(is); // 如果本身是pause狀態的則顯示?幀繼續暫停

    4. 檢測video是否為attached_pic (專輯封面)

    // 4 檢測video是否為attached_picif (is->queue_attachments_req) {// attached_pic 附帶的圖片。比如說一些MP3,AAC音頻文件附帶的專輯封面,所以需要注意的是音頻文件不一定只存在音頻流本身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;}
  • AV_DISPOSITION_ATTACHED_PIC 是?個標志。如果?個流中含有這個標志的話,那么就是說這個是 *.mp3等 ?件中的?個 Video Stream 。并且該流只有?個 AVPacket ,也就是attached_pic 。這個 AVPacket 中所存儲的內容就是這個 *.mp3等 ?件的封?圖?。因此,也可以很好的解釋了?章開頭提到的為什么 st->disposition &AV_DISPOSITION_ATTACHED_PIC 這個操作可以決定是否可以繼續向緩沖區中添加 AVPacket 。
  • 5. 檢測隊列是否已經有?夠數據

  • ?頻、視頻、字幕隊列都不是?限?的,如果不加以限制?直往隊列放?packet,那將導致隊列占??量的內存空間,影響系統的性能,所以必須對隊列的緩存??進?控制。
  • PacketQueue默認情況下會有??限制,達到這個??后,就需要等待10ms,以讓消費者——解碼線程能有時間消耗。
  • // 5 檢測隊列是否已經有足夠數據/* if the queue are full, no need to read more *//* 緩存隊列有足夠的包,不需要繼續讀取數據 */if (infinite_buffer < 1 &&(is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE|| (stream_has_enough_packets(is->audio_st, is->audio_stream, &is->audioq) &&stream_has_enough_packets(is->video_st, is->video_stream, &is->videoq) &&stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->subtitleq)))) {/* wait 10 ms */SDL_LockMutex(wait_mutex);// 如果沒有喚醒則超時10ms退出,比如在seek操作時這里會被喚醒SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);SDL_UnlockMutex(wait_mutex);continue;}
  • 緩沖區滿有兩種可能:
  • audioq,videoq,subtitleq三個PacketQueue的總字節數達到了MAX_QUEUE_SIZE(15M,為什么是15M?這?只是?個經驗計算值,?如4K視頻的碼率以50Mbps計算,則15MB可以緩存2.4秒,從這么計算實際上如果我們真的是播放4K?源,15MB是偏?的數值,有些?源?較坑 同?個?件位置附近的pts差值超過5秒,此時如果視頻要緩存5秒才能做同步,那15MB的緩存??就不夠了)
  • ?頻、視頻、字幕流都已有夠?的包(stream_has_enough_packets),注意:3者要同時成?
  • 第?種好理解,看下第?種中的stream_has_enough_packets:
  • static int stream_has_enough_packets(AVStream *st, int stream_id, PacketQueue *queue) {return stream_id < 0 || // 沒有該流queue->abort_request || // 請求退出(st->disposition & AV_DISPOSITION_ATTACHED_PIC) || // 是ATTACHED_PICqueue->nb_packets > MIN_FRAMES // packet數>25&& (!queue->duration || // 滿足PacketQueue總時長為0av_q2d(st->time_base) * queue->duration > 1.0); //或總時長超過1s }
  • 有這么?種情況包是夠?的:
  • 流沒有打開(stream_id < 0),沒有相應的流返回邏輯true
  • 有退出請求(queue->abort_request)
  • 配置了AV_DISPOSITION_ATTACHED_PIC
  • packet隊列內包個數?于MIN_FRAMES(>25),并滿?PacketQueue總時?為0或總時?超過1s
  • 6. 檢測碼流是否已經播放結束

  • ?暫停狀態才進?步檢測碼流是否已經播放完畢(注意:數據播放完畢和碼流數據讀取完畢是兩個概念。)
  • PacketQueue和FrameQueue都消耗完畢,才是真正的播放完畢
  • // 6 檢測碼流是否已經播放結束if (!is->paused // 非暫停&& // 這里的執行是因為碼流讀取完畢后 插入空包所致(!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))) {if (loop != 1 // a 是否循環播放&& (!loop || --loop)) {// stream_seek不是ffmpeg的函數,是ffplay封裝的,每次seek的時候會調用stream_seek(is, start_time != AV_NOPTS_VALUE ? start_time : 0, 0, 0);} else if (autoexit) { // b 是否自動退出ret = AVERROR_EOF;goto fail;}}
  • 這?判斷播放已完成的條件需要同時滿?滿?:

  • 不在暫停狀態
  • ?頻未打開;或者打開了,但是解碼已解完所有packet,?定義的解碼器(decoder)serial等于PacketQueue的serial,并且FrameQueue中沒有數據幀
  • PacketQueue.serial -> packet.serail -> decoder.pkt_serial(賦值路徑) decoder.finished = decoder.pkt_serial
  • is->auddec.finished == is->audioq.serial 最新的播放序列的packet都解碼完畢
  • frame_queue_nb_remaining(&is->sampq) == 0 對應解碼后的數據也播放完畢
  • 視頻未打開;或者打開了,但是解碼已解完所有packet,?定義的解碼器(decoder)serial等于PacketQueue的serial,并且FrameQueue中沒有數據幀
  • 在確認?前碼流已播放結束的情況下,?戶有兩個變量可以控制播放器?為:

  • loop: 控制播放次數(當前這次也算在內,也就是最?就是1次了),0表示?限次
  • autoexit:?動退出,也就是播放完成后?動退出。
  • loop條件簡化的?常不友好,其意思是:如果loop==1,那么已經播了1次了,?需再seek重新播放;如果loop不是1,==0,隨意,?限次循環;減1后還?于0(–loop),也允許循環

  • 是否循環播放:如果循環播放,即是將?件seek到起始位置 stream_seek(is, start_time != AV_NOPTS_VALUE ?start_time : 0, 0, 0); ,這?講的的起始位置不?定是從頭開始,具體也要看?戶是否指定了起始播放位置
  • 是否?動退出:如果播放完畢?動退出
  • 7. 使?av_read_frame讀取數據包

  • 讀取數據包很簡單,但要注意傳?的packet,av_read_frame不會釋放其數據,?是每次都重新申請數據
  • // 7.讀取媒體數據,得到的是音視頻分離后、解碼前的數據ret = av_read_frame(ic, pkt); // 調用不會釋放pkt的數據,需要我們自己去釋放packet的數據

    8. 檢測數據是否讀取完畢

    // 8 檢測數據是否讀取完畢if (ret < 0) {if ((ret == AVERROR_EOF || avio_feof(ic->pb))&& !is->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; // 文件讀取完畢}if (ic->pb && ic->pb->error)break;SDL_LockMutex(wait_mutex);SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);SDL_UnlockMutex(wait_mutex);continue; // 繼續循環} else {is->eof = 0;}
  • 數據讀取完畢后,放對應?頻、視頻、字幕隊列插?“空包”,以通知解碼器沖刷buffer,將緩存的所有數據都解出來frame并去出來。
  • 然后繼續在for{}循環,直到收到退出命令,或者loop播放,或者seek等操作。
  • 9. 檢測是否在播放范圍內

  • 播放器可以設置:-ss 起始位置,以及 -t 播放時?
  • // 9 檢測是否在播放范圍內/* check if packet is in play range specified by user, then queue, otherwise discard */stream_start_time = ic->streams[pkt->stream_index]->start_time; // 獲取流的起始時間pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts; // 獲取packet的時間戳// 這里的duration是在命令行時用來指定播放長度pkt_in_play_range = duration == AV_NOPTS_VALUE ||(pkt_ts - (stream_start_time != AV_NOPTS_VALUE ? stream_start_time : 0)) *av_q2d(ic->streams[pkt->stream_index]->time_base) -(double) (start_time != AV_NOPTS_VALUE ? start_time : 0) / 1000000<= ((double) duration / 1000000);
  • 從流獲取的參數
  • stream_start_time:是從當前流AVStream->start_time獲取到的時間,如果沒有定義具體的值則默認為AV_NOPTS_VALUE,即該值是?效的;那stream_start_time有意義的就是0值;
  • pkt_ts:當前packet的時間戳,pts有效就?pts的,pts?效就?dts的;ffplay播放的參數
  • ffplay播放的參數
  • duration: 使?"-t value"指定的播放時?,默認值AV_NOPTS_VALUE,即該值?效不?參考
  • start_time:使?“-ss value”指定播放的起始位置,默認AV_NOPTS_VALUE,即該值?效不?參考
  • pkt_in_play_range的值為0或1。
  • 當沒有指定duration播放時?時,很顯然duration == AV_NOPTS_VALUE的邏輯值為1,所以pkt_in_play_range為1;
  • 當duration被指定(-t value)且有效時,主要判斷
  • (pkt_ts - (stream_start_time != AV_NOPTS_VALUE ? stream_start_time : 0)) *av_q2d(ic->streams[pkt->stream_index]->time_base) -(double) (start_time != AV_NOPTS_VALUE ? start_time : 0) / 1000000<= ((double) duration / 1000000);
  • 實質就是當前時間戳 pkt_ts - start_time 是否 < duration,這?分為:
  • stream_start_time是否有效:有效就?實際值,?效就是從0開始
  • start_time 是否有效,有效就?實際值,?效就是從0開始
  • 即是pkt_ts - stream_start_time - start_time < duration (為了簡單,這?沒有考慮時間單位)
  • 10. 到這步才將數據插?對應的隊列

    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->disposition & AV_DISPOSITION_ATTACHED_PIC)) {//printf("pkt pts:%ld, dts:%ld\n", pkt->pts, pkt->dts);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);// // 不入隊列則直接釋放數據}

    3. 退出線程處理

  • 主要包括以下步驟:
  • 如果解復?器有打開則關閉avformat_close_input
  • 調?SDL_PushEvent發送退出事件FF_QUIT_EVENT
    a. 發送的FF_QUIT_EVENT退出播放事件由event_loop()函數相應,收到FF_QUIT_EVENT后調?
    do_exit()做退出操作。
  • 消耗互斥量wait_mutex
  • ret = 0;fail:if (ic && !is->ic)avformat_close_input(&ic);if (ret != 0) {SDL_Event event;event.type = FF_QUIT_EVENT;event.user.data1 = is;SDL_PushEvent(&event);}SDL_DestroyMutex(wait_mutex);

    總結

    以上是生活随笔為你收集整理的ffplay.c学习-2-数据读取线程的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。