智能弹幕不挡脸效果就是使刷屏弹幕无法遮挡想看的内容。目前很多视频网站 & App 都已经实现,且还运用到直播上,提高了观看的体验。具体效果可以看看斗鱼等直播平台(记得确认该直播间是否有此功能)。接下来从客户端角度探讨如何实现智能弹幕,下面的效果的代码实现都在 – 新弹幕系统 chenqihui/QHDanmu2Demo,请同步工程了解。
       iOS客户端 首先想到是使用 mask 蒙层来实现。
CAGradientLayer
       实现之后会发现它只能进行线性渐隐的绘制。
CAShapeLayer & UIBezierPath
       这一次绘制出了镂空的多边形,此方案基本满足弹幕蒙层的需求,不足在于它无法实现边缘的渐隐效果。
UIView
       只是采用 View 的话,比较容易绘制需要显示的区域(即整个 view 的 alpha 值为0,需要显示的位置添加 alpha 值为1的 view)。
       原本想着可以反选,即在整个 alpha 值为1的 view 上添加 alpha 值为0的 子view,从而实现镂空效果,再加入绘制四周阴影来实现。但事实上 父view 的 alpha 值为1后,在图层上已经是整个可见了,已无法再隐藏。
CGGradientRef 的圆半径方向渐变 UIImage
       这其实使用了它来绘制一张 UIImage 来实现,它的不足在于其绘制多边形的能力有限。
带 Alpha 的 UIImage
       从上述的描述可以看出,从苹果提供的 API 来同时实现有 渐隐 & 多边形镂空 效果暂时都没成功。
       因此得借助其他方式,其实关键还是生成带有 alpha 值的 UIImage。这里通过 Python 生成一个简单的带 alpha 值的圆形图片。按理也可以使用 PS 生成。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 
 | import numpy as npimport math
 import matplotlib.pyplot as plt
 
 data = []
 
 for i in range(400):
 print i+1
 item = []
 for j in range(400):
 weight = math.sqrt((math.pow((i-200), 2) + math.pow((j-200), 2)))
 if weight < 50:
 item.append([0,0,0,0])
 elif weight <= 100:
 ap = int(255.0/50*(weight-50))
 item.append([0,0,0,ap])
 else:
 item.append([0,0,0,255])
 data.append(item)
 
 print 'start output img'
 
 img = np.array(data,
 dtype=np.uint8)
 
 plt.imsave('img.png', img)
 
 | 
自生成的带 Alpha 值的 UIImage
       如果放在程序里面,不太好放很多已经生成的图片,所以还是需要 iOS 自己生成 alpha 值的 UIImage,生成的逻辑跟 Python 原理是一样的。
| 12
 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
 
 | - (void)p_addCustomUIImage {UIView *v = [[UIView alloc] initWithFrame:_showMainV.bounds];
 v.backgroundColor = [UIColor whiteColor];
 
 UIGraphicsBeginImageContextWithOptions(v.bounds.size, NO, [UIScreen mainScreen].scale);
 CGContextTranslateCTM(UIGraphicsGetCurrentContext(),
 v.frame.origin.x,
 v.frame.origin.y);
 [v.layer renderInContext:UIGraphicsGetCurrentContext()];
 UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
 UIGraphicsEndImageContext();
 
 CGImageRef originalMaskImage = [image CGImage];
 float width = CGImageGetWidth(originalMaskImage);
 float height = CGImageGetHeight(originalMaskImage);
 
 int strideLength = ceil(width);
 unsigned char * alphaData = calloc(strideLength * height, sizeof(unsigned char));
 CGContextRef alphaOnlyContext = CGBitmapContextCreate(alphaData,
 width,
 height,
 8,
 strideLength,
 NULL,
 kCGImageAlphaOnly);
 
 CGContextDrawImage(alphaOnlyContext, CGRectMake(0, 0, width, height), originalMaskImage);
 
 CGPoint c = CGPointMake(width/2, height/2);
 float r = 80;
 for (int y = 0; y < height; y++) {
 for (int x = 0; x < width; x++) {
 unsigned char val = alphaData[y*strideLength + x];
 float weight = sqrt(pow(x - c.x, 2) + pow(y - c.y, 2));
 if (weight < r) {
 val = 0;
 }
 else if (weight <= r * 2) {
 val = (int)(255.0/r*(weight-r));
 }
 else {
 val = 255;
 }
 alphaData[y*strideLength + x] = val;
 }
 }
 
 CGImageRef alphaMaskImage = CGBitmapContextCreateImage(alphaOnlyContext);
 UIImage *resultImage = [UIImage imageWithCGImage:alphaMaskImage];
 
 UIImageView *i = [[UIImageView alloc] initWithFrame:_showMainV.bounds];
 i.backgroundColor = [UIColor clearColor];
 i.image = resultImage;
 i.contentMode = UIViewContentModeScaleAspectFill;
 _showMainV.maskView = i;
 }
 
 | 

总结
       说了这么多,如果真的运用到智能弹幕上,虽然有些方案可以简单实现效果,只需要接入人形的坐标点即可。但是 MaskView 频繁使用的话,在 UIImage 的实时高效生成,会十分消耗性能,并且运用到直播与视频上的同步性也需要考虑。
       那重新回顾虎牙的介绍,有这一段说法
虎牙AI技术负责人吴晓东向我们介绍了AI智能弹幕功能的特点:
- 常规处理弹幕的做法是“离线(Offline)”和云上处理,需要面对的只是识别问题;而虎牙是针对直播进行实时端上处理;
- 有的网站采用类似PS蒙版技术,采用人工方式为特定视频添加蒙版来模糊弹幕;而虎牙则采用人景分离技术,让人物与场景分离,让弹幕在人物之后,场景之前;
- AI智能弹幕与传统直播弹幕相比,在几乎不增加带宽的前提下,把每帧的mask随视频流编码。而常规方法在视频点播的中则需要大量的流量来支撑弹幕传输;
       采用人景分离,那如果通过 OpenGL,将人像再绘制一次。从而输出多一个 UIView,将弹幕View 放置在景物View 与 人物View 直接。那么此部分开发实现就移到了 播放器 部分,无须弹幕做而外的工作,因此智能弹幕的介绍与实现就等后面 播放器 部分再研究。
补充
       人脸识别的数据,在 rtmp 上可以通过 SEI 帧传输,即播放器解析 SEI 数据后进行人景分离。至于 SEI 可以参考下面 金山云 的文章: