Skip to content

Instantly share code, notes, and snippets.

@ShellWen
Last active December 22, 2024 16:05
Show Gist options
  • Save ShellWen/b929b4d60bc852685b955a70e7cb9daf to your computer and use it in GitHub Desktop.
Save ShellWen/b929b4d60bc852685b955a70e7cb9daf to your computer and use it in GitHub Desktop.
Video Fake Play
// ==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