这篇文章已经意义不大了,因为我们在播放本地视频时可以让 Potplayer 使用视频超分辨率 RTX VSR,不需要在浏览器中播放了。
前些时候我写过一篇文章,尝试了 NVIDIA 的视频超分辨率技术。
我下载的资源里有不少低分辨率的视频,现在我习惯把它们放在 Chrome 浏览器里播放,以提高观看时的清晰度。(我是 1440p 显示器,所以 1080p 视频对我来说也算低分辨率)
但是在浏览器里观看视频的体验不是很好,因为我不能用键盘控制视频的进度、音量,特别是不能一键切换上一个/下一个视频,使得播放多个视频时非常麻烦。所以我写了个用户脚本(UserScript)来优化体验(代码在文末)。
像这样一个有很多视频的文件夹:
我把其中一个拖到浏览器中播放:
首先第一个问题就是 Chrome 会自动把视频声音拉满,导致我不得不手动降低视频音量。
在观看时也不能用键盘调整进度。
看完了也不能直接切换到下一个视频。虽然有其他方法可以切换视频,但都做不到一键切换(其他方法例如回到资源管理器里把下一个视频拖进来,或者用 Ctrl
+ O
打开文件,或者直接把文件夹拖到浏览器里当做目录)。
我参照我平时使用 Potplayer 的习惯,为一些键盘快捷键绑定了简单的功能,并且让左手和右手都能单独操作。
左手快捷键:
F
全屏播放/退出全屏A
D
后退/前进 5 秒钟W
S
增加/减少 10% 音量Q
E
手动切换到上一个/下一个视频(*)- 左侧
Shfit
或R
切换循环模式:不循环/单个循环/全部循环
右手快捷键:
Enter
全屏播放/退出全屏←
→
后退/前进 5 秒钟↑
↓
增加/减少 10% 音量PageUp
PageDown
手动切换到上一个/下一个视频(*)- 右侧
Shift
切换循环模式:不循环/单个循环/全部循环
再加上浏览器自带了用空格键来暂停播放/恢复播放的功能,这样基本的观看体验就有保证了。
另外我还设置了让视频在播放时默认使用 30% 音量,自动启用单个循环。
接下来我简单说下怎么用。
首先要有一个脚本管理器,我用的是 Tampermonkey。
然后在浏览器的扩展管理中点击详情,启用“允许访问文件网址”。
这是因为本地文件的网址是 file:///
开头的,必须启用这个设置才能让 Tampermonkey 的脚本运行在本地文件页面上。
最后在 Tampermonkey 里新建一个脚本,复制文末的代码粘贴进去保存。
接下来打开一个本地视频,点击 Tampermonkey 的扩展图标,看到这个脚本有在运行就 OK 了。
前面已经说了一些快捷键可以控制视频播放,其中切换视频需要额外说明一下。
因为脚本是在网页里运行的,没有权限自动读取你硬盘上的文件,也就不知道相邻的视频是哪个文件。所以当你按下 Q
E
或者 PageUp
PageDown
之后,脚本会弹出一个对话框让你选择文件夹,你需要选择这个视频所在的文件夹:
(只需要选择一次,之后切换视频时不需要再次选择)
然后允许脚本查看文件:
这样脚本就可以获得文件夹里的文件列表,然后找到上一个/下一个视频的名字,这样才能切换视频。
并且脚本只能获取到文件名,获取不到路径。比如列表里下一个文件实际路径是 C:\download\阡陌菌-ScarletQ-3D-MMD-舞啪-崩坏三-崩坏3rd-布洛妮娅-大力推荐\2021-10-26-鸭鸭H.mp4
,但脚本只知道文件名 2021-10-26-鸭鸭H.mp4
,那就只能假定下一个文件和当前播放的文件是相同的路径(也就是在同一个文件夹里)。这就导致了你在选择文件夹时,不应该选择其他文件夹,这样脚本会猜错文件的实际路径(因为脚本始终只会使用第一个播放的视频的路径)。
如果你要看另一个文件夹里的视频,应该把视频拖到浏览器里成为一个单独的标签页。也就是让不同文件夹的视频分开播放。
在切换视频时,脚本会显示新视频在播放列表中的位置,如 2/10。标题栏上也会显示新视频的标题。(不过地址栏里的 URL 不会变成新视频的 URL,这是因为一个技术问题)
如果你已经安装了 Tampermonkey,你可以点击这个链接直接安装脚本:增强播放本地视频的体验-v0.2.user.js
或者手动复制代码:
// ==UserScript==
// @name Enhance the experience of playing local videos
// @namespace Play local Video
// @version 0.2
// @description 增强在浏览器中播放本地视频时的便利性。你还可以切换到(当前视频文件夹里的)上一个/下一个视频。
// @author xuejianxianzun
// @match file:///*
// @icon https://saber.love/f/video-icon.png
// @grant none
// ==/UserScript==
// 增强在浏览器中播放视频的便利性。
// 要使用本脚本,需要注意:
// 1. 需要在扩展管理中打开 Tampermonkey 的“详情”,然后启用“允许访问文件网址”。否则这个脚本不会运行。
// 2. 本脚本切换上一个/下一个视频时,只能在当前视频的文件夹里切换。如果你要播放另一个文件夹里的视频,请新开一个标签页操作。
// 你可以使用以下按键操作视频:
// 空格 暂停/播放视频
// Enter 或 F 全屏/退出全屏
// ← → 或 A D 后退/前进 5 秒钟
// ↑ ↓ 或 W S 增加/减少 10% 音量
// PageUp PageDown 或 Q E 切换到上一个/下一个视频
// Shift 或 R 切换循环模式:不循环/单个循环/全部循环
// ---------- Config ----------
// 默认把视频音量设置为 30%。因为浏览器播放视频时音量是 100%,声音太大了,所以我需要减少音量
const defaultVolume = 0.3
// 每次增加/减少音量时的音量值。默认为 10%。
const stepVolume = 0.1
// 每次前进/后退时的秒数,默认为 5 秒
const stepTime = 5
// 默认显示控制按钮
const showControls = true
// 循环模式
let loopMode = 'single'
const loopModeList = ['no', 'single', 'all']
// 默认不全屏
// 如果设置为 true 会出现意外情况,切换视频时,一个能全屏,一个不能全屏,这样来回变化。
// 而且一开始就全屏的话不会显示播放列表进度
const autoFullScreen = false
// 视频文件的后缀名列表
const videoExts = [
'mp4',
'webm',
'mkv',
'avi',
'ts',
'rmvb',
'flv',
'mpg',
'rm',
'mpeg',
'mov',
'm4v',
'wmv',
'asf',
'divx',
'vob',
]
// ---------- Code ----------
let video = document.querySelector('video')
let source = video.querySelector('source')
let videoFileNameList = []
if (!video || !source) {
console.log('Error: video not found!')
} else {
initVideo()
}
function initVideo () {
video.volume = defaultVolume
video.controls = showControls
if (autoFullScreen && videoFileNameList.length > 0) {
video.requestFullscreen()
}
video.onended = function () {
switch (loopMode) {
case 'single':
video.play()
showToast('单个循环')
break
case 'all':
next()
break
case 'no':
break
}
}
}
window.addEventListener('keydown', (ev) => {
switch (ev.code) {
case 'KeyF':
case 'Enter':
switchFullScreen()
break
case 'ArrowLeft':
case 'KeyA':
backward()
break
case 'ArrowRight':
case 'KeyD':
forward()
break
case 'ArrowUp':
case 'KeyW':
increaseVolume()
break
case 'ArrowDown':
case 'KeyS':
reduceVolume()
break
case 'PageUp':
case 'KeyQ':
prev()
break
case 'PageDown':
case 'KeyE':
next()
break
case 'KeyR':
switchLoopMode()
break
}
if (ev.shiftKey) {
switchLoopMode()
}
})
function switchLoopMode () {
const index = loopModeList.findIndex(str => str === loopMode)
loopMode = loopModeList[index === loopModeList.length - 1 ? 0 : index + 1]
switch (loopMode) {
case 'single':
showToast('单个循环')
break
case 'all':
showToast('全部循环')
break
case 'no':
showToast('不循环')
break
}
}
function switchFullScreen () {
if (!document.fullscreenElement) {
video.requestFullscreen({
navigationUI: 'hide'
})
} else {
document.exitFullscreen()
}
}
function forward () {
video.currentTime = video.currentTime + stepTime
const number = Math.round(video.currentTime / video.duration * 100)
showToast(`${number}%`)
}
function backward () {
video.currentTime = video.currentTime - stepTime
const number = Math.round(video.currentTime / video.duration * 100)
showToast(`${number}%`)
}
function increaseVolume () {
const volume = video.volume + stepVolume
video.volume = volume > 1 ? 1 : volume
const number = Math.round(video.volume * 100)
showToast(`${number}%`)
}
function reduceVolume () {
const volume = video.volume - stepVolume
video.volume = volume < 0 ? 0 : volume
const number = Math.round(video.volume * 100)
showToast(`${number}%`)
}
async function getVideoList () {
videoFileNameList = []
const directoryHandle = await window.showDirectoryPicker({
id: 'forVideo'
})
for await (const [fileName, fileHandle] of directoryHandle.entries()) {
if (fileHandle.kind === 'file' && videoExts.includes(getFileExt(fileName))) {
videoFileNameList.push(fileName)
}
}
return videoFileNameList
}
async function findCurrentVideoIndex () {
if (videoFileNameList.length === 0) {
console.warn('file list is empty, open directory')
showToast('播放列表为空,请选择视频文件夹')
await getVideoList()
}
const currentName = getCurrentVideoFileName()
const index = videoFileNameList.findIndex((name, index) => name === currentName)
return index
}
async function next () {
const index = await findCurrentVideoIndex()
if (index === -1) {
return
}
// 如果现在播放的是最后一个文件,则不处理
if (index === videoFileNameList.length - 1) {
showToast(`${index + 1}/${videoFileNameList.length}`)
return
}
playVideo(index + 1)
}
async function prev () {
const index = await findCurrentVideoIndex()
if (index === -1) {
return
}
// 如果现在播放的是第一个文件,则不处理
if (index === 0) {
showToast(`${index + 1}/${videoFileNameList.length}`)
return
}
playVideo(index - 1)
}
function playVideo (index) {
const fileName = videoFileNameList[index]
const URL = decodeURIComponent(source.src)
const array = URL.split('/')
array[array.length - 1] = encodeURIComponent(fileName)
const newURL = array.join('/')
if (!!document.fullscreenElement) {
document.exitFullscreen()
}
video.remove()
video = document.createElement('video')
source = document.createElement('source')
source.setAttribute('src', newURL)
source.setAttribute('type', `video/${getFileExt(fileName)}`)
video.appendChild(source)
video.load()
document.body.appendChild(video)
initVideo()
video.play()
document.title = fileName
console.log(`play list: ${index + 1}/${videoFileNameList.length}`)
console.log(fileName)
showToast(`${index + 1}/${videoFileNameList.length}`)
// history.pushState({}, fileName, newURL)
}
function getFileExt (flieName) {
const array = flieName.split('.')
return array[array.length - 1].toLowerCase()
}
function getCurrentVideoFileName () {
const URL = decodeURIComponent(source.src)
const array = URL.split('/')
return array[array.length - 1]
}
// --------- Toast -----------
const css = `.xzToast {
position: fixed;
z-index: 2147483647;
margin: auto;
font-size: 14px;
pointer-events: none;
display: inline-block;
max-width: 500px;
color: #fff;
background-color: #14ad27;
padding: 8px 10px;
border-radius: 4px;
}`
const e = document.createElement('style')
e.innerHTML = css
document.body.append(e)
function showToast (msg) {
const span = document.createElement('span')
span.textContent = msg
span.style.opacity = '0' // 先使提示完全透明
span.classList.add('xzToast')
// 把提示添加到页面上
document.body.appendChild(span)
// 设置 left,使其居中
// 默认的中间点是窗口的中间
let centerPoint = window.innerWidth / 2
const rect = span.getBoundingClientRect()
let left = centerPoint - rect.width / 2
span.style.left = left + 'px'
// 设置 top
let lastTop = window.innerHeight / 2 - 20
// 出现动画
enter(span, 'up', lastTop)
// 消失动画
window.setTimeout(() => {
span.remove()
}, 1000)
}
// 提示出现的动画
function enter (el, way, lastTop) {
const startTop = lastTop + 20 // 初始 top 值
const once = 2
const total = 20
let numberOfTimes = 0 // 执行次数
const frame = function (timestamp) {
numberOfTimes++
// 计算总共上移了多少像素
const move = once * numberOfTimes
// 计算不透明度
const opacity = move / total
if (move <= total && opacity <= 1) {
if (way === 'up') {
el.style.top = startTop - move + 'px'
}
el.style.opacity = opacity.toString()
// 请求下一帧
window.requestAnimationFrame(frame)
}
}
window.requestAnimationFrame(frame)
}
// 一些技术文档:
// KeyboardEvent.code 列表:
// https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
// 元素全屏与退出全屏:
// https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullscreen
// 打开文件夹:
// https://developer.mozilla.org/en-US/docs/Web/API/Window/showDirectoryPicker
// 要改变播放的视频,需要重新生成一个 video(和 source)元素。直接改变 src 是没用的。
// https://stackoverflow.com/questions/5235145/changing-source-on-html5-video-tag
// 使用 history.pushState() 无刷新改变页面标题和 URL
// https://developer.mozilla.org/zh-CN/docs/Web/API/History/pushState
// 但是播放本地视频时协议是 file:/// ,这会导致 history.pushState 报错,所以我注释了 history.pushState 代码
增强在浏览器中播放视频的体验
-
Google Chrome 111Windows -
Google Chrome 111Android 11 saber酱你有好多好看的视频喔~我也想看看你的
-
Google Chrome 112Windows 不知道有啥用( ̄▽ ̄)
tql了,不过最好有个软件吧,比如potplayer了?