ijkplayer
源码解析系列文章
本章主要介绍媒体文件的读取
1.播放器初始化准备工作
2.打开媒体Stream
3.read_thread 读线程工作
业务方在使用IJKFFMoviePlayerController
的时候,一般的流程是这样的:
- 初始化播放器实例,
- 设置入拉流地址,
- 准备播放
- 开始播放
播放器代码的实现一般是这样;
- (void)viewDidLoad
{
[super viewDidLoad];
/// 初始化播放器
self.player = [[IJKFFMoviePlayerController alloc] initWithContentURL:self.url withOptions:options];
self.player.view.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
self.player.view.frame = self.view.bounds;
self.player.scalingMode = IJKMPMovieScalingModeAspectFit;
self.player.shouldAutoplay = YES;
[self.player setPauseInBackground:YES];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
///准备播放
[self.player prepareToPlay];
}
1.初始化播放器相关
播放器的初始化会调用到OC层的
- (id)initWithContentURLString:(NSString *)aUrlString withOptions:(IJKFFOptions *)options
方法,该方法主要实现了播放器在iOS侧的一些实例对象的创建,消息队列的创建,音频Session的设置,数据源的设置 等功能;
- 1.注册
ffmpeg
- 2.
ffmpeg
版本检查 - 3.创建消息队列loop
- 4.创建
IJKSDLGLView
渲染视图 - 5.设置输出的
ijkplayer log
日志级别 - 6.初始化音频
AudioSession
类型
1.1 注册ffmpeg
/// 注册编码器
avcodec_register_all();
//注册ffmpeg 相关复用器/解复用器、以及各种协议
av_register_all();
ijkav_register_all();
// 注册网络相关协议
avformat_network_init();
// 初始化刷新AVPacket(用于seek等操作)
av_init_packet(&flush_pkt);
1.2 ffmpeg
版本检查
///检查ffmpeg 版本是否匹配
const char *actualVersion = av_version_info();
const char *expectVersion = kIJKFFRequiredFFmpegVersion;
if (0 == strcmp(actualVersion, expectVersion)) {
return YES;
} else {
/// 省略代码...
return NO;
}
1.3 创建消息队列loop
///消息池,处理播放过程中的各种消息
_msgPool = [[IJKFFMoviePlayerMessagePool alloc] init];
1.4 创建IJKSDLGLView
渲染视图
/// 渲染View的创建(基于OpenGL ES2.0)
_glView = [[IJKSDLGLView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
1.5 设置输出的ijkplayer log
日志级别
#ifdef DEBUG
[IJKFFMoviePlayerController setLogLevel:k_IJK_LOG_DEBUG];
#else
[IJKFFMoviePlayerController setLogLevel:k_IJK_LOG_SILENT];
#endif
1.6 初始化音频AudioSession
类型
///内部调用AVAudioSession
[[IJKAudioKit sharedInstance] setupAudioSession];
2.设置播放数据源
初始化ijkplayer
的时候传入播放地址 最终调用到 - (id)initWithContentURL:(NSURL *)aUrl withOptions:(IJKFFOptions *)options;
方法,
这里只是把播放器地址设置进了_mediaPlayer
实例中;
/// 把播放地址设置给 IjkMediaPlayer 实例
ijkmp_set_data_source(_mediaPlayer, [_urlString UTF8String]);
3.准备播放
[IJKFFMoviePlayerController prepareToPlay]
准备播放;
调用堆栈如下:
prepareToPlay
ijkmp_prepare_async
ijkmp_prepare_async_l
ffp_prepare_async_l
stream_open
3.1最核心的方法是stream_open()
的内部实现, stream_open()
函数 实现了如下功能:
- 1.初始化
视频packet queue
、音频packet queue
、字幕packet queue
- 2.初始化
视频Frame queue
、音频Frame queue
、字幕Frame queue
- 3.初始化
音频时钟Clock
、视频时钟Clock
、外部时钟Clock
- 4.创建读取线程 及函数
read_thread
- 5.创建视频刷新线程 及其函数
video_refresh_thread
3.1.1.初始化视频packet queue
、音频packet queue
、 字幕packet queue
视频FrameQueue
大小3 ,字幕FrameQueue
大小16,AudioQueue
大小9;keep_last
表示保留上一帧,这里只有视频需要保留;
/// 初始化视频FrameQueue 并设置size大小3
if (frame_queue_init(&is->pictq, &is->videoq, ffp->pictq_size, 1) < 0)
goto fail;
// /初始化字幕FrameQueue size 大小16
if (frame_queue_init(&is->subpq, &is->subtitleq, SUBPICTURE_QUEUE_SIZE, 0) < 0)
goto fail;
/// 初始音频FrameQueue size 大小9
if (frame_queue_init(&is->sampq, &is->audioq, SAMPLE_QUEUE_SIZE, 1) < 0)
goto fail;
if (packet_queue_init(&is->videoq) < 0 ||
packet_queue_init(&is->audioq) < 0 ||
packet_queue_init(&is->subtitleq) < 0)
goto fail;
3.1.2.初始化视频Frame queue
、音频Frame queue
、 字幕Frame queue
/// 这里 packet queue 是在 VideoState 初始化的时候完成的
///videoq、subtitleq、audioq
is = av_mallocz(sizeof(VideoState));
3.1.3.初始化音频时钟Clock
、视频时钟Clock
、外部时钟Clock
init_clock(&is->vidclk, &is->videoq.serial);
init_clock(&is->audclk, &is->audioq.serial);
init_clock(&is->extclk, &is->extclk.serial);
3.1.4.创建读取线程 及函数read_thread
is->read_tid = SDL_CreateThreadEx(&is->_read_tid, read_thread, ffp, "ff_read");
3.1.5.创建视频刷新线程 及其函数video_refresh_thread
is->video_refresh_tid = SDL_CreateThreadEx(&is->_video_refresh_tid, video_refresh_thread, ffp, "ff_vout");
3.2 当播放器的初始化没有问题时,调用 read_thread()
开始读取媒体的AVPacket
;
- 1.
avformat_alloc_context
创建上下文 - 2.
ic->interrupt_callback.callback = decode_interrupt_cb
; - 3.
avformat_open_input
打开媒体文件 - 4.日志打印媒体信息
av_dump_format(ic, 0, is->filename, 0)
; - 5.查找
音频/视频/字母AVStream
索引,并把Index
记录到st_index[AVMEDIA_TYPE_NB]
- 6.设置视频显示的窗口大小
set_default_window_size(codecpar->width, codecpar->height, sar)
- 7.
stream_component_open()
打开音频、视频、字幕解码器,并创建相应的解码线程并初始化 - 8.
for
循环读取数据
3.2.1.avformat_alloc_context
创建上下文
ic = avformat_alloc_context();
3.2.2.ic->interrupt_callback.callback = decode_interrupt_cb
;
ic->interrupt_callback.callback = decode_interrupt_cb;
3.2.3.avformat_open_input
打开媒体文件
err = avformat_open_input(&ic, is->filename, is->iformat, &ffp->format_opts);
3.2.4.日志打印媒体信息av_dump_format(ic, 0, is->filename, 0)
;
av_dump_format(ic, 0, is->filename, 0);
3.2.5.查找音频/视频/字母AVStream
,并把Index 记录到st_index[AVMEDIA_TYPE_NB]
for (i = 0; i < ic->nb_streams; i++) {
AVStream *st = ic->streams[i];
enum AVMediaType type = st->codecpar->codec_type;
st->discard = AVDISCARD_ALL;
if (type >= 0 && ffp->wanted_stream_spec[type] && st_index[type] == -1)
if (avformat_match_stream_specifier(ic, st, ffp->wanted_stream_spec[type]) > 0)
st_index[type] = i;
// choose first h264
if (type == AVMEDIA_TYPE_VIDEO) {
enum AVCodecID codec_id = st->codecpar->codec_id;
video_stream_count++;
if (codec_id == AV_CODEC_ID_H264) {
h264_stream_count++;
if (first_h264_stream < 0)
first_h264_stream = i;
}
}
}
3.2.6.设置视频显示的窗口大小 set_default_window_size(codecpar->width, codecpar->height, sar)
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);
}
3.2.7.stream_component_open
打开音频、视频、字幕解码器,并创建相应的解码线程并初始化
if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
stream_component_open(ffp, st_index[AVMEDIA_TYPE_AUDIO]);
} else {
ffp->av_sync_type = AV_SYNC_VIDEO_MASTER;
is->av_sync_type = ffp->av_sync_type;
}
ret = -1;
if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
ret = stream_component_open(ffp, 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(ffp, st_index[AVMEDIA_TYPE_SUBTITLE]);
}
ffp_notify_msg1(ffp, FFP_MSG_COMPONENT_OPEN);
3.2.8.for
循环 读取数据
for 循环读数据是一段很长的代码,主要包括以下步骤
- 1.检测是否退出
- 2.检测是否暂停/继续
- 3.检测是否需要seek
- 4.检测video是否为attached_pic
- 5.检测队列是否有足够的数据
- 6.检测是否已经播放结束(是否循环播放、是否自动退出)
- 7.使用 av_read_frame 读取数据包
- 8.检测数据是否读取完毕
- 9.检测是否在播放范围
- 10.将数据插入相应的 frame queue 中(音频/视频/字幕)
3.2.8.1 检测是否退出
for (;;) {
if (is->abort_request)
break;
省略代码...
}
3.2.8.2.检测是否暂停/继续
if (is->paused != is->last_paused) {
is->last_paused = is->paused;
if (is->paused)
is->read_pause_return = av_read_pause(ic);
else
av_read_play(ic);
}
3.2.8.3.检测是否需要seek
/// 是否有seek请求
if (is->seek_req) {
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
///暂停播放
ffp_toggle_buffering(ffp, 1);
ffp_notify_msg3(ffp, FFP_MSG_BUFFERING_UPDATE, 0, 0);
/// 文件的seek 操作
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->filename);
} else {
/// seek成功
if (is->audio_stream >= 0) {
/// 把packet queue 原先的数据清空,重启编码器,
packet_queue_flush(&is->audioq);
/// flush_pkt的作用是刷新,serial+1,重开一个播放序列
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) {
if (ffp->node_vdec) {
ffpipenode_flush(ffp->node_vdec);
}
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->latest_video_seek_load_serial = is->videoq.serial;
is->latest_audio_seek_load_serial = is->audioq.serial;
is->latest_seek_load_start_at = av_gettime();
}
ffp->dcc.current_high_water_mark_in_ms = ffp->dcc.first_high_water_mark_in_ms;
is->seek_req = 0;
is->queue_attachments_req = 1;
is->eof = 0;
#ifdef FFP_MERGE
if (is->paused)
step_to_next_frame(is);
#endif
completed = 0;
SDL_LockMutex(ffp->is->play_mutex);
if (ffp->auto_resume) {
is->pause_req = 0;
if (ffp->packet_buffering)
is->buffering_on = 1;
ffp->auto_resume = 0;
stream_update_pause_l(ffp);
}
if (is->pause_req)
step_to_next_frame_l(ffp);
SDL_UnlockMutex(ffp->is->play_mutex);
if (ffp->enable_accurate_seek) {
is->drop_aframe_count = 0;
is->drop_vframe_count = 0;
SDL_LockMutex(is->accurate_seek_mutex);
if (is->video_stream >= 0) {
is->video_accurate_seek_req = 1;
}
if (is->audio_stream >= 0) {
is->audio_accurate_seek_req = 1;
}
SDL_CondSignal(is->audio_accurate_seek_cond);
SDL_CondSignal(is->video_accurate_seek_cond);
SDL_UnlockMutex(is->accurate_seek_mutex);
}
/// 发送 seek 的消息通知
ffp_notify_msg3(ffp, FFP_MSG_SEEK_COMPLETE, (int)fftime_to_milliseconds(seek_target), ret);
ffp_toggle_buffering(ffp, 1);
}
3.2.8.4 检测video是否为attached_pic
AV_DISPOSITION_ATTACHED_PIC
是一个标识,如果一个流种包含这个标识的话,那就说明这个流是 一个*.mp3
的文件,并且该流只有一个AVPacket
也就是attached_pick
,所以这个AVPacket
中存储的就是这个 *.mp3
文件的封面图片,所以if
条件成立后,将该AVPacket
送进了 Video queue
队列中;
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(©, &is->video_st->attached_pic)) < 0)
goto fail;
packet_queue_put(&is->videoq, ©);
packet_queue_put_nullpacket(&is->videoq, is->video_stream);
}
is->queue_attachments_req = 0;
}
3.2.8.5 检测队列是否有足够的数据
播放器同时满足infinite_buffer <1
&& !seek_req
&& 音频packe> MIN_FRAMES && 音频packe> MIN_FRAMES && 音频packe> MIN_FRAMES
条件,则 表示缓冲队列有足够的包,不需要继续读取数据;
具体 判断缓冲队列是否有足够的packes
可调用stream_has_enough_packets()
方法,具体细节查看 stream_has_enough_packets()
方法;
if (ffp->infinite_buffer<1 && !is->seek_req &&
#ifdef FFP_MERGE
(is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE
#else
(is->audioq.size + is->videoq.size + is->subtitleq.size > ffp->dcc.max_buffer_size
#endif
|| ( stream_has_enough_packets(is->audio_st, is->audio_stream, &is->audioq, MIN_FRAMES)
&& stream_has_enough_packets(is->video_st, is->video_stream, &is->videoq, MIN_FRAMES)
&& stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->subtitleq, MIN_FRAMES)))) {
if (!is->eof) {
ffp_toggle_buffering(ffp, 0);
}
/* wait 10 ms */
SDL_LockMutex(wait_mutex);
SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
SDL_UnlockMutex(wait_mutex);
continue;
}
static int stream_has_enough_packets(AVStream *st, int stream_id, PacketQueue *queue, int min_frames) {
3.2.8.6 检测是否已经播放结束(是否循环播放、是否自动退出)
(!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))
表示 没有视频流 或者 视频播放完毕
ffplay
的loop
表示该视频流循环播放的次数,默认该值 为1;可以通过参数设置该值;如果是循环播放,则seek
到文件的 指定位置,如果start_time
有值,则seek
到start_time
位置;
/// (!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))
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))) {
if (ffp->loop != 1 && (!ffp->loop || --ffp->loop)) {
/// 是否循环播放 (loop == 1表示只播放一次)
/// seek 到媒体文件头部或 start_time 位置,重新播放
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_statistic_l(ffp);
if (completed) {
av_log(ffp, AV_LOG_INFO, "ffp_toggle_buffering: eof\n");
SDL_LockMutex(wait_mutex);
// infinite wait may block shutdown
while(!is->abort_request && !is->seek_req)
SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 100);
SDL_UnlockMutex(wait_mutex);
if (!is->abort_request)
continue;
} else {
completed = 1;
ffp->auto_resume = 0;
// TODO: 0 it's a bit early to notify complete here
ffp_toggle_buffering(ffp, 0);
toggle_pause(ffp, 1);
if (ffp->error) {
av_log(ffp, AV_LOG_INFO, "ffp_toggle_buffering: error: %d\n", ffp->error);
ffp_notify_msg1(ffp, FFP_MSG_ERROR);
} else {
av_log(ffp, AV_LOG_INFO, "ffp_toggle_buffering: completed: OK\n");
ffp_notify_msg1(ffp, FFP_MSG_COMPLETED);
}
}
}
}
3.2.8.7 使用 av_read_frame 读取数据包
av_read_frame
是ffmpeg
的API
,用于获取未解码的数据包;
/// 读取媒体数据,获得AVPacket(未解码的数据)
ret = av_read_frame(ic, pkt);
3.2.8.8 检测数据是否读取完毕
如果读取AVPacket
失败,则对音频/视频/字幕 队列
插入 空包
,通过解码器刷新buffer
, 将缓存的数据都解码出 frame
并处理;
/// 读取媒体AVPacket
ret = av_read_frame(ic, pkt);
if (ret < 0) {
/// 获取AVPacket 失败
int pb_eof = 0;
int pb_error = 0;
if ((ret == AVERROR_EOF || avio_feof(ic->pb)) && !is->eof) {
/// 如果是文件尾部,则标记
ffp_check_buffering_l(ffp);
pb_eof = 1;
// check error later
}
if (ic->pb && ic->pb->error) {
pb_eof = 1;
pb_error = ic->pb->error;
}
if (ret == AVERROR_EXIT) {
/// 读取Packet 失败的请求,标记异常退出
pb_eof = 1;
pb_error = AVERROR_EXIT;
}
if (pb_eof) {
/// 音频/视频/字幕 插入空Packet 将缓存的packet都解出来 Frame 并处理
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 (pb_error) {
///(同上,err的情况) 音频/视频/字幕 插入空Packet 将缓存的packet都解出来 Frame 并处理
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;
}
3.2.8.9 检测是否在播放范围
stream_start_time
是当前AVStream->start_time
获取到的时间,如果没有定义则为AV_NOPTS_VALUE
,则无效stream_start_time == 0
;
pkt_ts
表示当前packet
的时间戳,pts
有效就用pts
,无效就用dts
;pkt_in_play_range
的值为0 或者1;
当 pkt_ts - start_time
是否 小于duration
为判断,
/* 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;
pkt_in_play_range = ffp->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)(ffp->start_time != AV_NOPTS_VALUE ? ffp->start_time : 0) / 1000000
<= ((double)ffp->duration / 1000000);
if (pkt->stream_index == is->audio_stream && pkt_in_play_range) {
//判断音频packet 是否在播放范围
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 是否在播放范围
packet_queue_put(&is->videoq, pkt);
} else if (pkt->stream_index == is->subtitle_stream && pkt_in_play_range) {
//判断字幕packet 是否在播放范围
packet_queue_put(&is->subtitleq, pkt);
} else {
// 不符合条件丢弃
av_packet_unref(pkt);
}
3.2.8.10 将数据插入相应的 frame queue 中(音频/视频/字幕)
/* check if packet is in play range specified by user, then queue, otherwise discard */
/// 分别将音/视频/字幕 AVPacket 送入 相应的队列
stream_start_time = ic->streams[pkt->stream_index]->start_time;
pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;
pkt_in_play_range = ffp->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)(ffp->start_time != AV_NOPTS_VALUE ? ffp->start_time : 0) / 1000000
<= ((double)ffp->duration / 1000000);
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);
}
至此 stream_open()
开启read_thread
线程读取AVPacket
的 介绍基本介绍完了;
本文篇幅较长,需要极大的耐心,需要读者参考ijkplayer 源码一起阅读,会有更好的效果;
书读百遍其义自见,代码也一样
网友评论