视频网页全屏

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

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