Last active
December 22, 2024 16:05
-
-
Save ShellWen/b929b4d60bc852685b955a70e7cb9daf to your computer and use it in GitHub Desktop.
Video Fake Play
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Video Fake Play | |
// @namespace http://tampermonkey.net/ | |
// @version 0.1 | |
// @author ShellWen Chen<[email protected]> | |
// @license Apache-2.0 | |
// @description 允许在不加载视频流的情况下“播放”视频,减少带宽与解码开销 | |
// @match *://*.zhihuishu.com/* | |
// @match *://*.chaoxing.com/* | |
// @match *://*.edu.cn/* | |
// @match *://*.org.cn/* | |
// @match *://*.xueyinonline.com/* | |
// @match *://*.hnsyu.net/* | |
// @match *://*.qutjxjy.cn/* | |
// @match *://*.ynny.cn/* | |
// @match *://*.hnvist.cn/* | |
// @match *://*.fjlecb.cn/* | |
// @match *://*.gdhkmooc.com/* | |
// @match *://*.cugbonline.cn/* | |
// @match *://*.zjelib.cn/* | |
// @match *://*.cqrspx.cn/* | |
// @match *://*.neauce.com/* | |
// @match *://*.icve.com.cn/* | |
// @match *://*.course.icve.com.cn/* | |
// @match *://*.courshare.cn/* | |
// @match *://*.webtrn.cn/* | |
// @match *://*.zjy2.icve.com.cn/* | |
// @match *://*.zyk.icve.com.cn/* | |
// @match *://*.icourse163.org/* | |
// @run-at document-start | |
// @grant none | |
// @updateURL https://gist.github.com/ShellWen/b929b4d60bc852685b955a70e7cb9daf/raw/video-fake-play.user.js | |
// @homepageURL https://gist.github.com/ShellWen/b929b4d60bc852685b955a70e7cb9daf | |
// @supportURL https://gist.github.com/ShellWen/b929b4d60bc852685b955a70e7cb9daf | |
// ==/UserScript== | |
;(function () { | |
'use strict' | |
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) | |
class VideoFakePlayer { | |
constructor(videoElement) { | |
this.video = videoElement | |
this.isActive = false | |
this.fakeCurrentTime = 0 | |
this.playing = false | |
this.duration = 0 | |
this.originalSrc = '' | |
this.mediaSource = null | |
this.setupProxy() | |
} | |
setupProxy() { | |
const originalCurrentTimeDescriptor = Object.getOwnPropertyDescriptor( | |
HTMLMediaElement.prototype, | |
'currentTime' | |
) | |
const originalPausedDescriptor = Object.getOwnPropertyDescriptor( | |
HTMLMediaElement.prototype, | |
'paused' | |
) | |
const originalDurationDescriptor = Object.getOwnPropertyDescriptor( | |
HTMLMediaElement.prototype, | |
'duration' | |
) | |
Object.defineProperties(this.video, { | |
currentTime: { | |
get: () => | |
this.isActive | |
? this.fakeCurrentTime | |
: originalCurrentTimeDescriptor.get.call(this.video), | |
set: (value) => { | |
if (this.isActive) { | |
this.fakeCurrentTime = Math.min(Math.max(0, value), this.duration) | |
this.dispatchTimeUpdate() | |
} else { | |
originalCurrentTimeDescriptor.set.call(this.video, value) | |
} | |
}, | |
configurable: true, | |
}, | |
paused: { | |
get: () => | |
this.isActive | |
? !this.playing | |
: originalPausedDescriptor.get.call(this.video), | |
configurable: true, | |
}, | |
duration: { | |
get: () => | |
this.isActive | |
? this.duration | |
: originalDurationDescriptor.get.call(this.video), | |
configurable: true, | |
}, | |
}) | |
this.video.addEventListener( | |
'loadedmetadata', | |
this.handleLoadedMetadata.bind(this), | |
true | |
) | |
this.video.addEventListener( | |
'durationchange', | |
this.handleDurationChange.bind(this), | |
true | |
) | |
this.overrideMethods() | |
} | |
overrideMethods() { | |
const originalPlay = this.video.play | |
const originalPause = this.video.pause | |
const originalLoad = this.video.load | |
this.video.play = async () => { | |
try { | |
if (!this.isActive) { | |
while (1) { | |
try { | |
await originalPlay.call(this.video) | |
break | |
} catch (error) { | |
// AbortError: The fetching process for the media resource was aborted by the user agent at the user's request. | |
if (error.name !== 'AbortError') { | |
throw error | |
} | |
delay(200) | |
} | |
} | |
await this.activate() | |
} | |
this.playing = true | |
this.startTimeUpdate() | |
this.video.dispatchEvent(new Event('play')) | |
this.video.dispatchEvent(new Event('playing')) | |
return Promise.resolve() | |
} catch (error) { | |
return Promise.reject(error) | |
} | |
} | |
this.video.pause = () => { | |
if (this.isActive) { | |
this.playing = false | |
this.stopTimeUpdate() | |
this.video.dispatchEvent(new Event('pause')) | |
} else { | |
originalPause.call(this.video) | |
} | |
} | |
this.video.load = () => { | |
if (!this.isActive) { | |
originalLoad.call(this.video) | |
} | |
} | |
} | |
async handleLoadedMetadata() { | |
if (this.isActive) return | |
// 保存原始时长 | |
this.duration = this.video.duration | |
// 如果视频正在播放,等待第一帧以确保获取到尺寸信息 | |
if (!this.video.paused) { | |
await new Promise((resolve) => { | |
const handler = () => { | |
this.video.removeEventListener('timeupdate', handler) | |
resolve() | |
} | |
this.video.addEventListener('timeupdate', handler) | |
}) | |
} | |
} | |
handleDurationChange() { | |
if (!this.isActive && this.video.duration > 0) { | |
this.duration = this.video.duration | |
} | |
} | |
async activate() { | |
if (this.isActive) return | |
this.isActive = true | |
this.fakeCurrentTime = this.video.currentTime | |
// 保存原始源 | |
this.originalSrc = this.video.src | |
// 创建空的MediaSource | |
this.mediaSource = new MediaSource() | |
const objectUrl = URL.createObjectURL(this.mediaSource) | |
// 替换视频源为空的MediaSource | |
this.video.src = objectUrl | |
// 创建黑色覆盖层 | |
await this.createOverlay() | |
// 移除原始源的所有请求 | |
this.cleanupOriginalSource() | |
} | |
async createOverlay() { | |
const canvas = document.createElement('canvas') | |
canvas.width = this.video.videoWidth || this.video.clientWidth || 640 | |
canvas.height = this.video.videoHeight || this.video.clientHeight || 360 | |
const wrapper = document.createElement('div') | |
wrapper.style.cssText = ` | |
position: relative; | |
width: 100%; | |
height: 100%; | |
display: inline-block; | |
` | |
canvas.style.cssText = ` | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
z-index: 1; | |
` | |
const ctx = canvas.getContext('2d') | |
ctx.fillStyle = 'black' | |
ctx.fillRect(0, 0, canvas.width, canvas.height) | |
this.video.parentNode.insertBefore(wrapper, this.video) | |
wrapper.appendChild(this.video) | |
wrapper.appendChild(canvas) | |
this.video.style.opacity = '0' | |
} | |
cleanupOriginalSource() { | |
// 中断所有相关的网络请求 | |
if (window.performance && window.performance.getEntriesByType) { | |
const requests = performance.getEntriesByType('resource') | |
const videoRequests = requests.filter( | |
(req) => | |
req.name === this.originalSrc || | |
req.initiatorType === 'video' || | |
req.name.includes('.m3u8') || | |
req.name.includes('.ts') || | |
req.name.includes('.mp4') | |
) | |
// 尝试中断这些请求 | |
videoRequests.forEach((req) => { | |
const controller = new AbortController() | |
controller.abort() | |
}) | |
} | |
// 清除原始源 | |
this.video.removeAttribute('src') | |
this.video.load() | |
} | |
startTimeUpdate() { | |
if (!this.timeUpdateInterval) { | |
let lastTime = performance.now() | |
this.timeUpdateInterval = setInterval(() => { | |
if (!this.playing || !this.isActive) return | |
const now = performance.now() | |
const delta = (now - lastTime) / 1000 | |
lastTime = now | |
const rate = this.video.playbackRate || 1 | |
this.fakeCurrentTime += delta * rate | |
if (this.fakeCurrentTime >= this.duration) { | |
this.fakeCurrentTime = this.duration | |
this.playing = false | |
this.stopTimeUpdate() | |
this.video.dispatchEvent(new Event('ended')) | |
} | |
this.dispatchTimeUpdate() | |
}, 50) | |
} | |
} | |
stopTimeUpdate() { | |
if (this.timeUpdateInterval) { | |
clearInterval(this.timeUpdateInterval) | |
this.timeUpdateInterval = null | |
} | |
} | |
dispatchTimeUpdate() { | |
this.video.dispatchEvent(new Event('timeupdate')) | |
} | |
} | |
// 处理新的视频元素 | |
function handleNewVideo(video) { | |
if (!video.dataset.bandwidthSaved) { | |
video.dataset.bandwidthSaved = 'true' | |
new VideoFakePlayer(video) | |
} | |
} | |
// 观察新添加的视频元素 | |
const observer = new MutationObserver((mutations) => { | |
mutations.forEach((mutation) => { | |
mutation.addedNodes.forEach((node) => { | |
if (node.nodeName === 'VIDEO') { | |
handleNewVideo(node) | |
} | |
}) | |
}) | |
}) | |
// 开始观察DOM变化 | |
observer.observe(document.documentElement, { | |
childList: true, | |
subtree: true, | |
}) | |
// 处理现有的视频元素 | |
document.querySelectorAll('video').forEach(handleNewVideo) | |
})() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment