Greasy Fork is available in English.

Maximize Video(improve)

Maximize all video players.Support Piture-in-picture.

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