Enhanced Video Playing Experience for Viu.com

Automatically Full HD Video Quality (Paid Member) and Disable Auto Playing when the page is randomly reloaded

  1. // ==UserScript==
  2. // @name Enhanced Video Playing Experience for Viu.com
  3. // @version 1.5
  4. // @description Automatically Full HD Video Quality (Paid Member) and Disable Auto Playing when the page is randomly reloaded
  5. // @match https://www.viu.com/ott/*/vod/*
  6. // @icon https://www.google.com/s2/favicons?domain=viu.com
  7. // @grant unsafeWindow
  8. // @grant window.onurlchange
  9. // @namespace https://greatest.deepsurf.us/users/371179
  10. // ==/UserScript==
  11. (function $$() {
  12. 'use strict';
  13.  
  14. if (!document || !document.documentElement) return window.requestAnimationFrame($$);
  15.  
  16. const uWin = window.unsafeWindow || window;
  17.  
  18.  
  19. if ([document.hidden, window.requestAnimationFrame, Object.defineProperty, window.performance].some(x => x === undefined)) throw 'Your browser is too outdated.';
  20. var navStart = performance.timeOrigin || performance.timing.navigationStart || null;
  21. if (navStart === null) throw 'Your browser is too outdated.';
  22. navStart = Math.ceil(navStart);
  23.  
  24. var lastPlayingStatus = null;
  25. var settings = {}
  26. var falseReloaded = false;
  27. var lastUserClickAt = 0;
  28. var lastUserClickStatus = 0;
  29. var forceQuality = "1080";
  30. const __jver__ = "20210617b";
  31.  
  32. const isPassiveOptionEnable = window.queueMicrotask?true:false; //here just a simple trick for checking browser is modern or not
  33.  
  34.  
  35. function isVideoPlaying(video){
  36. return video.currentTime > 0 && !video.paused && !video.ended && video.readyState > video.HAVE_CURRENT_DATA;
  37. }
  38.  
  39. function pageInit() {
  40.  
  41.  
  42. var _userPaused = null;
  43.  
  44. Object.defineProperty(settings, 'userPaused', {
  45. get() {
  46. return _userPaused;
  47. },
  48. set(nv) {
  49. _userPaused = nv;
  50. console.log('[viu] userPaused: ', _userPaused);
  51. }
  52. })
  53.  
  54.  
  55.  
  56. function doSomething(obj) {
  57. //for viu page auto reload ( it is confirmed that the page could randomly reload)
  58.  
  59.  
  60. if (!obj) return;
  61. let {
  62. version,
  63. datetime,
  64. pageVisible,
  65. withVideo,
  66. userPaused
  67. } = obj;
  68.  
  69. if (version != __jver__) return;
  70.  
  71. const isReload = (t) => (t > 0 && t - 2 < navStart && t + 480 > navStart); // max 155ms => 155*3=465 => 480ms
  72.  
  73. if (isReload(datetime) && withVideo) {
  74. console.log('[viu] reloaded page', obj)
  75. if (userPaused || !pageVisible) settings.userPaused = true;
  76. }
  77.  
  78. console.log('[viu] reload time diff: ' + (navStart - datetime) + ' => ' + isReload(datetime))
  79.  
  80.  
  81. }
  82.  
  83. var _saveObj = localStorage.__zvpp_unload1__;
  84. if (typeof _saveObj == 'string' && _saveObj.length > 0) {
  85.  
  86. var _jObj = null;
  87. try {
  88. _jObj = JSON.parse(_saveObj);
  89. } catch (e) {}
  90. if (_jObj) {
  91. doSomething(_jObj);
  92. }
  93. }
  94.  
  95.  
  96. delete localStorage.__viu_js_fewunaznqjrx__
  97. delete localStorage.__viu_js_fewunaznqjr2__
  98. delete localStorage.__viu_js_fewunaznqjra__
  99. delete localStorage.__zvpp_unload1__
  100.  
  101. if (localStorage.__zvpp_ver__ !== __jver__) {
  102. for (var k in localStorage) {
  103. if (k.indexOf('__zvpp_') === 0) localStorage.removeItem(k)
  104. }
  105. localStorage.__zvpp_ver__ = __jver__
  106. }
  107.  
  108.  
  109.  
  110.  
  111. }
  112.  
  113.  
  114. function getVideoFromEvent(evt) {
  115. return (evt && evt.target && evt.target.nodeName == 'VIDEO') ? evt.target : null;
  116. }
  117.  
  118. function hasOffsetParent(v) {
  119. return v && v.offsetParent !== null && v.offsetParent.nodeType === 1; //is DOM valid on the page
  120. }
  121.  
  122. function noAutoStart(evt) {
  123. const video = getVideoFromEvent(evt);
  124.  
  125. if (video) {
  126. if (lastUserClickStatus===2) {
  127. //do nothing for user click
  128. } else if (settings.userPaused === true) {
  129. video.autoplay = false;
  130. video.pause();
  131. } else {
  132. if (lastPlayingStatus === true && document.hidden === true) {} else if (document.hidden === true) {
  133. video.autoplay = false;
  134. video.pause();
  135. }
  136. }
  137. }
  138.  
  139. }
  140.  
  141. const delayCall = function(p, f, d) {
  142.  
  143. if (delayCall[p] > 0) clearTimeout(delayCall[p])
  144. delayCall[p] = setTimeout(f, d)
  145. }
  146.  
  147.  
  148. function loader(detection) {
  149. return function() {
  150. let oldHref = document.location.href,
  151. bodyDOM = document.querySelector("body");
  152. const observer = new MutationObserver(function(mutations) {
  153. if (oldHref != document.location.href) {
  154. oldHref = document.location.href;
  155. detection();
  156. window.requestAnimationFrame(function() {
  157. let tmp = document.querySelector("body");
  158. if (tmp != bodyDOM) {
  159. bodyDOM = tmp;
  160. observer.observe(bodyDOM, config);
  161. }
  162. })
  163. }
  164. });
  165. const config = {
  166. childList: true,
  167. subtree: true
  168. };
  169. observer.observe(bodyDOM, config);
  170. }
  171. }
  172.  
  173. const anyRecentClick=()=>lastUserClickAt + 5000 > +new Date;
  174.  
  175.  
  176. function pageEnableAutoQualityAtStart() {
  177. //default 1080p
  178.  
  179.  
  180.  
  181. var cid = 0;
  182. var mDate = 0;
  183. const TIMEOUT = 8000; // just in case DOM is not found
  184.  
  185.  
  186. var gn = function(evt) {
  187.  
  188. const video = getVideoFromEvent(evt);
  189. if (!video) return;
  190.  
  191.  
  192. delayCall('$$video_init', function() {
  193.  
  194. if (video.hasAttribute('_viu_js_hooked')) return;
  195. video.setAttribute('_viu_js_hooked', '')
  196. video.addEventListener('playing', function(evt) {
  197.  
  198. const video = getVideoFromEvent(evt);
  199. if (!video) return;
  200. delayCall('$$video_playing_switch', function() {
  201. if (video.paused === false && hasOffsetParent(video)) {
  202. if (document.hidden !== true && settings.userPaused === true) settings.userPaused = false;
  203. }
  204. }, 300);
  205. }, true)
  206. video.addEventListener('pause', function(evt) {
  207. const video = getVideoFromEvent(evt);
  208. if (!video) return;
  209. delayCall('$$video_playing_switch', function() {
  210.  
  211. if (video.paused === true && hasOffsetParent(video)) {
  212. if (document.hidden !== true && (settings.userPaused === null || settings.userPaused === false)) settings.userPaused = true;
  213. }
  214. }, 300);
  215. }, true)
  216.  
  217. }, 800);
  218.  
  219. //call when the first video event fires
  220. if(lastUserClickStatus===1 && lastUserClickAt+5000 > +new Date){
  221. //url change: 2598 1443 1542 1975
  222. //without url change : 561
  223. console.log(`[viu] click and video status change ${+new Date -lastUserClickAt}`)
  224. //user perform action and video status changed
  225. lastUserClickStatus=2;
  226. }
  227. if(lastUserClickStatus!=2){
  228. // clear lastUserClickStatus
  229. delayCall('$$user_click_action_w38',function(){
  230. if(lastUserClickStatus!==2) lastUserClickStatus=0;
  231. },300)
  232. }else{
  233. // extend duration of status 2 for 800ms
  234. delayCall('$$user_click_action_w38',function(){
  235. if(lastUserClickStatus===2) lastUserClickStatus=0;
  236. },800)
  237. }
  238.  
  239. //disable autostart
  240. noAutoStart(evt);
  241.  
  242. mDate = +new Date + TIMEOUT;
  243. var jn = function(btn1080) {
  244. //button is found
  245. const bool = btn1080.matches(':not([aria-disabled=""]):not([aria-disabled="true"]):not([aria-checked="true"]):not([aria-checked=""])');
  246. console.log('resolution button found');
  247. if (bool) {
  248.  
  249. Promise.resolve()
  250. .then(()=>new Promise(r=>{
  251. const tf = function(){
  252.  
  253. let bool = document.querySelectorAll('button[id^="resolution_"]').length>=2 && document.querySelectorAll('button[id^="resolution_"].vjs-selected').length===1
  254. if(!bool)return setTimeout(tf,30);
  255.  
  256. let video = document.querySelector('video[id*="-video-viu-player"][src]')
  257. if(!video)return setTimeout(tf,30);
  258. const bReady = video.currentTime>0 && !video.ended && video.readyState>video.HAVE_CURRENT_DATA;
  259. if(!bReady)return setTimeout(tf,30);
  260.  
  261.  
  262.  
  263. console.log('video ready');
  264.  
  265. r();
  266.  
  267. }
  268. tf();
  269. }))
  270. .then(()=>{
  271.  
  272. let btn=document.querySelector('button#resolution_list.bmpui-ui-qualitysettingstogglebutton[aria-pressed]');
  273.  
  274. if(btn){
  275. btn.dispatchEvent(new Event('mouseenter'))
  276. btn.className.replace(/\b(bmpui-off)\b/,'bmpui-on');
  277. btn.setAttribute('aria-pressed','true');
  278. }
  279.  
  280. btn1080.dispatchEvent(new Event('mouseenter'))
  281.  
  282. })
  283. .then(()=>new Promise((resolve)=>setTimeout(resolve,8)))
  284. .then(()=>btn1080.click())
  285. .then(()=>new Promise((resolve)=>setTimeout(resolve,20)))
  286. .then(()=>{
  287.  
  288. btn1080.dispatchEvent(new Event('mouseleave'))
  289.  
  290.  
  291. let btn=document.querySelector('button#resolution_list.bmpui-ui-qualitysettingstogglebutton[aria-pressed]');
  292. if(btn){
  293. btn.dispatchEvent(new Event('mouseleave'))
  294. btn.className.replace(/\b(bmpui-on)\b/,'bmpui-off');
  295. btn.setAttribute('aria-pressed','false');
  296. }
  297.  
  298.  
  299. })
  300. .then(()=>new Promise((resolve)=>setTimeout(resolve,8)))
  301. .then(()=>{
  302.  
  303. let pElm=btn1080;
  304. let menuUI=null;
  305. while(pElm && pElm.parentNode){
  306. let checkClsName=pElm.className.replace(/\b(bmpui-ui-settings-panel|customize-video-option-panel)\b/gi,'@@');
  307. checkClsName=checkClsName.replace(/\b[a-zA-Z0-9_\-]+\b/gi,'').replace(/\s+/g,' ').trim();
  308. if(checkClsName=='@@ @@'){
  309. menuUI=pElm;
  310. break;
  311. }
  312. pElm=pElm.parentNode;
  313. }
  314.  
  315. if(menuUI){
  316. if(!/\b(bmpui-hidden)\b/.test(menuUI.className)) menuUI.className=menuUI.className.trim()+' bmpui-hidden';
  317. }
  318.  
  319.  
  320. })
  321. .catch((e)=>0)
  322.  
  323.  
  324. }
  325. }
  326. var zn = function() {
  327. //query when the video is loading/loaded/ready...
  328. if (cid > 0 && mDate < +new Date) {
  329. cid = clearInterval(cid);
  330. return;
  331. }
  332. var btn1080 = document.querySelector(`button[id^="resolution_${forceQuality}"]`)
  333. //var btn1080 = document.querySelector(`.vjs-menu-item[data-r="${forceQuality}"]`);
  334. if (!btn1080) return;
  335. if (cid > 0) cid = clearInterval(cid);
  336. if (btn1080.matches('[__userscript_viu_loaded]')) return true;
  337. btn1080.setAttribute('__userscript_viu_loaded', 'true');
  338. window.requestAnimationFrame(() => jn(btn1080)); // prevent too fast
  339. }
  340. if (cid > 0) cid = clearInterval(cid);
  341. if (!zn()) cid = setInterval(zn, 33);
  342. }
  343.  
  344. document.addEventListener('loadstart', gn, true)
  345. document.addEventListener('durationchange', gn, true)
  346. document.addEventListener('loadedmetadata', gn, true)
  347. document.addEventListener('loadeddata', gn, true)
  348. //document.addEventListener('progress', gn, true)
  349. document.addEventListener('canplay', gn, true)
  350. //document.addEventListener('canplaythrough', gn, true)
  351.  
  352. document.addEventListener('playing',function(evt){
  353.  
  354. if(!evt||!evt.target||evt.target.nodeName!="VIDEO")return;
  355. let video = evt.target;
  356.  
  357. requestAnimationFrame(()=>{
  358.  
  359. if(!isVideoPlaying(video)) return;
  360. let unmuteBtn = document.querySelector('button.bmpui-unmute-button.unmute-button.bmpui-muted');
  361. if(video.muted && unmuteBtn) unmuteBtn.click();
  362.  
  363. })
  364.  
  365.  
  366. },true)
  367.  
  368.  
  369. }
  370.  
  371.  
  372.  
  373. const detection1 = function() {
  374.  
  375. console.log('[viu] viu.com reloaded url - detection #1')
  376.  
  377.  
  378.  
  379. }
  380. const detection2 = function() {
  381.  
  382. console.log('[viu] viu.com reloaded url - detection #2')
  383.  
  384.  
  385.  
  386. }
  387.  
  388. const detection3 = function(info) {
  389.  
  390. console.log('[viu] viu.com reloaded url - detection #3', info)
  391.  
  392.  
  393. }
  394.  
  395. function handleBeforeUnload(event) {
  396. //core event handler for detection of false reloading
  397.  
  398.  
  399. console.log('[viu] viu.com reloaded url - detection #4')
  400.  
  401. const video = document.querySelector('video#viu-player_html5_api') || document.querySelector('video');
  402.  
  403. var saveObj = {};
  404. saveObj.version = __jver__;
  405. saveObj.datetime = +new Date();
  406. saveObj.pageVisible = !(document.hidden === true);
  407. saveObj.withVideo = (video && video.nodeName == "VIDEO")
  408. saveObj.userPaused = (settings.userPaused === true);
  409.  
  410. localStorage.__zvpp_unload1__ = JSON.stringify(saveObj);
  411.  
  412.  
  413. navStart = (+new Date) - 1;
  414.  
  415. // Cancel the event
  416. //event.preventDefault();
  417. //return (event.returnValue = ""); // Legacy method for cross browser support
  418.  
  419.  
  420. }
  421.  
  422. function handleVisibilityChange() {
  423. //enable if document.hidden exists
  424. const video = document.querySelector('video#viu-player_html5_api') || document.querySelector('video'); // just in case
  425. if (!video) lastPlayingStatus = null;
  426. if (document.hidden === false) {
  427. console.log('[viu] page show')
  428. } else if (document.hidden === true) {
  429. console.log('[viu] page hide')
  430. if (video) lastPlayingStatus = !video.paused
  431. } else {
  432. lastPlayingStatus = null;
  433. }
  434. }
  435.  
  436.  
  437. function isMenuBtn(evt) {
  438. return evt && evt.target && evt.target.nodeType === 1 && (evt.target.className || "").indexOf('vjs-menu-item') === 0;
  439. }
  440.  
  441. function setQualityAfterClick() {
  442. delayCall('$$change_video_quality', function() {
  443.  
  444. let selectedQuality=-1
  445.  
  446. let elm = document.querySelector('button[id^="resolution_"].vjs-selected');
  447. if(!elm)return;
  448. let regexp=/\d+/.exec(elm.id);
  449. if(regexp){
  450. selectedQuality = regexp[0];
  451. }
  452. //let attr = document.querySelector('.vjs-menu-item.vjs-selected[data-r]').getAttribute('data-r');
  453. if (+selectedQuality > 0) {
  454. forceQuality = (+selectedQuality).toString();
  455. console.log(`[viu] video quality set at ${selectedQuality}`)
  456. }
  457. }, 300)
  458. }
  459.  
  460.  
  461. pageInit();
  462. pageEnableAutoQualityAtStart();
  463.  
  464. if (window.onurlchange === null) window.addEventListener('urlchange', detection3, true); // feature is supported
  465.  
  466. window.addEventListener("load", loader(detection1), true);
  467.  
  468. uWin.addEventListener("load", loader(detection2), true);
  469.  
  470. uWin.addEventListener("beforeunload", handleBeforeUnload, true);
  471.  
  472. if (typeof document.hidden !== "undefined") document.addEventListener("visibilitychange", handleVisibilityChange, true);
  473.  
  474.  
  475. function handleMouseDown(evt) {
  476. lastUserClickAt = +new Date;
  477. lastUserClickStatus =1;
  478. delayCall('$$user_click_action_w38',function(){
  479. if(lastUserClickStatus===1) lastUserClickStatus=0;
  480. },5000)
  481. if (isMenuBtn(evt)) setQualityAfterClick();
  482. }
  483. document.addEventListener("mousedown", handleMouseDown, isPassiveOptionEnable?{
  484. passive: true,
  485. capture: true
  486. }:true)
  487.  
  488.  
  489.  
  490.  
  491. function onReady(){
  492.  
  493. setTimeout(function(){
  494.  
  495.  
  496. for(const s of document.querySelectorAll('link[rel="stylesheet"][href*=".css"]')){
  497.  
  498. if(!s.hasAttribute('type')) s.setAttribute('type','text/css');
  499. if(!s.hasAttribute('charset')) s.setAttribute('charset','utf-8');
  500.  
  501. }
  502.  
  503.  
  504. },40);
  505.  
  506. }
  507.  
  508.  
  509. if (document.readyState != 'loading') {
  510. onReady();
  511. } else {
  512. window.addEventListener("DOMContentLoaded", onReady, false);
  513. }
  514.  
  515.  
  516.  
  517. // Your code here...
  518. })(unsafeWindow || window);