原生js实现emby番剧截图小工具

萌新的第一个项目: 为朋友的emby服务器添加番剧截图功能

emby简介

Emby(原名Media Browser)是一个主从式架构的媒体服务器软件,可以用来整理服务器上的视频和音频,并将音频和视频流式传输到客户端设备。

起源

那是一个风和日丽的下午,我一如既往的打开emby,点进还热乎着的⟪我心危⟫最新一集….映入眼帘的便是山田的美颜(什么小学生作文),马上使用windows+shift+s开始截图并把图片发给好基友。很快我就发现,这种截图方式并不方便,于是脑子里萌生了一个想法:为什么不在emby中嵌入一个截图按钮呢?

计划已定,开始实施

基本思路

查阅资料可知,实现网页播放器截图的步骤有四步:

  1. 创建2d canvas画布
  2. 用drawImage方法绘制当前视频帧图
  3. 将图像转化为dataURL格式或Blob对象
  4. 下载url/保存到剪贴板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 实例化video元素
const videoElem = document.getElementsByClassName('htmlvideoplayer')[0];

// 创建画布
const canvas = document.createElement('canvas');

// 将画布的宽高设置为video的宽高
const [w, h] = [videoElem.videoWidth, videoElem.videoHeight];
canvas.width = w;
canvas.height = h;

// 绘制视频帧图
canvas.getContext('2d').drawImage(videoElem, 0, 0, w, h);

将画布内容转换为toDataURL

const dataURL = canvas.toDataURL('image/png')

或者将画布内容转换为toBlob

1
2
3
4
5
6
7
8
9
10
11
12
13
function canvasToBlob(canvas) {
return new Promise((resolve) => {
canvas.toBlob((blob) => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onload = () => {
const imageUrl = reader.result;
resolve(imageUrl);
}
}, 'image/png', 1);
})
}

注意!

当你在本地(使用 file:// 协议)加载 HTML 文件时,浏览器将其视为来自 null 原始域的请求,因此不允许访问本地视频文件。这通常被称为跨源资源共享(CORS)限制。
你可以启动一个本地live server,或者将视频文件上传到web服务器上,这样就可以用HTTP/HTTPS协议访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 下载图片
// 设置一个临时的<a>元素模拟点击下载
const aElem = document.createElement('a');
aElem.href = imageUrl;
aElem.download = 'screenshot.webp'
aElem.click();
// 复制到剪贴板
canvasToBlob(canvas).then((blob) => {
const item = new ClipboardItem({ 'image/png': blob });
navigator.clipboard.write([item]).then(() => {
console.log('截图已复制到剪贴板');
}).catch(err => {
console.error('无法复制截图到剪贴板', err);
});
});

添加截图按钮

好了,大体思路和框架都已建成,接下来就要对html动手了。
用css选择器把emby播放器下方的按钮所在div的class属性找出来,然后把它作为我们截图按钮的父节点记录下来,最后用js的appendChild方法实现截图按钮的插入。
当然,也不要忘记用innerHTML把svg图像绑定到按钮上。
这样一来就大功告成了,源码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function tryGetBtnArea() {
return document.querySelector('.view:not(.hide) .videoOsdBottom-maincontrols .videoOsdBottom-buttons')
}

function tryBindScreenshotBtn(parentElem) {
const newBtn = document.createElement('button');
newBtn.addEventListener('click', async (e) => {
const dataUrl = await screenshot();
if (!dataUrl) {
console.error('获取截图失败: 未找到视频')
return;
}
download_image(dataUrl);
})
// xxx是svg相关代码
newBtn.innerHTML = xxx;
parentElem.appendChild(newBtn);
console.warn('截图插件载入成功')
}

最终效果

截图按钮在按钮栏的最后一个,是一个小相机。
截图按钮在按钮栏的最后一个,是一个小相机。
至此成功复制到剪贴板。
至此成功复制到剪贴板。