x265源码分析之帧类型决策

Lookahead

Lookahead是预处理模块,负责对输入的待编码图像进行下采样、复杂度预计算、帧类型决策等编码前期处理。

在Lookahead中维护了两个队列,外部输入的待编码图像按照输入的顺序放入到m_inputQueue队列中等待预处理,当m_inputQueue中数量超过指定阈值时,就会开始预处理,预处理完毕的就会根据DTS顺序放入到m_outputQueue中等待取出编码。

1
2
3
4
5
6
7
8
class Lookahead : public JobProvider
{
public:
// 输入队列,PTS顺序
PicList m_inputQueue; // input pictures in order received
// 输出队列,DTS顺序
PicList m_outputQueue; // pictures to be encoded, in encode order
}

create

每个Encoder都持有一个Lookahead,在Encoder创建时,Lookahead也会被创建。Lookahead内部拥有一个线程池,每次预处理操作都是在子线程中进行。lookaheadThreads默认是0,默认Lookahead和FrameEncoder共用一个线程池。线程池中的线程会在此时全部启动并阻塞等待着唤醒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void Encoder::create()
{
int pools = m_numPools;
ThreadPool* lookAheadThreadPool = 0;
// lookaheadThreads默认是0
if (m_param->lookaheadThreads > 0)
{
lookAheadThreadPool = ThreadPool::allocThreadPools(p, pools, 1);
}
else
lookAheadThreadPool = m_threadPool; // 和FrameEncoder共用线程池
m_lookahead = new Lookahead(m_param, lookAheadThreadPool);
if (pools)
{
m_lookahead->m_jpId = lookAheadThreadPool[0].m_numProviders++;
lookAheadThreadPool[0].m_jpTable[m_lookahead->m_jpId] = m_lookahead;
}
if (m_param->lookaheadThreads > 0)
for (int i = 0; i < pools; i++)
lookAheadThreadPool[i].start(); // 启动线程池中的所有线程
m_lookahead->m_numPools = pools;

if (!m_lookahead->create())
m_aborted = true;
}

addPicture

接入一个输入图像,放入到m_inputQueue,每次add都会尝试一次支持预处理任务

1
2
3
4
5
void Lookahead::addPicture(Frame& curFrame, int sliceType)
{
checkLookaheadQueue(m_inputCount); // 尝试一次支持预处理任务
addPicture(curFrame); // 放入到m_inputQueue
}

将输入图像放入到m_inputQueue中

1
2
3
4
5
6
7
void Lookahead::addPicture(Frame& curFrame)
{
m_inputLock.acquire();
m_inputQueue.pushBack(curFrame);
m_inputLock.release();
m_inputCount++;
}

checkLookaheadQueue

尝试执行一次预处理任务,判断当前m_inputQueue中的数量是否大于阈值。

1
2
3
4
5
6
7
/* The number of frames that must be queued in the lookahead before it may
* make slice decisions. Increasing this value directly increases the encode
* latency. The longer the queue the more optimally the lookahead may make
* slice decisions, particularly with b-adapt 2. When cu-tree is enabled,
* the length of the queue linearly increases the effectiveness of the
* cu-tree analysis. Default is 40 frames, maximum is 250 */
int lookaheadDepth; // 进行预处理必须的帧数,只有待处理帧数大于该值才会进行处理

当filled为true时,getDecidedPicture从m_outputQueue中返回数据,如果m_outputQueue为空,则会阻塞等待

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void Lookahead::checkLookaheadQueue(int &frameCnt)
{
/* determine if the lookahead is (over) filled enough for frames to begin to
* be consumed by frame encoders */
if (!m_filled)
{
if (!m_param->bframes & !m_param->lookaheadDepth)
m_filled = true; /* zero-latency */
else if (frameCnt >= m_param->lookaheadDepth + 2 + m_param->bframes)
m_filled = true; /* full capacity plus mini-gop lag */ // 当剩余输入的帧数量达到一定值,才会开启编码,为了避免encode阻塞
}

m_inputLock.acquire();
if (m_pool && m_inputQueue.size() >= m_fullQueueSize)
tryWakeOne(); // 从线程池中唤醒一个空闲线程,执行findJob
m_inputLock.release();
}

findJob

子线程中执行,子线程每次被唤醒都会调用该方法来获取需要处理的任务,任务执行完成则继续等待下次唤醒。虽然Lookahead绑定了线程池,可以多线程同时处理,但是帧类型决策是不支持多线程的,所以通过m_sliceTypeBusy来保证同一时间只会有一个线程真正的进行预处理任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void Lookahead::findJob(int /*workerThreadID*/)
{
bool doDecide;

m_inputLock.acquire();
// 虽然预处理是可以被多个线程同时处理的,但是帧类型决策是不能同时多个线程一起的,所以当一个线程标记m_sliceTypeBusy后,其它线程不可以再进行帧类型决策
if (m_inputQueue.size() >= m_fullQueueSize && !m_sliceTypeBusy && m_isActive)
doDecide = m_sliceTypeBusy = true; // 标记当前线程进行帧类型决策
else
doDecide = m_helpWanted = false; // 标记当前线程不进行帧类型决策
m_inputLock.release();
// 如果已经有线程在处理了,则当前线程退出任务
if (!doDecide)
return;

ProfileLookaheadTime(m_slicetypeDecideElapsedTime, m_countSlicetypeDecide);
ProfileScopeEvent(slicetypeDecideEV);

slicetypeDecide(); // 帧类型决策

m_inputLock.acquire();
if (m_outputSignalRequired)
{
m_outputSignal.trigger(); // 唤醒等待线程,如果在getDecidedPicture中阻塞了
m_outputSignalRequired = false;
}
m_sliceTypeBusy = false;
m_inputLock.release();
}

slicetypeDecide

子线程中执行,进行帧类型决策,每一次帧类型决策都是决策到下一个非b帧为止,决策完成的帧会放入到输出队列中等待编码线程进行处理。

下采样

对每个要预处理的帧,都初始化一个1/2下采样的图像,因为下采样任务是可以多线程并行的,所以是通过线程池分发的,会同步等待所有任务都处理完成。

frames
存储所有参与本次帧决策的帧,数据来源为m_inputQueue中每一帧的下采样数据,长度为lookaheadDepth + 1,frames[0]为m_lastNonB(上一个非b帧)。

list
存放的是本次帧决策中连续B帧以及B帧的前后参考帧,长度为bframes + 2。(最终被真正被决策的帧)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 需要预处理的帧列表
PreLookaheadGroup pre(*this);
Lowres* frames[X265_LOOKAHEAD_MAX + X265_BFRAME_MAX + 4]; // 所有参与到帧类型决策的帧的下采样图像
Frame* list[X265_BFRAME_MAX + 4]; // b帧列表
memset(frames, 0, sizeof(frames));
memset(list, 0, sizeof(list));
int maxSearch = X265_MIN(m_param->lookaheadDepth, X265_LOOKAHEAD_MAX);
maxSearch = X265_MAX(1, maxSearch);

{
ScopedLock lock(m_inputLock);

Frame *curFrame = m_inputQueue.first();
int j;
// (连续b帧 + 后非b帧)数量的帧放入到list中进行b帧的决策
for (j = 0; j < m_param->bframes + 2; j++)
{
if (!curFrame) break;
list[j] = curFrame;
curFrame = curFrame->m_next;
}

curFrame = m_inputQueue.first();
frames[0] = m_lastNonB;
// 在m_inputQueue遍历lookaheadDepth个图像
for (j = 0; j < maxSearch; j++)
{
if (!curFrame) break;
frames[j + 1] = &curFrame->m_lowres;
// 如果当前帧没有进行下采样处理,加入到预处理任务组中
if (!curFrame->m_lowresInit)
pre.m_preframes[pre.m_jobTotal++] = curFrame;

curFrame = curFrame->m_next;
}

maxSearch = j;
}

/* perform pre-analysis on frames which need it, using a bonded task group */
if (pre.m_jobTotal)
{
// 线程池分发任务,异步处理,进行1/2下采样,并计算帧内预测的成本估计,frames[i].costEst[0][0]
if (m_pool)
pre.tryBondPeers(*m_pool, pre.m_jobTotal);
pre.processTasks(-1);
// 等待任务组处理完成
pre.waitForExit();
}

帧类型决策

帧类型决策,决策B帧,如果没有开启B帧,则无需决策。

1
2
3
4
5
6
7
8
9
// 开启了b帧,并且前向的非b帧有值
if (m_lastNonB &&
((m_param->bFrameAdaptive && m_param->bframes) ||
m_param->rc.cuTree || m_param->scenecutThreshold || m_param->bHistBasedSceneCut ||
(m_param->lookaheadDepth && m_param->rc.vbvBufferSize)))
{
if(!m_param->rc.bStatRead)
slicetypeAnalyse(frames, false);
}

决策帧准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int numFrames, origNumFrames, keyintLimit, framecnt;
int maxSearch = X265_MIN(m_param->lookaheadDepth, X265_LOOKAHEAD_MAX);
int cuCount = m_8x8Blocks;
int resetStart;
bool bIsVbvLookahead = m_param->rc.vbvBufferSize && m_param->lookaheadDepth;

// 统计未决策的帧数量
for (framecnt = 0; framecnt < maxSearch; framecnt++)
{
Lowres *fenc = frames[framecnt + 1];
if (!fenc || fenc->sliceType != X265_TYPE_AUTO)
break;
}
// 如果当前列表中的帧都已经决策过,则跳过
if (!framecnt)
{
if (m_param->rc.cuTree)
cuTree(frames, 0, bKeyframe);
return;
}
frames[framecnt + 1] = NULL;
// 关键帧最大间隔
int keylimit = m_param->keyframeMax;
// 关键帧当前间隔
int keyFrameLimit = keylimit + m_lastKeyframe - frames[0]->frameNum - 1;
// 如果当前未决策帧中需要插入关键帧,则截断,numFrames为截断后的剩余帧数量,framecnt为截断前数量,numFrames < framecnt表明中间要插入一个关键帧
origNumFrames = numFrames = m_param->bIntraRefresh ? framecnt : X265_MIN(framecnt, keyintLimit);
if (bIsVbvLookahead)
numFrames = framecnt;
else if (m_param->bOpenGOP && numFrames < framecnt) // 对于OpenGOP来说,可以参考下个GOP的关键帧,所以numFrames+1
numFrames++;
else if (numFrames == 0) // numFrames为0,表明当前第一帧就必须是关键帧,则直接将frames[1]定义为I帧,返回,在帧类型修正中会决定是否修正为IDR
{
frames[1]->sliceType = X265_TYPE_I;
return;
}

参考帧成本估算

成本估算搜索的是运动矢量,CostEstimateGroup表示一个任务组,组内的任务会并行处理。这里主要针对每一帧计算其所有可以的前后参考帧组合下的成本,方便后面进行最优路径搜索时进行比较。frames[i].costEst中存储了估算结果,costEst是个二维数组,第一维是当前帧与前向参考帧的距离,第二维是当前帧与后向参考帧的距离。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
if (m_bBatchMotionSearch)
{
// 成本估计任务组,frames为数据源
CostEstimateGroup estGroup(*this, frames);
// b表示第几帧,从1开始,由于第1帧是上一个非B帧,所以从第2帧开始计算
for (int b = 2; b < numFrames; b++)
{
// 每帧预测的帧范围, 前向参考帧范围[b - (bframes + 1), b),后向参考帧范围(b, b + (bframes + 1)]
for (int i = 1; i <= m_param->bframes + 1; i++)
{
int p0 = b - i; // 前向参考帧
if (p0 < 0)
continue;

/* Skip search if already done */
if (frames[b]->lowresMvs[0][i][0].x != 0x7FFF)
continue;

/* perform search to p1 at same distance, if possible */
int p1 = b + i; // 后向参考帧,前向参考帧和后向参考帧按照相同的距离去搜索
if (p1 >= numFrames || frames[b]->lowresMvs[1][i][0].x != 0x7FFF)
p1 = b;

estGroup.add(p0, p1, b); // 添加任务
}
}
/* auto-disable after the first batch if pool is small */
m_bBatchMotionSearch &= m_pool->m_numWorkers >= 4;
estGroup.finishBatch(); // 异步处理所有任务,阻塞等待任务完成

if (m_bBatchFrameCosts)
{
/* pre-calculate all frame cost estimates, using many worker threads */
for (int b = 2; b < numFrames; b++)
{
// 前向参考帧范围[b - (bframes + 1), b)
for (int i = 1; i <= m_param->bframes + 1; i++)
{
if (b < i)
continue;

/* only measure frame cost in this pass if motion searches
* are already done */
if (frames[b]->lowresMvs[0][i][0].x == 0x7FFF)
continue;

int p0 = b - i; // 前向参考帧,前向参考帧和后向参考帧按照不同的距离去搜索
// 后向参考帧范围[b, b + (bframes )]
for (int j = 0; j <= m_param->bframes; j++)
{
int p1 = b + j; // 后向参考帧
if (p1 >= numFrames)
break;

/* ensure P1 search is done */
if (j && frames[b]->lowresMvs[1][j][0].x == 0x7FFF)
continue;

/* ensure frame cost is not done */
if (frames[b]->costEst[i][j] >= 0)
continue;

estGroup.add(p0, p1, b); // 添加任务
}
}
}

/* auto-disable after the first batch if the pool is not large */
m_bBatchFrameCosts &= m_pool->m_numWorkers > 12;
estGroup.finishBatch(); // 异步处理所有任务,阻塞等待任务完成
}
}

场景切换

检测决策数组中第一帧是否和上个非B帧是场景切换,如果是则需要重新定义上一个参考帧,本次决策退出

1
2
3
4
5
6
7
8
9
10
11
bool isScenecut = false;

/* When scenecut threshold is set, use scenecut detection for I frame placements */
if (!m_param->bHistBasedSceneCut || (m_param->bHistBasedSceneCut && frames[1]->bScenecut))
isScenecut = scenecut(frames, 0, 1, true, origNumFrames); // 检测当前frames[0]和frames[1]是否是场景切换

if (isScenecut && (m_param->bHistBasedSceneCut || m_param->scenecutThreshold))
{
frames[1]->sliceType = X265_TYPE_I; // 如果是场景切换,则将第一帧frames[1]定义为I帧,并退出
return;
}

B帧决策

B帧决策策略有三种,一种是X265_B_ADAPT_TRELLIS最优决策,一种是X265_B_ADAPT_FAST最快决策,最后一种就是没有决策X265_B_ADAPT_NONE。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
    int numBFrames = 0;
int numAnalyzed = numFrames;

if (m_param->bframes)
{
if (m_param->bFrameAdaptive == X265_B_ADAPT_TRELLIS)
{
...
}
else if (m_param->bFrameAdaptive == X265_B_ADAPT_FAST)
{
...
}
else
{
...
}

{
// 遍历第一段连续B帧,检测是否有场景切换,如果有则将该帧设置为P帧
for (int j = 1; j < numBFrames + 1; j++)
{
bool isNextScenecut = false;
if (!m_param->bHistBasedSceneCut || (m_param->bHistBasedSceneCut && frames[j + 1]->bScenecut))
isNextScenecut = scenecut(frames, j, j + 1, false, origNumFrames); // 下一帧是否是场景切换
if (isNextScenecut || (bForceRADL && frames[j]->frameNum == preRADL))
{
frames[j]->sliceType = X265_TYPE_P; // 将当前帧设置为P帧,下一帧会放到下一次处理
numAnalyzed = j;
break;
}
}
}
resetStart = bKeyframe ? 1 : X265_MIN(numBFrames + 2, numAnalyzed + 1); // 只保留第一段连续B帧的预测,也就是numBFrames+2长度
}
else
{
for (int j = 1; j <= numFrames; j++)
frames[j]->sliceType = X265_TYPE_P; // 没有开启B帧情况下,每一帧都是P帧

resetStart = bKeyframe ? 1 : 2;
}

// 将第一段连续B帧后面的帧类型都重置为0
for (int j = resetStart; j <= numFrames; j++)
{
frames[j]->sliceType = X265_TYPE_AUTO;
/* If any frame marked as scenecut is being restarted for sliceDecision,
* undo scene Transition flag */
if (j <= maxp1 && frames[j]->bScenecut && m_isSceneTransition)
m_isSceneTransition = false;
}
}
X265_B_ADAPT_TRELLIS

最优路径决策主要是根据参与决策的帧数量,依次计算1~n个帧长度下,每个长度下所有的可能路径组合,选出每个长度下最优的路径,最后算出n个长度的最优路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
每个长度的帧路径组合有bframes+1条,每条路径的最后一帧一定是P,P前面可选的是0个B...1个B...bframes个B,再前面的就是剩余长度下的最优路径。

# numFrames = 20, bframes = 4
[
(前19个帧最优路径) p
(前18个帧最优路径) B P
(前17个帧最优路径) BB p
(前16个帧最优路径) BBB p
(前15个帧最优路径) BBBB p
]

计算上面每条路径的最优成本,根据上面已经计算过的参考帧成本

Example:
numFrames = 1, bframes = 4
[
P
]
numFrames = 2, bframes = 4
[
P P
B P
]
numFrames = 3, bframes = 4
[
BP P
P B P
BB P
]
numFrames = 4, bframes = 4
[
BBP P
BP B P
P BB P
BBB P
]
numFrames = 5, bframes = 4
[
BBBP P
BBP B P
BP BB P
P BBB P
BBBB P
]
numFrames = 6, bframes = 4
[
BBBBP P
BBBP B P
BBP BB P
BP BBB P
P BBBB P
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (m_param->bFrameAdaptive == X265_B_ADAPT_TRELLIS)
{
if (numFrames > 1)
{
char best_paths[X265_BFRAME_MAX + 1][X265_LOOKAHEAD_MAX + 1] = { "", "P" };
// numFrames长度对应的路径,numFrames可能大于X265_BFRAME_MAX + 1,best_paths是循环存储的
int best_path_index = numFrames % (X265_BFRAME_MAX + 1);

// 计算2~numFrames长度的帧最优路径
for (int j = 2; j <= numFrames; j++)
slicetypePath(frames, j, best_paths);

numBFrames = (int)strspn(best_paths[best_path_index], "B"); // 统计第一个连续B帧的数量

// 根据最优路径结果设置帧类型
for (int j = 1; j < numFrames; j++)
frames[j]->sliceType = best_paths[best_path_index][j - 1] == 'B' ? X265_TYPE_B : X265_TYPE_P;
}
frames[numFrames]->sliceType = X265_TYPE_P;
}
X265_B_ADAPT_FAST

快速决策就是遍历所有帧,每次决策下一帧的帧类型,对于下一帧来说,只有B帧和P帧两种情况。计算当下一帧为B帧时的成本为cost1b1,下下帧的成本为cost2p1;当下一帧为P帧时的成本为cost1p0,下下帧的成本为cost2p0。在快速决策策略下,假设每帧的参考帧都是前后相邻的两帧,只会考虑到前后相邻两帧的影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
            CostEstimateGroup estGroup(*this, frames);

int64_t cost1p0, cost2p0, cost1b1, cost2p1;

for (int i = 0; i <= numFrames - 2; )
{
cost2p1 = estGroup.singleCost(i + 0, i + 2, i + 2, true); // i+2作为P帧,参考帧为i
cost1b1 = estGroup.singleCost(i + 0, i + 2, i + 1); // i+1作为B帧,参考帧为i和i+2

cost1p0 = estGroup.singleCost(i + 0, i + 1, i + 1); // i+1作为P帧,参考帧为i
cost2p0 = estGroup.singleCost(i + 1, i + 2, i + 2); // i+2作为P帧,参考帧为i+1
// 如果下一帧作为P帧的成本小,则将下一帧设置为P帧
if (cost1p0 + cost2p0 < cost1b1 + cost2p1)
{
frames[i + 1]->sliceType = X265_TYPE_P;
i += 1;
continue;
}

// arbitrary and untuned
#define INTER_THRESH 300
#define P_SENS_BIAS (50 - m_param->bFrameBias)
frames[i + 1]->sliceType = X265_TYPE_B; // 否则,将下一帧设置为B帧,参考第i帧
// 如果下一帧为B帧,则根据bframes,将后续帧尝试设置为B帧
int j;
for (j = i + 2; j <= X265_MIN(i + m_param->bframes, numFrames - 1); j++)
{
int64_t pthresh = X265_MAX(INTER_THRESH - P_SENS_BIAS * (j - i - 1), INTER_THRESH / 10);
int64_t pcost = estGroup.singleCost(i + 0, j + 1, j + 1, true); // 后续每一帧尝试参考第i帧,计算成本
if (pcost > pthresh * cuCount || frames[j + 1]->intraMbs[j - i + 1] > cuCount / 3) // 如果该帧的运动矢量中帧内CU数量超过了1/3,则认为该帧不适合作为B帧
break;
frames[j]->sliceType = X265_TYPE_B;
}
// j为连续B帧后的P帧
frames[j]->sliceType = X265_TYPE_P;
i = j; // 从j开始
}
frames[numFrames]->sliceType = X265_TYPE_P; // 确保最后一帧是P帧
numBFrames = 0;
while (numBFrames < numFrames && frames[numBFrames + 1]->sliceType == X265_TYPE_B)
numBFrames++; // 统计第一个连续B帧的数量
X265_B_ADAPT_NONE

没有B帧决策策略就是完全根据bframes来,分配固定的连续B帧。

1
2
3
4
5
numBFrames = X265_MIN(numFrames - 1, m_param->bframes); // 统计第一个连续B帧的数量
for (int j = 1; j < numFrames; j++)
frames[j]->sliceType = (j % (numBFrames + 1)) ? X265_TYPE_B : X265_TYPE_P;

frames[numFrames]->sliceType = X265_TYPE_P;

帧类型修正

根据外部设定的最大连续数量、B参考帧数量、关键帧间隔数量等条件,对帧类型预测的结果进行修正,使结果满足外部设定的条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
int bframes, brefs; // b帧的数量(包含b参考帧), b参考帧的数量
if (!m_param->analysisLoad || m_param->bAnalysisType == HEVC_INFO)
{
// 修正帧类型,直到下一个非b帧停止,bframes是循环次数,也是b帧的数量
for (bframes = 0, brefs = 0;; bframes++)
{
Lowres& frm = list[bframes]->m_lowres;

// 处理b参考帧数量超出情况
if (frm.sliceType == X265_TYPE_BREF && !m_param->bBPyramid && brefs == m_param->bBPyramid)
{
// 如果当前帧是b参考帧,但是b参考帧没有开启或者已经超过了数量,则改为b帧(默认bBPyramid为1,一个连续的b帧中只能出现一次b参考帧)
frm.sliceType = X265_TYPE_B;
}
else if (frm.sliceType == X265_TYPE_BREF && m_param->bBPyramid && brefs &&
m_param->maxNumReferences <= (brefs + 3))
{
// 如果当前帧是b参考帧,但是总参考帧数量已经超过了最大值,则改为b帧
frm.sliceType = X265_TYPE_B;
}

// 处理gop首帧
if (((!m_param->bIntraRefresh || frm.frameNum == 0) && frm.frameNum - m_lastKeyframe >= m_param->keyframeMax &&
(!m_extendGopBoundary || frm.frameNum - m_lastKeyframe >= m_param->keyframeMax + m_param->gopLookahead)) ||
(frm.frameNum == (m_param->chunkStart - 1)) || (frm.frameNum == m_param->chunkEnd))
{
// Close GOP首帧为IDR,Open GOP只有第一个GOP的第一帧才是IDR,后面的首帧都是I
if (frm.sliceType == X265_TYPE_AUTO || frm.sliceType == X265_TYPE_I)
frm.sliceType = m_param->bOpenGOP && m_lastKeyframe >= 0 ? X265_TYPE_I : X265_TYPE_IDR;
}

if (frm.bIsFadeEnd){
frm.sliceType = m_param->bOpenGOP && m_lastKeyframe >= 0 ? X265_TYPE_I : X265_TYPE_IDR;
}

// 关键帧更新和标记
if ((frm.sliceType == X265_TYPE_I && frm.frameNum - m_lastKeyframe >= m_param->keyframeMin) || (frm.frameNum == (m_param->chunkStart - 1)) || (frm.frameNum == m_param->chunkEnd))
{
// 如果当前帧是I帧,并且当前帧数距离上一个关键帧已经超过了最小关键帧的距离,需要产生新的关键帧
// OpenGOP下将当前帧标记为关键帧,CloseGOP下将当前帧改为IDR帧
if (m_param->bOpenGOP)
{
m_lastKeyframe = frm.frameNum;
frm.bKeyframe = true;
}
else
frm.sliceType = X265_TYPE_IDR;
}
// 如果当前帧是IDR,需要产生新的关键帧
if (frm.sliceType == X265_TYPE_IDR)
{
/* Closed GOP */
m_lastKeyframe = frm.frameNum;
frm.bKeyframe = true;
}

// b帧数量已经满了,或者下一帧为空
if (bframes == m_param->bframes || !list[bframes + 1])
{
// 如果当前帧类型没有决策,或者当前帧类型是b帧或者b参考帧,则当前帧为P帧
// 如果当前帧类型是I帧或者P帧,则不变
if (frm.sliceType == X265_TYPE_AUTO || IS_X265_TYPE_B(frm.sliceType))
frm.sliceType = X265_TYPE_P;
}
if (frm.sliceType == X265_TYPE_BREF)
brefs++; // b参考帧计数
if (frm.sliceType == X265_TYPE_AUTO) // 帧类型暂定为b帧
frm.sliceType = X265_TYPE_B;
else if (!IS_X265_TYPE_B(frm.sliceType)) // 当前帧不是b帧也不是b参考帧,则退出循环
break;
}
}
else
{
for (bframes = 0, brefs = 0;; bframes++)
{
Lowres& frm = list[bframes]->m_lowres;
if (frm.sliceType == X265_TYPE_BREF)
brefs++;
if ((IS_X265_TYPE_I(frm.sliceType) && frm.frameNum - m_lastKeyframe >= m_param->keyframeMin)
|| (frm.frameNum == (m_param->chunkStart - 1)) || (frm.frameNum == m_param->chunkEnd))
{
m_lastKeyframe = frm.frameNum;
frm.bKeyframe = true;
}
if (!IS_X265_TYPE_B(frm.sliceType))
break;
}
}
// 第bframes帧为非B帧,该帧记录前面的B帧数量
list[bframes]->m_lowres.leadingBframes = bframes;
// 更新最新的非B帧
m_lastNonB = &list[bframes]->m_lowres;

if (m_param->bBPyramid && bframes > 1 && !brefs)
{
// 如果开启B参考帧(B帧),选取中间的b帧为参考B帧,参考B帧前面的b帧的后向参考为参考B帧,参考B帧后面的b帧的前向参考为参考B帧
list[bframes / 2]->m_lowres.sliceType = X265_TYPE_BREF;
brefs++;
}

加入输出队列

入队顺序为先加入最后的非B帧,再加入B参考帧,最后再加入B帧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
    m_inputLock.acquire();

// 收集PTS
int64_t pts[X265_BFRAME_MAX + 1];
for (int i = 0; i <= bframes; i++)
{
Frame *curFrame;
curFrame = m_inputQueue.popFront();
pts[i] = curFrame->m_pts;
maxSearch--;
}
m_inputLock.release();

m_outputLock.acquire();
/* add non-B to output queue */
int idx = 0;
// m_reorderedPts为DTS
list[bframes]->m_reorderedPts = pts[idx++];
m_outputQueue.pushBack(*list[bframes]); //先将最后的非B帧入队

// 加入B参考帧
if (brefs)
{
for (int i = 0; i < bframes; i++)
{
if (list[i]->m_lowres.sliceType == X265_TYPE_BREF)
{
list[i]->m_reorderedPts = pts[idx++];
m_outputQueue.pushBack(*list[i]);
}
}
}

// 加入B帧
for (int i = 0; i < bframes; i++)
{
/* push all the B frames into output queue except B-ref, which already pushed into output queue */
if (list[i]->m_lowres.sliceType != X265_TYPE_BREF)
{
list[i]->m_reorderedPts = pts[idx++];
m_outputQueue.pushBack(*list[i]);
}
}

m_outputLock.release();
}