笔记本中使用触控板、手机中滑动屏幕时。当你想要看下面的信息,则需要向上滚动。 这种滚动形式被成为“自然滚动”,很明显地,这和我们现实的逻辑相同。而移动式的交互和 PC 中基于鼠标、按键的控制逻辑却是相反的。由此在视频播放中,使用触控板调节音量,就出现了一种矛盾,向上滑想调大,但是这种操作在浏览器被定义为向下操作,于是音量变小了。

行文只代表研究思路,不一定是最优解。

并不是所有触控板都支持滚动操作,支持的大部分都有一定的精细程度。


事情の始末

最近用笔记本比较多,除了 Photoshop 之类的软件外,我很多场景都想只用触控板来解决,于是就在看视频想调整音量的时候发现了上面提到的问题。 当然我主要用的就是哔哩哔哩,所以就想试试能不能写个脚本解决这个问题。

Wheel Event

Wheel 是这样一个事件,他会接收鼠标或类似输入设备的参数,他分别会返回如下参数,(来自MDN,有省略)。

参数 说明
WheelEvent.deltaX 返回 double 值,该值表示滚轮的横向滚动量。
WheelEvent.deltaY 返回 double 值,该值表示滚轮的纵向滚动量。
WheelEvent.deltaZ 返回 double 值,该值表示滚轮的z轴方向上的滚动量。

有了事件我们得知道如何判断,当然我是参考自 StackOverflow 的这个回答得出来了大致的思路,即鼠标滚轮与触控板由于精细程度的原因他们得到的滚动量数值有着较明显的差异。

但是答案中的代码经过我的测试始终没法得到一个满意的结果,于是我自己做了以下测试。

See the Pen wheel event test by Max C. Foo (@maxchang3) on CodePen.


(测试滚动最大速度(差),你也可以自己试试你的值为多少)

当然,这并不是真正意义上的最大最小增量,例如触控板使劲儿一甩能甩出 1000+,鼠标如罗技的一些型号有快速滚动的功能也能达到这个数值。但是考虑到我们调整音量的时候一般不会进行太快的动作,而是趋向于精细寻找我们想要的区间。 由此,我们发现触控板的精细程度是远大于鼠标的,最小滚动量可以达个位数,对于调整音量来说,比鼠标一格代表10+-要好太多了,这也是我想使用触控板调整音量的原因。

所以为了区分触控板,我们必须要找到一个比较良好阈值,但是很明显这和硬件都是有区别的,并且他们的区间是有所重复的,互相都能达到,因此在滚动量这里我们并不能精准的区分触控板与鼠标。这里实际上我还没有找到正确答案。

在调节音量的场景下,我们的速度基本不会达到很高,而鼠标的滚动最低值也经过我的测试取值 100 来分别是否是触控板基本已经符合我们调音量的要求了。

所以我写了如下代码

1
2
3
4
5
function handler(e) {
let isTrackpad = (e.wheelDeltaY && Math.abs(e.wheelDeltaY) < 100);
console.log(isTrackpad)
}
document.addEventListener("wheel", handler, false);

对于触控板的判断,确实有时候会出问题,但是对于鼠标的判断基本是精准的,毕竟鼠标一般到不了 100 以下。

操作播放器 - 界面

上面的思路基本完成后,问题就来了,我们如何让新的滚轮事件触发音量调节呢,首先肯定要阻止默认的事件:

1
2
e.stopImmediatePropagation();
e.stopPropagation();

但是怎么改音量呢? b 站播放器给出了一个全局对象 window.player,其中就有一系列对播放器的操作,例如 window.player.setVolume(volume) 就可以调整音量。 window.player.getVolume() 就可以获取音量值。当然如果没有这个对象你也可以手动获取一下 video 标签来用原生的方法修改。

不过这两个方法都无法触发音量提示。 经过翻看相关混淆的方法。发现想把他原生的 tuneVolume 或者是 showHint 之类的方法调用出来不是很容易。 于是最后是相当于重写了一下相关方法。

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
// 初始化计时器
let hintTimer = undefined
// 初始化 hinter 标签,因为默认网页打开不存在这个标签,只有调整音量时会创建,我们就直接盛事儿直接创建出来就是了。
const initHint = () => {
const hintCode = `<div class="bpx-player-volume-hint" style="opacity: 0; display: none;"><span class="bpx-player-volume-hint-icon"><span class="bpx-common-svg-icon" style=""><svg viewBox="0 0 22 22"><use xlink:href="#bpx-svg-sprite-volume"></use></svg></span><span class="bpx-common-svg-icon" style="display: none;"><svg viewBox="0 0 22 22"><use xlink:href="#bpx-svg-sprite-volume-off"></use></svg></span></span><span class="bpx-player-volume-hint-text">20%</span></div>`
if (document.querySelector('.bpx-player-volume-hint')) return
document.querySelector('.bpx-player-video-area').insertAdjacentHTML('beforeend', hintCode)
}

const showHint = (volume) => {
// 弹出提示,先清空计时器
window.clearTimeout(hintTimer)
// 获取相关标签
const volHint = document.querySelector('.bpx-player-volume-hint')
const volText = volHint.querySelector('.bpx-player-volume-hint-text')
const volIconArea = volHint.querySelector('.bpx-player-volume-hint-icon')
const volIcons = volIconArea.querySelectorAll('.bpx-common-svg-icon')
// 对静音特殊处理,修改显示静音的图标
if (volume == 0) {
volText.innerText = '静音'
volIcons[0].style.display = 'none'
volIcons[1].style.display = 'inline-flex'
} else {
volIcons[0].style.display = 'inline-flex'
volIcons[1].style.display = 'none'
volText.innerText = `${Math.ceil(volume * 100)}%`
}
// 开始显示音量提示
volHint.style.display = ''
volHint.style.opacity = 1
// 显示渐隐动画
hintTimer = window.setTimeout((function () {
const animation = volHint.animate({ opacity: 0 }, 300);
animation.onfinish = function () {
animation.cancel()
volHint.style.display = 'none'
volHint.style.opacity = 0
}
}), 1e3)
}

操作播放器 - 数值

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
const handler = (e) => {
// 同时接管触控板与鼠标滚轮出发下的提醒,从而避免提醒重复显示的情况,因此对于鼠标触发的值需要额外放大魔数以计算增长音量与wheelDeltaY的关系。
// 触控板下wheelDeltaY更为精准因此明显小于鼠标,这里以100为阈值。经过测试(罗技MX Anywhere2s下)鼠标最低约在240或360左右,这里为了防止硬件差异选择100。
// 灵感来自:https://stackoverflow.com/questions/10744645/detect-touchpad-vs-mouse-in-javascript 在这里不需要过于精准的判断,基本够用。
console.log(e.wheelDeltaY)
let isTrackpad = (e.wheelDeltaY && Math.abs(e.wheelDeltaY) < 100);
let isFullscreen = (Array.from(document.querySelector('body').classList)).includes("player-fullscreen-fix") || isFullScreen();
if (!(isFullscreen)) return false //非全屏下不接管事件
//阻止默认事件
e.stopImmediatePropagation();
e.stopPropagation();
let flag = -1 // Trackpad 值转为 1 与 -1 ,在触控板与鼠标情况下进行加减的逆转。
let MAGIC_NUMBER = 36000
if (isTrackpad && isFullscreen) { flag = 1; MAGIC_NUMBER /= 72; }
let newVolume = window.player.getVolume() - flag * e.wheelDeltaY / MAGIC_NUMBER;
newVolume = (newVolume < 1) ? Math.max(newVolume, 0) : 1 // 小于0取0,大于1取1
window.player.setVolume(newVolume)
showHint(window.player.getVolume())
}
// 初始化相关内容
const init = () => {
initHint()
// 添加事件监听,兼容 firefox
document.addEventListener("DOMMouseScroll", handler, false);
document.addEventListener("wheel", handler, false);
}
window.onload = init

这里数值的计算还是比较有意思的,因为你要保证你接管后的触控板和鼠标轻微挪动的增量是一个刻度(1%),但是问题来了,为什么我要两个都接管呢?不能只取触控板的情况吗。

因为这里是我拿最后的源码倒推的过程,实际上是,当我只管触控板的时候,经常会和原有的事件冲突,并且一个更重要的问题,这样调整音量确实是管用的,但是——他不会像你默认事件一样显示数值。

The End

最后实现还是不优雅,但是又不能不能用,当然如果能精准的区分触控板,直接调用播放器的音量调整(带弹窗和数据同步的那种,我估计这个肯定是能找到的,当然我没……)估计是最好的了。

然后就是,你可以在 GreasyFork 上下载到他~

就这样吧,拜拜。