做需求重温小学奥数,视频流大小卡混排实现原理大揭秘

111 阅读6分钟

背景

很多视频流app都有大小卡混排的页面,下面举4个例子。 在实现上,整个视频流的数据可能是一个一维列表的形式,比如[大卡,小卡,小卡,大卡,小卡,小卡...],然后客户端再把这个一维列表渲染成一行大卡,几行小卡的形式。不是任意一个一维列表都能正确渲染出页面,大卡在列表中的位置需要满足一定的要求。本文介绍针对固定每隔n行出一个大卡的情况,怎样给出一个合理的一维列表。

我们先定义下面几个变量:

  • numPerRow:每行出几个小卡。
  • firstRow:第一个大卡出现在第几行,从1开始数。
  • intervalRow:两个大卡之间有多少行小卡。

那么第一个大卡的下标为(firstRow1)×numPerRow(firstRow-1)\times numPerRow,之后每个大卡的下标间隔为numPerRow×intervalRow+1numPerRow\times intervalRow+1。 因此大卡的下标集合为BigI={(firstRow1)×numPerRow+n×(numPerRow×intervalRow+1)nZ0}BigI=\{(firstRow-1)\times numPerRow + n\times (numPerRow\times intervalRow+1) | n\in \mathbb{Z}_{\geq 0}\}

在实现上我们有几个问题。视频流是分次拉取的,所以我们要知道这一刷中哪几个是大卡。一种实现方式是每次下发一个周期,也就是每次下发numPerRow×intervalRow+1numPerRow\times intervalRow+1个视频,这样每一刷的第一个都是大卡,且仅有一个大卡。但是这样的方式限制了每一刷返回多少个视频,而这个值是一个会影响体验的重要指标,经过多轮实验,我们终于找到一个完美的值,我们不能因为要实现大小卡混发而随意改动这个值。所以我们要用较为复杂的方法计算每一刷中哪几个视频是大卡。同时如果在一刷的最后一行是小卡,而数量不满numPerRow,就会出现右下角缺角,这也是很不好的体验,我们要避免。所以我们不能保证每一刷返回的视频数相同,我们采用把最后缺角的一行过滤掉的办法,最大限度减少对每一刷返回视频数量的影响。于是我们面临两个问题,一个是要感知本刷哪几个视频是大卡,一个是要感知一刷最后一行有几个视频,如果出现缺角就执行过滤。下面我们说说怎样解决这两个问题。

1. 本刷哪几个视频是大卡。

视频流是分次拉取的,一般每次请求,客户端会带一个offset过来,表示已经出了多少个视频,包括大卡与小卡。给定一个offset,我们要知道这一刷中哪几个是大卡。一种实现方式是每次下发一个周期,也就是每次下发numPerRow×intervalRow+1numPerRow\times intervalRow+1个视频,这样每一刷的第一个都是大卡,且仅有一个大卡。但是这样的方式限制了每一刷返回多少个视频,而这个值是一个会影响体验的重要指标,经过多轮实验,我们终于找到一个完美的值,我们不能因为要实现大小卡混发而随意改动这个值。所以我们要用较为复杂的方法计算每一刷中哪几个视频是大卡。

这个问题基本等同于问截至目前为止,下一个大卡在哪个位置。再后面的大卡位置通过加间隔就能获得。

也就是给定一个offset,我们要找到一个最小的ioffseti\geq offset使得iBigIi\in BigI

i=(firstRow1)×numPerRow+ni×(numPerRow×intervalRow+1)i=(firstRow-1)\times numPerRow + n_i\times (numPerRow\times intervalRow+1),我们有

(firstRow1)×numPerRow+ni×(numPerRow×intervalRow+1)offset(firstRow-1)\times numPerRow + n_i\times (numPerRow\times intervalRow+1)\geq offset,且

(firstRow1)×numPerRow+(ni1)×(numPerRow×intervalRow+1)<offset(firstRow-1)\times numPerRow + (n_i-1)\times (numPerRow\times intervalRow+1)< offset

我们把问题抽象一下,变成

a×ni+bca\times n_i + b\geq ca×(ni1)+b<c(a>0,niZ)a\times (n_i-1) + b< c (a>0,n_i\in \mathbb{Z})

于是nicban_i\geq \frac{c-b}{a}ni1<cban_i-1<\frac{c-b}{a}

x=cbax=\frac{c-b}{a},也就是说nin_i是最小的大于等于xx的整数,这就是数学上天花板的定义,记为

ni=x=cba=offset(firstRow1)×numPerRow(numPerRow×intervalRow+1)n_i=\lceil x\rceil=\lceil \frac{c-b}{a}\rceil=\lceil \frac{offset-(firstRow-1)\times numPerRow}{(numPerRow\times intervalRow+1)}\rceil

只要算出nin_i我们就能算出ii。虽然Go的标准库提供了math.Ceil函数来计算天花板,但是它的入参是float64,浮点数存在精度问题。如果一个数xx的精确值为1,那么x=1\lceil x\rceil=1。但是如果表示用float64表示,有可能x=1.00001x=1.00001,math.Ceil(x)=2,导致结果差了1,这样的误差显然是不可接受的,所以我们不能用这个方法来计算天花板。

为了算天花板,我们请出它的兄弟,地板。一个实数rr的地板ff,是一个满足frf\leq r的最大的整数,记作f=rf=\lfloor r\rfloor。我们发现在编程语言中,有一个与天花板和地板有千丝万缕关系的运算,整数除法。在包括Go在内的很多编程语言中,如果a和b均为整数类型,a/b的结果也为整数类型,其运算规则为计算ab\frac{a}{b}的值,保留整数部分,丢弃小数部分。下文用/int/_{int}来表示整数除法。事实上,

ab0\frac{a}{b}\geq 0时,a/intb=aba/_{int}b=\lfloor \frac{a}{b}\rfloor; 当ab0\frac{a}{b}\leq 0时,a/intb=aba/_{int}b=\lceil \frac{a}{b}\rceil

这样当ab0\frac{a}{b}\leq 0时,我们会算天花板了,但当ab>0\frac{a}{b}> 0时,我们还是不会算。但其实我们可以用地板来计算天花板,下面我们介绍怎样做。

我们证明当a和b均为正整数时,ab=a+b1b\left\lceil \frac{a}{b} \right\rceil = \left\lfloor \frac{a + b - 1}{b} \right\rfloor

ab\frac{a}{b}为整数时,ab=ab\lceil \frac{a}{b}\rceil=\frac{a}{b}。 且aba+b1b<ab+1\frac{a}{b}\leq \frac{a + b - 1}{b} <\frac{a}{b}+1, 因此a+b1b=ab=ab\left\lfloor \frac{a + b - 1}{b} \right\rfloor=\frac{a}{b}=\left\lceil \frac{a}{b} \right\rceil

ab\frac{a}{b}不为整数时,我们把aa写作a=k×b+ca=k\times b+c,其中k与c为整数,且0<c<b0< c < b。那么ab=k+cb\frac{a}{b}=k+\frac{c}{b}。 易得ab=k+1\lceil \frac{a}{b}\rceil=k+1a+b1b=k+1+c1b\frac{a + b - 1}{b}=k+1+\frac{c-1}{b}。 易得k+1k+1+c1b<k+2k+1\leq k+1+\frac{c-1}{b}< k+2, 因此a+b1b=k+1=ab\left\lfloor \frac{a + b - 1}{b} \right\rfloor=k+1=\left\lceil \frac{a}{b} \right\rceil。 证完。

这样我们就打通了整个计算流程。

offset(firstRow1)×numPerRowoffset\leq (firstRow-1)\times numPerRow时,实际上表示当前从未展示过大卡,所以i=(firstRow1)×numPerRowi=(firstRow-1)\times numPerRow。用上面公式计算会有问题,因为上面的讨论把nin_i的范围从非负整数放大到所有整数。

offset>(firstRow1)×numPerRowoffset> (firstRow-1)\times numPerRow时,

ni=offset(firstRow1)×numPerRow(numPerRow×intervalRow+1)=offset(firstRow1)×numPerRow+(numPerRow×intervalRow+1)1(numPerRow×intervalRow+1)=(offset(firstRow1)×numPerRow+numPerRow×intervalRow)/int(numPerRow×intervalRow+1)n_i=\lceil \frac{offset-(firstRow-1)\times numPerRow}{(numPerRow\times intervalRow+1)}\rceil=\lfloor \frac{offset-(firstRow-1)\times numPerRow+(numPerRow\times intervalRow+1)-1}{(numPerRow\times intervalRow+1)}\rfloor=(offset-(firstRow-1)\times numPerRow+numPerRow\times intervalRow)/_{int}(numPerRow\times intervalRow+1)

i=(firstRow1)×numPerRow+ni×(numPerRow×intervalRow+1)i=(firstRow-1)\times numPerRow + n_i\times (numPerRow\times intervalRow+1)

2. 本刷应该返回多少个视频

如果在一刷的最后一行是小卡,而数量不满numPerRow,就会出现右下角缺角,这也是很不好的体验,我们要避免,所以我们不能保证每一刷返回的视频数相同。我们采用把最后缺角的一行过滤掉的办法,最大限度减少对每一刷返回视频数量的影响。我们用maxLimit表示每一刷最多出多少个视频。

也就是给定offsetmaxLimit,我们要求一个最大的小于等于offset+maxLimit1offset+maxLimit-1的下标jj使得第jj个视频要么是一个大卡,要么所在行有numPerRow个小卡。

如果offset+maxLimit1<(firstRow1)×numPerRowoffset+maxLimit-1<(firstRow-1)\times numPerRow,也就是到最后一个大卡都没出,那么j=mj×numPerRow1offset+maxLimit1j=m_j\times numPerRow-1\leq offset+maxLimit-1

j=(mj+1)×numPerRow1>offset+maxLimit1j=(m_j+1)\times numPerRow-1> offset+maxLimit-1

那么用跟上面类似的套路,mj=offset+maxLimitnumPerRow=(offset+maxLimit)/intnumPerRowm_j=\lfloor \frac{offset+maxLimit}{numPerRow}\rfloor=(offset+maxLimit)/_{int}numPerRow

j=(offset+maxLimit1)/intnumPerRow×numPerRowj=(offset+maxLimit-1)/_{int}numPerRow\times numPerRow

下面讨论offset+maxLimit1(firstRow1)×numPerRowoffset+maxLimit-1\geq(firstRow-1)\times numPerRow的情况。我们可以先求小于等于offset+maxLimit1offset+maxLimit-1的最大的大卡的位置ll

也就是l=(firstRow1)×numPerRow+nl×(numPerRow×intervalRow+1)offset+maxLimit1l=(firstRow-1)\times numPerRow + n_l\times (numPerRow\times intervalRow+1)\leq offset+maxLimit-1,且

(firstRow1)×numPerRow+(nl+1)×(numPerRow×intervalRow+1)>offset+maxLimit1(firstRow-1)\times numPerRow + (n_l+1)\times (numPerRow\times intervalRow+1)> offset+maxLimit-1

nl=offset+maxLimit1(firstRow1)×numPerRownumPerRow×intervalRow+1=(offset+maxLimit1(firstRow1)×numPerRow)/int(numPerRow×intervalRow+1)n_l=\lfloor\frac{offset+maxLimit-1-(firstRow-1)\times numPerRow}{numPerRow\times intervalRow+1}\rfloor=(offset+maxLimit-1-(firstRow-1)\times numPerRow)/_{int}(numPerRow\times intervalRow+1)

那么j=l+mj×numPerRowj=l+m_j\times numPerRow0mjintervalRow0\leq m_j\leq intervalRow且m为整数。

同理,mj=offset+maxLimit1lnumPerRow=(offset+maxLimit1l)/intnumPerRowm_j=\lfloor \frac{offset+maxLimit-1-l}{numPerRow}\rfloor=(offset+maxLimit-1-l)/_{int}numPerRow

这样j就能计算出来了。joffset+1j-offset+1就是这一刷要出的视频的数量,offset+maxLimit1joffset+maxLimit-1-j就是要裁掉的视频的数量。

3. 第n行的第一个视频的index

除了大卡与小卡两种,我们有时还有插入另外一些类型的大卡的需求。我们的做法是这些特殊大卡插入到某一行中,单独占一行,不影响大卡小卡的位置计算。如果要把特殊大卡插入到第n行(从1开始算),我们需要计算它应该在整个视频流的第k位,也就是要求第n行的第一个视频的index。

我们分情况来计算。

n<firstRown<firstRow时,前面全是小卡,所以k=(n1)×numPerRowk=(n-1)\times numPerRow

nfirstRown\geq firstRow时,我们计算有几个完整周期(一个大卡+intervalRow行小卡),用completeBlocks表示,以及最后的不完整周期有几行,用remainingRows表示。于是

completeBlocks=(nfirstRow)/int(intervalRow+1)completeBlocks=(n-firstRow)/_{int}(intervalRow+1) remainingRows=(nfirstRow)mod(intervalRow+1)remainingRows=(n-firstRow)\bmod(intervalRow+1)

remainingRows=0remainingRows=0时,k=(firstRow1)×numPerRow+completeBlocks×(intervalRow×numPerRow+1)k=(firstRow-1)\times numPerRow+completeBlocks\times (intervalRow\times numPerRow+1); 当remainingRows>0remainingRows>0时,k=(firstRow1)×numPerRow+completeBlocks×(intervalRow×numPerRow+1)+(remainingRows1)×numPerRow+1k=(firstRow-1)\times numPerRow+completeBlocks\times (intervalRow\times numPerRow+1)+(remainingRows-1)\times numPerRow+1