上海交通大学 Canvas 平台课程视频播放器至尊版焕然一新插件

优化上海交通大学 Canvas 平台课程视频播放器的功能

  1. // ==UserScript==
  2. // @name 上海交通大学 Canvas 平台课程视频播放器至尊版焕然一新插件
  3. // @namespace http://tampermonkey.net/
  4. // @version 4.0.5
  5. // @description 优化上海交通大学 Canvas 平台课程视频播放器的功能
  6. // @author danyang685
  7. // @match https://oc.sjtu.edu.cn/*
  8. // @match https://courses.sjtu.edu.cn/*
  9. // @match https://vshare.sjtu.edu.cn/play/*
  10. // @match https://v.sjtu.edu.cn/jy-application-canvas-sjtu-ui/*
  11. // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAACPhJREFUWEfFl3mM1dUVx7/n3N/vzcIAI8x7Q2DezCBSVCLEMswMi7hFg7hExSVxqVZNWou1TZqa2jY10aRNGxNjtDYmtikVuxCtUWJjUYtxQWYcxaWWgbLMxgyzCgzM8t7vnm9zHx0LCMU2Tfr+/N3lfO653/O95wn+zz/5b+K3YFE8VnGoGBiZHJlpomUH8mXj+Qvb2sb+0/2+MMCWaWdMYTx2ljOtMaIcIomAk0g4KAZBiRV22CvbOIq/Lz3YNfRFYE4J8FbFvMkxxlYQnBsCqWdrAnajBEPjUcQQpCqK2Lt/fHpMPZOOs2AoUbGOZBzNpwI5KQABaanMzjcvyyjSqZBuo12lIuYNzy0ZbN929AmbKrJ1gNwA4DAEbRT2gDpXaE31A53vCVCAPf53QoAQvDmdXU7oAlH8qb23vaM2XXWJQecpuNUT5czxraNPtyVTs1rI2wCUC/DW4Un64KSDuTRdfCnI3rKByRvn45PcKQEKwTM1jYBVuny8qe7T3QfCoqaK6iu8WGt6anHn0P7RGnEua6SDoESp/TAfm7oKwBaC8lFjf8ezYV3LzJmllotuJniofqDzDwLY0RCfy0Dz9OxiqpxdrOPPLuztPTwx+Z0ZtbWa5+l0doGYLIDgbAJeAAGREOgR4SYBdlGi1oa+PR9OrN1cVVUSjblrKdbR0N/55kkBWmbOrPA5dyugO/IofqM4NezrurtHmtO1M8L9Q3GnAGUhxaQ0k9hd2EysVqiLIaxH4a75vDn32yX72trC8Nby2nIfscQLViWRvrqsZ0/7BMQxGWjK1FxD4jQhF1PQ6uheHkcyHIvcC8GVMLwhER5fvK9jW0hly2mnT9WikdTOffuGrgfYUllda5Q1IK+CyAYIHvPmnYNbeQSUB0mJOvrb194A+MKnCZLmaTOz1GgVFKMkVgngTO1h592lBG8E+KuRMvdEMJtN6XRZmRbXGvUy0KoFshfQl9v62z5OA1JSUX2PCO4FsF6V68y0AeBCAO+RKBL41xsG9u44FiBdtRKqeQ875MxdArFBmh6A8NsgXh8Z6Pj+hUCyJVN9qxhWi+JDQg4VNiZGITJJKD+r72/7YD3gqiuqfyCC6wV4yGBbBXK9QrfC/KB3LtvY1/7cZwDBWn2m707vuWHpYNfepsqa2ZLPJ9T4fggXuyRaXffp7o5PMD81nD70CIR1QjSFayCwAEQXwA+ocubkvsn3hHJ7f8aMdN6nQiXsIfGYWtKXlOoADqJEU3pdlErWBX0VruDtyjmZmLnVSYq/XtrVNVoou8zshaB/gsSfGwc6HiyUFBbFVtF/OwVzIGwj5TQACxXo0SR6xLvkfqd8uK6vc1eY35zJ3k3KHaTc1zjQvil8C9mpyVTfLEmyqX6ou7MA0JLJzjHIksV9Hc8Exwpe8G5F9RUU/FjANfX9nW9srqw6x3m9BSpZEJuDIYK6wmApgXgQg1BMBjlOES/Ex0Z5W4XrKPhJQ1/Hugk3DJ4iol3hugoA706vqveq1cE8QvAgzncz2a+Tcrc5vTKU05Z09qsC+SbAcVA+CD4AYMVxzkYQf4MUHqcnqfqa0P8RghemTUk9OnfnzvEwf0tFzYUADzYOdASLPgJAlWJN4ra8s9OLorFPcknqZgHuSmJ3eajbcE1FiZ/mxc4XSBnEZpMyBcCXIdwBSEziNYi9pKINRmuKYtub5N2zELQo3RNGmzUqo9snaUkY72/s6/qoAFC4b0vOFmInBcudj5/zLrkMIt9wJjfWDba1hnlBWDkf32vg2gi6hpCRYDwCzITymfrejtcKOkn3XatJ/E4i4ybOvUhgrdf4d47JeUiSJmg0l86PLOntCkIGggfAuYvzUbQpztt1ifMbI2iGHo9S5YGJkmlOZ28k5bCKeAO/RUEHKHkBM4DsoI9/Ojq0c2RSOnuLd+51SThbhL+A8HtMZAec1BblZfN4ihc7078uHtizvQAQmg11udvzkXsh8sllKrKT+WQ7XfRUMBkjfw7gchUU5yP3ZJT470gQW/ABhcBAqMxT4unyqfFfBg/krhT6j6DR10g2RKZ30dl0A+YIdSPByxONNizr3dVXACioPp29TRQt9DKbyllE9LxYchNE1gjlAQqvFmCeEY+rygDJuUJJAJRSMQZaCtB94vMbDaUHJMrPB7mWwt9Hpr8w4CLCTJ00GbG8va/jmWDHn1nxlsraRjWrDhCeci3gNwpiAf1DIEoVbg2cz9IsEtFhT9T982ktFUqKgiKC/aLyovMYS8R+Gdo0Ovuhg5bBc6Ugeso0OUtEhoNejrHi0HpFMnpH2IDkGSqwJLFWEVmkIt8FsF+V9yfiDknOSiVy8ZH3JWgwKFFoZi5SlhC4j8RcAj+KhFs9ZBkhh0XwPrxd41L+N3Xd3QPHABxxrpqlNKuxyL3jzK8i0I3Y3mQ+ukDINQJMJbCeHhus1PYs6eoaC+YSLHqkcrjKPK+G4CtAQZgPe+eakPgFqlIenM+rO1cUY419nRtP+BxvAqLSTPVNCuyC8ACJ8wzaZ95vUdEZKriFwHIBikj0iWKXsdAd1wKoBhie2NBwrPfG3aq6BAhe4d9kSI6z84sl9/TRjc7nOqItmdmVoF0nTF5xjNWcv4iGZovcgMvlPJ2rJWWRCM6FwAGiAg4TsjM0oIDbIZSpBn+BKDUhX3GkiurFOcMLywc6u0/aEU0MBAihX6lm20Rcp4fNomiDkh0Eeyg8oIiG85EUpfIozsUYk1w+iTRKa6JDFicrhDLoRd93hnLCrjD1rwTnO866/9WQHD+weXrVLKdupZB96qMPmcpPMepcktUKHjDwbaE7X8TC6TMgqkTZZqatCWx72C+CzhPwSzT/UuPQ3q7jY3xOhMdPCB2tT9x5Aswx020R0ArvYitKJh/2o+2TWLRInMYeekjFhuF1UBNN+djPovGc4BeWxK82Du08eKLgpwSYWBSaUpKLKJZRkbyZ7HWC/VDL50EJWoG36RCrFA0pR48k/r3FQ91dJ/tDcsIqOBnlxPfQXrsxZoloFmGTnTAncEIy8tDhCNLjzfX8uxN/YQ2cCuZ/Nf4P8iTQXa0LxcMAAAAASUVORK5CYII=
  12. // @grant GM_info
  13. // @grant GM_addStyle
  14. // @grant unsafeWindow
  15. // @license MIT
  16. // ==/UserScript==
  17.  
  18. /*-----------------------------------------------
  19. 本项目主页: https://gf.qytechs.cn/zh-CN/scripts/432918
  20. 水源社区讨论贴: https://shuiyuan.sjtu.edu.cn/t/topic/28688
  21. -----------------------------------------------*/
  22.  
  23. (function () {
  24. 'use strict';
  25. let script_version = GM_info.script.version; // 本脚本的版本号
  26. let window = unsafeWindow; // 脚本中使用GM函数后,必须使用 unsafeWindow 才可覆盖原有 window 事件回调函数
  27.  
  28. // 登录(不可用)页面,要求选 jAccount 或校外用户登录(不可用)
  29. let is_canvas_login_page = location.pathname == "/login/canvas" && location.origin == "https://oc.sjtu.edu.cn"; // 允许带 hash 的登录(不可用)页面
  30. // 点播页面,包括 Canvas 内置的和 courses.sjtu 网站上的
  31. let is_canvas_vod_page = location.href.startsWith("https://courses.sjtu.edu.cn/lti/app/lti/vodVideo/playPage");
  32. // 直播页面,Canvas 内置的
  33. let is_canvas_live_page = location.href.startsWith("https://courses.sjtu.edu.cn/lti/app/lti/liveVideo/index.d2j");
  34. // 课程视频 LTI 插件页面
  35. let is_canvas_lti162_page = new RegExp("https://oc\.sjtu\.edu\.cn/courses/\\d*/external_tools/(162|8199)").test(location.href);
  36. // 课程视频 LTI 插件页面
  37. let is_article_page = new RegExp("https://oc\.sjtu\.edu\.cn/courses/\\d*/modules/items/\\d*").test(location.href);
  38. // vshare 视频页面
  39. let is_vshare_page = location.href.startsWith("https://vshare.sjtu.edu.cn/play/")
  40. // 新 Canvas 课堂视频页面,2024秋季学期启用
  41. let is_new_canvas_video_page = location.href.startsWith("https://v.sjtu.edu.cn/jy-application-canvas-sjtu-ui/");
  42. // 处于 iframe 内
  43. let is_iframe = (self != top);
  44.  
  45. // 检查是否为安卓设备
  46. function isAndroidPhone() {
  47. const isAndroid = navigator.userAgent.toLowerCase().includes("android");
  48. const isSmallScreen = Math.min(window.screen.width, window.screen.height) < 500; // 收紧了安卓设备的范围
  49. return isAndroid && isSmallScreen;
  50. }
  51.  
  52. // 允许进一步缩放
  53. if (isAndroidPhone()) {
  54. // 怎么会一点也不起作用呢?一定是哪里出了问题,不应当不应当!
  55. // document.getElementById("viewport").setAttribute("content", "height=520, initial-scale=0, minimum-scale=0.25, maximum-scale=1.0, user-scalable=yes");
  56. }
  57.  
  58. // 到达 Canvas 登录(不可用)页时,自动跳转到 jAccount 登录(不可用)页
  59. if (is_canvas_login_page) {
  60. location.replace("https://oc.sjtu.edu.cn/login/openid_connect");
  61. }
  62.  
  63. // 新 Canvas 课堂视频页面,2024秋季学期启用
  64. else if (is_new_canvas_video_page) {
  65. console.log('新 Canvas 课堂视频页面,2024秋季学期启用')
  66.  
  67.  
  68. // 功能需求1,去除视频区域的姓名学号水印
  69. // https://shuiyuan.sjtu.edu.cn/t/topic/28688/480
  70. GM_addStyle(`
  71. #kmd-watermark-cvs {
  72. display: none !important;
  73. }
  74. `)
  75.  
  76. // 功能需求2,去除暂停视频的遮罩效果
  77. // https://shuiyuan.sjtu.edu.cn/t/topic/28688/480
  78. GM_addStyle(`
  79. div.player-pause-mask {
  80. display: none !important;
  81. }
  82. `)
  83.  
  84.  
  85. function AfterVideoLoaded() {
  86. console.log('视频加载完成');
  87. // 虽然可以直接暴露video元素,但是破坏了其他功能
  88.  
  89. // Array.from(document.getElementsByClassName("jkp-hover-wrap")).forEach(element => {
  90. // element.remove();
  91. // });
  92. // Array.from(document.getElementsByClassName("jkp-content-wrap")).forEach(element => {
  93. // element.remove();
  94. // });
  95. // Array.from(document.getElementsByClassName("jkp-default-slot-wrap")).forEach(element => {
  96. // element.remove();
  97. // });
  98.  
  99. console.log(document.getElementById("DraggableBox"))
  100. console.log(document.getElementsByClassName("jkp-default-slot-wrap"))
  101. // 全屏后,副屏dom会被整体移动到 .jkp-default-slot-wrap 里面
  102.  
  103. }
  104.  
  105. // 功能需求3,全屏时的双屏显示小屏鼠标滚轮控制大小
  106. // https://shuiyuan.sjtu.edu.cn/t/topic/28688/493
  107. function smallVideoWheelScale(event) {
  108. // 阻止默认的滚动行为,防止页面滚动
  109. event.preventDefault();
  110. const elements = document.getElementsByClassName("second-player-wrapper__body");
  111. if (elements.length == 0) {
  112. return;
  113. }
  114. const element = elements[0];
  115. var sizeDelta = -event.deltaY * 0.3;
  116. var currentWidth = parseFloat(window.getComputedStyle(element).width);
  117. var currentHeight = parseFloat(window.getComputedStyle(element).height);
  118. var ratio = currentHeight / currentWidth;
  119. currentWidth += sizeDelta
  120. if (currentWidth < 50) {
  121. currentWidth = 50;
  122. }
  123. currentHeight = currentWidth * ratio;
  124.  
  125. element.style.width = currentWidth + 'px';
  126. element.style.height = currentHeight + 'px';
  127. }
  128.  
  129. const observer = new MutationObserver(function (mutationsList, observer) {
  130. for (const mutation of mutationsList) {
  131. if (mutation.type === 'childList') {
  132. if (mutation.addedNodes.length) {
  133. // console.log("------------------");
  134. // console.log('新增子节点');
  135. mutation.addedNodes.forEach(element => {
  136. // console.log(element);
  137. // 右下角小窗口是div#DraggableBox里面的div.second-player-wrapper__body
  138. if (element.className != undefined && element.className.includes("second-player-wrapper__body")) {
  139. if (element.parentNode.id == "DraggableBox") {
  140. // console.log("发现你了")
  141. element.removeEventListener('wheel', smallVideoWheelScale);
  142. element.addEventListener('wheel', smallVideoWheelScale);
  143. }
  144.  
  145. }
  146. });
  147.  
  148.  
  149. }
  150. if (mutation.removedNodes.length) {
  151. // console.log("------------------");
  152. // console.log('移除子节点');
  153. mutation.removedNodes.forEach(element => {
  154. // console.log(element);
  155. });
  156. }
  157. } else if (mutation.type === 'attributes') {
  158. // console.log("------------------");
  159. // console.log(`属性${mutation.attributeName}发生变化`);
  160. // console.log(mutation.target);
  161. // console.log(mutation.target.className);
  162.  
  163. if (mutation.target.className.includes("jy-progress-bar")) {
  164. // observer.disconnect();
  165. AfterVideoLoaded();
  166. }
  167. }
  168. }
  169. });
  170.  
  171. const targetElement = document.getElementsByTagName('body')[0];
  172. observer.observe(
  173. targetElement,
  174. {
  175. attributes: true, // 监测属性变化
  176. childList: true, // 监测子元素的添加或移除
  177. subtree: true // 监测后代节点的变化
  178. }
  179. );
  180. }
  181.  
  182. // LTI插件页面
  183. else if (is_canvas_lti162_page) {
  184. $(document).ready(function () {
  185. const oc_course_id = location.href.match(new RegExp("courses/(\\d*)/"))[1];
  186.  
  187. // 去除了页面中多余的滚动条
  188. let clear_resize_event_interval = setInterval(function () {
  189. $(window).off("resize"); //删除疑似画蛇添足的resize事件,瞎删除2百次就行吧
  190. $(".tool_content_wrapper").attr("style", "").css("text-align", "center"); // 使iframe居中显示以便顺利纯享
  191. $("#tool_content")
  192. .css("height", "510px")
  193. .css("width", "1020px");
  194. }, 100);
  195. setTimeout(() => clearInterval(clear_resize_event_interval), 10000);
  196.  
  197. // 移除了【课程导航菜单】隐藏控制按钮点击时不好看的外轮廓
  198. GM_addStyle("#courseMenuToggle:hover {box-shadow:none !important;} #courseMenuToggle:focus {box-shadow:none !important;}")
  199.  
  200. // 关灯纯色元素
  201. $(".ic-Layout-columns").append($('<div class="light-turn-off"></div>'))
  202. $(".light-turn-off")
  203. .css("position", "fixed")
  204. .css("inset", "0")
  205. .css("background-color", "black")
  206. .css("z-index", "101")
  207. .css("display", "none"); // 页面最高z-index为100
  208. $("#tool_content")
  209. .css("position", "relative") // 写错了写错了,这里怎么可以是fixed呢
  210. .css("z-index", "102"); // 页面最高z-index为100
  211.  
  212. // 自动更新cookie,防止页面会话失效
  213. function help_refresh_session() {
  214. $("body").append('<iframe src="' + location.href + '" style="display:none; height:0;" id="session_updater_iframe">');
  215. }
  216.  
  217. let pending_request_refresh = false;
  218.  
  219. // 将相关重要跨域传递给子页面
  220. $(window).on("message", function (event) {
  221. let origin = event.origin || event.originalEvent.origin;
  222. let data = event.data || event.originalEvent.data;
  223.  
  224. if (origin == "https://courses.sjtu.edu.cn") {
  225. console.log("收到子页面的message", data);
  226.  
  227. const command_text = data.slice(0, data.indexOf("!") + 1);
  228.  
  229. switch (command_text) {
  230. case "online!": {
  231. document.getElementById("tool_content").contentWindow.postMessage(
  232. JSON.stringify({
  233. "message_type": "config_tranfer",
  234. "course_canvasid": oc_course_id,
  235. "course_name": $("#context_title").attr("value"),
  236. "course_fullname": $("#context_label").attr("value"),
  237. "user_name": $("#lis_person_name_full").attr("value"),
  238. "user_id": $("#custom_canvas_user_id").attr("value")
  239. }), "https://courses.sjtu.edu.cn");
  240. break;
  241. }
  242. case "help!": {
  243. help_refresh_session();
  244. break;
  245. }
  246. case "helpr!": {
  247. help_refresh_session();
  248. pending_request_refresh = true;
  249. break;
  250. }
  251. case "done!": { // 新创建的附属页面完成了一次登录(不可用),会话已更新
  252. $("#session_updater_iframe").remove();
  253. if (pending_request_refresh) { // 需要发送刷新指令
  254. pending_request_refresh = false;
  255. document.getElementById("tool_content").contentWindow.postMessage(
  256. JSON.stringify({
  257. "message_type": "request_refresh"
  258. }), "https://courses.sjtu.edu.cn");
  259. console.log("子页面要求在更新会话后刷新,刷新。");
  260. }
  261. break;
  262. }
  263. case "lightoff!": {
  264. // 修复了乱改z-index导致的某些场景中的界面样式错乱
  265. $(".ic-Layout-columns").css("z-index", "101");
  266. const color = data.slice(data.indexOf("!") + 1 - data.length);
  267. console.log(color)
  268. $(".light-turn-off").css("background-color", color)
  269. $(".light-turn-off").show(); // 搞渐变不好整啊,这finish和queue咋用啊。。。
  270. break;
  271. }
  272. case "lighton!": {
  273. // 修复了乱改z-index导致的某些场景中的界面样式错乱
  274. $(".ic-Layout-columns").css("z-index", "");
  275. $(".light-turn-off").hide();
  276. break;
  277. }
  278. case "goback!": {
  279. console.log("返回上一级!")
  280. // if(history.length>1) history.back(); // 这个不好用,会导致仅iframe被后退
  281. if (document.referrer.startsWith("https://oc.sjtu.edu.cn/courses/" + oc_course_id) && !document.referrer.endsWith("/external_tools/162")) {
  282. location.replace(document.referrer);
  283. } else {
  284. console.log("无法返回合适的上一级")
  285. location.replace("https://oc.sjtu.edu.cn/courses/" + oc_course_id);
  286. }
  287. break;
  288. }
  289. }
  290. }
  291. })
  292. })
  293. }
  294.  
  295. // Canvas 课程文章
  296. else if (is_article_page) {
  297. let content_wrapper = $("#content-wrapper");
  298. if (content_wrapper.length) {
  299. let vshare_iframe = $(content_wrapper[0]).find("iframe");
  300. if (vshare_iframe.length) {
  301. let item = vshare_iframe[0];
  302. item.setAttribute('allowFullScreen', '') // 允许 Canvas 课程文章内嵌的网页视频全屏播放
  303. item.src = item.src; // 但会导致每次打开,播放量增加2
  304. }
  305.  
  306. let iframe_title = $(content_wrapper[0]).find(".ui-listview-text");
  307. console.log(`length ${iframe_title}`)
  308. if (iframe_title.length) {
  309.  
  310. let item = $(iframe_title[0]).find("a");
  311. item.css("text-decoration", "underline") // 使 Canvas 课程文章内嵌的网页视频顶部标签链接更加醒目
  312. .css("color", "#33d")
  313. .css("text-shadow", "0px 0px 10px white");
  314. }
  315. }
  316.  
  317. }
  318.  
  319. // 课程视频页面
  320. else if (is_canvas_live_page || is_canvas_vod_page) {
  321. console.log("%cCanvas课程视频加强!%c当前链接:%c" + location.href, "color:blue;", "color:green;", "color:#0FF;");
  322.  
  323. const player_version = document.getElementsByTagName('html')[0].outerHTML.match(new RegExp("href=\"/lti/app/css/base\\.css\\?v=([0-9]*)\""))[1].slice(0, 8);
  324. const canvasCourseId = new URLSearchParams(location.search).get('canvasCourseId')
  325.  
  326. function numberClamp(val, min = undefined, max = undefined) {
  327. if (min !== undefined) {
  328. val = val < min ? min : val;
  329. }
  330. if (max !== undefined) {
  331. val = val > max ? max : val;
  332. }
  333. return val;
  334. }
  335.  
  336. let auto_refresh_flag = false,
  337. allow_long_live_flag = false, // 嘿嘿,嘿嘿
  338. allow_live_earlier = false,
  339. vlist_ajax_called_flag = true,
  340. live_video_links = new Array();
  341. canvas_live_vod_enhance_pre();
  342. let video_list_loaded_flag = false; // 通过捕获 getVideoInfoById 的调用来进行视频加载状态的检查
  343. {
  344. let raw_getVideoInfoById = getVideoInfoById;
  345. getVideoInfoById = function (id) {
  346. let vlist_ajax_called_flag_waiting_interval = setInterval(function () {
  347. console.log("直播列表加载完成检测中")
  348. if (!vlist_ajax_called_flag) {
  349. return; // 哦原来它没有返回值啊,那太好了,可以用俺的土办法
  350. }
  351. clearInterval(vlist_ajax_called_flag_waiting_interval);
  352.  
  353. let data = undefined;
  354.  
  355.  
  356. /*
  357. while(!vlist_ajax_called_flag){
  358. (async () => await new Promise(resolve => setTimeout(resolve, 10)))(); // 抄来的高级代码啊哈哈哈哈,嘤嘤嘤根本等不来那个ajax callback
  359. console.log("waiting for vlist_ajax_called_flag");
  360. }
  361. */
  362.  
  363. if (allow_live_earlier) {
  364. let newf = undefined;
  365. eval("newf = " + raw_getVideoInfoById.toString()
  366. .replace("getVideoInfoById", "")
  367. .replace("noStart();\n\t\t\t\t\treturn;", "")
  368. .replace("setTimeout(overLive, (courEndTime -currentTime));", "setTimeout(overLive, (courEndTime -currentTime)+15*60*1000);") // 允许持续观看直播至课后20分钟
  369. );
  370. raw_getVideoInfoById = newf;
  371. console.log("allow_live");
  372. }
  373. raw_getVideoInfoById(id);
  374. video_list_loaded_flag = true; // 如果没有视频的时候,它就不会被调用了emmmm。。。
  375. console.log("loaded!");
  376. }, 10);
  377. }
  378. }
  379. let onload_check_interval = setInterval(function () {
  380. if (!video_list_loaded_flag) { // 或许用 $("body .loading").css("display")!="none" 判断的话会有极小概率出现线程间不同步(或许??)
  381. console.log("loading...");
  382. // 20241123注意到后台启动浏览器标签页时有时会卡在这里,不知道如何解决。getVideoInfoById没成功覆盖?
  383. return;
  384. }
  385. clearInterval(onload_check_interval);
  386. console.log("页面加载完成")
  387. const t0 = Date.now();
  388. canvas_live_vod_enhance();
  389. console.log(`页面加载完成后任务运行完成,耗时:${Date.now() - t0}毫秒`)
  390. }, 50);
  391.  
  392. function canvas_live_vod_enhance_pre() {
  393. if (is_canvas_vod_page) {
  394. // 阻止向服务器回报观看日志
  395. if (window.openQuestion) openQuestion = function () { };
  396. else {
  397. console.log("%c日志阻断失败", "color:#0FF;");
  398. }
  399. if (window.addVodPlayLog) addVodPlayLog = function () { };
  400. else {
  401. console.log("%c日志阻断失败", "color:#0FF;");
  402. }
  403. if (window.updateLiveCount) updateLiveCount = function () { };
  404. else {
  405. console.log("%c日志阻断失败", "color:#0FF;");
  406. }
  407. if (window.updateVodPlayLog) updateVodPlayLog = function () { };
  408. else {
  409. console.log("%c日志阻断失败", "color:#0FF;");
  410. }
  411. //dateVodPlayLog = updateLiveCount = addVodPlayLog = function () {};
  412. } else {
  413. if (window.updateLivePlayLog) updateLivePlayLog = function () { };
  414. if (window.updateLiveCount) updateLiveCount = function () { };
  415. if (window.addLivePlayLog) addLivePlayLog = function () { };
  416. //dateLivePlayLog=updateLiveCount=addLivePlayLog = function () {};
  417. }
  418.  
  419. // 当前处于纯享模式
  420. if (location.hash == "#pure") {
  421. sessionStorage.setItem("pure", 1); // 一直就pure了
  422. location.hash = ""
  423. }
  424.  
  425. // 允许通过点击顶部当前标签页重新载入当前网页
  426. $(".tab-item--active").click(function () {
  427. location.replace(location.href);
  428. });
  429.  
  430. if (is_canvas_live_page) {
  431. // 将直播中同一场课程的多个节次合并显示
  432. let old_creatCourseList_func = creatCourseList;
  433. creatCourseList = function (lst) {
  434. let joint_live_list = Array.from(function* getJointLiveItemList(lst) {
  435. // 注意一会别忘了写长度为1的判定。……啊哈根本不需要写哈哈哈哈哈
  436. for (let i = 1; i < lst.length; i++) {
  437. let is_continious = Date.parse(lst[i].courBeginTime) - Date.parse(lst[i - 1].courEndTime) <= 1000 * 60 * 20, // 间隔小于20分钟
  438. is_same_room = lst[i].clroName == lst[i - 1].clroName; // 在同一教室
  439. if (is_continious && is_same_room) { // 视为同一课程安排
  440. // TODO: 请优先返回后一节课程的录播流
  441. lst[i - 1].courEndTime = lst[i].courEndTime;
  442. lst[i] = lst[i - 1];
  443. console.log("joint!")
  444. } else {
  445. yield lst[i - 1]; // 之前那个
  446. }
  447. }
  448. yield lst[lst.length - 1]; // 最后一个
  449. }(lst));
  450. old_creatCourseList_func(joint_live_list);
  451. }
  452. } else {
  453. // 修复了使用Firefox或Safari浏览器时右侧视频列表视频时间显示为NaN的问题
  454. // 此功能官方已修复,但是这段代码还是留着吧,又没有副作用
  455. {
  456. let newf = undefined;
  457. eval(creatCourseList.toString()
  458. .replace("function creatCourseList(data) ", "newf = function(data)")
  459. // .replace("data[i].courseBeginTime;", "data[i].courseBeginTime;console.log('starttime:'+startTime);")
  460. // 看来这里没问题
  461.  
  462. )
  463. creatCourseList = newf;
  464. } {
  465. let newf = undefined;
  466. eval(dateFormat.toString()
  467. .replace("function dateFormat(timestamp, formats)", "newf = function(timestamp, formats)")
  468. .replace('timestamp.replace("-","/")', "timestamp") // 难道是故意创造的不兼容?
  469. )
  470. dateFormat = newf;
  471. }
  472. }
  473.  
  474. $(document).ajaxComplete(function (event, xhr, settings) { // woc jQuery好厉害,但它不够快。明天再改。。。。。。。
  475. if (xhr.responseJSON == undefined) {
  476. console.log("怎么办怎么办" + settings.url + "请求到了undefined。")
  477. console.log(event, xhr, settings)
  478. return
  479. }
  480. const response_json = xhr.responseJSON;
  481. switch (settings.url) {
  482. case "/lti/liveVideo/findLiveList": {
  483. // console.log("ajax done!!!",event,xhr,settings);
  484. if (xhr.status != 200) { // 这是因为后台页面正在进行登录(不可用)
  485. location.reload();
  486. }
  487. let live_list = response_json.body.list;
  488. if (live_list.length) {
  489. let this_id = (new URLSearchParams(location.search).get("id") || live_list[0].id).replaceAll(" ", "+"); // 为什么加号会变成空格??
  490. console.log("活跃的视频ID:" + this_id);
  491. live_list = live_list.filter(live_item => live_item.id == this_id);
  492. if (live_list.length) {
  493. let course_start_time_span = (Date.parse(live_list[0].courBeginTime) - new Date().getTime()) / 1000 / 60;
  494. // 允许在课前25分钟即开始观看课程直播
  495. if (course_start_time_span < 25 || allow_long_live_flag) {
  496. allow_live_earlier = true;
  497. vlist_ajax_called_flag = true; // 否则是来不及的
  498. console.log("try_allow_live");
  499. }
  500. }
  501. } else {
  502. video_list_loaded_flag = true; // 直接置位视频列表已加载的标志
  503. console.log(`检测到 ${settings.url}`)
  504. }
  505. break;
  506. }
  507. case "/lti/vodVideo/findVodVideoList": {
  508. // console.log("ajax done!!!",event,xhr,settings);
  509. if (xhr.status != 200) { // 这是因为后台页面正在进行登录(不可用)
  510. location.reload();
  511. }
  512. let vod_list = response_json.body.list;
  513. if (vod_list.length == 0) {
  514. video_list_loaded_flag = true; // 直接置位视频列表已加载的标志
  515. console.log(`检测到 ${settings.url}`)
  516. }
  517. break;
  518. }
  519. case "/lti/liveVideo/getLiveVideoInfos": { // 直播源的信息
  520. // console.log("ajax done!!!",event,xhr,settings);
  521. if (xhr.status != 200) { // 这是因为后台页面正在进行登录(不可用)
  522. location.reload();
  523. }
  524. let live_list = response_json.body.videoPlayResponseVoList;
  525. live_list.forEach(function (item) {
  526. live_video_links.push(item.rtmpUrlHdv)
  527. })
  528. console.log(live_video_links)
  529. break;
  530. }
  531. default: {
  532. break;
  533. }
  534. }
  535. });
  536. }
  537.  
  538. function canvas_live_vod_enhance() {
  539. let is_from_default_lti_entry = document.referrer.startsWith("https://oc.sjtu.edu.cn/"),
  540. video_count = $(".lti-list .item-text").length || $(".lti-list .item-infos").length,
  541. is_live_playing = $(".lti-list .live-course-item--avtive .icon-play").length,
  542. no_live_video_played = false,
  543. just_clicked_screen = false; // 刚刚点过屏幕
  544.  
  545. let is_single_video = undefined,
  546. use_storage_flag = true;
  547.  
  548. // 在右侧视频列表顶部用文字显示视频总数
  549. $(".lti-list>.list-title").append($('<span style="font-size: 50%;">(' + video_count + '条视频)</span>'));
  550. // 移除视频列表标题区的不正确的默认提示文字
  551. $(".lti-list>.list-title").attr("title", "");
  552.  
  553.  
  554. let usercode = undefined; { // 学号
  555. let ma = document.getElementsByTagName('html')[0].outerHTML.match(new RegExp("loginUserCode = '([0-9A-Za-z\\-%]*)';"));
  556. if (ma) {
  557. usercode = decodeURIComponent(ma[1]);
  558. }
  559. }
  560.  
  561. function setStorage(k, v) {
  562. if (use_storage_flag) {
  563. localStorage.setItem("canvasnb_plugin_" + k, v);
  564. }
  565. }
  566.  
  567. function getStorage(k) {
  568. if (use_storage_flag) {
  569. return localStorage.getItem("canvasnb_plugin_" + k);
  570. } else {
  571. return null;
  572. }
  573. }
  574.  
  575. function isVideoStuck(vid) {
  576. // 浏览器升级至Chrome 97后,安装本插件直接打开【课程视频】页面时,视频将开始转圈圈,不能播放,需要刷新。此时将自动刷新页面
  577. return $(`#loading-service-0000${vid} .kmd-flex-center.kmd-loading-view.kmd-loading-bg.kmd-full`).length != 0;
  578. }
  579. let videoStarted = false;
  580. function isVideoBuffering() {
  581. let badCnt = 0;
  582. $('video#kmd-video-player').each(function (index) {
  583. if ($(this)[0].readyState < 2) // 1: HAVE_METADATA, 2: HAVE_CURRENT_DATA, 3: HAVE_FUTURE_DATA, 4: HAVE_ENOUGH_DATA
  584. {
  585. badCnt += 1;
  586. }
  587. // console.log(`video ${index} ${$(this)[0].readyState}`)
  588. });
  589. // console.log(`badCnt ${badCnt}`)
  590. return badCnt != 0;
  591. }
  592.  
  593. // 首先获取本页面的课程ID
  594. let course_id = undefined; {
  595. // 本科生课程0-9A-Z,研究生课程额外含有a-z和-,感谢@icebreak的反馈
  596. // 修复某些情况下获取到错误错误ID的情况
  597. let ma = document.getElementsByTagName('html')[0].outerHTML.match(new RegExp('canvasCourseId="([0-9A-Za-z\\-%]*)";'));
  598. if (ma) {
  599. course_id = decodeURIComponent(ma[1]);
  600. }
  601. }
  602. console.log("课程ID:" + course_id);
  603. if (!course_id) {
  604. console.log("%c课程ID获取失败!", "color:red;")
  605. use_storage_flag = false;
  606. course_id = "UNKNOWN_COURSE"
  607. }
  608.  
  609. // 再获取本视频的视频ID
  610. let video_id = $(".lti-list .list-item--active").attr("id") || $(".lti-list .live-course-item--avtive").attr("id"),
  611. userid = getStorage("course_" + course_id + "_last_userid"),
  612. username = getStorage("course_" + course_id + "_last_username"),
  613. last_play = getStorage(usercode + "_course_" + course_id + "_lastplay_video_id");
  614.  
  615. // 修复了部分场景下无法自动跳转到上一次观看的点播视频的bug
  616. if (course_id != "UNKNOWN_COURSE" && last_play != null) {
  617. if (is_canvas_vod_page) {
  618. if (new URLSearchParams(location.search).get('id') === null) { // 适配20211201防串课更新
  619. location.replace(location.href + "&id=" + last_play); // 适配20211201防串课更新
  620. return;
  621. }
  622. } else {
  623. // 适配20211201防串课更新
  624. $(".tab-demand>a").attr("href", "/lti/app/lti/vodVideo/playPage?canvasCourseId=" + encodeURIComponent(canvasCourseId) + "&id=" + last_play); // 直播页面顶部切换到点播页面的按钮
  625. }
  626. }
  627.  
  628. // 修复了右侧视频栏中已激活的视频在部分场景下仍然可点击的bug
  629. if (video_id && video_id.length > 10) { // 10是我瞎写的
  630. let new_search = "?canvasCourseId=" + encodeURIComponent(canvasCourseId) + "&id=" + video_id; // 适配20211201防串课更新
  631.  
  632. let new_url = new URL(location); // 这里原来不能用location啊,呜呜呜之前写的一直是错的
  633. new_url.search = new_search;
  634.  
  635. if (location.href != new_url && location.search != new_search) { // 点播页面检查的是当前url
  636. console.log("链接未包含视频id,原search:" + location.search + "目标search:" + new_search)
  637. console.log("raw url: " + location.href + " , push new url: " + new_url);
  638. window.history.pushState('data', document.title, new_url);
  639. }
  640. if (is_canvas_live_page) {
  641. urlId = video_id; // 直播页面检查的是内部变量
  642. }
  643. }
  644.  
  645. // 移除自带的反馈和帮助按钮,我的优秀用户已经不需要看那个入门资料了
  646. $(".lti-page-tab>.tab-help").remove()
  647.  
  648.  
  649. // 跳转到合适的直播页面
  650. function redirectToVodPage() {
  651. // 修复了自动跳转到点播页面时,需要二次跳转到上次视频的bug
  652. let last_play = getStorage(usercode + "_course_" + course_id + "_lastplay_video_id");
  653. if (last_play != null) {
  654. // 适配20211201防串课更新
  655. location.replace("https://courses.sjtu.edu.cn/lti/app/lti/vodVideo/playPage?canvasCourseId=" + encodeURIComponent(canvasCourseId) + "&id=" + last_play); // 适配20211201防串课更新
  656. return true;
  657. } else {
  658. // 适配20211201防串课更新
  659. if (location.href != "https://courses.sjtu.edu.cn/lti/app/lti/vodVideo/playPage?canvasCourseId=" + encodeURIComponent(canvasCourseId)) {
  660. // 避免了潜在的死循环可能(对于未开放录播的课程)
  661. location.replace("https://courses.sjtu.edu.cn/lti/app/lti/vodVideo/playPage?canvasCourseId=" + encodeURIComponent(canvasCourseId));
  662. return true;
  663. }
  664. }
  665. return false; // 适配了未开放录播因此无处跳转的课程
  666. }
  667.  
  668. let no_live_available = false;
  669.  
  670. if (is_canvas_live_page) {
  671. let should_redirect_flag = false;
  672. let will_not_play = true;
  673. if (video_count != 0) {
  674. let item_date = $(".live-course-item--avtive .item-infos p:nth-child(2) span:nth-child(2)").text(),
  675. item_time = $(".live-course-item--avtive .item-infos p:nth-child(3) span:nth-child(2)").text().split("~")[0],
  676. next_date_span_minute = (Date.parse(item_date + " " + item_time) - new Date().getTime()) / 1000 / 60; // 距离正式开始直播的分钟数
  677.  
  678. console.log(`距离上课还有${next_date_span_minute}分钟!`)
  679. // 当距离上课还有超过40分钟时,访问【课程视频】自动切换到点播页面
  680. if (next_date_span_minute > 40 && !allow_long_live_flag) {
  681. should_redirect_flag = true;
  682. console.log('距离上课还有超过40分钟时,访问【课程视频】自动切换到点播页面')
  683. }
  684. if (next_date_span_minute > 25 && !allow_live_earlier) {
  685. setTimeout(function () {
  686. if (is_iframe) {
  687. refresh_session(true); // 刷新页面,开始观看直播
  688. } else {
  689. location.reload(); // 这个恐怕很难有用吧,猜猜会话多久过期?
  690. // 经过测试,在未重复登录(不可用)的情况下,会话可以连续使用长达15小时以上
  691. // 但在多次重复进入【课程视频】页面后,旧的会话可能迅速过期
  692. // 那就每五分钟刷新一次吧!
  693. }
  694. }, 5 * 60 * 1000); // ((60 * (next_date_span_minute - 25)) + 30) * 1000
  695.  
  696. // 在开始直播时自动刷新页面,以便进行无人值守的录屏
  697. const video_not_start_text_shown_check_interval = setInterval(function () {
  698. if ($("#playerDiv>.live-video-tips").length) {
  699. clearInterval(video_not_start_text_shown_check_interval);
  700. } else {
  701. return;
  702. }
  703. $(".live-video-tips").html("课程将于<span></span>后开始,课前25分钟自动开放")
  704.  
  705. function updateTimeleftText() {
  706. const time_left = (Date.parse(item_date + " " + item_time) - new Date().getTime()) / 1000 - 25 * 60 + 30;
  707. //据说这样比较好理解
  708. var time_second = parseInt(time_left);
  709. var time_day = 0;
  710. var time_hour = 0;
  711. var time_minute = 0;
  712. while (time_second > 24 * 60 * 60) {
  713. time_second -= 24 * 60 * 60;
  714. time_day += 1;
  715. }
  716. while (time_second > 60 * 60) {
  717. time_second -= 60 * 60;
  718. time_hour += 1;
  719. }
  720. while (time_second > 60) {
  721. time_second -= 60;
  722. time_minute += 1;
  723. }
  724. var time_str = "";
  725. if (time_day > 0) {
  726. time_str += `${time_day}天`;
  727. }
  728. if (time_hour > 0 || time_str.length > 0) {
  729. time_str += `${time_hour}时`;
  730. }
  731. time_str += `${time_minute}分`;
  732. $(".live-video-tips>span").text(time_str);
  733. }
  734. setInterval(updateTimeleftText, 20000);
  735. updateTimeleftText();
  736. }, 5)
  737.  
  738.  
  739. }
  740. } else {
  741. should_redirect_flag = true;
  742. }
  743. if (is_from_default_lti_entry) {
  744. if (window.innerHeight == 0) { // 我是用于更新会话的工具iframe
  745. console.log("会话更新已完成!课程ID:%c" + course_id, "color:#0FF;");
  746. top.postMessage("done!", "https://oc.sjtu.edu.cn");
  747. return;
  748. } else if (should_redirect_flag) {
  749. if (redirectToVodPage()) return;
  750. }
  751. }
  752. if (should_redirect_flag && !allow_live_earlier) {
  753. no_live_available = true; // 无可用的直播视频时,停止等待视频
  754. }
  755. }
  756.  
  757.  
  758. // 从canvas内直接打开【视频点播】时,自动切换到上次观看的视频
  759. if (is_canvas_vod_page) {
  760. if (new URLSearchParams(location.search).get('id') == null) { // 适配20211201防串课更新
  761. if (redirectToVodPage()) return; // 适配了未开放录播因此无处跳转的课程
  762. }
  763. if (video_id != undefined) {
  764. setStorage(usercode + "_course_" + course_id + "_lastplay_video_id", video_id);
  765. }
  766. }
  767.  
  768. console.log("视频ID:" + video_id);
  769.  
  770. if (is_iframe) {
  771. console.log("发送message:online!");
  772. top.window.postMessage("online!", "https://oc.sjtu.edu.cn");
  773. }
  774.  
  775. $(window).on("message", function (event) {
  776. let origin = event.origin || event.originalEvent.origin;
  777. let data = event.data || event.originalEvent.data;
  778.  
  779. if (is_iframe && origin == "https://oc.sjtu.edu.cn") { // oc.sjtu仅会向iframe内的course.sjtu发送消息
  780. console.log("收到父页面的message", data);
  781. let message = JSON.parse(data);
  782. if (message["message_type"] == "config_tranfer") {
  783. setStorage("course_" + course_id + "_realname", message["course_name"]);
  784. setStorage("course_" + course_id + "_canvasid", message["course_canvasid"]);
  785. setStorage("course_" + course_id + "_fullname", message["course_fullname"]);
  786. setStorage("course_" + course_id + "_last_userid", message["user_id"]);
  787. setStorage("course_" + course_id + "_last_username", message["user_name"]);
  788. userid = message["user_id"];
  789. username = message["user_name"];
  790. console.log("config_message:", message);
  791. console.log("userid:", userid);
  792. console.log("username:", username);
  793. } else if (message["message_type"] == "request_refresh") {
  794. location.reload(); // 刷新页面
  795. }
  796. }
  797. });
  798.  
  799. // 覆盖已有设定,有一说一,光屏蔽F12有用吗,Chrome还可以 Ctrl+Shift+I 呢。
  800. window.onkeydown = window.onkeyup = window.onkeypress = window.oncontextmenu = undefined;
  801.  
  802. // 移除了视频下方课程信息区域,压缩页面高度
  803. $(".course-details").empty();
  804.  
  805. // 移除直播视频上的两个无意义图标
  806. $(".live-review-icon").remove();
  807. $(".icon-play").remove();
  808.  
  809. // 将全局右键屏蔽改为仅应用至视频区域
  810. $("#rtcMain").bind('contextmenu', function (event) {
  811. // console.log("contextmenu");
  812. event.preventDefault();
  813. return false;
  814. });
  815.  
  816.  
  817. // 无视频可播放时,这部分依然需要执行
  818. // 修复了未开放录播的课程的页面外观
  819. if (is_canvas_vod_page) {
  820. $("#rtcMain").css("background-color", "transparent");
  821. $(".video-box").css("background-color", "black"); // 否则这里就会一片空白
  822. } else {
  823. $("#rtcMain").css("background-color", "black");
  824. }
  825.  
  826. // 播放器大致加载出来之后做的事
  827. function afterVideoPreloaded() {
  828. if (!sessionStorage.getItem("pure")) {
  829. $("#rtcMain").css("position", "relative");
  830. $("#rtcContent").css("width", "100%").css("height", "406px"); // 20241025适配新版本的尺寸的微小变化
  831. $(".lti-video").css("margin-top", "26px").css("margin-bottom", "0");
  832. $(".video-box")
  833. .css("width", "unset")
  834. .css("height", "unset");
  835.  
  836. // 将课程名移动到标题栏的空白区域中
  837. const course_name = $(".list-title.courser-video").text().trim();
  838. if (!is_iframe) {
  839. $('<div style="display: inline-block; font-size: 28px; font-weight: bold; margin-left: 200px; margin-top: -2px; position: absolute;"></div>')
  840. .text(course_name)
  841. .insertBefore($("#btn_about_canvasnb"));
  842. document.title = `${course_name} ${$(".tab-item--active").text()}`;
  843. }
  844. $(".list-title.courser-video").remove();
  845. }
  846. }
  847.  
  848. // 播放器完全加载出来之后做的事
  849. function afterVideoLoaded() {
  850. let is_user_interacted = false; // 用户已进行交互的标志,感谢 Teruteru 的辛苦调试,现在不会在视频播放前卡住了
  851.  
  852. // 特殊判断只有一个视频流的点播视频
  853. is_single_video = $(".cont-item-2").length == 0;
  854.  
  855. // 隐藏画面上的音量控制
  856. $(".voice-icon").hide();
  857.  
  858. function lightoffControl(on, bgcolor) {
  859. if (on) {
  860. $("#lightControl>span").text("开灯")
  861. let color = bgcolor || getStorage(usercode + "_black_color");
  862. if (color == null) {
  863. color = "black";
  864. }
  865. setStorage(usercode + "_black_color", color);
  866. $(".light-turn-off").css("background-color", color)
  867. $(".light-turn-off").show();
  868.  
  869. if (is_iframe) { // 父页面也关灯
  870. top.postMessage(`lightoff!${color}`, "https://oc.sjtu.edu.cn");
  871. }
  872. } else {
  873. $("#lightControl>span").text("关灯")
  874. $(".light-turn-off").hide()
  875. if (is_iframe) {
  876. top.postMessage("lighton!", "https://oc.sjtu.edu.cn");
  877. }
  878. }
  879. }
  880.  
  881. // 是否正在播放/暂停
  882. function getPlay() {
  883. return $(".tool-btn__play").css("display") == "none"; // 修复了不恰当的“播放"判据
  884. }
  885.  
  886. // 设置播放/暂停
  887. function setPlay(status, quiet = false) {
  888. if (status) {
  889. console.log("请求播放!");
  890. kmplayer.play("play");
  891. if (!quiet) {
  892. putText("状态:播放");
  893. is_user_interacted = true;
  894. }
  895. } else {
  896. console.log("请求暂停!");
  897. kmplayer.play("pause");
  898. if (!quiet) {
  899. putText("状态:暂停");
  900. }
  901. }
  902. }
  903.  
  904. // 切换播放/暂停
  905. function togglePlay() {
  906. setPlay(!getPlay());
  907. }
  908.  
  909. function getMuted() {
  910. return $(".tool-btn__voice").length == 0;
  911. }
  912.  
  913. function setMuted(muted) {
  914. kmplayer.voice((muted) ? "voice" : "muted");
  915. }
  916.  
  917. function setSpeed(speed) {
  918. for (let i = 0; i < kmplayer.ids.length; i++) {
  919. kmplayer.allInstance['type' + (i + 1)].playbackRate(speed);
  920. }
  921. }
  922.  
  923. function changeSpeed(speed) {
  924. console.log("设定倍速:" + speed);
  925. setStorage(usercode + "_speed_val", speed);
  926. setSpeed(speed);
  927. clearTimeout(timeout_reset_speed_text);
  928. $("#timesContorl>span").html(speed);
  929. timeout_reset_speed_text = setTimeout(function () {
  930. $("#timesContorl>span").html("倍速"); // 修改倍速后,状态栏短暂显示倍速数值
  931. }, 500)
  932. $("#timesContorl>ul>li").each(function (idx, elem) {
  933. if (elem.id == speed) {
  934. $(elem).attr("class", "times-active");
  935. } else {
  936. $(elem).attr("class", "");
  937. }
  938. });
  939. }
  940.  
  941. // 读取播放速度
  942. function getSpeed() {
  943. return $("#player-00001 #kmd-video-player")[0].playbackRate;
  944. }
  945.  
  946. // 调整倍速
  947. function increaseSpeed(delta) {
  948. // 缝补修复了不恰当的倍速控制功能逻辑
  949. // 20221212小注释,这个delta参数只有2个取值,0.1和-0.1,那么0.1进1档,-0.1退1档岂不美哉?
  950. let is_using_default_speed = (Math.abs(getSpeed() - default_time_speed) < 0.005);
  951. let time_speed_base = is_using_default_speed ? default_time_speed : current_time_speed;
  952. // 20221212这个好像是用来四舍五入的?忘了……
  953. // 20221212哦!想起来了,之前是按键盘Z,可以临时回到默认播放速度来着
  954.  
  955. if (delta > 0) {
  956. current_time_speed = Math.floor((time_speed_base + 0.0001) * 100) / 100; // 仅保留小数点后一位,末尾数为偶数
  957. } else {
  958. current_time_speed = Math.ceil((time_speed_base - 0.0001) * 100) / 100; // 仅保留小数点后一位,末尾数为偶数
  959. }
  960.  
  961.  
  962. current_time_speed += delta;
  963.  
  964. // 限制最大倍速范围为 0.6-6.0,超强十倍变速,过低倍速会导致声音变怪
  965. // 20221212更新,因canvas播放器底层代码限制,倍速仅支持0.5、1.0、1.25、1.5、2.0、4.0、8.0共7档
  966. // 20221212更新,16倍七段超强变速!(也不是不行)
  967. // 20241123更新,由于教育技术中心已经放开此限制,应用户要求,已经恢复至原功能
  968. current_time_speed = numberClamp(current_time_speed, 0.6, 6.0);
  969. current_time_speed = Math.round(current_time_speed * 100) / 100; // 最多两位,避免浮点数误差
  970. changeSpeed(current_time_speed);
  971. putText(((delta > 0) ? "增加" : "减小") + "倍速:" + current_time_speed);
  972. }
  973.  
  974. // 调整音量
  975. function increaseVolume(volDelta) {
  976. let newVol = Math.round((getVolume() + volDelta) * 100);
  977. newVol = numberClamp(newVol, 0, 100);
  978. setVolumeMix(newVol / 100, 0);
  979. // 使用快捷方式调节音量时,会在画面左上角以渐隐文本提示音量变化
  980. putText(`音量: ${newVol}%`); // 调节时会在画面左上角显示当前音量
  981. }
  982.  
  983. function toggleFullscreen() {
  984. kmplayer.scrren(); // 这都能拼错?
  985. putText("切换全屏");
  986. }
  987.  
  988. // 设定播放位置
  989. function setTime(time) {
  990. console.log('set time!!!')
  991. time = numberClamp(time, Math.abs(getTimeDelta()), getDuration() - Math.abs(getTimeDelta()));
  992. $("#player-00001 #kmd-video-player")[0].currentTime = time;
  993. $("#player-00002 #kmd-video-player")[0].currentTime = time + getTimeDelta();
  994. }
  995.  
  996. // 读取播放位置
  997. function getTime() {
  998. return $("#player-00001 #kmd-video-player")[0].currentTime;
  999. }
  1000.  
  1001. // 重写状态栏播放状态
  1002. function updateTimeText() {
  1003. kmplayer.setViewSenTime(getTime()); // 时间显示同步
  1004. kmplayer.addSpeedRate(getTime()); // 进度条同步
  1005. }
  1006.  
  1007. // 读取音量
  1008. function getVolume() {
  1009. return kmplayer.volume / 100;
  1010. }
  1011.  
  1012. // 设定各通道音量
  1013. function setVolumeMix(volume_ratio, idx) {
  1014. volume_ratio = numberClamp(volume_ratio * 100, 0, 100);
  1015. $(".voice" + idx + "-rate").height(volume_ratio);
  1016. switch (idx) {
  1017. case 0:
  1018. kmplayer.volume = volume_ratio;
  1019.  
  1020. // 修复了调整子音量不为100%时调整总音量时音量产生短暂突变的bug
  1021. // 改变静音状态的函数里写入了一个50%音量,因此不能随意调用
  1022. if (volume_ratio == 0) {
  1023. setMuted(true);
  1024. } else if (getMuted()) {
  1025. setMuted(false);
  1026. }
  1027. rewriteVolume();
  1028. break;
  1029. case 1:
  1030. scene_audio_ratio = volume_ratio / 100;
  1031. break;
  1032. case 2:
  1033. computer_audio_ratio = volume_ratio / 100;
  1034. break;
  1035. default:
  1036. return;
  1037. }
  1038. setStorage(usercode + "_volume_" + idx + "_value", volume_ratio);
  1039. // console.log("channel" + idx + ": volume=" + volume_ratio)
  1040. }
  1041.  
  1042.  
  1043. const wait_user_interacted_interval = setInterval(function () {
  1044. if (is_user_interacted) {
  1045. // 感谢 Teruteru 的辛苦调试,现在不会在视频播放前卡住了
  1046. clearInterval(wait_user_interacted_interval);
  1047. // 支持了音量记忆功能,初始音量设置为100%
  1048. for (let idx = 0; idx < (is_single_video ? 1 : 3); idx++) {
  1049. let val = getStorage(usercode + "_volume_" + idx + "_value");
  1050.  
  1051. setVolumeMix((val != null ? parseInt(val) : 100) / 100, idx);
  1052. console.log(`恢复记忆音量,通道:${idx},数值:${val}%`);
  1053. }
  1054. }
  1055. }, 500);
  1056.  
  1057. // 为两路声音设置均衡
  1058. function rewriteVolume() {
  1059. // 修复了子音量在个别场景下调节无效的问题
  1060. // 0也是false,因此出现了音量到达0后再也无法恢复的bug
  1061. let main_vol = getMuted() ? 0 : kmplayer.volume; // 修复了引起静音按钮无效的bug
  1062. kmplayer.allInstance.type1.volume(main_vol * scene_audio_ratio);
  1063. if (!is_single_video) {
  1064. kmplayer.allInstance.type2.volume(main_vol * computer_audio_ratio);
  1065. }
  1066. }
  1067.  
  1068. // 为两路画面设置同步
  1069. function syncTime() {
  1070. let delta = kmplayer.allInstance.type2.currentTime() - kmplayer.allInstance.type1.currentTime() - getTimeDelta();
  1071. // console.log(`当前误差 delta=${delta}`)
  1072. // 20241123奇怪,为什么加了isVideoBuffering检查,视频播放就不太正常了
  1073. if (Math.abs(delta) > 0.3 && videoStarted) { //20241123适当改大一点
  1074. kmplayer.allInstance.type2.currentTime(kmplayer.allInstance.type1.currentTime() + getTimeDelta());
  1075. setSpeed(current_time_speed);
  1076. // console.log(`为两路画面设置同步 delta=${delta}`)
  1077. }
  1078. }
  1079.  
  1080. // 设定小画面的尺寸
  1081. function setSmallVideoSize(size) {
  1082. setStorage(usercode + "_zoom_ratio", size); // 重新打开时,记忆上次的小画面尺寸
  1083. let num2 = parseInt(size),
  1084. num1 = 100 - num2;
  1085. $("style").each(function (idx, elem) {
  1086. if (elem.innerHTML.includes(".style-type-2-1 .cont-item-2 {top: ")) {
  1087. $(elem).remove();
  1088. }
  1089. })
  1090. GM_addStyle(".style-type-2-1 .cont-item-2 {top: " + num1 + "%;left: " + num1 + "%; width: " + num2 + "%; height: " + num2 + "%;}");
  1091. }
  1092.  
  1093. let time_sync_delta = 0;
  1094.  
  1095. function setTimeDelta(time_delta) {
  1096. time_sync_delta = time_delta;
  1097. // 当前时间差
  1098. $("#syncControl>div>p:last>span").text(time_sync_delta.toFixed(2))
  1099. setStorage("video_" + video_id + "_timedelta", time_delta);
  1100. }
  1101.  
  1102. function getTimeDelta() {
  1103. return time_sync_delta;
  1104. }
  1105.  
  1106. function getDuration() {
  1107. return kmplayer.durationSec;
  1108. }
  1109.  
  1110. // 输出左上角渐隐提示文本
  1111. function putText(text) {
  1112. $("#custom-status-text>span").html(text);
  1113. $("#custom-status-text").finish().show().delay(1000).fadeOut(1000); // 完成前stoptruetrue的话,会导致delay不生效,为什么呢?
  1114. }
  1115.  
  1116. // 将时间转换为分:秒.毫秒
  1117. function timeSec2Text(time) {
  1118. let time_s = parseInt(time);
  1119. let time_ms = parseInt((time - parseInt(time)) * 1000);
  1120. let time_min = parseInt(time_s / 60);
  1121. let time_sec = parseInt(time_s % 60);
  1122. return ("00" + parseInt(time_min)).slice(time_min >= 100 ? -3 : -2) + ":" + ("0" + parseInt(time_sec)).slice(-2) + "." + ("00" + parseInt(time_ms)).slice(-3);
  1123. }
  1124.  
  1125. // 创建截图
  1126. function makeScreenshot(video_id, no_download = false) {
  1127. let se = $(".cont-item-" + video_id + " #kmd-video-player");
  1128. if (se) {
  1129. let el = se[0];
  1130. console.log(el);
  1131. let t = el.currentTime;
  1132. // 修正了此前自以为是的画面尺寸都是720p的想法
  1133. let w = el.videoWidth;
  1134. let h = el.videoHeight;
  1135. // 抄的jAccount验证码识别那个插件
  1136. $("body").append($('<canvas width="' + w + '" height="' + h + '" style="display: none;" id="screenshot_canvas"></canvas>'));
  1137. let canvas_el = document.getElementById("screenshot_canvas"),
  1138. current_time = timeSec2Text(t).replaceAll(":", "_").replaceAll(".", "_");
  1139. canvas_el.getContext("2d").drawImage(el, 0, 0);
  1140. let screenshot_img = canvas_el.toDataURL("image/jpeg");
  1141. $("#screenshot_canvas").remove();
  1142. if (no_download) { // 在新标签页打开,而不是下载
  1143. // console.log(screenshot_img);
  1144. window.open("", null, 'width=' + (w + 50) + ',height=' + (h + 50) + ',status=yes,toolbar=no,menubar=no,location=no').document.body.innerHTML = '<img src="' + screenshot_img + '">';
  1145. } else { // 下载
  1146. $("body").append($('<a href="' + screenshot_img + '" id="download_link" download="canvas_screenshot_' + current_time + '.jpg">下载</a>'));
  1147. $("#download_link")[0].click(); // 哦是我之前在浏览器里阻止了下载
  1148. $("#download_link").remove();
  1149. }
  1150. }
  1151. }
  1152.  
  1153. // 音频动态限幅器
  1154. // 20221212 收到反馈,突然没有声音了。因此移除了该功能,可以解决问题
  1155.  
  1156. // {
  1157. // let audio_context_enabled = false;
  1158. // const AudioContext = window.AudioContext || window.webkitAudioContext;
  1159. // let audioCtx = new AudioContext();
  1160.  
  1161. // const video1 = $("#player-00001 #kmd-video-player")[0];
  1162. // const video2 = $("#player-00002 #kmd-video-player")[0];
  1163. // const source1 = audioCtx.createMediaElementSource(video1);
  1164. // const source2 = audioCtx.createGain();
  1165. // if (video2) {
  1166. // audioCtx.createMediaElementSource(video2).connect(source2);
  1167. // }
  1168.  
  1169. // const compressor1 = audioCtx.createDynamicsCompressor(),
  1170. // gain_mix = audioCtx.createGain();
  1171.  
  1172. // // 去除了对电脑音频的自适应音量处理
  1173. // // 移除了音量增益
  1174. // source1.connect(gain_mix);
  1175. // source2.connect(gain_mix);
  1176.  
  1177. // compressor1.threshold.value = -30;
  1178. // compressor1.knee.value = 20;
  1179. // compressor1.ratio.value = 12;
  1180. // compressor1.attack.value = 0.010;
  1181. // compressor1.release.value = 0.010;
  1182.  
  1183. // gain_mix.gain.value = 1;
  1184. // gain_mix.connect(audioCtx.destination);
  1185.  
  1186. // function enableAudioEffect() {
  1187. // if (audio_context_enabled) {
  1188. // source1.disconnect(compressor1);
  1189. // compressor1.disconnect(gain_mix);
  1190. // source1.connect(gain_mix);
  1191. // putText("关闭自适应音量");
  1192. // } else {
  1193. // source1.disconnect(gain_mix);
  1194. // source1.connect(compressor1);
  1195. // compressor1.connect(gain_mix);
  1196. // putText("启用自适应音量");
  1197. // }
  1198. // audio_context_enabled = !audio_context_enabled;
  1199. // setStorage(usercode + "_auto_volume", audio_context_enabled);
  1200. // }
  1201.  
  1202. // // 鼠标右键点击音量图标启动自适应音量功能
  1203. // $("#voiceContorl").on("contextmenu", function (event) {
  1204. // enableAudioEffect();
  1205. // });
  1206.  
  1207. // // 支持了自适应音量功能的记忆
  1208. // if (getStorage(usercode + "_auto_volume") == 'true') {
  1209. // enableAudioEffect();
  1210. // }
  1211. // }
  1212.  
  1213. // 加快状态栏功能弹窗隐藏速度
  1214. function new_hoverleave_callback(cld, prt) {
  1215. let timer = null;
  1216. prt.on('mouseover', function () {
  1217. clearTimeout(timer);
  1218. cld.show();
  1219. })
  1220. prt.on("mouseout", function () {
  1221. timer = setTimeout(function () {
  1222. cld.hide();
  1223. }, 100);
  1224. });
  1225. }
  1226. new_hoverleave_callback($(".voice-volume"), $("#voiceContorl"));
  1227. new_hoverleave_callback($(".split-select"), $("#splitContorl"));
  1228. new_hoverleave_callback($("#timesContorl>ul"), $("#timesContorl"));
  1229.  
  1230. if (isAndroidPhone()) {
  1231. $('<div class="tool-bar-item tool-btn__times" style="position: relative; z-index: 20;"><span>安卓</span></div>')
  1232. .insertBefore("#voiceContorl");
  1233. }
  1234.  
  1235. // 增加图像参数调整选项,可进行亮度、对比度、不透明度的调节
  1236.  
  1237. $('<div id="effectControl" class="tool-bar-item tool-btn__times" style="position: relative; z-index: 20;"><span>图像</span><div><p>图像参数设定</p></div></div>')
  1238. .insertAfter("#splitContorl");
  1239.  
  1240. $("#effectControl")
  1241. .css("position", "relative")
  1242. .css("z-index", "20");
  1243.  
  1244. $("#effectControl>div")
  1245. .css("position", "absolute")
  1246. .css("display", "none")
  1247. .css("bottom", "25px")
  1248. .css("left", "-58px")
  1249. .css("height", "auto")
  1250. .css("padding-bottom", "10px")
  1251. .css("width", "150px")
  1252. .css("border-radius", "3px")
  1253. .css("background-color", "rgba(28, 32, 44, .9)");
  1254. $("#effectControl>div>p")
  1255. .css("font-size", "150%")
  1256. .css("font-weight", "bold");
  1257.  
  1258. // 统一图像参数调整滑块样式
  1259. GM_addStyle(`
  1260. input[id^=effect_slider_] {
  1261. -webkit-appearance: none;
  1262. height: 6px;
  1263. border-radius: 3px;
  1264. background: #0092E9;
  1265. outline: none;
  1266. margin: 7px 0px;
  1267. }
  1268. input[id^=effect_slider_]::-webkit-slider-thumb {
  1269. -webkit-appearance: none;
  1270. appearance: none;
  1271. width: 16px;
  1272. height: 16px;
  1273. border-radius: 16px;
  1274. background: white;
  1275. cursor: pointer;
  1276. }
  1277. `);
  1278.  
  1279. let effect_control_list = [{
  1280. for: "opacity",
  1281. label: "小窗不透明度",
  1282. default: 0.85,
  1283. min: 0.2,
  1284. max: 1.0
  1285. }, {
  1286. for: "brightnesssA",
  1287. label: "现场亮度",
  1288. default: 1,
  1289. min: 0.7,
  1290. max: 2.5
  1291. }, {
  1292. for: "brightnesssB",
  1293. label: "电脑亮度",
  1294. default: 1,
  1295. min: 0.7,
  1296. max: 2.5
  1297. }, {
  1298. for: "contrastA",
  1299. label: "现场对比度",
  1300. default: 1,
  1301. min: 0.5,
  1302. max: 2.5
  1303. }, {
  1304. for: "contrastB",
  1305. label: "电脑对比度",
  1306. default: 1,
  1307. min: 0.8,
  1308. max: 2.5
  1309. }, {
  1310. for: "blurA",
  1311. label: "现场清晰度",
  1312. default: 0,
  1313. min: 1.5,
  1314. max: 0
  1315. }, {
  1316. for: "blurB",
  1317. label: "电脑清晰度",
  1318. default: 0,
  1319. min: 2,
  1320. max: 0
  1321. }]
  1322.  
  1323. let effect_setting = JSON.parse(getStorage(usercode + "_effect"));
  1324. if (!effect_setting) {
  1325. effect_setting = Object.fromEntries(effect_control_list.map(x => [x.for, x.default])); // 哦越来越熟练了呢
  1326. }
  1327.  
  1328. setStorage(usercode + "_effect", JSON.stringify(effect_setting));
  1329.  
  1330. effect_control_list.forEach(function (v) {
  1331. if (effect_setting[v.for] == undefined) {
  1332. effect_setting[v.for] = v.default; // 其实上面那句可以删了,嘿但我就不删,看着多高级啊
  1333. }
  1334. $("#effectControl>div").append($('<div id="effect_container_' + v.for + '"></div'));
  1335. let container = $("#effectControl>div>#effect_container_" + v.for);
  1336. container.append($("<label>" + v.label + "</label>"));
  1337. container.append($('<input type="range" id="effect_slider_' + v.for + '" min="0" max="100">')); //嘤嘤嘤slider的默认样式好美啊
  1338. let range_slider = $("#effectControl>div #effect_slider_" + v.for);
  1339. range_slider[0].value = parseInt(100 * (effect_setting[v.for] - v.min) / (v.max - v.min));
  1340. range_slider.on("input change", function (event) {
  1341. let new_value = event.target.value / 100;
  1342. effect_setting[v.for] = v.min + (new_value * (v.max - v.min));
  1343. event.target.style.background = 'linear-gradient(to right, #0092E9 0%, #0092E9 ' + event.target.value + '%, white ' + event.target.value + '%, white 100%)';
  1344. setStorage(usercode + "_effect", JSON.stringify(effect_setting));
  1345. });
  1346. range_slider.trigger("input");
  1347. })
  1348. $("#effectControl>div").append('<input type="button" id="btn_reset_effect" value="还原默认">')
  1349. $("#btn_reset_effect").on("click", function () {
  1350. effect_control_list.forEach(function (v) {
  1351. effect_setting[v.for] = v.default;
  1352. let range_slider = $("#effectControl>div #effect_slider_" + v.for);
  1353. range_slider[0].value = parseInt(100 * (effect_setting[v.for] - v.min) / (v.max - v.min));
  1354. // 修复了部分场景下滑块显示样式不正确的bug
  1355. range_slider.trigger("change");
  1356. })
  1357. // 哦豁忘记这个了
  1358. setStorage(usercode + "_effect", JSON.stringify(Object.fromEntries(effect_control_list.map(x => [x.for, x.default])))); // 可以清理废弃字段
  1359. });
  1360. $("#btn_reset_effect")
  1361. .css("margin-top", "8px");
  1362.  
  1363. new_hoverleave_callback($("#effectControl>div"), $("#effectControl"));
  1364.  
  1365. // 有两路视频时,能够通过设定参考点的方式进行画面同步
  1366. if (!is_single_video) {
  1367. $('<div id="syncControl" class="tool-bar-item tool-btn__times" style="position: relative; z-index: 20;"><span>同步</span><div></div></div>')
  1368. .insertAfter("#timesContorl");
  1369. $("#syncControl")
  1370. .css("position", "relative")
  1371. .css("z-index", "20");
  1372. $("#syncControl>div")
  1373. .append('<p>双屏进度同步</p>')
  1374. .append('<p>在参考时刻<br/>分别按下按钮</p>')
  1375. .append('<input type="button" id="ref_scene">')
  1376. .append('<input type="button" id="ref_computer">')
  1377. .append('<p>还原</p>')
  1378. .append('<input type="button" id="ref_reset">')
  1379. .append('<p>当前时间差<span></span>秒</p>')
  1380. .css("position", "absolute")
  1381. .css("display", "none")
  1382. .css("bottom", "25px")
  1383. .css("left", "-58px")
  1384. .css("height", "auto")
  1385. .css("padding-bottom", "10px")
  1386. .css("width", "150px")
  1387. .css("border-radius", "3px")
  1388. .css("background-color", "rgba(28, 32, 44, .9)");
  1389. $("#syncControl>div>p:first")
  1390. .css("font-size", "150%")
  1391. .css("font-weight", "bold");
  1392. $("#syncControl>div>input")
  1393. .css("margin", "0 10px");
  1394.  
  1395. const sync_ref_name = {
  1396. ref_scene: "现场",
  1397. ref_computer: "电脑",
  1398. ref_reset: "还原"
  1399. }
  1400. $("#syncControl :button").each(function (idx, elem) {
  1401. elem.value = sync_ref_name[elem.id];
  1402. });
  1403.  
  1404. function clear_pending_sync_control_button() {
  1405. $("#syncControl :button").each(function (idx, elem) {
  1406. if (elem.value.includes("撤销")) {
  1407. elem.value = elem.value.substr(2, elem.value.length - 1);
  1408. }
  1409. });
  1410. }
  1411.  
  1412. let sync_ref_scene = -1;
  1413. let sync_ref_computer = -1;
  1414. let last_time_delta = getStorage("video_" + video_id + "_timedelta");
  1415. last_time_delta = last_time_delta == null ? 0 : parseFloat(last_time_delta);
  1416. setTimeDelta(last_time_delta);
  1417.  
  1418. function applyTimeDelta(value) {
  1419. setTimeDelta(value);
  1420. sync_ref_scene = -1;
  1421. sync_ref_computer = -1;
  1422. clear_pending_sync_control_button();
  1423. }
  1424.  
  1425. $("#syncControl :button").on("click", function (event) {
  1426. const elem = event.target
  1427. switch (elem.id) {
  1428. case "ref_scene": {
  1429. if (elem.value == "重设") {
  1430. sync_ref_scene = -1;
  1431. elem.value = sync_ref_name[elem.id];
  1432. } else {
  1433. sync_ref_scene = kmplayer.allInstance.type1.currentTime();
  1434. if (sync_ref_computer == -1) {
  1435. putText("现场画面参考时刻: " + timeSec2Text(sync_ref_scene) + ",请选取相应的电脑画面参考时刻");
  1436. event.target.value = "重设";
  1437. } else {
  1438. last_time_delta = sync_ref_computer - sync_ref_scene;
  1439. applyTimeDelta(last_time_delta);
  1440. putText("画面参考时间差" + last_time_delta.toFixed(3) + "s,画面已成功同步");
  1441. }
  1442. }
  1443. break;
  1444. }
  1445. case "ref_computer": {
  1446. if (elem.value == "重设") {
  1447. sync_ref_scene = -1;
  1448. elem.value = sync_ref_name[elem.id];
  1449. } else {
  1450. sync_ref_computer = kmplayer.allInstance.type2.currentTime();
  1451. if (sync_ref_scene == -1) {
  1452. putText("电脑画面参考时刻: " + timeSec2Text(sync_ref_computer) + ",请选取相应的现场画面参考时刻");
  1453. event.target.value = "重设";
  1454. } else {
  1455. last_time_delta = sync_ref_computer - sync_ref_scene;
  1456. applyTimeDelta(last_time_delta);
  1457. putText(`时间差:${last_time_delta.toFixed(2)}s,画面已成功同步`);
  1458. }
  1459. }
  1460. break;
  1461. }
  1462. case "ref_reset": {
  1463. if (last_time_delta == 0) {
  1464. putText("同步状态未设定");
  1465. } else {
  1466. if (getTimeDelta() == last_time_delta) {
  1467. applyTimeDelta(0);
  1468. putText("已清除自定义同步状态");
  1469. } else {
  1470. applyTimeDelta(last_time_delta);
  1471. putText(`已恢复同步状态,时间差:${last_time_delta.toFixed(2)}s,画面已成功同步`);
  1472. }
  1473. }
  1474. break;
  1475. }
  1476. }
  1477. });
  1478. new_hoverleave_callback($("#syncControl>div"), $("#syncControl"));
  1479. }
  1480.  
  1481. // 在播放控制栏增加了【关灯】按钮
  1482. if (!sessionStorage.getItem("pure")) {
  1483. $('<div id="lightControl" class="tool-bar-item tool-btn__times" style="position: relative; z-index: 20;"><span>关灯</span></div>')
  1484. .insertBefore(".tool-btn__scrren"); // 修复了单视频模式下不支持关灯的bug
  1485. }
  1486.  
  1487. // 在播放控制栏增加了【纯享】功能按钮,在浮窗中进行播放
  1488. $('<div id="aloneControl" class="tool-bar-item tool-btn__times" style="position: relative; z-index: 20;"><span>纯享</span></div>')
  1489. .insertBefore(".tool-btn__scrren");
  1490. $("#aloneControl").on("click", function () {
  1491. const w = window.screen.availWidth * 0.6,
  1492. h = window.screen.availHeight * 0.6,
  1493. // 居中显示新窗口
  1494. y = window.outerHeight / 2 + window.screenY - (h / 2),
  1495. x = window.outerWidth / 2 + window.screenX - (w / 2);
  1496. window.open(location.href + "#pure", null, `width=${w},height=${h},left=${x},top=${y},status=yes,toolbar=no,menubar=no,location=no`);
  1497.  
  1498. setPlay(false); //暂停播放
  1499. })
  1500.  
  1501. $("#lightControl").click(function () {
  1502. switch ($("#lightControl>span").text()) {
  1503. case "关灯": {
  1504. lightoffControl(true);
  1505. break;
  1506. }
  1507. case "开灯": {
  1508. lightoffControl(false);
  1509. break;
  1510. }
  1511. }
  1512. })
  1513. $("#lightControl").on("wheel", function (event) {
  1514. if (event.originalEvent.deltaY == 0) {
  1515. // 不要响应水平滚轮
  1516. return;
  1517. }
  1518. if ($("#lightControl>span").text() != "开灯") {
  1519. // 开灯时才有用
  1520. return;
  1521. }
  1522. let color = getStorage(usercode + "_black_color");
  1523. if (color == "black") {
  1524. color = "white";
  1525. } else {
  1526. color = "black";
  1527. }
  1528. lightoffControl(true, color);
  1529. })
  1530.  
  1531. let progressBar_operating_flag = false, // 手动拖动进度条的过程中,不应覆写进度条状态
  1532. current_time_speed = 1.0,
  1533. default_time_speed = 1.0;
  1534.  
  1535. let current_zoom_ratio = parseInt(getStorage(usercode + "_zoom_ratio")) || 45; // 重新打开时,记忆上次的小画面尺寸
  1536. setSmallVideoSize(current_zoom_ratio); // 首先设置一遍
  1537.  
  1538. // 使右侧视频列表的内容更紧凑了,并增加了教室显示
  1539. let this_teacher;
  1540. $(".lti-list .item-text").each(function (idx, elem) {
  1541. elem = $(elem);
  1542. elem.children("p:first").attr("title", ""); // 清空课程名title
  1543. elem.children(".classroom-name").css("display", "");
  1544. let course_name = elem.children("p:first").html();
  1545. let teacher_name = elem.children(".item-contour").html();
  1546. teacher_name = teacher_name.substr(3, teacher_name.length - 1).trim();
  1547. elem.children("p:first").html(course_name + "(" + teacher_name + ")"); // 课程名加教师名
  1548. elem.children(".item-contour").remove(); // 删除教师
  1549. let course_time = elem.children("p:last-child").html();
  1550. elem.children("p:last-child").html(course_time.trim().substr(0, course_time.length - 3));
  1551. if (elem.attr("class").includes("list-item--active")) {
  1552. this_teacher = teacher_name;
  1553. }
  1554.  
  1555. // 加粗突出显示未观看过的视频(感谢@evian_xian的反馈,确实好丑)
  1556. let elem_video_id = elem.parent().attr("id");
  1557. if (getStorage(usercode + "_video_" + elem_video_id + "_position") == null && video_id != elem_video_id) {
  1558. // 移除了未观看的视频的突出显示,没啥用啊
  1559. // elem.children().css("font-weight", "bold");
  1560. }
  1561. });
  1562.  
  1563. // 将右侧视频列表改为升序了
  1564. // $(".list-main").html($(".list-main>.list-item").get().reverse());
  1565.  
  1566. // 自动将当前视频条目定位到右侧视频列表中央
  1567. if (is_canvas_vod_page) {
  1568. const active_element = $(".lti-list .list-item--active")[0];
  1569. if (active_element === undefined) {
  1570. // 应该是走错教室了,那怎么办呢?
  1571. console.log("走错教室了!真是的!!!")
  1572.  
  1573. // 适配20211201防串课更新
  1574. // 现在应该不可能串课了
  1575. }
  1576. $(".list-main")[0].scrollTop = active_element.offsetTop - 250;
  1577. //$(".lti-list .list-item--active")[0].scrollIntoView({block: "center"}); // 这个不太行,把整个页面都给滚没了
  1578. }
  1579.  
  1580. // 新窗口中的纯享播放
  1581. if (sessionStorage.getItem("pure")) {
  1582. $("#aloneControl").remove();
  1583. // 移除所有其他元素
  1584. $(".lti-page").css("text-align", "center")
  1585. $(".main_container").css("display", "inline-block")
  1586. $(".courser-video.list-title").remove()
  1587. $(".lti-page-tab").remove()
  1588. $(".lti-list").hide()
  1589. // 使视频元素占满屏幕
  1590. $("body").append($(".main_container"));
  1591. $("body").css("overflow", "hidden"); // 修复了特殊情况下纯享模式出现滚动条的bug
  1592. $(".main_container")
  1593. .css("width", "100%")
  1594. .css("height", "100%")
  1595. .css("display", "inline-block");
  1596. $(".lti-video")
  1597. .css("width", "100%")
  1598. .css("height", "100%")
  1599. .css("margin", "0px");
  1600. $(".video-box")
  1601. .css("width", "100%")
  1602. .css("height", "100%");
  1603. $("#rtcMain")
  1604. .css("width", "100%")
  1605. .css("height", "100%");
  1606. if (is_canvas_live_page) {
  1607. // 直播没有进度条,稍微窄一些
  1608. $("#rtcContent")
  1609. .css("width", "100%")
  1610. .css("height", "calc(100% - 32px)"); // 20241025更新尺寸
  1611. }
  1612. else {
  1613. $("#rtcContent")
  1614. .css("width", "100%")
  1615. .css("height", "calc(100% - 40px)");
  1616. }
  1617.  
  1618. $(".kmd-container").on("contextmenu", function (event) { // 再把视频区域的右键锁上,否则将意外触发画面切换功能
  1619. event.preventDefault();
  1620. return false;
  1621. })
  1622.  
  1623. // 新窗口的纯享播放有标题了
  1624. const active_video_item = $(".list-item--active>div");
  1625. const t1 = active_video_item.children("p:first").text();
  1626. const t2 = active_video_item.children("p:last").text();
  1627. document.title = t1 + "—" + t2;
  1628. }
  1629.  
  1630. // 设定播放速度
  1631. let timeout_reset_speed_text = null;
  1632.  
  1633. // 重写原生全屏函数
  1634. {
  1635. function tabbarForcedActive() {
  1636. let is_some_setting_active =
  1637. ($("#timesContorl>ul").css("display") == "block" || false) ||
  1638. ($("#splitContorl>ul").css("display") == "block" || false); // 这两个不是div
  1639. $(".tool-bar>div:nth-child(2)>div>div").each(function (idx, el) {
  1640. let this_displayed = ($(el).css("display") == "block" || false);
  1641. is_some_setting_active = is_some_setting_active || this_displayed;
  1642. })
  1643. if (just_clicked_screen) {
  1644. return true;
  1645. }
  1646. return is_some_setting_active; // 修复了全屏时底部播放栏会在操作中异常收起的问题
  1647. }
  1648. tabbarForcedActive(); // 防止【函数未被使用】警告
  1649.  
  1650. function duringToggleFullscreen() {
  1651. if (isAndroidPhone()) {
  1652. // 在安卓设备上全屏时,自动以横屏方式全屏
  1653. screen.orientation.lock("landscape-primary");
  1654. }
  1655. }
  1656. let newf = undefined;
  1657. eval(kmplayer.scrren.toString()
  1658. .replace("scrren()", "newf = function()")
  1659. // 修复了缩放后播放控制栏自动弹出的响应范围随之变化的bug
  1660. // 修复了调整两画面位置后,播放控制栏自动弹出响应范围异常的bug
  1661. // 允许点击屏幕以唤起播放控制栏(安卓)
  1662. .replace("window.screen.height * 0.5", '(isAndroidPhone()?-1000:($("#rtcContent").height()*0.95-32))') // 缩小播放控制栏自动弹出的响应范围,使体验更加顺滑
  1663. .replace("event.clientY > rangeY", "(event.clientY > rangeY)||tabbarForcedActive()") // 改写保持收起的条件
  1664. .replace("if (!this.isFull) {", "duringToggleFullscreen();\nif (!this.isFull) {") // 在函数执行到一半的时候来这里执行一下这个函数吧
  1665. .replace("-32 + 'px'", "is_canvas_vod_page ? '-32px' : '-28px'") // 直播没有进度条,稍微窄一些
  1666. )
  1667. kmplayer.scrren = newf; // 这里居然不让用caller
  1668. }
  1669.  
  1670. // 设定音量
  1671. let scene_audio_ratio = 1.00;
  1672. let computer_audio_ratio = 1.00;
  1673.  
  1674. // 重写原生元素拖动函数
  1675. {
  1676. let newf = undefined;
  1677.  
  1678. eval(kmplayer.addDragEvent.toString()
  1679. .replace("addDragEvent()", "newf = function()")
  1680.  
  1681. // 修复了调整子音量不为100%时调整总音量时音量突变的bug
  1682. .replace("that.volume = that.limit(totalH, 100)", "setVolumeMix(totalH/100,0)")
  1683. .replace("that.setVolume(that.volume)", "") // 把写音量拿到写高度上面一行,避免音量最小为2而不能为0的bug
  1684. .replace("target.parentNode.style.height =", "//") // 高度无需在此设置了,注释掉
  1685. )
  1686. kmplayer.addDragEvent = newf;
  1687. }
  1688.  
  1689. // 双击画面全屏
  1690. $("video").on('dblclick', function (event) {
  1691. if (!(isAndroidPhone() && document.fullscreenElement != null)) { // 对安卓全屏状态下不启用
  1692. toggleFullscreen();
  1693. }
  1694. event.preventDefault(); // 阻断默认全屏行为
  1695. return false;
  1696. });
  1697.  
  1698. var just_clicked_screen_reset_timeout = null;
  1699.  
  1700. // 单击画面暂停/播放
  1701. $("video").on('click', function (event) {
  1702. if (!(isAndroidPhone() && document.fullscreenElement != null)) { // 对安卓全屏状态下不启用
  1703. togglePlay();
  1704. }
  1705.  
  1706. if (isAndroidPhone()) {
  1707. // 允许使用安卓设备时点击屏幕唤起播放控制栏
  1708. just_clicked_screen = true;
  1709. if (just_clicked_screen_reset_timeout) {
  1710. clearTimeout(just_clicked_screen_reset_timeout);
  1711. }
  1712. just_clicked_screen_reset_timeout = setTimeout(function () {
  1713. just_clicked_screen = false;
  1714. let tabbar = document.querySelector('.fixed-tool');
  1715. if (tabbar) {
  1716. tabbar.style.bottom = is_canvas_vod_page ? "-32px" : "-28px"; // 直播没有进度条,稍微窄一些
  1717. }
  1718. }, 2000);
  1719. }
  1720. });
  1721.  
  1722. // 修正视频变形问题,按视频原比例而非播放器窗口比例显示视频(但会导致画面黑边)
  1723. $("video").css("object-fit", "contain");
  1724.  
  1725. // 将黑边修改为空白边
  1726. $(".kmd-app-container").css("background-color", "transparent");
  1727. $(".kmd-app").css("background-color", "transparent");
  1728. $(".kmd-container").css("background-color", "transparent");
  1729. $(".kmd-player").css("background-color", "transparent");
  1730. $("#rtcContent").css("background-color", "black");
  1731.  
  1732. console.log("%c执行到这里!", "color:red;")
  1733.  
  1734. // 当视频链接过期时,自动刷新链接
  1735. function refreshVideoLink() { // 本函数效果不理想,更改视频源后,播放进度将自动复位,倒不如刷新整个页面
  1736. $.ajax({
  1737. type: "POST",
  1738. url: "/lti/vodVideo/getVodVideoInfos",
  1739. async: true,
  1740. traditional: true,
  1741. dataType: "json",
  1742. data: {
  1743. playTypeHls: true,
  1744. id: video_id,
  1745. isAudit: true
  1746. },
  1747. success: function (data) {
  1748. let video_link_array = data.body.videoPlayResponseVoList;
  1749. let current_time = getTime();
  1750. $(".kmd-wrapper video#kmd-video-player").each(function (idx, elem) {
  1751. elem.src = video_link_array[idx].rtmpUrlHdv;
  1752. })
  1753. setTime(current_time);
  1754. console.log(video_link_array);
  1755. }
  1756. });
  1757. }
  1758. $(".kmd-wrapper video#kmd-video-player").each(function (idx, elem) {
  1759. elem.onerror = function () {
  1760. console.log("video error: " + elem.error.code + "; details: " + elem.error.message);
  1761. if (is_iframe) {
  1762. refresh_session(true); // 然后在onmessage里刷新页面
  1763. } else { // 独立窗口中无法自动更新会话,切回大窗口
  1764. location.href("https://oc.sjtu.edu.cn/courses/" + getStorage("course_" + course_id + "_canvasid") + "/external_tools/162");
  1765. return;
  1766. }
  1767. }
  1768. })
  1769.  
  1770. // 倍速列表选项优化
  1771. // 移除了部分愚蠢的倍速数值
  1772. // 根据建议,补充了一个快得离谱的倍速
  1773. let speed_choice = [0.8, 0.9, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3, 6, 12]; // 16是该播放器支持的最高倍速
  1774. // let speed_choice = [0.5, 1.0, 1.25, 1.5, 2.0, 4.0, 8.0]; // 20221212新的列表
  1775. speed_choice.sort(function (a, b) {
  1776. return a - b;
  1777. });
  1778. let speed_choice_text = speed_choice.map(function (num) {
  1779. return '<li id="' + num + '">' + num + '倍</li>';
  1780. }).join("");
  1781. $("#timesContorl>ul").html(speed_choice_text);
  1782. $("#timesContorl>ul>#1").attr("class", "times-active"); // 默认1倍高亮
  1783.  
  1784. // 直播没有进度条,稍微窄一些
  1785. if (is_canvas_live_page) {
  1786. GM_addStyle("#rtcTool {height:32px;} #rtcTool .tool-bar {height:32px;}") // 20241025更新尺寸
  1787. $("#playerDiv").css("height", "445px");// 20241025适配新版本的尺寸的微小变化
  1788. }
  1789.  
  1790. // 在菜单中选择新倍速后自动关闭菜单
  1791. $("#timesContorl").bind("click", function (event) {
  1792. if (event.target.tagName == "LI") {
  1793. // 状态栏显示倍速
  1794. $("#timesContorl>ul").css("display", "none");
  1795. current_time_speed = parseFloat(event.target.id);
  1796. changeSpeed(current_time_speed); // 修复了通过播放控制栏设定倍速失效的bug
  1797. putText("设定倍速:" + current_time_speed);
  1798. }
  1799. return true;
  1800. });
  1801.  
  1802. if (is_canvas_live_page) {
  1803. // 移除了直播页面中自欺欺人的【暂停】按钮(看来不容易直接移除。。算了)
  1804. // 移除了直播页面中的播放时间显示
  1805. $(".tool-view-time").hide();
  1806. }
  1807. // 移除了点播页面缺乏使用场景、直播页面没有作用的【停止播放】按钮
  1808. $(".tool-btn__stop").remove();
  1809.  
  1810. // 允许使用播放器控制栏切换上一集下一集
  1811. if (is_canvas_vod_page) {
  1812. if ($(".list-item--active").prev().length) {
  1813. $('<div class="tool-bar-item tool-btn__next" style="display: block;"></div>').insertAfter($(".tool-btn__pause"));
  1814. }
  1815. if ($(".list-item--active").next().length) {
  1816. $('<div class="tool-bar-item tool-btn__prev" style="display: block;"></div>').insertAfter($(".tool-btn__pause"));
  1817. }
  1818. $(".tool-btn__prev").click(function () {
  1819. $(".list-item--active").next().click();
  1820. });
  1821. $(".tool-btn__next").click(function () {
  1822. $(".list-item--active").prev().click();
  1823. });
  1824. }
  1825.  
  1826. // 移除了直播页面中没有任何作用的【画质】按钮
  1827. $(".tool-btn__sharp").remove();
  1828.  
  1829. // 仅有一路视频时,禁用各类画面布局功能
  1830. if (!is_single_video) {
  1831. // 将画面选择按钮图标改为文字显示【分屏】
  1832. $("#splitContorl").attr("class", "tool-bar-item tool-btn__times")
  1833. $("#splitContorl").append($("<span>分屏</span>"))
  1834.  
  1835. // 增加了画中画模式,并允许从设置中直接选择所需画面
  1836. let split_choice_id = ["split_scene_only", "split_computer_only", "split_big_small", "split_pip"];
  1837. let split_choice_desc = ["仅现场画面", "仅电脑画面", "一大一小", "进入画中画"];
  1838. let split_choice_class = ["style-type-1-1", "style-type-1-1", "style-type-2-1", "style-type-2-1 style-type-2-1-pip"];
  1839. let split_choice_text = split_choice_id.map(function (id, idx) {
  1840. return '<li id="' + id + '" class="' + split_choice_class[idx] + '">' + split_choice_desc[idx] + '</li>';
  1841. }).join("");
  1842. $("#splitContorl>ul").html(split_choice_text);
  1843. $("#splitContorl>ul>#split_big_small").css("background", "white").css("color", "#0a0a0a"); // 默认一大一小
  1844.  
  1845. // 安卓端似乎不支持画中画
  1846. if (!document.pictureInPictureEnabled) {
  1847. $("#splitContorl>ul>#split_pip").remove();
  1848. }
  1849.  
  1850. // 切换画中画设定
  1851. function enable_PiP(flag) {
  1852. if (flag) {
  1853. let video2 = $(".cont-item-2 #kmd-video-player"); // 右下角视频
  1854. video2.on('enterpictureinpicture', function () {
  1855. $(".cont-item-2").hide(); // 进入画中画,隐藏原小窗
  1856. $("#split_pip").html("退出画中画");
  1857. });
  1858. video2.on('leavepictureinpicture', function () {
  1859. $(".cont-item-1").css("display", ""); // 退出画中画,恢复原小窗
  1860. $(".cont-item-2").css("display", ""); // 退出画中画,恢复原小窗
  1861. $("#split_pip").html("进入画中画");
  1862. $("#split_big_small").click();
  1863. });
  1864. video2[0].requestPictureInPicture();
  1865. } else {
  1866. document.exitPictureInPicture();
  1867. }
  1868. }
  1869.  
  1870. let split_replaying_flag = false,
  1871. split_changed = 0;
  1872. $("#splitContorl").bind("click", function (event) {
  1873. if (event.target.tagName == "LI") {
  1874. split_changed++;
  1875. $(event.target).css("background", "white").css("color", "#0a0a0a");
  1876. $(event.target).siblings().each(function (idx, elem) {
  1877. $(elem).css("background", "").css("color", "");
  1878. });
  1879. let changed_flag = false;
  1880. if ($("#split_pip").length && $("#split_pip").html().includes("退出")) { // 修复了在不支持画中画的设备上无法切换成【仅电脑画面】模式的bug
  1881. enable_PiP(false);
  1882. $("#split_pip").html("进入画中画");
  1883. changed_flag = true;
  1884. }
  1885. switch ($(event.target).attr("id")) {
  1886. case "split_computer_only": // 仅电脑画面
  1887. $("#player-00002").parent(".rtc-cont-item").attr("class", "rtc-cont-item cont-item-1");
  1888. $("#player-00001").parent(".rtc-cont-item").attr("class", "rtc-cont-item cont-item-2");
  1889. if (!split_replaying_flag) {
  1890. split_replaying_flag = true;
  1891. setTimeout(function () {
  1892. $(event.target).click();
  1893. split_replaying_flag = false;
  1894. }, 1);
  1895. }
  1896. break;
  1897. case "split_scene_only": // 仅现场画面
  1898. $("#player-00001").parent(".rtc-cont-item").attr("class", "rtc-cont-item cont-item-1");
  1899. $("#player-00002").parent(".rtc-cont-item").attr("class", "rtc-cont-item cont-item-2");
  1900. if (!split_replaying_flag) {
  1901. split_replaying_flag = true;
  1902. setTimeout(function () {
  1903. $(event.target).click();
  1904. split_replaying_flag = false;
  1905. }, 1);
  1906. }
  1907. break;
  1908. case "split_pip":
  1909. if ($(event.target).html().includes("进入") && !changed_flag) {
  1910. enable_PiP(true);
  1911. // $(event.target).html("退出画中画");
  1912. }
  1913. break;
  1914. default:
  1915. break;
  1916. }
  1917.  
  1918. $("#rtcContent").attr("class", $(event.target).attr("class")); // 虽然网页里已经定义过这个事件,但是好像有些晚,所以我在这重复操作一遍。
  1919. } else { // 鼠标左右键点击工具栏【画面】按钮可进行布局布局快捷调整
  1920. let active_mode = undefined;
  1921. $("#splitContorl>ul>li").each(function (idx, elem) {
  1922. // $("#split_big_small").css("background") 会返回
  1923. // rgb(255, 255, 255) none repeat scroll 0% 0% / auto padding-box border-box
  1924. if (elem.style.background == "white") {
  1925. active_mode = $(elem).attr("id")
  1926. }
  1927. });
  1928. if (active_mode) {
  1929. switch (active_mode) {
  1930. case "split_computer_only": // 仅电脑画面
  1931. $("#split_scene_only").click();
  1932. break;
  1933. case "split_scene_only": // 仅现场画面
  1934. $("#split_computer_only").click();
  1935. break;
  1936. case "split_pip":
  1937. $("#split_pip").click();
  1938. break;
  1939. case "split_big_small":
  1940. // 交换
  1941. let cl1 = $("#player-00001").parent(".rtc-cont-item").attr("class"),
  1942. cl2 = $("#player-00002").parent(".rtc-cont-item").attr("class");
  1943. $("#player-00001").parent(".rtc-cont-item").attr("class", cl2);
  1944. $("#player-00002").parent(".rtc-cont-item").attr("class", cl1);
  1945. break;
  1946. default:
  1947. break;
  1948. }
  1949. }
  1950. }
  1951. });
  1952. $("#splitContorl").bind("contextmenu", function (event) {
  1953. if (event.target.tagName != "LI") {
  1954. // 鼠标左右键点击工具栏【画面】按钮可进行布局布局快捷调整
  1955. let active_mode = undefined;
  1956. $("#splitContorl>ul>li").each(function (idx, elem) {
  1957. // $("#split_big_small").css("background") 会返回
  1958. // rgb(255, 255, 255) none repeat scroll 0% 0% / auto padding-box border-box
  1959. if (elem.style.background == "white") {
  1960. active_mode = $(elem).attr("id")
  1961. }
  1962. });
  1963. if (active_mode) {
  1964. switch (active_mode) {
  1965. case "split_computer_only": // 切换到一大一小
  1966. case "split_scene_only": // 切换到一大一小
  1967. $("#split_big_small").click();
  1968. break;
  1969. case "split_pip": // 退出画中画
  1970. $("#split_pip").click();
  1971. break;
  1972. case "split_big_small": // 切换到单屏
  1973. // 交换
  1974. if ($("#player-00001").parent(".rtc-cont-item").attr("class") == "rtc-cont-item cont-item-1") {
  1975. $("#split_scene_only").click();
  1976. } else {
  1977. $("#split_computer_only").click();
  1978. }
  1979. break;
  1980. default:
  1981. break;
  1982. }
  1983. }
  1984. }
  1985. })
  1986.  
  1987. $("#player-00001").parent(".rtc-cont-item").attr("class", "rtc-cont-item cont-item-1");
  1988. $("#player-00002").parent(".rtc-cont-item").attr("class", "rtc-cont-item cont-item-2");
  1989.  
  1990. // 直播中没有电脑视频流时,以【仅现场画面】模式启动
  1991. if (is_canvas_live_page) {
  1992. $("#split_scene_only").click();
  1993. let computer_video = $(".cont-item-2 #kmd-video-player");
  1994. computer_video.one("playing", function () {
  1995. console.log("playing....", 321);
  1996. if (split_changed == 2) {
  1997. $("#split_big_small").click(); // 切换回一大一小模式
  1998. }
  1999. });
  2000. if (computer_video[0].currentTime > 0) { //防止刚才趁程序不注意就开始了播放
  2001. computer_video.trigger("playing");
  2002. }
  2003. }
  2004. } else {
  2005. $("#splitContorl").remove();
  2006. }
  2007.  
  2008. // 有两路视频时,开启子音量控制功能
  2009. $(".voice-volume .voice-max").attr("class", "voice-max voice0-max");
  2010. $(".voice-volume .voice-rate").attr("class", "voice-rate voice0-rate");
  2011. if (!is_single_video) {
  2012. $(".voice-volume").css("width", "80px").css("text-align", "center");
  2013. $(".voice-volume").append($('<div class="voice-max voice1-max" style="width: 8px;"><div class="voice-rate voice1-rate voice-rate-child" style="height: 100%;"><div class="tool-rate-tip"></div></div></div>'));
  2014. $(".voice-volume").append($('<div class="voice-max voice2-max" style="width: 8px;"><div class="voice-rate voice2-rate voice-rate-child" style="height: 100%;"><div class="tool-rate-tip"></div></div></div>'));
  2015. $(".voice-volume>div").css("display", "inline-block").css("margin", "0 9px");
  2016. $(".voice-volume").css("transform", "translateX(-35px)");
  2017. $(".voice-volume").append($('<span class="voice-desc voice0-desc">总控</span>'));
  2018. $(".voice-volume").append($('<span class="voice-desc voice1-desc">现场</span>'));
  2019. $(".voice-volume").append($('<span class="voice-desc voice2-desc">电脑</span>'));
  2020. $(".voice-volume>span").css("position", "absolute")
  2021. .css("top", "50%")
  2022. .css("font-size", "12px")
  2023. .css("width", "50px");
  2024. $(".voice-volume>.voice0-desc").css("transform", "translateX(-87px) translateY(50px)");
  2025. $(".voice-volume>.voice1-desc").css("transform", "translateX(-62px) translateY(50px)");
  2026. $(".voice-volume>.voice2-desc").css("transform", "translateX(-37px) translateY(50px)");
  2027. }
  2028.  
  2029. // 鼠标位于进度条上或拖动进度条时浮窗显示时刻
  2030. $("#rtcContent").append('<div id="custom-progress-hover"><svg viewBox="-3 -3 37.6 23"> <polygon points="17.3,20 0,0 34.6,0" style="fill:#0092E9AA; stroke:black; stroke-width:3;"/></svg><span>12:00</span></div>');
  2031. $("#custom-progress-hover") // 进度条,百万大制作
  2032. .css("position", "absolute")
  2033. .css("text-align", "center")
  2034. .css("display", "none")
  2035. .css("background-color", "#0092E9AA")
  2036. .css("border", "2px solid black")
  2037. .css("border-radius", "10px")
  2038. .css("box-shadow", "#AAA 0px 0px 10px")
  2039. .css("margin", "8px 8px")
  2040. .css("z-index", "11")
  2041. .css("padding", "5px 5px;")
  2042. .css("width", "60px")
  2043. .css("height", "25px");
  2044. $("#custom-progress-hover span")
  2045. .css("user-select", "none")
  2046. .css("position", "absolute")
  2047. .css("top", "50%")
  2048. .css("transform", "translateX(-50%) translateY(-50%)")
  2049. .css("font-weight", "bold")
  2050. .css("font-size", "14px");
  2051. $("#custom-progress-hover svg")
  2052. .css("position", "absolute")
  2053. .css("top", "50%")
  2054. .css("width", "15px")
  2055. .css("transform", "translateX(-50%) translateY(140%)");
  2056.  
  2057. // 时移浮窗显示
  2058. $(".tool-progress").on("mousemove", function (event) {
  2059. let progress_ratio = (event.pageX - $(".tool-progress").offset().left) / $(".tool-progress").width();
  2060. progress_ratio = numberClamp(progress_ratio, 0, 1);
  2061.  
  2062. $("#custom-progress-hover").css("display", "block");
  2063. $("#custom-progress-hover").css("top", $("#rtcTool").position().top - 48);
  2064. $("#custom-progress-hover").css("left", event.pageX - $("#rtcTool").offset().left - 40);
  2065.  
  2066. let progress_sec = parseInt(progress_ratio * getDuration());
  2067.  
  2068. $("#custom-progress-hover>span").html(parseInt(progress_sec / 60) + ":" + ("0" + parseInt(progress_sec % 60)).slice(-2));
  2069.  
  2070. if (progressBar_operating_flag) {
  2071. setTime(progress_sec);
  2072. }
  2073. });
  2074.  
  2075. $(".tool-progress").on("mouseleave", function (event) {
  2076. $("#custom-progress-hover").css("display", "none");
  2077. });
  2078.  
  2079. // 扩展进度条的可点击范围高度,便于操作
  2080. GM_addStyle(".tool-progress {z-index: 12;}")
  2081. GM_addStyle(".tool-progress > div {height:4px !important; position:absolute !important;}")
  2082. $(".tool-progress").append('<div style="width: 100%; height: 25px !important; background-color: transparent; transform: translateY(-16px); cursor:auto; z-index: 11;" class="tool-progress"></div>');
  2083.  
  2084. // 在画面左上角显示自动渐隐的文本,显示状态变化
  2085. $("#rtcContent").append('<div id="custom-status-text"><span></span></div>');
  2086. $("#custom-status-text")
  2087. .css("position", "absolute")
  2088. .css("text-align", "center")
  2089. .css("display", "none")
  2090. .css("background-color", "#CCCA")
  2091. .css("margin", "5px 5px")
  2092. .css("z-index", "11")
  2093. .css("padding", "0px 2px") // 嘤嘤嘤因为这里不小心加了分号,竟然一直没生效过
  2094. .css("height", "20px")
  2095. .css("top", 0)
  2096. .css("left", 0);
  2097.  
  2098. $("#custom-status-text>span")
  2099. .css("font-size", "16px")
  2100. .css("line-height", "20px")
  2101. .css("user-select", "none") // 禁止选中
  2102. .css("color", "blue");
  2103.  
  2104. // 修复了暂停状态下改变进度条导致意外继续播放且进度条不再刷新的问题
  2105. function repair_pause_playing() {
  2106. if (!getPlay()) {
  2107. setTimeout(function () {
  2108. if (!getPlay()) {
  2109. setPlay(false, true); // 这种情况下不展示消息提醒
  2110. }
  2111. }, 1);
  2112. }
  2113. }
  2114. $("body").on("mouseup", function (event) {
  2115. // console.log("mouseup",event.originalEvent.button);
  2116. if (event.originalEvent.button != 0) { // 仅响应鼠标左键
  2117. return;
  2118. }
  2119.  
  2120. let target_el_class = $(event.target).attr("class");
  2121. // 使用按钮进行播放和暂停也可以显示文字提示了
  2122. if (Object.prototype.toString.call(target_el_class) === "[object String]") {
  2123. if ($(event.target).attr("class").includes("tool-btn__play")) {
  2124. putText("状态:播放");
  2125. if (is_single_video) {
  2126. setPlay(true, true); // 修复了部分场景下播放/暂停按钮失效的问题
  2127. }
  2128. return;
  2129. } else if ($(event.target).attr("class").includes("tool-btn__pause")) {
  2130. putText("状态:暂停");
  2131. if (is_single_video) {
  2132. setPlay(false, true); // 修复了部分场景下播放/暂停按钮失效的问题
  2133. }
  2134. return;
  2135. }
  2136. }
  2137.  
  2138. progressBar_operating_flag = false;
  2139. repair_pause_playing();
  2140. //return true;
  2141. });
  2142. $("body").on("mousedown", function (event) {
  2143. // console.log("mousedown",event.originalEvent.button);
  2144. if (event.originalEvent.button != 0) { // 仅响应鼠标左键
  2145. return;
  2146. }
  2147.  
  2148. if (!$(event.target).attr("class")) {
  2149.  
  2150. } else if ($(event.target).attr("class").startsWith("tool-progress")) {
  2151. // 避免鼠标操作进度条时自动刷新进度条
  2152. progressBar_operating_flag = true;
  2153. } else if ($(event.target).attr("class").startsWith("voice-")) {
  2154. // 允许鼠标点击音量条任意位置设定音量并解除静音
  2155. let click_height = event.pageY;
  2156. let clicked_idx_match = $(event.target).attr("class").match(new RegExp("voice(\\d)-")); // 这里改成另一个格式的RegExp之后要转义反斜杠了
  2157. if (clicked_idx_match) {
  2158. let clicked_idx = parseInt(clicked_idx_match[1]);
  2159. let voicebar_height = $(".voice0-max").height();
  2160. let voicebar_base = $(".voice0-max").offset().top;
  2161. let volume_ratio = 1 - (click_height - voicebar_base) / voicebar_height;
  2162. setVolumeMix(volume_ratio, clicked_idx);
  2163. }
  2164. }
  2165. //return true;
  2166. });
  2167.  
  2168. let in_ultra_slow_mode = false,
  2169. is_mute_before_ultra_slow_mode; // 在进入超慢速模式前是否静音,一定要静音否则声音太怪了
  2170. $("body").on("keyup", function (event) {
  2171. // console.log("keyup:",event.target);
  2172. switch (event.keyCode) {
  2173. case 37: // 方向键左
  2174. case 39: // 方向键右
  2175. case 68: // 字母D,上一帧
  2176. case 70: { // 字母F,下一帧
  2177. repair_pause_playing();
  2178. break;
  2179. }
  2180. case 76: { // L键
  2181. // 使用L键进入超慢速寻找模式,松开后立即暂停
  2182.  
  2183. changeSpeed(current_time_speed); // 恢复到之前的播放速度
  2184. setPlay(false, true);
  2185. in_ultra_slow_mode = false;
  2186. if (!is_mute_before_ultra_slow_mode) {
  2187. setMuted(false);
  2188. }
  2189. break
  2190. }
  2191. default:
  2192. break;
  2193. }
  2194. });
  2195.  
  2196. $("body").on("keydown", function (event) {
  2197. //console.log("keydown:", event.target, event.keyCode);
  2198.  
  2199. // 左右方向键进行时移
  2200. // Ctrl+左右方向键进行快速时移
  2201. // Ctrl+Shift+左右方向键进行超快速时移
  2202. const SMALL_TIME_STEP = 3;
  2203. const MIDDLE_TIME_STEP = 15;
  2204. const BIG_TIME_STEP = 59;
  2205. let step = SMALL_TIME_STEP;
  2206. if (event.ctrlKey && event.shiftKey) {
  2207. step = BIG_TIME_STEP;
  2208. } else if (event.ctrlKey) {
  2209. step = MIDDLE_TIME_STEP;
  2210. }
  2211. switch (event.keyCode) {
  2212. case 32: { // 空格,暂停/播放
  2213. if (!is_canvas_vod_page) break; //仅针对点播
  2214. togglePlay();
  2215. break;
  2216. }
  2217. case 49: { // 数字1,左屏幕截图
  2218. makeScreenshot(1, event.shiftKey);
  2219. break;
  2220. }
  2221. case 50: { // 数字2,右屏幕截图
  2222. makeScreenshot(2, event.shiftKey);
  2223. break;
  2224. }
  2225. case 37: { // 方向键左,快退
  2226. if (!is_canvas_vod_page) break; //仅针对点播
  2227. setTime(getTime() - step);
  2228. putText("快退");
  2229. // console.log("minus:", step, "to:", target_time);
  2230. break;
  2231. }
  2232. case 39: { // 方向键右,快进
  2233. if (!is_canvas_vod_page) break; //仅针对点播
  2234. setTime(getTime() + step);
  2235. putText("快进");
  2236. // console.log("add:", step, "to:", target_time);
  2237. break;
  2238. }
  2239. case 38: { // 方向键上,音量加
  2240. increaseVolume(0.1);
  2241. break;
  2242. }
  2243. case 40: { // 方向键下,音量减
  2244. increaseVolume(-0.1);
  2245. break;
  2246. }
  2247. case 68: { // 字母D,上一帧
  2248. if (!is_canvas_vod_page) break; //仅针对点播
  2249. if (getPlay()) setPlay(false, true); // 暂停
  2250. let target_time = getTime() - 1 / 30;
  2251. setTime(target_time);
  2252. putText("上一帧 当前时刻:" + timeSec2Text(getTime()));
  2253. // console.log("add:", step, "to:", target_time);
  2254. break;
  2255. }
  2256. case 70: { // 字母F,下一帧
  2257. if (!is_canvas_vod_page) break; //仅针对点播
  2258. if (getPlay()) setPlay(false, true); // 保持暂停状态
  2259. let target_time = getTime() + 1 / 30;
  2260. setTime(target_time);
  2261. putText("下一帧 当前时刻:" + timeSec2Text(getTime()));
  2262. // console.log("minus:", step, "to:", target_time);
  2263. break;
  2264. }
  2265. case 76: { // L键
  2266. // 使用L键进入超慢速寻找模式,松开后立即暂停
  2267. if (!is_canvas_vod_page) break; //仅针对点播
  2268. if (!in_ultra_slow_mode) {
  2269. in_ultra_slow_mode = true;
  2270. is_mute_before_ultra_slow_mode = getMuted();
  2271. if (!is_mute_before_ultra_slow_mode) {
  2272. setMuted(true);
  2273. }
  2274. changeSpeed(0.3); // 0.3倍速似乎较为合适
  2275. if (!getPlay()) {
  2276. setPlay(true, true);
  2277. }
  2278. }
  2279. putText("超慢速播放中");
  2280. break;
  2281. }
  2282. case 67: { // 字母C
  2283. if (!is_canvas_vod_page) break; //仅针对点播
  2284. increaseSpeed(0.1); // 每次快捷键将会增减0.1倍速
  2285. break;
  2286. }
  2287. case 88: { // 字母X
  2288. if (!is_canvas_vod_page) break; //仅针对点播
  2289. increaseSpeed(-0.1); // 每次快捷键将会增减0.1倍速
  2290. break;
  2291.  
  2292. }
  2293. case 90: { // 字母Z
  2294. if (!is_canvas_vod_page) break; //仅针对点播
  2295. let is_using_default_speed = (Math.abs(getSpeed() - default_time_speed) < 0.005);
  2296. let is_speed_no_change = (Math.abs(current_time_speed - default_time_speed) < 0.005);
  2297. if (is_speed_no_change) { // 在恢复倍速是显示原倍速情况
  2298. putText(`倍速未改变:${current_time_speed}`);
  2299. } else {
  2300. if (is_using_default_speed) {
  2301. changeSpeed(current_time_speed);
  2302. putText(`切换倍速:${default_time_speed}->${current_time_speed}`);
  2303. } else {
  2304. changeSpeed(default_time_speed);
  2305. putText(`切换倍速:${default_time_speed},原倍速:${current_time_speed}`);
  2306. }
  2307. }
  2308. break;
  2309. }
  2310. case 77: { // 字母M
  2311. // 使用M键切换静音状态
  2312. if (getMuted()) {
  2313. putText("切换静音:解除静音");
  2314. }
  2315. else {
  2316. putText("切换静音:静音");
  2317. }
  2318. setMuted(!getMuted());
  2319. break;
  2320. }
  2321. case 13: { // 使用Enter键切换全屏
  2322. toggleFullscreen();
  2323. putText("切换全屏");
  2324. break;
  2325. }
  2326. default:
  2327. return true;
  2328. }
  2329. });
  2330.  
  2331. $("body").on("mousewheel", function (event) {
  2332. event.preventDefault();
  2333. event.stopPropagation();
  2334. return false;
  2335. });
  2336.  
  2337. $("body").on("wheel", function (event) {
  2338. // console.log("wheel",event.originalEvent.deltaY);
  2339.  
  2340.  
  2341. const target = event.target;
  2342.  
  2343. if (event.originalEvent.deltaY == 0) {
  2344. // 不要响应水平滚轮
  2345. return;
  2346. }
  2347. // 仅在视频上使用滚轮生效
  2348. if (target.tagName == "VIDEO") {
  2349. // 在小画面上使用鼠标滚轮缩放画面
  2350. if (target == $(".cont-item-2 #kmd-video-player")[0]) {
  2351. let max_ratio = 85,
  2352. min_ratio = 15;
  2353. current_zoom_ratio -= event.originalEvent.deltaY / 25;
  2354. current_zoom_ratio = numberClamp(current_zoom_ratio, min_ratio, max_ratio);
  2355. setSmallVideoSize(current_zoom_ratio);
  2356. } else {
  2357. let volDelta = 2 * parseInt(100 * (-event.originalEvent.deltaY) / 2500) / 100;
  2358. increaseVolume(volDelta);
  2359. }
  2360. } else {
  2361. if (target == $("#timesContorl")[0] || target == $("#timesContorl>span")[0]) {
  2362. // 使用鼠标滚轮在倍速图标上调整倍速
  2363. const direction = event.originalEvent.deltaY < 0;
  2364. // 每次操作加快 0.2 倍
  2365. increaseSpeed((direction ? 1 : -1) * 0.1); // 每次鼠标滚轮将会增减0.1倍速
  2366. } else if (target == $("#voiceContorl")[0] || $(".voice0-max")[0].contains(target)) {
  2367. // 使用鼠标滚轮在音量图标上调整主音量
  2368. let volDelta = 2 * parseInt(100 * (-event.originalEvent.deltaY) / 2500) / 100;
  2369. increaseVolume(volDelta);
  2370. }
  2371. }
  2372. });
  2373.  
  2374. function onStartPlay() {
  2375. if (is_canvas_vod_page) {
  2376. // 打开后默认不自动播放视频
  2377. setPlay(false, true);
  2378. setTimeout(function () {
  2379. // 打开视频后,自动跳转到上次观看的进度
  2380. let last_playback = getStorage(usercode + "_video_" + video_id + "_position")
  2381. if (last_playback != null) {
  2382. last_playback = parseFloat(last_playback);
  2383. if (last_playback > 10) { // 仅调整进度超过10秒的视频
  2384. let restore_time_interval = setInterval(function () {
  2385. // 20241123注意到在视频加载完成前,此处代码会持续引起seek out of range警告
  2386. if (!isVideoBuffering()) {
  2387. videoStarted = true;
  2388. }
  2389. if (!videoStarted) return;
  2390. kmplayer.allInstance.type1.currentTime(parseFloat(last_playback));
  2391. if (kmplayer.allInstance.type1.currentTime() >= last_playback - 2) {
  2392. clearInterval(restore_time_interval);
  2393. putText("已恢复到上次的播放进度:" + ("0" + parseInt(last_playback / 60)).slice(-2) + ":" + ("0" + parseInt(last_playback % 60)).slice(-2));
  2394. }
  2395. }, 10);
  2396. }
  2397. }
  2398. setTimeout(function () {
  2399. setPlay(false, true);
  2400. }, 1);
  2401. // 打开视频后,自动载入上次的默认播放速度
  2402. if (getStorage(usercode + "_speed_val") !== null) {
  2403. current_time_speed = parseFloat(getStorage(usercode + "_speed_val")); // 传入字符串时,无效
  2404. console.log("load current speed: " + current_time_speed);
  2405. // 应用记忆中的播放速度
  2406. changeSpeed(current_time_speed);
  2407. }
  2408. }, 1);
  2409.  
  2410. // 修改默认音量设定为不静音,并移除静音说明
  2411. setMuted(false);
  2412. $(".mute-tip").hide();
  2413. }
  2414.  
  2415.  
  2416. setInterval(function () {
  2417. if (!is_single_video) { // 否则会导致直播视频加载异常
  2418. syncTime(); // 开启画面同步控制功能
  2419. if ($(".kmd-wrapper #kmd-video-player")[0].paused != $(".kmd-wrapper #kmd-video-player")[1].paused) {
  2420. // setPlay(false, true); // 修复禁用启动后自动播放时的冲突导致的可能的暂停不彻底
  2421. setPlay(true, true); // 避免切换窗口导致画面全部暂停,不妨改成全部播放吧
  2422. }
  2423. }
  2424. }, 1000)
  2425.  
  2426. let periodic_job_interval = setInterval(periodic_job, 75); // 75ms 的定时任务
  2427.  
  2428.  
  2429. function periodic_job() {
  2430.  
  2431. if (is_canvas_live_page) {
  2432. if ($('#playerDiv').text() == "直播已结束!") { // 检查直播状态是否已经结束(否则会导致下方过程中出错)
  2433. console.log("直播已经结束")
  2434. clearInterval(periodic_job_interval);
  2435. }
  2436. }
  2437.  
  2438. if (is_canvas_vod_page) {
  2439. updateTimeText(); // 优化了状态栏播放进度显示精度
  2440.  
  2441. if ($(".kmd-wrapper #kmd-video-player")[0].paused == getPlay()) {
  2442. const force_pauce_interval = setInterval(function () {
  2443. console.log("播放状态不匹配!暂停");
  2444. setPlay(false, true); // 进一步修复了特殊情况下可能自动播放视频的bug
  2445. if ($(".kmd-wrapper #kmd-video-player")[0].paused != getPlay()) {
  2446. clearInterval(force_pauce_interval);
  2447. console.log("播放状态不匹配,暂停成功");
  2448. }
  2449. }, 150);
  2450. }
  2451.  
  2452.  
  2453. // 记录当前进度
  2454. if (getPlay()) {
  2455. if (getStorage(usercode + "_video_" + video_id + "_position") && getTime() < 5) { // 本次不算播放
  2456. } else {
  2457. setStorage(usercode + "_video_" + video_id + "_position", getTime());
  2458. }
  2459. }
  2460. }
  2461.  
  2462. {
  2463. let player1_height = $("#player-00001 #kmd-video-player")[0].offsetHeight;
  2464. // 修复了一处误写导致的画面清晰度滤镜参数错乱
  2465. let player2_height = $("#player-00002 #kmd-video-player")[0] ? $("#player-00002 #kmd-video-player")[0].offsetHeight : 0;
  2466.  
  2467. let screen_switched = $(".cont-item-1 .kmd-app-container").attr("id") != "player-00001";
  2468. let bA = screen_switched ? effect_setting.brightnesssA : effect_setting.brightnesssB;
  2469. let bB = !screen_switched ? effect_setting.brightnesssA : effect_setting.brightnesssB;
  2470. let cA = screen_switched ? effect_setting.contrastA : effect_setting.contrastB;
  2471. let cB = !screen_switched ? effect_setting.contrastA : effect_setting.contrastB;
  2472. let bl1 = effect_setting.blurA * player1_height / 1000;
  2473. let bl2 = effect_setting.blurB * player2_height / 1000;
  2474. let blA = screen_switched ? bl1 : bl2; // 这个居然和画面大小也有关。。。
  2475. let blB = !screen_switched ? bl1 : bl2;
  2476. let op = effect_setting.opacity;
  2477.  
  2478. let filter1 = "brightness(" + bB + ") contrast(" + cB + ") blur(" + blB + "px)";
  2479. let filter2 = "brightness(" + bA + ") contrast(" + cA + ") blur(" + blA + "px) opacity(" + op + ")";
  2480.  
  2481. $(".cont-item-1 #kmd-video-player").css("filter", filter1);
  2482. $(".cont-item-2 #kmd-video-player").css("filter", filter2);
  2483. }
  2484.  
  2485.  
  2486. // 有两路视频时
  2487. if (!is_single_video) {
  2488. rewriteVolume(); // 为两路声音设置均衡
  2489. }
  2490.  
  2491.  
  2492. // console.log(`周期任务运行完成,耗时:${Date.now()-t0}毫秒`)
  2493. }
  2494.  
  2495. // 使用安卓设备时默认打开新标签页播放
  2496. if (isAndroidPhone()) {
  2497. console.log("正在使用安卓移动设备");
  2498. if (is_iframe && is_from_default_lti_entry) {
  2499. console.log("申请返回上层页面");
  2500. $("#btn_play_in_new_tab").click();
  2501. top.postMessage("goback!", "https://oc.sjtu.edu.cn");
  2502. }
  2503. }
  2504. }
  2505.  
  2506.  
  2507.  
  2508. // 20221212 发现这里才有setPlay这个函数……
  2509. $(".lti-page-tab").append($('<button class="tab-help" id="btn_go_shuiyuan">学累了?看看水源!</button>'));
  2510. $("#btn_go_shuiyuan").on("click", function () {
  2511. window.open("https://shuiyuan.sjtu.edu.cn/");
  2512. setPlay(false); // 暂停播放
  2513. })
  2514.  
  2515. // 在右上角增加【关于本插件】按钮
  2516. $(".lti-page-tab").append($('<button class="tab-help" id="btn_about_canvasnb">关于本插件</button>'));
  2517. $("#btn_about_canvasnb").on("click", function () {
  2518. window.open("https://gf.qytechs.cn/zh-CN/scripts/432918");
  2519. setPlay(false); // 暂停播放
  2520. })
  2521.  
  2522. // 终于能在页面里显示版本号了
  2523. $("#btn_about_canvasnb").attr("title", `播放器版本:${player_version}\n本插件版本:v${script_version}`)
  2524.  
  2525.  
  2526. if (is_iframe || true) { // 任意条件下均显示【在新标签页中打开】按钮
  2527. $(".lti-page-tab").append($('<button class="tab-help" id="btn_play_in_new_tab">在新标签页中打开</button>'));
  2528.  
  2529. $("#btn_play_in_new_tab").on("click", function (event) {
  2530. setPlay(false); // 暂停播放
  2531. window.open(location.href);
  2532. });
  2533.  
  2534. if (true) { // 【在新标签页中打开】右键功能
  2535. $("#btn_play_in_new_tab").on("contextmenu", function (event) { // 右击能在新标签页打开有意思的东西
  2536. let totalX = this.clientWidth,
  2537. clickX = event.offsetX;
  2538. var video_link;
  2539. if (!is_single_video) {
  2540. if (clickX < totalX / 2) { // 打开左边的视频
  2541. if (is_canvas_vod_page) {
  2542. video_link = $(".cont-item-1 #kmd-video-player").attr("src");
  2543. } else {
  2544. video_link = live_video_links[$(".cont-item-1 .kmd-app-container").attr("id") == "player-00001" ? 0 : 1];
  2545. }
  2546. } else { // 打开右边的视频
  2547. if (is_canvas_vod_page) {
  2548. video_link = $(".cont-item-2 #kmd-video-player").attr("src");
  2549. } else {
  2550. video_link = live_video_links[$(".cont-item-1 .kmd-app-container").attr("id") == "player-00001" ? 1 : 0];
  2551. }
  2552. }
  2553. } else { // 只有一个视频
  2554. if (is_canvas_vod_page) {
  2555. video_link = $(".cont-item-1 #kmd-video-player").attr("src");
  2556. } else {
  2557. video_link = live_video_links[0];
  2558. }
  2559. }
  2560. $("body").append($('<a href="' + video_link + '" id="download_link" referrerpolicy="origin">KKKK</a>'));
  2561. $("#download_link")[0].click(); // 哦是我之前在浏览器里阻止了下载
  2562. $("#download_link").remove();
  2563. event.preventDefault();
  2564. return false;
  2565. })
  2566. }
  2567. }
  2568.  
  2569. setTimeout(function () {
  2570. console.log("视频播放准备工作完成")
  2571. const t0 = Date.now();
  2572. onStartPlay();
  2573. console.log(`视频播放准备工作完成后任务运行完成,耗时:${Date.now() - t0}毫秒`)
  2574. }, 50);
  2575. }
  2576.  
  2577. function processNoLiveAvailable() {
  2578. clearInterval(video_proload_check_interval);
  2579. console.log("无可用的直播视频,退出");
  2580. if (is_from_default_lti_entry) {
  2581. redirectToVodPage(); //20241123偶然发现的一处功能问题,修复之
  2582. }
  2583. }
  2584.  
  2585. let video_proload_check_interval = setInterval(function () {
  2586. if ($("video").length) { // 直到video元素和#timesContorl元素和视频生成,大概能表明播放器完全加载出来了
  2587. clearInterval(video_proload_check_interval);
  2588. console.log("视频播放器预加载完成")
  2589. let video_loaded_check_interval = setInterval(function () {
  2590. if (kmplayer.ids.length && $("#player-00001 #kmd-video-player")[0] != undefined) { // 直到video元素和#timesContorl元素和视频生成,大概能表明播放器完全加载出来了
  2591. clearInterval(video_loaded_check_interval);
  2592. console.log("视频播放器加载完成")
  2593. const t0 = Date.now();
  2594. afterVideoLoaded();
  2595. console.log(`视频播放器加载完成后任务运行完成,耗时:${Date.now() - t0}毫秒`)
  2596. } else {
  2597. console.log("视频播放器加载等待中");
  2598. if ($(".not-data").length || no_live_video_played || no_live_available) {
  2599. processNoLiveAvailable();
  2600. }
  2601. }
  2602. }, 20);
  2603. const t0 = Date.now();
  2604. afterVideoPreloaded();
  2605. console.log(`视频播放器预加载完成后任务运行完成,耗时:${Date.now() - t0}毫秒`)
  2606. } else {
  2607. console.log("视频播放器预加载等待中");
  2608. if ($(".not-data").length || no_live_video_played || no_live_available) {
  2609. processNoLiveAvailable();
  2610. }
  2611. }
  2612. }, 50);
  2613.  
  2614.  
  2615.  
  2616. function refresh_session(andRefresh = false) {
  2617. console.log("refresh session!")
  2618. if (andRefresh) {
  2619. top.postMessage("helpr!", "https://oc.sjtu.edu.cn");
  2620. } else {
  2621. top.postMessage("help!", "https://oc.sjtu.edu.cn");
  2622. }
  2623. }
  2624.  
  2625. if (is_iframe) {
  2626. $(document).on('visibilitychange', function (event) { //从外面回来时
  2627. if (!document.hidden && auto_refresh_flag) {
  2628. refresh_session();
  2629. }
  2630. })
  2631. if (auto_refresh_flag) {
  2632. setInterval(refresh_session, 20 * 60 * 1000); // 20分钟更新一次,防止页面失效
  2633. }
  2634. }
  2635.  
  2636. // 禁止标题文字被选中,改善体验……算了不改了,没改善。
  2637. // $(".list-title").css("user-select","none");
  2638.  
  2639. // 本插件顺利生效时,顶部标签卡颜色为金色
  2640. $(".tab-item--active").css("color", "gold");
  2641. $(".tab-item--active").css("border-bottom", "3px solid gold");
  2642.  
  2643.  
  2644. // 将画面背景色由廉价的灰色改为圣洁白
  2645. $(".lti-page").css("background-color", "transparent");
  2646.  
  2647. // 将部分白色背景色改为透明色
  2648. GM_addStyle(`
  2649. .lti-video {
  2650. background-color: transparent;
  2651. }
  2652. .list-title {
  2653. background-color: transparent;
  2654. }
  2655. .list-item {
  2656. background-color: transparent;
  2657. }
  2658. .list-main {
  2659. background-color: transparent;
  2660. }
  2661. .lti-page-tab {
  2662. background-color: transparent;
  2663. }
  2664. `)
  2665.  
  2666. // 关灯纯色元素
  2667. $(".loading").css("z-index", "103"); // 这个元素要始终能够遮盖整个画面
  2668. $("#rtcMain")
  2669. .css("position", "absolute")
  2670. .css("height", "")
  2671. .css("z-index", "102"); // 父框架最高z-index为100,遮罩为101
  2672. $("body").append($('<div class="light-turn-off"></div>'))
  2673.  
  2674. $(".light-turn-off")
  2675. .css("position", "fixed")
  2676. .css("inset", "0")
  2677. .css("background-color", "black")
  2678. .css("z-index", "101")
  2679. .css("display", "none"); // 父框架最高z-index为100,遮罩为101
  2680.  
  2681. // 通过增加边框使右侧视频列表更有质感
  2682. // 缩窄并加密了右侧视频列表
  2683. $(".lti-page").append('<div class="main_container"></div>');
  2684. $(".main_container")
  2685. .append($(".lti-video"))
  2686. .append($(".lti-list"))
  2687. .css("display", "flex");
  2688. $(".lti-list")
  2689. .css("border", "2px solid black")
  2690. .css("border-radius", "10px")
  2691. .css("width", "auto")
  2692. .css("flex-grow", "1");
  2693.  
  2694. GM_addStyle(".lti-list>.list-title {border-bottom: 1px solid black;}");
  2695. GM_addStyle(".list-item {padding: 5px; margin-bottom: 3px;}");
  2696. GM_addStyle(".lti-list {height: 460px; max-width: 225px;}"); //否则视频列表文字过多会导致视频列表内容越界
  2697.  
  2698.  
  2699. //GM_addStyle(".list-item:hover {border-bottom: 1px solid black;}");
  2700.  
  2701. $(".role-teacher").remove(); // 删除教师专属元素(空白),否则影响右侧视频列表的优美外观
  2702.  
  2703. GM_addStyle(".list-main {height: 400px; padding: 0 8px; margin-top: 6px;}");
  2704.  
  2705. GM_addStyle(".live-course-item .item-infos {margin-left: 0;}");
  2706.  
  2707. // 减小了页面总宽度,并居中显示了新窗口
  2708. GM_addStyle(".lti-page {max-width: 970px; width: 100%; margin: auto;}");
  2709.  
  2710. // 加密了播放控制栏上的文字按钮
  2711. GM_addStyle("#rtcTool .tool-bar .tool-bar-item {margin-left: 13px;}");
  2712. GM_addStyle(".tool-bar .tool-btn__times {margin-left: 5px !important;}");
  2713. GM_addStyle(".tool-bar .tool-btn__voice {margin-right: 8px !important;}"); // 不然不对称了
  2714. GM_addStyle(".tool-bar .tool-btn__muted {margin-right: 8px !important;}"); // 不然不对称了
  2715.  
  2716. // 更新播放控制栏图标为矢量图标
  2717. GM_addStyle(`
  2718. #rtcTool .tool-btn__play {
  2719. background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="white" d="M176 480C148.6 480 128 457.6 128 432v-352c0-25.38 20.4-47.98 48.01-47.98c8.686 0 17.35 2.352 25.02 7.031l288 176C503.3 223.8 512 239.3 512 256s-8.703 32.23-22.97 40.95l-288 176C193.4 477.6 184.7 480 176 480z"></path></svg>');
  2720. }
  2721. #rtcTool .tool-btn__pause {
  2722. background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path fill="white" d="M272 63.1l-32 0c-26.51 0-48 21.49-48 47.1v288c0 26.51 21.49 48 48 48L272 448c26.51 0 48-21.49 48-48v-288C320 85.49 298.5 63.1 272 63.1zM80 63.1l-32 0c-26.51 0-48 21.49-48 48v288C0 426.5 21.49 448 48 448l32 0c26.51 0 48-21.49 48-48v-288C128 85.49 106.5 63.1 80 63.1z"></path></svg>');
  2723. }
  2724. #rtcTool .tool-btn__voice {
  2725. background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="white" d="M412.6 182c-10.28-8.334-25.41-6.867-33.75 3.402c-8.406 10.24-6.906 25.35 3.375 33.74C393.5 228.4 400 241.8 400 255.1c0 14.17-6.5 27.59-17.81 36.83c-10.28 8.396-11.78 23.5-3.375 33.74c4.719 5.806 11.62 8.802 18.56 8.802c5.344 0 10.75-1.779 15.19-5.399C435.1 311.5 448 284.6 448 255.1S435.1 200.4 412.6 182zM473.1 108.2c-10.22-8.334-25.34-6.898-33.78 3.34c-8.406 10.24-6.906 25.35 3.344 33.74C476.6 172.1 496 213.3 496 255.1s-19.44 82.1-53.31 110.7c-10.25 8.396-11.75 23.5-3.344 33.74c4.75 5.775 11.62 8.771 18.56 8.771c5.375 0 10.75-1.779 15.22-5.431C518.2 366.9 544 313 544 255.1S518.2 145 473.1 108.2zM534.4 33.4c-10.22-8.334-25.34-6.867-33.78 3.34c-8.406 10.24-6.906 25.35 3.344 33.74C559.9 116.3 592 183.9 592 255.1s-32.09 139.7-88.06 185.5c-10.25 8.396-11.75 23.5-3.344 33.74C505.3 481 512.2 484 519.2 484c5.375 0 10.75-1.779 15.22-5.431C601.5 423.6 640 342.5 640 255.1S601.5 88.34 534.4 33.4zM301.2 34.98c-11.5-5.181-25.01-3.076-34.43 5.29L131.8 160.1H48c-26.51 0-48 21.48-48 47.96v95.92c0 26.48 21.49 47.96 48 47.96h83.84l134.9 119.8C272.7 477 280.3 479.8 288 479.8c4.438 0 8.959-.9314 13.16-2.835C312.7 471.8 320 460.4 320 447.9V64.12C320 51.55 312.7 40.13 301.2 34.98z"></path></svg>');
  2726. }
  2727. #rtcTool .tool-btn__muted {
  2728. background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="white" d="M301.2 34.85c-11.5-5.188-25.02-3.122-34.44 5.253L131.8 160H48c-26.51 0-48 21.49-48 47.1v95.1c0 26.51 21.49 47.1 48 47.1h83.84l134.9 119.9c5.984 5.312 13.58 8.094 21.26 8.094c4.438 0 8.972-.9375 13.17-2.844c11.5-5.156 18.82-16.56 18.82-29.16V64C319.1 51.41 312.7 40 301.2 34.85zM513.9 255.1l47.03-47.03c9.375-9.375 9.375-24.56 0-33.94s-24.56-9.375-33.94 0L480 222.1L432.1 175c-9.375-9.375-24.56-9.375-33.94 0s-9.375 24.56 0 33.94l47.03 47.03l-47.03 47.03c-9.375 9.375-9.375 24.56 0 33.94c9.373 9.373 24.56 9.381 33.94 0L480 289.9l47.03 47.03c9.373 9.373 24.56 9.381 33.94 0c9.375-9.375 9.375-24.56 0-33.94L513.9 255.1z"></path></svg>');
  2729. }
  2730. #rtcTool .tool-btn__pause {
  2731. background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path fill="white" d="M272 63.1l-32 0c-26.51 0-48 21.49-48 47.1v288c0 26.51 21.49 48 48 48L272 448c26.51 0 48-21.49 48-48v-288C320 85.49 298.5 63.1 272 63.1zM80 63.1l-32 0c-26.51 0-48 21.49-48 48v288C0 426.5 21.49 448 48 448l32 0c26.51 0 48-21.49 48-48v-288C128 85.49 106.5 63.1 80 63.1z"></path></svg>');
  2732. }
  2733. #rtcTool .tool-btn__prev {
  2734. background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path fill="white" d="M31.1 64.03c-17.67 0-31.1 14.33-31.1 32v319.9c0 17.67 14.33 32 32 32C49.67 447.1 64 433.6 64 415.1V96.03C64 78.36 49.67 64.03 31.1 64.03zM267.5 71.41l-192 159.1C67.82 237.8 64 246.9 64 256c0 9.094 3.82 18.18 11.44 24.62l192 159.1c20.63 17.12 52.51 2.75 52.51-24.62v-319.9C319.1 68.66 288.1 54.28 267.5 71.41z"></path></svg>');
  2735. }
  2736. #rtcTool .tool-btn__next {
  2737. background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path fill="white" d="M287.1 447.1c17.67 0 31.1-14.33 31.1-32V96.03c0-17.67-14.33-32-32-32c-17.67 0-31.1 14.33-31.1 31.1v319.9C255.1 433.6 270.3 447.1 287.1 447.1zM52.51 440.6l192-159.1c7.625-6.436 11.43-15.53 11.43-24.62c0-9.094-3.809-18.18-11.43-24.62l-192-159.1C31.88 54.28 0 68.66 0 96.03v319.9C0 443.3 31.88 457.7 52.51 440.6z"></path></svg>');
  2738. }
  2739. `)
  2740.  
  2741. // 移除了底部课程信息区域,压缩页面高度
  2742. $(".course-details").height(0);
  2743.  
  2744. // 将播放控制栏底色由廉价的灰色改为至尊黑
  2745. GM_addStyle("#rtcTool .tool-bar {background-color: black;}");
  2746. GM_addStyle("#rtcContent {background-color: black;}");
  2747.  
  2748. $(".tool-bar").click() // 随便点一下,不知道有没有用
  2749.  
  2750. console.log("%c施工完毕,辛苦了!", "color:#0FF;");
  2751. }
  2752. }
  2753.  
  2754. // 将 vshare 网站的视频播放器替换为浏览器内置播放器
  2755. else if (is_vshare_page) {
  2756. function doVshareEnhance() {
  2757. // 恢复页面对基本操作的响应
  2758. let document = window.document;
  2759. document.onkeydown = document.oncontextmenu = undefined;
  2760. document.body.oncontextmenu = document.body.onselectstart = undefined;
  2761. $(window).off("resize");
  2762.  
  2763. // 避免 vshare 页面视频过大超出画面范围
  2764. document.getElementsByTagName("html")[0].style["min-height"] = "auto"
  2765. document.getElementsByTagName("body")[0].style["min-height"] = "auto"
  2766. $(".video-container")
  2767. .css("height", "")
  2768. .css("padding", "20px 15px")
  2769. .css("width", "100%")
  2770.  
  2771. // 将 vshare 视频标题移至标题栏
  2772. $(".header").css("text-align", "center").css("overflow", "hidden");
  2773. $(".header>.logo").css("position", "absolute").css("left", "0");
  2774. $(".header").append($(".video-container>.video-title-container"))
  2775. $(".header>.video-title-container").css("margin-top", "7px").css("display", "inline-block");
  2776. $(".out-container").css("height", "calc(100% - 116px)").css("margin", "58px 0").css("min-height", "auto");
  2777.  
  2778. // 移除原播放器
  2779. let v_src = $("#video-share_html5_api").attr("src");
  2780. $("#video-share_html5_api")[0].pause(); // 终止原视频的播放
  2781. $("#video-share_html5_api")[0].src = ""; // 终止原视频的播放
  2782.  
  2783. // 移除了 vshare 右上角的名字显示
  2784. $(".header>.user").remove();
  2785.  
  2786. // 创建新播放器
  2787. $(".video-wrapper").replaceWith($('<div class="video-elem-container"></div>'));
  2788. // 避免 vshare 页面视频过大超出画面范围
  2789. $(".video-elem-container")
  2790. .css("padding", "0 20px")
  2791. .css("width", "100%")
  2792. .css("height", "100%")
  2793. .css("margin", "auto")
  2794. .css("text-align", "center");
  2795. $(".video-elem-container").append($('<video src="' + v_src + '"id="new_player" crossorigin="anonymous" controls></video>'));
  2796. $("#new_player").css("height", "calc(100% - 40px)").css("max-width", "100%").css("min-width", "30%").css("background-color", "black");
  2797.  
  2798. // 调整 vshare 页面标题位置
  2799. $(".video-title-container").css("text-align", "center");
  2800. $(".video-title-container>div").css("float", "none");
  2801.  
  2802. function enable_audio_delay() {
  2803. // 允许通过滑块调整 vshare 页面视频的音画时差时延(仅支持将音频滞后)
  2804. $(".slider-audio-delay-container")
  2805. .append($('<span>声音叠加时延<span>0</span>毫秒</span>'))
  2806. .append($('<input type="range" min="0" max="1000" value="0" class="slider-audio-delay styled-slider">'));
  2807.  
  2808. // 音频时延功能,参考 https://mdn.github.io/webaudio-examples/media-source-buffer/
  2809. const AudioContext = window.AudioContext || window.webkitAudioContext;
  2810. let audioCtx = new AudioContext();
  2811. let source = audioCtx.createMediaElementSource($("video")[0]);
  2812. let delayNode = audioCtx.createDelay(1.0); // 创建支持最长1秒延时的音频延迟器
  2813.  
  2814. source.connect(delayNode);
  2815. delayNode.connect(audioCtx.destination); // 输出
  2816.  
  2817. $(".slider-audio-delay").on("input", function (event) {
  2818. let delay_time = event.target.value;
  2819. $(event.target.parentElement).find("span>span").text(delay_time);
  2820. delayNode.delayTime.exponentialRampToValueAtTime(delay_time / 1000 + 0.000001, audioCtx.currentTime + 0.1);
  2821. })
  2822. }
  2823. $(".video-elem-container").append($('<div class="slider-audio-delay-container slider-box"></div>'))
  2824.  
  2825. $(".slider-audio-delay-container")
  2826. .append($('<span id="btn_enable_audio_delay" title="在Chrome99下发现该功能可能导致视频无法自行播放,请按需打开">点击启用声音延时功能</span>'))
  2827.  
  2828. $("#btn_enable_audio_delay").click(function () {
  2829. $("#btn_enable_audio_delay").remove();
  2830. enable_audio_delay();
  2831. })
  2832.  
  2833. // 允许通过滑块调整 vshare 页面视频的倍速(0.5-5倍),10倍变速
  2834. $(".video-elem-container").append($('<div class="video-playback-rate-container slider-box"></div>'))
  2835. $(".video-playback-rate-container")
  2836. .append($('<span>视频播放倍速<span>1.0</span>倍速</span>'))
  2837. .append($('<input type="range" min="5" max="50" value="10" class="video-playback-rate styled-slider">'));
  2838.  
  2839. $(".video-playback-rate").on("input", function (event) {
  2840. let playback_rate = event.target.value;
  2841. $(event.target.parentElement).find("span>span").text((playback_rate / 10).toFixed(1));
  2842. $("#new_player")[0].playbackRate = playback_rate / 10;
  2843. });
  2844.  
  2845. // 创建自定义样式的滑块元素,参考 https://www.w3schools.com/howto/howto_js_rangeslider.asp
  2846. GM_addStyle(".styled-slider {-webkit-appearance: none; width: calc(100% - 14em); height: 8px; border-radius: 3px; background: #DDD; outline: none;}")
  2847. GM_addStyle(".styled-slider::-webkit-slider-thumb {-webkit-appearance: none; appearance: none; width: 25px; height: 15px; border-radius: 3px; background: #666; cursor: pointer;}")
  2848. GM_addStyle(".styled-slider::-moz-range-thumb {width: 25px; height: 15px; border-radius: 3px; background: #666; cursor: pointer;}")
  2849.  
  2850. GM_addStyle(".slider-box {height: 20px; max-width: 800px; margin: auto;}");
  2851. GM_addStyle(".slider-box>span {display: inline-block; width: 13em;}");
  2852. GM_addStyle(".slider-box>span>span {display: inline-block; width: 2.5em;}");
  2853.  
  2854. // 新样式的视频说明文字
  2855. $(".video-desc-container.empty").remove()
  2856. if ($(".video-desc-container").length) {
  2857. $(".video-title").css("width", "fit-content");
  2858. $(".video-title").css("margin", "auto");
  2859. $(".video-desc-container").attr("style", "position: absolute;top: 0;display:none; z-index: 1;background-color: #fffe;border: 2px solid black;border-radius: 16px;box-shadow: rgb(0 0 0 / 70%) 1px 1px 10px;margin: -28px 0px;padding: 13px 16px;width: 240px;min-height: 300px;");
  2860.  
  2861. // 鼠标悬停时显示,离开时隐藏
  2862. let desc_hide_timeout = undefined;
  2863. $(".video-title").on("mousemove", function (event) {
  2864. $(".video-desc-container").css("left", event.pageX - 120)
  2865. clearTimeout(desc_hide_timeout);
  2866. $(".video-desc-container").show();
  2867. });
  2868. $(".video-desc-container").on("mousemove", function () {
  2869. clearTimeout(desc_hide_timeout);
  2870. });
  2871. $(".video-title,.video-desc-container").on("mouseleave", function () {
  2872. clearTimeout(desc_hide_timeout);
  2873. desc_hide_timeout = setTimeout(function () {
  2874. $(".video-desc-container").hide()
  2875. }, 30)
  2876. });
  2877. }
  2878. }
  2879. if ($(".video-wrapper").length) {
  2880. let wait_video_interval = setInterval(function () {
  2881. if (!$("#video-share_html5_api").attr("src")) {
  2882. return;
  2883. }
  2884. clearInterval(wait_video_interval);
  2885. console.log("vshare 页面加载完成")
  2886. const t0 = Date.now();
  2887. doVshareEnhance();
  2888. console.log(`vshare 页面加载完成后任务运行完成,耗时:${Date.now() - t0}毫秒`)
  2889. }, 10);
  2890. }
  2891. }
  2892. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址