import { getFriendlyTitle } from '../title'; const canvas = document.createElement("canvas"); class Screenshot { video: HTMLVideoElement; url = ""; videoTime: number; blob: Blob; timeStamp = new Date().getTime(); withDanmaku = false constructor(video: HTMLVideoElement, videoTime: number, withDanmaku = false) { this.video = video; this.videoTime = videoTime; // this.url = URL.createObjectURL(this.blob); this.withDanmaku = withDanmaku this.createUrl(); } async createUrl() { canvas.width = this.video.videoWidth; canvas.height = this.video.videoHeight; const context = canvas.getContext("2d"); if (context === null) { logError("视频截图失败: canvas 未创建或创建失败."); return; } context.drawImage(this.video, 0, 0); if (this.withDanmaku) { const danmakuCanvas = dq('canvas.bilibili-player-video-danmaku') as HTMLCanvasElement if (danmakuCanvas !== null) { context.drawImage(danmakuCanvas, 0, 0, this.video.videoWidth, this.video.videoHeight) } } canvas.toBlob(blob => { if (blob === null) { logError("视频截图失败: 创建 blob 失败."); return; } this.blob = blob; this.url = URL.createObjectURL(blob); }, "image/png"); } get filename() { return `${getFriendlyTitle()} @${this.time.replace(/:/g, "-")} ${this.timeStamp.toString()}.png`; } get id() { return this.videoTime.toString() + this.timeStamp.toString(); } get time() { const hour = Math.trunc(this.videoTime / 3600).toString(); const minute = Math.trunc(this.videoTime / 60).toString(); const second = (this.videoTime % 60).toFixed(2); if (hour === "0") { return `${minute.padStart(2, "0")}:${second.padStart(5, "0")}`; } return `${hour}:${minute.padStart(2, "0")}:${second.padStart(5, "0")}`; } revoke() { URL.revokeObjectURL(this.url); } } export const takeScreenshot = (video: HTMLVideoElement, withDanmaku = false) => { const time = video.currentTime return new Screenshot(video, time, withDanmaku) } resources.applyStyle("videoScreenshotStyle"); document.body.insertAdjacentHTML("beforeend", /*html*/`
`); Vue.component("video-screenshot", { props: { objectUrl: String, filename: String, time: String, }, template: /*html*/`
{{time}}
`, methods: { discard() { this.$emit("discard"); }, save() { this.$el.querySelector(".link").click(); this.discard(); }, }, }); const screenShotsList = new Vue({ el: ".video-screenshot-container", data: { screenshots: [] as Screenshot[], batchFilename: getFriendlyTitle() + ".zip", }, methods: { discard(screenshot: Screenshot) { this.screenshots.splice(this.screenshots.indexOf(screenshot), 1); screenshot.revoke(); }, async saveAll() { const zip = new JSZip(); this.screenshots.forEach((it: Screenshot) => { zip.file(it.filename, it.blob, { date: new Date(it.timeStamp), }); }); const blob = await zip.generateAsync({ type: "blob" }); const link = this.$el.querySelector(".batch-link"); link.href = URL.createObjectURL(blob); link.click(); URL.revokeObjectURL(link.href); link.href = ""; this.discardAll(); }, discardAll() { this.screenshots.forEach((it: Screenshot) => it.revoke()); this.screenshots = []; }, }, computed: { showBatch() { return this.screenshots.length >= 2; }, }, }); const buttonHtml = /*html*/`
`; Observer.videoChange(async () => { const video = await SpinQuery.select("#bofqi video") as HTMLVideoElement; if (video === null) { return; } const time = await SpinQuery.select(".bilibili-player-video-time"); if (time === null || document.querySelector(".video-take-screenshot")) { return; } time.insertAdjacentHTML("afterend", buttonHtml); const screenshotButton = document.querySelector(".video-take-screenshot") as HTMLElement; screenshotButton.addEventListener("click", async e => { const video = await SpinQuery.select("#bofqi video") as HTMLVideoElement; const screenshot = takeScreenshot(video, e.shiftKey); screenShotsList.screenshots.unshift(screenshot); }); document.addEventListener("keydown", e => { if (document.activeElement && ["input", "textarea"].includes(document.activeElement.nodeName.toLowerCase())) { return; } if (e.ctrlKey && e.altKey && e.key.toLowerCase() === "c") { e.stopPropagation(); e.preventDefault(); screenshotButton.click(); } }); if (settings.touchVideoPlayer) { document.querySelectorAll(".video-take-screenshot").forEach(it => it.classList.add("touch")); } }); export default { export: { takeScreenshot, screenShotsList, }, unload: () => document.querySelectorAll(".bilibili-player-video-control-bottom .video-take-screenshot,.video-screenshot-container") .forEach(it => (it as HTMLElement).setAttribute("style", "display: none !important")), reload: () => document.querySelectorAll(".bilibili-player-video-control-bottom .video-take-screenshot,.video-screenshot-container") .forEach(it => (it as HTMLElement).setAttribute("style", "display: flex !important")), };