视频网页全屏(改)

让所有视频网页全屏,开启画中画功能

当前为 2024-11-12 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Maximize Video(improve)
  3. // @name:zh-CN 视频网页全屏(改)
  4. // @namespace https://github.com/ryomahan
  5. // @description Maximize all video players.Support Piture-in-picture.
  6. // @description:zh-CN 让所有视频网页全屏,开启画中画功能
  7. // @author 冻猫, ryomahan
  8. // @include *
  9. // @exclude *www.w3school.com.cn*
  10. // @version 12.4
  11. // @run-at document-end
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. ;(() => {
  16. const gv = {
  17. isFull: false,
  18. isIframe: false,
  19. autoCheckCount: 0,
  20. }
  21.  
  22. //Html5规则[播放器最外层],适用于无法自动识别的自适应大小HTML5播放器
  23. const html5Rules = {
  24. "www.acfun.cn": [".player-container .player"],
  25. "www.bilibili.com": ["#bilibiliPlayer"],
  26. "www.douyu.com": ["#js-player-video-case"],
  27. "www.huya.com": ["#videoContainer"],
  28. "www.twitch.tv": [".player"],
  29. "www.youtube.com": ["#ytd-player"],
  30. "www.yy.com": ["#player"],
  31. "*weibo.com": ['[aria-label="Video Player"]', ".html5-video-live .html5-video"],
  32. "v.huya.com": ["#video_embed_flash>div"],
  33. }
  34.  
  35. //通用html5播放器
  36. const generalPlayerRules = [".dplayer", ".video-js", ".jwplayer", "[data-player]"]
  37.  
  38. if (window.top !== window.self) {
  39. gv.isIframe = true
  40. }
  41.  
  42. if (navigator.language.toLocaleLowerCase() == "zh-cn") {
  43. gv.btnText = {
  44. max: "网页全屏",
  45. pip: "画中画",
  46. tip: "Iframe内视频,请用鼠标点击视频后重试",
  47. }
  48. } else {
  49. gv.btnText = {
  50. max: "Maximize",
  51. pip: "PicInPic",
  52. tip: "Iframe video. Please click on the video and try again",
  53. }
  54. }
  55.  
  56. const tool = {
  57. print(log) {
  58. const now = new Date()
  59. const year = now.getFullYear()
  60. const month = (now.getMonth() + 1 < 10 ? "0" : "") + (now.getMonth() + 1)
  61. const day = (now.getDate() < 10 ? "0" : "") + now.getDate()
  62. const hour = (now.getHours() < 10 ? "0" : "") + now.getHours()
  63. const minute = (now.getMinutes() < 10 ? "0" : "") + now.getMinutes()
  64. const second = (now.getSeconds() < 10 ? "0" : "") + now.getSeconds()
  65. const timenow = "[" + year + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second + "]"
  66. console.log(timenow + "[Maximize Video] > " + log)
  67. },
  68. getRect(element) {
  69. const rect = element.getBoundingClientRect()
  70. const scroll = tool.getScroll()
  71. return {
  72. pageX: rect.left + scroll.left,
  73. pageY: rect.top + scroll.top,
  74. screenX: rect.left,
  75. screenY: rect.top,
  76. }
  77. },
  78. isHalfFullClient(element) {
  79. const client = tool.getClient()
  80. const rect = tool.getRect(element)
  81. if (
  82. (Math.abs(client.width - element.offsetWidth) < 21 && rect.screenX < 20) ||
  83. (Math.abs(client.height - element.offsetHeight) < 21 && rect.screenY < 10)
  84. ) {
  85. if (
  86. Math.abs(element.offsetWidth / 2 + rect.screenX - client.width / 2) < 21 &&
  87. Math.abs(element.offsetHeight / 2 + rect.screenY - client.height / 2) < 21
  88. ) {
  89. return true
  90. } else {
  91. return false
  92. }
  93. } else {
  94. return false
  95. }
  96. },
  97. isAllFullClient(element) {
  98. const client = tool.getClient()
  99. const rect = tool.getRect(element)
  100. if (
  101. Math.abs(client.width - element.offsetWidth) < 21 &&
  102. rect.screenX < 20 &&
  103. Math.abs(client.height - element.offsetHeight) < 21 &&
  104. rect.screenY < 10
  105. ) {
  106. return true
  107. } else {
  108. return false
  109. }
  110. },
  111. getScroll() {
  112. return {
  113. left: document.documentElement.scrollLeft || document.body.scrollLeft,
  114. top: document.documentElement.scrollTop || document.body.scrollTop,
  115. }
  116. },
  117. getClient() {
  118. return {
  119. width: document.compatMode == "CSS1Compat" ? document.documentElement.clientWidth : document.body.clientWidth,
  120. height: document.compatMode == "CSS1Compat" ? document.documentElement.clientHeight : document.body.clientHeight,
  121. }
  122. },
  123. addStyle(css) {
  124. const style = document.createElement("style")
  125. style.type = "text/css"
  126. const node = document.createTextNode(css)
  127. style.appendChild(node)
  128. document.head.appendChild(style)
  129. return style
  130. },
  131. matchRule(str, rule) {
  132. return new RegExp("^" + rule.split("*").join(".*") + "$").test(str)
  133. },
  134. createButton(id) {
  135. const btn = document.createElement("tbdiv")
  136. btn.id = id
  137. btn.onclick = () => {
  138. maximize.playerControl()
  139. }
  140. document.body.appendChild(btn)
  141. return btn
  142. },
  143. async addTip(str) {
  144. if (!document.getElementById("catTip")) {
  145. const tip = document.createElement("tbdiv")
  146. tip.id = "catTip"
  147. tip.innerHTML = str
  148. ;(tip.style.cssText =
  149. 'transition: all 0.8s ease-out;background: none repeat scroll 0 0 #27a9d8;color: #FFFFFF;font: 1.1em "微软雅黑";margin-left: -250px;overflow: hidden;padding: 10px;position: fixed;text-align: center;bottom: 100px;z-index: 300;'),
  150. document.body.appendChild(tip)
  151. tip.style.right = -tip.offsetWidth - 5 + "px"
  152. await new Promise((resolve) => {
  153. tip.style.display = "block"
  154. setTimeout(() => {
  155. tip.style.right = "25px"
  156. resolve("OK")
  157. }, 300)
  158. })
  159. await new Promise((resolve) => {
  160. setTimeout(() => {
  161. tip.style.right = -tip.offsetWidth - 5 + "px"
  162. resolve("OK")
  163. }, 3500)
  164. })
  165. await new Promise((resolve) => {
  166. setTimeout(() => {
  167. document.body.removeChild(tip)
  168. resolve("OK")
  169. }, 1000)
  170. })
  171. }
  172. },
  173. }
  174.  
  175. const setButton = {
  176. init() {
  177. if (!document.getElementById("playerControlBtn")) {
  178. init()
  179. }
  180. if (gv.isIframe && tool.isHalfFullClient(gv.player)) {
  181. window.parent.postMessage("iframeVideo", "*")
  182. return
  183. }
  184. this.show()
  185. },
  186. show() {
  187. gv.player.removeEventListener("mouseleave", handle.leavePlayer, false)
  188. gv.player.addEventListener("mouseleave", handle.leavePlayer, false)
  189.  
  190. if (!gv.isFull) {
  191. document.removeEventListener("scroll", handle.scrollFix, false)
  192. document.addEventListener("scroll", handle.scrollFix, false)
  193. }
  194. gv.controlBtn.style.display = "block"
  195. gv.controlBtn.style.visibility = "visible"
  196. if (document.pictureInPictureEnabled && gv.player.nodeName != "OBJECT" && gv.player.nodeName != "EMBED") {
  197. gv.picinpicBtn.style.display = "block"
  198. gv.picinpicBtn.style.visibility = "visible"
  199. }
  200. this.locate()
  201. },
  202. locate() {
  203. let escapeHTMLPolicy
  204. const hasTrustedTypes = Boolean(window.trustedTypes && window.trustedTypes.createPolicy)
  205. if (hasTrustedTypes) {
  206. escapeHTMLPolicy = window.trustedTypes.createPolicy('myEscapePolicy', {
  207. createHTML: (string, sink) => string
  208. });
  209. }
  210. const playerRect = tool.getRect(gv.player)
  211. gv.controlBtn.style.opacity = "0.5"
  212. gv.controlBtn.innerHTML = hasTrustedTypes ? escapeHTMLPolicy.createHTML(gv.btnText.max) : gv.btnText.max
  213. gv.controlBtn.style.top = playerRect.screenY - 20 + "px"
  214. // 网页全屏按钮位置,Maximize button
  215. gv.controlBtn.style.left = playerRect.screenX - 64 + gv.player.offsetWidth + "px"
  216. gv.picinpicBtn.style.opacity = "0.5"
  217. gv.picinpicBtn.innerHTML = hasTrustedTypes ? escapeHTMLPolicy.createHTML(gv.btnText.pip) : gv.btnText.pip
  218. gv.picinpicBtn.style.top = gv.controlBtn.style.top
  219. // 画中画按钮位置,PicInPic button
  220. gv.picinpicBtn.style.left = playerRect.screenX - 64 + gv.player.offsetWidth - 54 + "px"
  221. },
  222. }
  223.  
  224. const handle = {
  225. getPlayer(e) {
  226. if (gv.isFull) {
  227. return
  228. }
  229. gv.mouseoverEl = e.target
  230. const hostname = document.location.hostname
  231. let players = []
  232. for (let i in html5Rules) {
  233. if (tool.matchRule(hostname, i)) {
  234. for (let html5Rule of html5Rules[i]) {
  235. if (document.querySelectorAll(html5Rule).length > 0) {
  236. for (let player of document.querySelectorAll(html5Rule)) {
  237. players.push(player)
  238. }
  239. }
  240. }
  241. break
  242. }
  243. }
  244. if (players.length == 0) {
  245. for (let generalPlayerRule of generalPlayerRules) {
  246. if (document.querySelectorAll(generalPlayerRule).length > 0) {
  247. for (let player of document.querySelectorAll(generalPlayerRule)) {
  248. players.push(player)
  249. }
  250. }
  251. }
  252. }
  253. if (players.length == 0 && e.target.nodeName != "VIDEO" && document.querySelectorAll("video").length > 0) {
  254. const videos = document.querySelectorAll("video")
  255. for (let v of videos) {
  256. const vRect = v.getBoundingClientRect()
  257. if (
  258. e.clientX >= vRect.x - 2 &&
  259. e.clientX <= vRect.x + vRect.width + 2 &&
  260. e.clientY >= vRect.y - 2 &&
  261. e.clientY <= vRect.y + vRect.height + 2 &&
  262. v.offsetWidth > 399 &&
  263. v.offsetHeight > 220
  264. ) {
  265. players = []
  266. players[0] = handle.autoCheck(v)
  267. gv.autoCheckCount = 1
  268. break
  269. }
  270. }
  271. }
  272. if (players.length > 0) {
  273. const path = e.path || e.composedPath()
  274. for (let v of players) {
  275. if (path.indexOf(v) > -1) {
  276. gv.player = v
  277. setButton.init()
  278. return
  279. }
  280. }
  281. }
  282. switch (e.target.nodeName) {
  283. case "VIDEO":
  284. case "OBJECT":
  285. case "EMBED":
  286. if (e.target.offsetWidth > 399 && e.target.offsetHeight > 220) {
  287. gv.player = e.target
  288. setButton.init()
  289. }
  290. break
  291. default:
  292. handle.leavePlayer()
  293. }
  294. },
  295. autoCheck(v) {
  296. let tempPlayer,
  297. el = v
  298. gv.playerChilds = []
  299. gv.playerChilds.push(v)
  300. while ((el = el.parentNode)) {
  301. if (Math.abs(v.offsetWidth - el.offsetWidth) < 15 && Math.abs(v.offsetHeight - el.offsetHeight) < 15) {
  302. tempPlayer = el
  303. gv.playerChilds.push(el)
  304. } else {
  305. break
  306. }
  307. }
  308. return tempPlayer
  309. },
  310. leavePlayer() {
  311. if (gv.controlBtn.style.visibility == "visible") {
  312. gv.controlBtn.style.opacity = ""
  313. gv.controlBtn.style.visibility = ""
  314. gv.picinpicBtn.style.opacity = ""
  315. gv.picinpicBtn.style.visibility = ""
  316. gv.player.removeEventListener("mouseleave", handle.leavePlayer, false)
  317. document.removeEventListener("scroll", handle.scrollFix, false)
  318. }
  319. },
  320. scrollFix(e) {
  321. clearTimeout(gv.scrollFixTimer)
  322. gv.scrollFixTimer = setTimeout(() => {
  323. setButton.locate()
  324. }, 20)
  325. },
  326. hotKey(e) {
  327. //默认退出键为ESC。需要修改为其他快捷键的请搜索"keycode",修改为按键对应的数字。
  328. if (e.keyCode == 27) {
  329. maximize.playerControl()
  330. }
  331. //默认画中画快捷键为F2。
  332. if (e.keyCode == 113) {
  333. handle.pictureInPicture()
  334. }
  335. },
  336. async receiveMessage(e) {
  337. switch (e.data) {
  338. case "iframePicInPic":
  339. tool.print("messege:iframePicInPic")
  340. if (!document.pictureInPictureElement) {
  341. await document
  342. .querySelector("video")
  343. .requestPictureInPicture()
  344. .catch((error) => {
  345. tool.addTip(gv.btnText.tip)
  346. })
  347. } else {
  348. await document.exitPictureInPicture()
  349. }
  350. break
  351. case "iframeVideo":
  352. tool.print("messege:iframeVideo")
  353. if (!gv.isFull) {
  354. gv.player = gv.mouseoverEl
  355. setButton.init()
  356. }
  357. break
  358. case "parentFull":
  359. tool.print("messege:parentFull")
  360. gv.player = gv.mouseoverEl
  361. if (gv.isIframe) {
  362. window.parent.postMessage("parentFull", "*")
  363. }
  364. maximize.checkParent()
  365. maximize.fullWin()
  366. if (getComputedStyle(gv.player).left != "0px") {
  367. tool.addStyle("#htmlToothbrush #bodyToothbrush .playerToothbrush {left:0px !important;width:100vw !important;}")
  368. }
  369. gv.isFull = true
  370. break
  371. case "parentSmall":
  372. tool.print("messege:parentSmall")
  373. if (gv.isIframe) {
  374. window.parent.postMessage("parentSmall", "*")
  375. }
  376. maximize.smallWin()
  377. break
  378. case "innerFull":
  379. tool.print("messege:innerFull")
  380. if (gv.player.nodeName == "IFRAME") {
  381. gv.player.contentWindow.postMessage("innerFull", "*")
  382. }
  383. maximize.checkParent()
  384. maximize.fullWin()
  385. break
  386. case "innerSmall":
  387. tool.print("messege:innerSmall")
  388. if (gv.player.nodeName == "IFRAME") {
  389. gv.player.contentWindow.postMessage("innerSmall", "*")
  390. }
  391. maximize.smallWin()
  392. break
  393. }
  394. },
  395. pictureInPicture() {
  396. if (!document.pictureInPictureElement) {
  397. if (gv.player) {
  398. if (gv.player.nodeName == "IFRAME") {
  399. gv.player.contentWindow.postMessage("iframePicInPic", "*")
  400. } else {
  401. gv.player.parentNode.querySelector("video").requestPictureInPicture()
  402. }
  403. } else {
  404. document.querySelector("video").requestPictureInPicture()
  405. }
  406. } else {
  407. document.exitPictureInPicture()
  408. }
  409. },
  410. }
  411.  
  412. const maximize = {
  413. playerControl() {
  414. if (!gv.player) {
  415. return
  416. }
  417. this.checkParent()
  418. if (!gv.isFull) {
  419. if (gv.isIframe) {
  420. window.parent.postMessage("parentFull", "*")
  421. }
  422. if (gv.player.nodeName == "IFRAME") {
  423. gv.player.contentWindow.postMessage("innerFull", "*")
  424. }
  425. this.fullWin()
  426. if (gv.autoCheckCount > 0 && !tool.isHalfFullClient(gv.playerChilds[0])) {
  427. if (gv.autoCheckCount > 10) {
  428. for (let v of gv.playerChilds) {
  429. v.classList.add("videoToothbrush")
  430. }
  431. return
  432. }
  433. const tempPlayer = handle.autoCheck(gv.playerChilds[0])
  434. gv.autoCheckCount++
  435. maximize.playerControl()
  436. gv.player = tempPlayer
  437. maximize.playerControl()
  438. } else {
  439. gv.autoCheckCount = 0
  440. }
  441. } else {
  442. if (gv.isIframe) {
  443. window.parent.postMessage("parentSmall", "*")
  444. }
  445. if (gv.player.nodeName == "IFRAME") {
  446. gv.player.contentWindow.postMessage("innerSmall", "*")
  447. }
  448. this.smallWin()
  449. }
  450. },
  451. checkParent() {
  452. if (gv.isFull) {
  453. return
  454. }
  455. gv.playerParents = []
  456. let full = gv.player
  457. while ((full = full.parentNode)) {
  458. if (full.nodeName == "BODY") {
  459. break
  460. }
  461. if (full.getAttribute) {
  462. gv.playerParents.push(full)
  463. }
  464. }
  465. },
  466. fullWin() {
  467. if (!gv.isFull) {
  468. document.removeEventListener("mouseover", handle.getPlayer, false)
  469. gv.backHtmlId = document.body.parentNode.id
  470. gv.backBodyId = document.body.id
  471. gv.leftBtn.style.display = "block"
  472. gv.rightBtn.style.display = "block"
  473. gv.picinpicBtn.style.display = ""
  474. gv.controlBtn.style.display = ""
  475. this.addClass()
  476. const hostname = document.location.hostname
  477.  
  478. // 为 Youtube 做特殊处理
  479. if (
  480. hostname.includes("www.youtube.com")
  481. && !document.querySelector("#player-theater-container #movie_player")
  482. && document.querySelector(".html5-video-container").clientWidth - document.querySelector(".ytp-chrome-bottom").clientWidth > 24
  483. ) {
  484. document.querySelector("#movie_player .ytp-size-button").click()
  485. gv.ytbStageChange = true
  486. }
  487.  
  488. // 为 B 站做特殊处理
  489. if (hostname.includes("bilibili")) {
  490. document.querySelector(".right-container").style.display = "none"
  491. document.querySelector("#biliMainHeader").style.display = "none"
  492. }
  493. }
  494. gv.isFull = true
  495. },
  496. addClass() {
  497. document.body.parentNode.id = "htmlToothbrush"
  498. document.body.id = "bodyToothbrush"
  499. for (let v of gv.playerParents) {
  500. v.classList.add("parentToothbrush")
  501. //父元素position:fixed会造成层级错乱
  502. if (getComputedStyle(v).position == "fixed") {
  503. v.classList.add("absoluteToothbrush")
  504. }
  505. }
  506. gv.player.classList.add("playerToothbrush")
  507. if (gv.player.nodeName == "VIDEO") {
  508. gv.backControls = gv.player.controls
  509. gv.player.controls = true
  510. }
  511. window.dispatchEvent(new Event("resize"))
  512. },
  513. smallWin() {
  514. document.body.parentNode.id = gv.backHtmlId
  515. document.body.id = gv.backBodyId
  516. for (let v of gv.playerParents) {
  517. v.classList.remove("parentToothbrush")
  518. v.classList.remove("absoluteToothbrush")
  519. }
  520. gv.player.classList.remove("playerToothbrush")
  521. if (document.location.hostname == "www.youtube.com" && gv.ytbStageChange && document.querySelector("#player-theater-container #movie_player")) {
  522. document.querySelector("#movie_player .ytp-size-button").click()
  523. gv.ytbStageChange = false
  524. }
  525. if (gv.player.nodeName == "VIDEO") {
  526. gv.player.controls = gv.backControls
  527. }
  528. gv.leftBtn.style.display = ""
  529. gv.rightBtn.style.display = ""
  530. gv.controlBtn.style.display = ""
  531. document.addEventListener("mouseover", handle.getPlayer, false)
  532. window.dispatchEvent(new Event("resize"))
  533. const hostname = document.location.hostname
  534. if (hostname.includes("bilibili")) {
  535. document.querySelector(".right-container").style.removeProperty("display")
  536. document.querySelector("#biliMainHeader").style.removeProperty("display")
  537. }
  538. gv.isFull = false
  539. },
  540. }
  541.  
  542. const init = () => {
  543. gv.picinpicBtn = document.createElement("tbdiv")
  544. gv.picinpicBtn.id = "picinpicBtn"
  545. gv.picinpicBtn.onclick = () => {
  546. handle.pictureInPicture()
  547. }
  548. document.body.appendChild(gv.picinpicBtn)
  549. gv.controlBtn = tool.createButton("playerControlBtn")
  550. gv.leftBtn = tool.createButton("leftFullStackButton")
  551. gv.rightBtn = tool.createButton("rightFullStackButton")
  552.  
  553. if (getComputedStyle(gv.controlBtn).position != "fixed") {
  554. tool.addStyle(
  555. [
  556. "#htmlToothbrush #bodyToothbrush .parentToothbrush .bilibili-player-video {margin:0 !important;}",
  557. "#htmlToothbrush, #bodyToothbrush {overflow:hidden !important;zoom:100% !important;}",
  558. "#htmlToothbrush #bodyToothbrush .parentToothbrush {overflow:visible !important;z-index:auto !important;transform:none !important;-webkit-transform-style:flat !important;transition:none !important;contain:none !important;}",
  559. "#htmlToothbrush #bodyToothbrush .absoluteToothbrush {position:absolute !important;}",
  560. "#htmlToothbrush #bodyToothbrush .playerToothbrush {position:fixed !important;top:0px !important;left:0px !important;width:100vw !important;height:100vh !important;max-width:none !important;max-height:none !important;min-width:0 !important;min-height:0 !important;margin:0 !important;padding:0 !important;z-index:2147483646 !important;border:none !important;background-color:#000 !important;transform:none !important;}",
  561. "#htmlToothbrush #bodyToothbrush .parentToothbrush video {object-fit:contain !important;}",
  562. "#htmlToothbrush #bodyToothbrush .parentToothbrush .videoToothbrush {width:100vw !important;height:100vh !important;}",
  563. '#playerControlBtn {text-shadow: none;visibility:hidden;opacity:0;display:none;transition: all 0.5s ease;cursor: pointer;font: 12px "微软雅黑";margin:0;width:64px;height:20px;line-height:20px;border:none;text-align: center;position: fixed;z-index:2147483647;background-color: #27A9D8;color: #FFF;} #playerControlBtn:hover {visibility:visible;opacity:1;background-color:#2774D8;}',
  564. '#picinpicBtn {text-shadow: none;visibility:hidden;opacity:0;display:none;transition: all 0.5s ease;cursor: pointer;font: 12px "微软雅黑";margin:0;width:53px;height:20px;line-height:20px;border:none;text-align: center;position: fixed;z-index:2147483647;background-color: #27A9D8;color: #FFF;} #picinpicBtn:hover {visibility:visible;opacity:1;background-color:#2774D8;}',
  565. "#leftFullStackButton{display:none;position:fixed;width:1px;height:100vh;top:0;left:0;z-index:2147483647;background:#000;}",
  566. "#rightFullStackButton{display:none;position:fixed;width:1px;height:100vh;top:0;right:0;z-index:2147483647;background:#000;}",
  567. ].join("\n")
  568. )
  569. }
  570. document.addEventListener("mouseover", handle.getPlayer, false)
  571. document.addEventListener("keydown", handle.hotKey, false)
  572. window.addEventListener("message", handle.receiveMessage, false)
  573. tool.print("Ready")
  574. }
  575.  
  576. init()
  577. })()