惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Fortinet All Blogs
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
有赞技术团队
有赞技术团队
www.infosecurity-magazine.com
www.infosecurity-magazine.com
大猫的无限游戏
大猫的无限游戏
爱范儿
爱范儿
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Threatpost
V
Visual Studio Blog
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - Franky
人人都是产品经理
人人都是产品经理
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
The Cloudflare Blog
N
News and Events Feed by Topic
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
酷 壳 – CoolShell
酷 壳 – CoolShell
V
V2EX
AWS News Blog
AWS News Blog
S
SegmentFault 最新的问题
T
Tailwind CSS Blog
Hugging Face - Blog
Hugging Face - Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Spread Privacy
Spread Privacy
J
Java Code Geeks
博客园 - 聂微东
T
Tor Project blog
宝玉的分享
宝玉的分享
博客园 - 叶小钗
Webroot Blog
Webroot Blog
博客园 - 【当耐特】
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
H
Heimdal Security Blog
Y
Y Combinator Blog
T
The Blog of Author Tim Ferriss
MongoDB | Blog
MongoDB | Blog
I
InfoQ
Security Latest
Security Latest
Martin Fowler
Martin Fowler
Hacker News: Ask HN
Hacker News: Ask HN
P
Privacy International News Feed
C
CERT Recently Published Vulnerability Notes
Latest news
Latest news
雷峰网
雷峰网
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Cisco Blogs
H
Help Net Security
L
LINUX DO - 最新话题
L
LINUX DO - 热门话题

DaraW

GMTC 北京 2021 小程序开发实践专场所见所想 从一次 Bug 定位来看 Taro H5 的组件实现 浅谈 B 站新 BV 号的设计 复盘:Vue.js 是如何成功的 你可能不知道的 Git React 应用性能优化一例 Thrift RPC Mock 方案探索 前端函数式编程 多说一句再见 《你不知道的JS上卷》阅读小记之setTimeout的this指向问题 ES6实现内部类的写法 IntelliJ IDEA在Linux下字体不正常解决方案 node-thunkify源码阅读笔记 如何监听JS变量的变化 JS实现自定义事件 制作列带有斑马条纹背景的表格 git用户名莫名其妙变化及挽救措施 记一次前端性能优化实战 将setTimeout函数队列
前端视频质量监控
DaraW · 2018-09-07 · via DaraW

背景

业务中使用到视频播放后,一些不确定的因素例如用户端网络异常、CDN 异常等等,导致视频加载缓慢和发生卡顿,这些质量问题会给用户体验带来较大的伤害,也会影响产品的留存转化率。因此对线上视频进行质量监控意义很大,可以让我们明确知道用户端的异常发生概率,以便做进一步的优化。

在我们的业务中,是不支持用户自己控制视频播放/暂停以及播放进度的,所以我们不需要考虑一些用户控制的边界情况;另外一部分视频会在挂载到真实 DOM 前做预加载(preload)处理,因此我们要留意下这类视频和无预加载的视频的差异。
另外我们的业务只在 PC 上,而且用户使用的都是现代浏览器(Chrome 为主),所以不需要考虑移动端的兼容性以及老浏览器的兼容性。

思路与实现

video Element 在播放视频的过程中会触发一系列的媒体事件(Media events),通过查阅 MDN Media events 列表,可以发现值得关注的几个事件分别是 canplay canplaythrough error loadeddata loadedmetadata loadstart play playing waiting

首先 error 事件可以不用特别关注,和常规错误监控一样处理即可;重点在于如何统计加载耗时及卡顿现象。

接下来我们拿一个视频简单做下试验,看下这几个事件触发的时机以及先后顺序。
测试环境:macOS 10.13.6 / Chrome 68

1
2
3
4
5
6
7
8
['loadstart', 'loadedmetadata', 'loadeddata', 'waiting', 'canplay', 'canplaythrough', 'error', 'play', 'playing', 'ended'].forEach((eventName) => {
video.addEventListener(eventName, () => {
const key = `_${eventName}_time`;
video[key] = Date.now();

console.log(eventName, Date.now());
});
});

对于一个无预加载的视频来说,从开始加载到正常播放,输出日志为:

event time cost
play 1536561748438 0
waiting 1536561748438 0
loadstart 1536561748455 17
loadedmetadata 1536561748588 133
loadeddata 1536561748613 25
canplay 1536561748621 8
playing 1536561748625 4
canplaythrough 1536561748627 2

对于一个有预加载处理的视频来说,对应的输出日志为:

event time cost(ms)
loadstart 1536562506189 0
loadedmetadata 1536562506271 82
loadeddata 1536562506304 33
canplay 1536562506305 1
canplaythrough 1536562506307 2
play 1536562526391 20084
playing 1536562526392 1

观察上述事件触发的顺序,对应 MDN 上对事件的描述,与视频初始化相关的几个事件 loadstart loadedmetadata loadeddata 在视频加载的时候会依次触发,分别代表着开始加载、元信息加载成功、首帧加载成功,因此在统计视频加载延迟的时候,我们基本可以确定统计这三个事件触发的时间差即为加载延迟。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const logLatency = (video) => {
if (video._loadstart_time && video._loadedmetadata_time && video._loadeddata_time) {
const loadedmetadataCost = video._loadedmetadata_time - video._loadstart_time;
const loadeddataCost = video._loadeddata_time - video._loadstart_time;

logger.log('Latency', [loadedmetadataCost, loadeddataCost]);
}
};

['loadstart', 'loadedmetadata', 'loadeddata'].forEach((eventName) => {
video.addEventListener(eventName, function callback() {
const key = `_${eventName}_time`;
video[key] = Date.now();


if (eventName === 'loadeddata') {
logLatency(video);
}


video.removeEventListener(eventName, callback);
});
});

接下来我们在播放过程中用 Chrome Dev Tool 的 Network 模拟弱网(网络抖动),输出日志为:

event time cost(ms)
waiting 1536565784729 0
canplay 1536565785075 346
playing 1536565785076 1

观察上述事件触发的顺序,对应 MDN 上对事件的描述,与视频初始化相关的几个事件 waiting canplay playing 在视频播放过程中发生卡顿到再次播放的时候会依次触发,分别代表着等待、加载了足够的数据可以播放、play 事件后有足够多的数据可以开始播放或者从卡顿缓冲中恢复过来。在统计视频卡顿的时候,我们基本从这三个事件入手。

对比初次加载时的事件触发顺序,可以不用关注 canplay 事件,只关注 waitingplaying,但是这里又有一个问题就是无预加载的视频在刚开始播放时会触发 waitingplaying,预加载的视频在刚开始播放时只会触发 playing,所以我们要忽略掉第一次 playing 事件,从第二次开始记录 waitingplaying 的时间差,即为卡顿时长(卡顿缓冲耗时)。

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
const logBuffer = (video) => {
video._buffer_times += 1;


if (video._buffer_times === 1) {
return;
}

if (video._waiting_time && video._playing_time) {
const bufferCost = video._playing_time - video._waiting_time;
logger.log('Buffer', [bufferCost]);
}
};

video._buffer_times = 0;

['waiting', 'playing', 'ended'].forEach((eventName) => {
video.addEventListener(eventName, () => {
const key = `_${eventName}_time`;
video[key] = Date.now();


if (eventName === 'playing') {
logBuffer(video);
}


if (eventName === 'ended') {
video._buffer_times = 0;
}
});
});

注意在视频播放结束的时候,我们要把 _buffer_times 计数器重置为 0,否则在该视频二次播放的时候,初次加载就被认为是卡顿。

优化

当卡顿超过一定时长时(暂定 1 秒),用户可能会失去耐心关闭页面或者刷新页面,如果我们一味的等待视频缓冲完再统计,可能会丢失这种情况的数据。另外实际上当卡顿时间超过忍耐时长后,再统计具体的时间已经没有太大意义了。因此我们可以对上面的方案进行优化,增加卡顿超时直接统计的逻辑。

原理也很简单,在 waiting 时设置一个定时器与 playing 监听器回调竞争:

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
const BufferTimeout = 1000; 

const logBuffer = (video, timeout) => {
if (video._buffer_reported) {
video._buffer_reported = false;
return;
}

video._buffer_times += 1;
video._buffer_reported = true;


if (video._buffer_times === 1) {
return;
}


if (timeout) {
logger.log('Buffer ', [BufferTimeout]);
return;
}


if (video._waiting_time && video._playing_time) {
const bufferCost = video._playing_time - video._waiting_time;
logger.log('Buffer', [bufferCost]);
}
};

video._buffer_times = 0;
video._buffer_reported = false;

['waiting', 'playing', 'ended'].forEach((eventName) => {
video.addEventListener(eventName, () => {
const key = `_${eventName}_time`;
video[key] = Date.now();


if (eventName === 'waiting') {
setTimeout(() => {
logBuffer(video, true);
}, BufferTimeout);
}


if (eventName === 'playing') {
logBuffer(video);
}


if (eventName === 'ended') {
video._buffer_times = 0;
}
});
});

其实这里的超时逻辑还可以用 Promise.race 来实现,不过现在这样通过 Flag 来控制竞态也是没啥问题的。

参考