downm3u8

m3u8

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/468813/1213558/downm3u8.js

  1. function sectionm3u8menu(m3u8url) {
  2. let $section = document.createElement('section');
  3. $section.setAttribute('id', 'down-my-section');
  4. $section.innerHTML = `
  5.  
  6. <style>
  7. /*全局设置*/
  8. html, body {
  9. margin: 0;
  10. padding: 0;
  11. }
  12. body::-webkit-scrollbar { display: none}
  13. p {
  14. margin: 0;
  15. }
  16. [v-cloak] {
  17. display: none;
  18. }
  19. #m-app {
  20. height: 100%;
  21. display: inherit;
  22. width: 100%;
  23. text-align: center;
  24. padding: 10px 50px 80px;
  25. box-sizing: border-box;
  26. }
  27. .m-p-action {
  28. margin: 20px auto;
  29. max-width: 1100px;
  30. width: 100%;
  31. font-size: 35px;
  32. text-align: center;
  33. font-weight: bold;
  34. display: block;
  35. }
  36. .m-p-other, .m-p-tamper, .m-p-github, .m-p-language, .m-p-mse{
  37. position: fixed;
  38. right: 50px;
  39. background-color: #eff3f6;
  40. background-image: linear-gradient(-180deg, #fafbfc, #eff3f6 90%);
  41. color: #24292e;
  42. border: 1px solid rgba(27, 31, 35, .2);
  43. border-radius: 3px;
  44. cursor: pointer;
  45. display: inline-block;
  46. font-size: 14px;
  47. font-weight: 600;
  48. line-height: 20px;
  49. padding: 6px 12px;
  50. z-index: 99;
  51. }
  52. .m-p-help {
  53. position: fixed;
  54. right: 50px;
  55. top: 50px;
  56. width: 30px;
  57. height: 30px;
  58. color: #666666;
  59. z-index: 2;
  60. line-height: 30px;
  61. font-weight: bolder;
  62. border-radius: 50%;
  63. border: 1px solid rgba(27, 31, 35, .2);
  64. cursor: pointer;
  65. background-color: #eff3f6;
  66. background-image: linear-gradient(-180deg, #fafbfc, #eff3f6 90%);
  67. }
  68. .m-p-github:hover, .m-p-other:hover, .m-p-tamper:hover, .m-p-help:hover, .m-p-language:hover, .m-p-mse:hover{
  69. opacity: 0.9;
  70. }
  71. .m-p-language {
  72. bottom: 70px;
  73. }
  74. .m-p-other {
  75. bottom: 150px;
  76. }
  77. .m-p-tamper {
  78. bottom: 30px;
  79. }
  80. .m-p-github {
  81. bottom: 190px;
  82. }
  83. .m-p-mse {
  84. bottom: 110px;
  85. }
  86. /*广告*/
  87. .m-p-refer {
  88. position: absolute;
  89. left: 50px;
  90. bottom: 50px;
  91. }
  92. .m-p-refer .text {
  93. position: absolute;
  94. top: -80px;
  95. left: -40px;
  96. animation-name: upAnimation;
  97. transform-origin: center bottom;
  98. animation-duration: 2s;
  99. animation-fill-mode: both;
  100. animation-iteration-count: infinite;
  101. animation-delay: .5s;
  102. }
  103. .m-p-refer .close {
  104. display: block;
  105. position: absolute;
  106. top: -110px;
  107. right: -50px;
  108. padding: 0;
  109. margin: 0;
  110. width: 50px;
  111. height: 50px;
  112. border-radius: 50%;
  113. border: none;
  114. cursor: pointer;
  115. z-index: 3;
  116. transition: 0.3s all;
  117. background-size: 30px 30px;
  118. background-repeat: no-repeat;
  119. background-position: center center;
  120. background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAAk1BMVEUAAAD////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////ROyVeAAAAMHRSTlMA1Sq7gPribxkJx6Ey8onMsq+GTe10QF8kqJl5WEcvIBDc0sHAkkk1FgO2ZZ+dj1FHfPqwAAACNElEQVRIx6VW6ZqqMAwtFlEW2Rm3EXEfdZa+/9PdBEvbIVXu9835oW1yjiQlTWQE/iYPuTObOTzMNz4bQFRlY2FgnFXRC/o01mytiafP+BPvQZk56bcLSOXem1jpCy4QgXvRtlEVCARfUP65RM/hp29/+0R7eSbhoHlnffZ8h76e6x1tyw9mxXaJ3nfTVLd89hQr9NfGceJxfLIXmONh6eNNYftNSESRmgkHlEOjmhgBbYcEW08FFQN/ro6dvAczjhgXEdQP76xHEYxM+igQq259gLrCSlwbD3iDtTMy+A4Yuk0B6zV8c+BcO2OgFIp/UvJdG4o/Rp1JQYXeZFflPEFMfvugiFGFXN587YtgX7C8lRGFXPCGGYCCzlkoxJ4xqmi/jrIcdYYh5pwxiwI/gt7lDDFrcLiMKhBJ//W78ENsJgVUsV8wKpjZBXshM6cCW0jbRAilICFxIpgGMmmiWGHSIR6ViY+DPFaqSJCbQ5mbxoZLIlU0Al/cBj6N1uXfFI0okLppi69StmumSFQRP6oIKDedFi3vRDn3j6KozCZlu0DdJb3AupJXNLmqkk9+X9FEHLt1Jq8oi1H5n01AtRlvwQZQl9hmtPY4JEjMDs5ftWJN4Xr4lLrV2OHiUDHCPgvA/Tn/hP4zGUBfjZ3eLJ+NIOfHxi8CMoAQtYfmw93v01O0e7VlqqcCsXML3Vsu94cxnb4c7ML5chG8JIP9b38dENGaj3+x+TpiA/AL/fen8In7H8l3ZjdJQt2TAAAAAElFTkSuQmCC);
  121. background-color: rgba(0, 0, 0, 0.5);
  122. }
  123. .m-p-refer .close:hover {
  124. background-color: rgba(0, 0, 0, 0.8);
  125. }
  126. .m-p-refer .link {
  127. border-radius: 4px;
  128. text-decoration: none;
  129. background-color: #4E84E6;
  130. transition: 0.3s all;
  131. }
  132. .m-p-refer .link:hover {
  133. top: -10px;
  134. color: #333333;
  135. border: 1px solid transparent;
  136. background: rgba(0, 0, 0, 0.6);
  137. box-shadow: 2px 11px 20px 0 rgba(0, 0, 0, 0.6);
  138. }
  139. @keyframes upAnimation {
  140. 0% {
  141. transform: rotate(0deg);
  142. transition-timing-function: cubic-bezier(0.215, .61, .355, 1)
  143. }
  144.  
  145. 10% {
  146. transform: rotate(-12deg);
  147. transition-timing-function: cubic-bezier(0.215, .61, .355, 1)
  148. }
  149.  
  150. 20% {
  151. transform: rotate(12deg);
  152. transition-timing-function: cubic-bezier(0.215, .61, .355, 1)
  153. }
  154.  
  155. 28% {
  156. transform: rotate(-10deg);
  157. transition-timing-function: cubic-bezier(0.215, .61, .355, 1)
  158. }
  159.  
  160. 36% {
  161. transform: rotate(10deg);
  162. transition-timing-function: cubic-bezier(0.755, .5, .855, .06)
  163. }
  164.  
  165. 42% {
  166. transform: rotate(-8deg);
  167. transition-timing-function: cubic-bezier(0.755, .5, .855, .06)
  168. }
  169.  
  170. 48% {
  171. transform: rotate(8deg);
  172. transition-timing-function: cubic-bezier(0.755, .5, .855, .06)
  173. }
  174.  
  175. 52% {
  176. transform: rotate(-4deg);
  177. transition-timing-function: cubic-bezier(0.755, .5, .855, .06)
  178. }
  179.  
  180. 56% {
  181. transform: rotate(4deg);
  182. transition-timing-function: cubic-bezier(0.755, .5, .855, .06)
  183. }
  184.  
  185. 60% {
  186. transform: rotate(0deg);
  187. transition-timing-function: cubic-bezier(0.755, .5, .855, .06)
  188. }
  189.  
  190. 100% {
  191. transform: rotate(0deg);
  192. transition-timing-function: cubic-bezier(0.215, .61, .355, 1)
  193. }
  194. }
  195. /*顶部信息录入*/
  196. .m-p-temp-url {
  197. padding-top: 10px;
  198. padding-bottom: 10px;
  199. width: 100%;
  200. color: #999999;
  201. text-align: left;
  202. font-style: italic;
  203. word-break: break-all;
  204. font-size: 12px;
  205. }
  206.  
  207. }
  208. .m-p-input-container input {
  209. flex: 1;
  210. margin-bottom: 20px;
  211. display: block;
  212. width: 380px;
  213. padding: 14px;
  214. font-size: 24px;
  215. border-radius: 4px;
  216. box-shadow: none;
  217. color: #444444;
  218. border: 1px solid #cccccc;
  219. min-width: 400px;
  220. }
  221. .m-p-input-container .range-input {
  222. margin-left: 10px;
  223. margin-bottom: 0;
  224. width: 100px;
  225. box-sizing: border-box;
  226. }
  227. .m-p-input-container div {
  228. position: relative;
  229. display: inline-block;
  230. margin-left: 10px;
  231. height: 40px;
  232. font-size:14px;
  233. color: white;
  234. cursor: pointer;
  235. border-radius: 4px;
  236. border: 1px solid #eeeeee;
  237. background-color: #3D8AC7;
  238. opacity: 1;
  239. transition: 0.3s all;
  240. }
  241. .m-p-input-container div:hover {
  242. opacity: 0.9;
  243. }
  244. .m-p-input-container div {
  245. width: 200px;
  246. }
  247. .m-p-input-container .disable {
  248. cursor: not-allowed;
  249. background-color: #dddddd;
  250. }
  251. /*下载状态*/
  252. .m-p-line {
  253. margin: 20px 0 50px;
  254. vertical-align: top;
  255. width: 100%;
  256. height: 5px;
  257. border-bottom: dotted;
  258. }
  259. .m-p-tips {
  260. width: 100%;
  261. color: #999999;
  262. text-align: left;
  263. font-style: italic;
  264. word-break: break-all;
  265. }
  266. .m-p-tips p {
  267. width: 100px;
  268. display: inline-block;
  269. }
  270. .m-p-tips.error-tips{
  271. color: #DC5350;
  272. }
  273. .m-p-segment {
  274. text-align: left;
  275. }
  276. .m-p-segment .item {
  277. display: inline-block;
  278. margin: 10px 6px;
  279. width: 50px;
  280. height: 40px;
  281. color: white;
  282. line-height: 40px;
  283. text-align: center;
  284. border-radius: 4px;
  285. cursor: help;
  286. border: solid 1px #eeeeee;
  287. background-color: #dddddd;
  288. transition: 0.3s all;
  289. }
  290. .m-p-segment .finish {
  291. background-color: #0ACD76;
  292. }
  293. .m-p-segment .error {
  294. cursor: pointer;
  295. background-color: #DC5350;
  296. }
  297. .m-p-segment .error:hover {
  298. opacity: 0.9;
  299. }
  300. .m-p-stream, .m-p-report, .m-p-cross, .m-p-final {
  301. margin-top: 10px;
  302. display: inline-block;
  303. width: 100%;
  304. height: 30px;
  305. line-height: 30px;
  306. font-size: 15px;
  307. color: white;
  308. cursor: pointer;
  309. border-radius: 4px;
  310. border: 1px solid #eeeeee;
  311. background-color: #3D8AC7;
  312. opacity: 1;
  313. transition: 0.3s all;
  314. }
  315. .m-p-stream {
  316. background-color: #0ACD76 !important;
  317. }
  318. .m-p-report {
  319. background-color: #e74c3c !important;
  320. text-decoration: none;
  321. }
  322. .m-p-final {
  323. text-decoration: none;
  324. }
  325. .m-p-force, .m-p-retry {
  326. position: absolute;
  327. right: 50px;
  328. display: inline-block;
  329. padding: 6px 12px;
  330. font-size: 18px;
  331. color: white;
  332. cursor: pointer;
  333. border-radius: 4px;
  334. border: 1px solid #eeeeee;
  335. background-color: #3D8AC7;
  336. opacity: 1;
  337. transition: 0.3s all;
  338. }
  339. .m-p-retry {
  340. right: 250px;
  341. }
  342. .m-p-force:hover, .m-p-retry:hover {
  343. opacity: 0.9;
  344. }
  345. .m-p-input-container {
  346. display: flex;
  347. flex-direction: column;
  348. justify-content: center;
  349. align-items: center;
  350. padding: 10px;
  351. box-sizing: border-box;
  352. }
  353.  
  354. .m-p-input-container input {
  355. width: 100%;
  356. margin-bottom: 10px;
  357. padding: 12px 10px;
  358. font-size: 14px;
  359. border: none;
  360. border-radius: 4px;
  361. box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.3);
  362. box-sizing: border-box;
  363. }
  364.  
  365. .m-p-input-container div {
  366. width: 100%;
  367. margin-bottom: 10px;
  368. padding: 10px;
  369. font-size: 14px;
  370. color: white;
  371. background-color: #136fbe;
  372. text-align: center;
  373. border-radius: 5px;
  374. cursor: pointer;
  375. box-sizing: border-box;
  376. }
  377.  
  378. @media screen and (min-width: 768px) {
  379. .m-p-input-container {
  380. flex-direction: row;
  381. justify-content: space-around;
  382. padding: 20px;
  383. }
  384.  
  385. .m-p-input-container input[type="text"] {
  386. width: calc(65% - 10px);
  387. margin-bottom: 0;
  388. margin-right: 10px;
  389. }
  390.  
  391. .m-p-input-container div {
  392. width: calc(20% - 5px);
  393. margin-bottom: 0;
  394. }
  395. }
  396.  
  397. .close-gobutton {
  398. position: absolute;
  399. top: 10px;
  400. right: 10px;
  401. background-color: #858585;
  402. color: #fff;
  403. border-radius: 50%;
  404. border: none;
  405. width: 30px;
  406. height: 30px;
  407. cursor: pointer;
  408. font-size: 20px;
  409. }
  410. </style>
  411.  
  412.  
  413. <div id="m-loading">
  414. </div>
  415. <section id="m-app" v-cloak>
  416. <!--顶部操作提示-->
  417. <section class="m-p-action g-box">{{tips}}</section>
  418.  
  419. <button class="close-gobutton">X</button>
  420.  
  421. <!--文件载入-->
  422. <div class="m-p-temp-url">【风筝】无删减版测试链接:https://dy2.yle888.vip/20220707/nP0uddUJ/2000kb/hls/index.m3u8</div>
  423. <section class="m-p-input-container">
  424. <input id="mym3u8val" type="text" v-model="url" :disabled="downloading" placeholder="请输入 m3u8或MP4 链接" >
  425.  
  426. <!--范围查询-->
  427. <template v-if="!downloading || rangeDownload.isShowRange">
  428. <div v-if="!rangeDownload.isShowRange" @click="getM3U8(true)">解析下载</div>
  429. <template v-else>
  430. <input class="range-input" type="number" v-model="rangeDownload.startSegment" :disabled="downloading" placeholder="起始片段">
  431. <input class="range-input" type="number" v-model="rangeDownload.endSegment" :disabled="downloading" placeholder="截止片段">
  432. </template>
  433. </template>
  434.  
  435. <!--还未开始下载-->
  436. <template v-if="!downloading">
  437. <div @click="getM3U8(false)">原格式下载</div>
  438. <div @click="getMP4">MP4下载</div>
  439. <div @click="getPlay">在线播放</div>
  440. </template>
  441. <div v-else-if="finishNum === rangeDownload.targetSegment && rangeDownload.targetSegment > 0" class="disable">下载完成</div>
  442. <div v-else @click="togglePause">{{ isPause ? '恢复下载' : '暂停下载' }}</div>
  443. </section>
  444. <div v-if="!downloading && isSupperStreamWrite" class="m-p-stream" @click="streamDownload(false)">特大视频原格式下载,边下载边保存,彻底解决大文件下载内存不足问题 </div>
  445. <div v-if="!downloading && isSupperStreamWrite" class="m-p-stream" @click="streamDownload(true)">特大视频 MP4 格式下载,边下载边保存,彻底解决大文件下载内存不足问题 </div>
  446.  
  447.  
  448. <template v-if="finishList.length > 0">
  449. <div class="m-p-line"></div>
  450. <!-- <div class="m-p-retry" v-if="errorNum && downloadIndex >= rangeDownload.targetSegment" @click="retryAll">重新下载错误片段</div> -->
  451. <div class="m-p-force" v-if="mediaFileList.length && !streamWriter" @click="forceDownload">强制下载现有片段</div>
  452. <div class="m-p-tips">待下载碎片总量:{{ rangeDownload.targetSegment }},已下载:{{ finishNum }},错误:{{ errorNum }},进度:{{ (finishNum / rangeDownload.targetSegment * 100).toFixed(2) }}%</div>
  453. <div class="m-p-tips" :class="[errorNum ? 'error-tips' : '']">若某视频碎片下载发生错误,将标记为红色,可点击相应图标进行重试</div>
  454. <section class="m-p-segment">
  455. <div class="item" v-for="(item, index) in finishList" :class="[item.status]" :title="item.title" @click="retry(index)">{{ index + 1 }}</div>
  456. </section>
  457. </template>
  458. </section>
  459.  
  460.  
  461. `
  462. $section.style.width = '80%';
  463. $section.style.height = '80%';
  464. $section.style.display = 'block';
  465. $section.style.maxWidth = '900px';
  466. $section.style.top = '50%';
  467. $section.style.left = '50%';
  468. $section.style.position = 'fixed';
  469. $section.style.zIndex = '999999999991';
  470. $section.style.backgroundColor = 'white';
  471. $section.style.transform = 'translate(-50%, -50%)';
  472. $section.style.borderRadius = '10px';
  473. $section.style.boxShadow = '0px 0px 20px rgba(0, 0, 0, 0.5)';
  474. $section.style.overflow = 'auto';
  475.  
  476. if (GM_info.script.namespace!="Z3JlYXN5Zm9yaw=="){
  477. return
  478. }
  479. document.body.appendChild($section);
  480.  
  481. //toastr.error('该网站限制了资源加载,已复制该视频链接。请在其他网站页面打开m3u8视频下载菜单进行下载', '', { positionClass: 'toast-top-center', showDuration: 300, hideDuration: 1000, timeOut: 3000, extendedTimeOut: 1000, showEasing: 'swing', hideEasing: 'linear', showMethod: 'fadeIn', hideMethod: 'fadeOut' });
  482. var app = new Vue({
  483. el: '#m-app',
  484.  
  485. data() {
  486. return {
  487. url: m3u8url, // 在线链接
  488. tips: 'm3u8、MP4 视频在线提取工具', // 顶部提示
  489. title: '', // 视频标题
  490. isPause: false, // 是否暂停下载
  491. isGetMP4: false, // 是否转码为 MP4 下载
  492. durationSecond: 0, // 视频持续时长
  493. isShowRefer: false, // 是否显示推送
  494. downloading: false, // 是否下载中
  495. beginTime: '', // 开始下载的时间
  496. errorNum: 0, // 错误数
  497. finishNum: 0, // 已下载数
  498. downloadIndex: 0, // 当前下载片段
  499. finishList: [], // 下载完成项目
  500. tsUrlList: [], // ts URL数组
  501. mediaFileList: [], // 下载的媒体数组
  502. isSupperStreamWrite: window.streamSaver && !window.streamSaver.useBlobFallback, // 当前浏览器是否支持流式下载
  503. streamWriter: null, // 文件流写入器
  504. streamDownloadIndex: 0, // 文件流写入器,正准备写入第几个视频片段
  505. rangeDownload: { // 特定范围下载
  506. isShowRange: false, // 是否显示范围下载
  507. startSegment: '', // 起始片段
  508. endSegment: '', // 截止片段
  509. targetSegment: 1, // 待下载片段
  510. },
  511. aesConf: { // AES 视频解密配置
  512. method: '', // 加密算法
  513. uri: '', // key 所在文件路径
  514. iv: '', // 偏移值
  515. key: '', // 秘钥
  516. decryptor: null, // 解码器对象
  517.  
  518. stringToBuffer: function (str) {
  519. return new TextEncoder().encode(str)
  520. },
  521. },
  522. }
  523. },
  524.  
  525. created() {
  526. this.getSource();
  527. window.addEventListener('keyup', this.onKeyup)
  528. setInterval(this.retryAll.bind(this), 2000) // 每两秒重新下载一遍错误片段,实现错误自动重试
  529. },
  530.  
  531. beforeDestroy() {
  532. window.removeEventListener('keyup', this.onKeyup)
  533. },
  534.  
  535. methods: {
  536. // 获取链接中携带的资源链接
  537. getSource() {
  538. let { href } = location
  539. if (href.indexOf('?source=') > -1) {
  540. this.url = href.split('?source=')[1]
  541. }
  542. },
  543.  
  544. // 获取顶部 window title,因可能存在跨域问题,故使用 try catch 进行保护
  545. getDocumentTitle(){
  546. let title = document.title;
  547. try {
  548. title = window.top.document.title
  549. } catch (error) {
  550. console.log(error)
  551. }
  552. return title
  553. },
  554.  
  555. // 退出弹窗
  556. onKeyup(event) {
  557. var inputBox = document.querySelector('#mym3u8val');
  558. if (inputBox && inputBox.style.display !== 'none' && event.keyCode === 13) {
  559. this.getM3U8();
  560. }
  561. },
  562.  
  563. // ajax 请求
  564. ajax(options) {
  565. options = options || {};
  566. let xhr = new XMLHttpRequest();
  567. if (options.type === 'file') {
  568. xhr.responseType = 'arraybuffer';
  569. }
  570.  
  571. xhr.onreadystatechange = function () {
  572. if (xhr.readyState === 4) {
  573. let status = xhr.status;
  574. if (status >= 200 && status < 300) {
  575. options.success && options.success(xhr.response);
  576. } else {
  577. options.fail && options.fail(status);
  578. }
  579. }
  580. };
  581.  
  582. xhr.open("GET", options.url, true);
  583. xhr.send(null);
  584. },
  585.  
  586. // 合成URL
  587. applyURL(targetURL, baseURL) {
  588. baseURL = baseURL || location.href
  589. if (targetURL.indexOf('http') === 0) {
  590. // 当前页面使用 https 协议时,强制使 ts 资源也使用 https 协议获取
  591. if(location.href.indexOf('https') === 0){
  592. return targetURL.replace('http://','https://')
  593. }
  594. return targetURL
  595. } else if (targetURL[0] === '/') {
  596. let domain = baseURL.split('/')
  597. return domain[0] + '//' + domain[2] + targetURL
  598. } else {
  599. let domain = baseURL.split('/')
  600. domain.pop()
  601. return domain.join('/') + '/' + targetURL
  602. }
  603. },
  604.  
  605. // 使用流式下载,边下载边保存,解决大视频文件内存不足的难题
  606. streamDownload(isMp4){
  607. var url = this.url;
  608. if (url=="" ){
  609. toastr.error("请先输入 m3u8 链接才能解析下载", '', { positionClass: 'toast-top-center', showDuration: 300, hideDuration: 1000, timeOut: 3000, extendedTimeOut: 1000, showEasing: 'swing', hideEasing: 'linear', showMethod: 'fadeIn', hideMethod: 'fadeOut' });
  610. return
  611. }
  612. if (url.toLowerCase().indexOf('m3u8') === -1) {
  613. alert('链接有误,请重新输入,必须是以.m3u8结尾的链接')
  614. return
  615. }
  616. this.isGetMP4 = isMp4
  617. this.title = new URL(this.url).searchParams.get('title') || this.title // 获取视频标题
  618. let fileName = this.title || this.formatTime(new Date(), 'YYYY_MM_DD hh_mm_ss')
  619. if(document.title !== 'm3u8 downloader'){
  620. fileName = this.getDocumentTitle()
  621. }
  622. this.streamWriter = window.streamSaver.createWriteStream(`${fileName}.${isMp4 ? 'mp4' : 'ts'}`).getWriter()
  623. this.getM3U8()
  624. },
  625.  
  626. // 解析为 mp4 下载
  627. getMP4() {
  628. this.isGetMP4 = true;
  629. this.getM3U8();
  630. },
  631. getPlay() {
  632. if (this.url=="" ) {
  633. toastr.error("请先输入 m3u8 链接才能解析播放", '', { positionClass: 'toast-top-center', showDuration: 300, hideDuration: 1000, timeOut: 3000, extendedTimeOut: 1000, showEasing: 'swing', hideEasing: 'linear', showMethod: 'fadeIn', hideMethod: 'fadeOut' });
  634. return
  635. }
  636. Playm3u8(this.url);
  637. },
  638. // 获取在线文件
  639. getM3U8(onlyGetRange) {
  640. if (!this.url) {
  641. alert('请输入链接')
  642. return
  643. }
  644. if (this.url.toLowerCase().indexOf('.mp4') >0) {
  645. var mp4url=this.url;
  646. toastr.success('正在后台下载,请稍后。', '', { positionClass: 'toast-bottom-right', showDuration: 300, hideDuration: 1000, timeOut: 3000, extendedTimeOut: 1000, showEasing: 'swing', hideEasing: 'linear', showMethod: 'fadeIn', hideMethod: 'fadeOut' });
  647. GM_download({
  648. url: mp4url,
  649. name: getFileNameFromUrl(mp4url),
  650. saveAs: false,
  651. onload: function() {
  652. toastr.success('下载完成', '', { positionClass: 'toast-bottom-right', showDuration: 300, hideDuration: 1000, timeOut: 3000, extendedTimeOut: 1000, showEasing: 'swing', hideEasing: 'linear', showMethod: 'fadeIn', hideMethod: 'fadeOut' });
  653. },
  654. onerror: function(err) {
  655. console.error('下载失败'+err , err);
  656. }
  657. });
  658. return
  659. }
  660. if (this.url.toLowerCase().indexOf('m3u8') === -1) {
  661. alert('链接有误,请重新输入')
  662. return
  663. }
  664. if (this.downloading) {
  665. alert('资源下载中,请稍后')
  666. return
  667. }
  668.  
  669. // 在下载页面才触发,代码注入的页面不需要校验
  670. // 当前协议不一致,切换协议
  671. if (location.href.indexOf('blog.luckly-mjw.cn') > -1 && this.url.indexOf(location.protocol) === -1) {
  672. //alert('当前协议不一致,跳转至正确页面重新下载')
  673. location.href = `${this.url.split(':')[0]}://blog.luckly-mjw.cn/tool-show/m3u8-downloader/index.html?source=${this.url}`
  674. return
  675. }
  676.  
  677. // 在下载页面才触发,修改页面 URL,携带下载路径,避免刷新后丢失
  678. if (location.href.indexOf('blog.luckly-mjw.cn') > -1) {
  679. window.history.replaceState(null, '', `${location.href.split('?')[0]}?source=${this.url}`)
  680. }
  681.  
  682. this.title = new URL(this.url).searchParams.get('title') || this.title // 获取视频标题
  683. this.tips = 'm3u8 文件下载中,请稍后'
  684. this.beginTime = new Date()
  685. this.ajax({
  686. url: this.url,
  687. success: (m3u8Str) => {
  688. this.tsUrlList = []
  689. this.finishList = []
  690.  
  691. // 提取 ts 视频片段地址
  692. m3u8Str.split('\n').forEach((item) => {
  693. // if (/.(png|image|ts|jpg|mp4|jpeg)/.test(item)) {
  694. // 放开片段后缀限制,下载非 # 开头的链接片段
  695. if (/^[^#]/.test(item)) {
  696. console.log(item)
  697. this.tsUrlList.push(this.applyURL(item, this.url))
  698. this.finishList.push({
  699. title: item,
  700. status: ''
  701. })
  702. }
  703. })
  704.  
  705. // 仅获取视频片段数
  706. if (onlyGetRange) {
  707. this.rangeDownload.isShowRange = true
  708. this.rangeDownload.endSegment = this.tsUrlList.length
  709. this.rangeDownload.targetSegment = this.tsUrlList.length
  710. return
  711. } else {
  712. let startSegment = Math.max(this.rangeDownload.startSegment || 1, 1) // 最小为 1
  713. let endSegment = Math.max(this.rangeDownload.endSegment || this.tsUrlList.length, 1)
  714. startSegment = Math.min(startSegment, this.tsUrlList.length) // 最大为 this.tsUrlList.length
  715. endSegment = Math.min(endSegment, this.tsUrlList.length)
  716. this.rangeDownload.startSegment = Math.min(startSegment, endSegment)
  717. this.rangeDownload.endSegment = Math.max(startSegment, endSegment)
  718. this.rangeDownload.targetSegment = this.rangeDownload.endSegment - this.rangeDownload.startSegment + 1
  719. this.downloadIndex = this.rangeDownload.startSegment - 1
  720. this.downloading = true
  721. }
  722.  
  723. // 获取需要下载的 MP4 视频长度
  724. if (this.isGetMP4) {
  725. let infoIndex = 0
  726. m3u8Str.split('\n').forEach(item => {
  727. if (item.toUpperCase().indexOf('#EXTINF:') > -1) { // 计算视频总时长,设置 mp4 信息时使用
  728. infoIndex++
  729. if (this.rangeDownload.startSegment <= infoIndex && infoIndex <= this.rangeDownload.endSegment) {
  730. this.durationSecond += parseFloat(item.split('#EXTINF:')[1])
  731. }
  732. }
  733. })
  734. }
  735.  
  736. // 检测视频 AES 加密
  737. if (m3u8Str.indexOf('#EXT-X-KEY') > -1) {
  738. this.aesConf.method = (m3u8Str.match(/(.*METHOD=([^,\s]+))/) || ['', '', ''])[2]
  739. this.aesConf.uri = (m3u8Str.match(/(.*URI="([^"]+))"/) || ['', '', ''])[2]
  740. this.aesConf.iv = (m3u8Str.match(/(.*IV=([^,\s]+))/) || ['', '', ''])[2]
  741. this.aesConf.iv = this.aesConf.iv ? this.aesConf.stringToBuffer(this.aesConf.iv) : ''
  742. this.aesConf.uri = this.applyURL(this.aesConf.uri, this.url)
  743.  
  744. // let params = m3u8Str.match(/#EXT-X-KEY:([^,]*,?METHOD=([^,]+))?([^,]*,?URI="([^,]+)")?([^,]*,?IV=([^,^\n]+))?/)
  745. // this.aesConf.method = params[2]
  746. // this.aesConf.uri = this.applyURL(params[4], this.url)
  747. // this.aesConf.iv = params[6] ? this.aesConf.stringToBuffer(params[6]) : ''
  748. this.getAES();
  749. } else if (this.tsUrlList.length > 0) { // 如果视频没加密,则直接下载片段,否则先下载秘钥
  750. this.downloadTS()
  751. } else {
  752. this.alertError('资源为空,请查看链接是否有效')
  753. }
  754. },
  755. fail: () => {
  756. this.alertError('链接不正确,请查看链接是否有效')
  757. }
  758. })
  759. },
  760.  
  761. // 获取AES配置
  762. getAES() {
  763. // alert('视频被 AES 加密,点击确认,进行视频解码')
  764. this.ajax({
  765. type: 'file',
  766. url: this.aesConf.uri,
  767. success: (key) => {
  768. // console.log('getAES', key)
  769. // this.aesConf.key = this.aesConf.stringToBuffer(key)
  770. this.aesConf.key = key
  771. this.aesConf.decryptor = new AESDecryptor()
  772. this.aesConf.decryptor.constructor()
  773. this.aesConf.decryptor.expandKey(this.aesConf.key);
  774. this.downloadTS()
  775. },
  776. fail: () => {
  777. this.alertError('视频已加密,可试用右下角入口的「无差别提取工具」')
  778. }
  779. })
  780. },
  781.  
  782. // ts 片段的 AES 解码
  783. aesDecrypt(data, index) {
  784. let iv = this.aesConf.iv || new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, index])
  785. return this.aesConf.decryptor.decrypt(data, 0, iv.buffer || iv, true)
  786. },
  787.  
  788. // 下载分片
  789. downloadTS() {
  790. this.tips = 'ts 视频碎片下载中,请稍后'
  791. let download = () => {
  792. let isPause = this.isPause // 使用另一个变量来保持下载前的暂停状态,避免回调后没修改
  793. let index = this.downloadIndex
  794. if (index >= this.rangeDownload.endSegment) {
  795. return
  796. }
  797. this.downloadIndex++
  798. if (this.finishList[index] && this.finishList[index].status === '') {
  799. this.finishList[index].status = 'downloading'
  800. this.ajax({
  801. url: this.tsUrlList[index],
  802. type: 'file',
  803. success: (file) => {
  804. this.dealTS(file, index, () => this.downloadIndex < this.rangeDownload.endSegment && !isPause && download())
  805. },
  806. fail: () => {
  807. this.errorNum++
  808. this.finishList[index].status = 'error'
  809. if (this.downloadIndex < this.rangeDownload.endSegment) {
  810. !isPause && download()
  811. }
  812. }
  813. })
  814. } else if (this.downloadIndex < this.rangeDownload.endSegment) { // 跳过已经成功的片段
  815. !isPause && download()
  816. }
  817. }
  818.  
  819. // 建立多少个 ajax 线程
  820. for (let i = 0; i < Math.min(6, this.rangeDownload.targetSegment - this.finishNum); i++) {
  821. download()
  822. }
  823. },
  824.  
  825. // 处理 ts 片段,AES 解密、mp4 转码
  826. dealTS(file, index, callback) {
  827. const data = this.aesConf.uri ? this.aesDecrypt(file, index) : file
  828. this.conversionMp4(data, index, (afterData) => { // mp4 转码
  829. this.mediaFileList[index - this.rangeDownload.startSegment + 1] = afterData // 判断文件是否需要解密
  830. this.finishList[index].status = 'finish'
  831. this.finishNum++
  832. if (this.streamWriter){
  833. for (let index = this.streamDownloadIndex; index < this.mediaFileList.length; index++) {
  834. if(this.mediaFileList[index]){
  835. this.streamWriter.write(new Uint8Array(this.mediaFileList[index]))
  836. this.mediaFileList[index] = null
  837. this.streamDownloadIndex = index + 1
  838. } else {
  839. break
  840. }
  841. }
  842. if (this.streamDownloadIndex >= this.rangeDownload.targetSegment){
  843. this.streamWriter.close()
  844. }
  845. } else if (this.finishNum === this.rangeDownload.targetSegment) {
  846. let fileName = this.title || this.formatTime(this.beginTime, 'YYYY_MM_DD hh_mm_ss')
  847. if(document.title !== 'm3u8 downloader'){
  848. fileName = this.getDocumentTitle()
  849. }
  850. this.downloadFile(this.mediaFileList, fileName)
  851. }
  852. callback && callback()
  853. })
  854. },
  855.  
  856. // 转码为 mp4
  857. conversionMp4(data, index, callback) {
  858. if (this.isGetMP4) {
  859. let transmuxer = new muxjs.Transmuxer({
  860. keepOriginalTimestamps: true,
  861. duration: parseInt(this.durationSecond),
  862. });
  863. transmuxer.on('data', segment => {
  864. if (index === this.rangeDownload.startSegment - 1) {
  865. let data = new Uint8Array(segment.initSegment.byteLength + segment.data.byteLength);
  866. data.set(segment.initSegment, 0);
  867. data.set(segment.data, segment.initSegment.byteLength);
  868. callback(data.buffer)
  869. } else {
  870. callback(segment.data)
  871. }
  872. })
  873. transmuxer.push(new Uint8Array(data));
  874. transmuxer.flush();
  875. } else {
  876. callback(data)
  877. }
  878. },
  879.  
  880. // 暂停与恢复
  881. togglePause() {
  882. this.isPause = !this.isPause
  883. !this.isPause && this.retryAll(true)
  884. },
  885.  
  886. // 重新下载某个片段
  887. retry(index) {
  888. if (this.finishList[index].status === 'error') {
  889. this.finishList[index].status = ''
  890. this.ajax({
  891. url: this.tsUrlList[index],
  892. type: 'file',
  893. success: (file) => {
  894. this.errorNum--
  895. this.dealTS(file, index)
  896. },
  897. fail: () => {
  898. this.finishList[index].status = 'error'
  899. }
  900. })
  901. }
  902. },
  903.  
  904. // 重新下载所有错误片段
  905. retryAll(forceRestart) {
  906. if (!this.finishList.length || this.isPause) {
  907. return
  908. }
  909.  
  910. let firstErrorIndex = this.downloadIndex // 没有错误项目,则每次都递增
  911. this.finishList.forEach((item, index) => { // 重置所有错误片段状态
  912. if (item.status === 'error') {
  913. item.status = ''
  914. firstErrorIndex = Math.min(firstErrorIndex, index)
  915. }
  916. })
  917. this.errorNum = 0
  918. // 已经全部下载进程都跑完了,则重新启动下载进程
  919. if (this.downloadIndex >= this.rangeDownload.endSegment || forceRestart) {
  920. this.downloadIndex = firstErrorIndex
  921. this.downloadTS()
  922. } else { // 否则只是将下载索引,改为最近一个错误的项目,从那里开始遍历
  923. this.downloadIndex = firstErrorIndex
  924. }
  925. },
  926.  
  927. // 下载整合后的TS文件
  928. downloadFile(fileDataList, fileName) {
  929. this.tips = 'ts 碎片整合中,请留意浏览器下载'
  930. let fileBlob = null
  931. let a = document.createElement('a')
  932. if (this.isGetMP4) {
  933. fileBlob = new Blob(fileDataList, { type: 'video/mp4' }) // 创建一个Blob对象,并设置文件的 MIME 类型
  934. a.download = fileName + '.mp4'
  935. } else {
  936. fileBlob = new Blob(fileDataList, { type: 'video/MP2T' }) // 创建一个Blob对象,并设置文件的 MIME 类型
  937. a.download = fileName + '.ts'
  938. }
  939. a.href = URL.createObjectURL(fileBlob)
  940. a.style.display = 'none'
  941. document.body.appendChild(a)
  942. a.click()
  943. a.remove()
  944. },
  945.  
  946. // 格式化时间
  947. formatTime(date, formatStr) {
  948. const formatType = {
  949. Y: date.getFullYear(),
  950. M: date.getMonth() + 1,
  951. D: date.getDate(),
  952. h: date.getHours(),
  953. m: date.getMinutes(),
  954. s: date.getSeconds(),
  955. }
  956. return formatStr.replace(
  957. /Y+|M+|D+|h+|m+|s+/g,
  958. target => (new Array(target.length).join('0') + formatType[target[0]]).substr(-target.length)
  959. )
  960. },
  961.  
  962. // 强制下载现有片段
  963. forceDownload() {
  964. if (this.mediaFileList.length) {
  965. let fileName = this.title || this.formatTime(this.beginTime, 'YYYY_MM_DD hh_mm_ss')
  966. if(document.title !== 'm3u8 downloader'){
  967. fileName = this.getDocumentTitle()
  968. }
  969. this.downloadFile(this.mediaFileList, fileName)
  970. } else {
  971. alert('当前无已下载片段')
  972. }
  973. },
  974.  
  975. // 发生错误,进行提示
  976. alertError(tips) {
  977. alert(tips)
  978. this.downloading = false
  979. this.tips = 'm3u8、MP4视频在线提取工具';
  980. },
  981.  
  982.  
  983. }
  984. })
  985.  
  986. }
  987. $("body").on('click', '.close-gobutton', function() {
  988. $("#down-my-section").remove();
  989. })
  990. //sectionm3u8menu("111111");
  991.  
  992.  
  993. function Playm3u8(url) {
  994. let videoFormat = url.split('.').pop().toLowerCase();
  995.  
  996. // 判断视频格式是否是网络常见的在线视频格式
  997. let supportedFormats = ['mp4', 'm3u8', 'webm', 'ogg'];
  998. if (!supportedFormats.includes(videoFormat)) {
  999. toastr.error("不支持的视频格式", '', { positionClass: 'toast-top-center', showDuration: 300, hideDuration: 1000, timeOut: 3000, extendedTimeOut: 1000, showEasing: 'swing', hideEasing: 'linear', showMethod: 'fadeIn', hideMethod: 'fadeOut' });
  1000. return;
  1001. }
  1002.  
  1003. $('<div id="floating-video-player"></div>').css({
  1004. position: 'fixed',
  1005. top: '50%',
  1006. left: '50%',
  1007. transform: 'translate(-50%, -50%)',
  1008. width: '80%',
  1009. height: '80%',
  1010. zIndex: 999999999992,
  1011. maxWidth: '800px',
  1012. maxHeight: '600px',
  1013. padding: 0
  1014. }).append(
  1015. $('<video></video>').attr({
  1016. width: '80%',
  1017. autoplay: 'autoplay',
  1018. controls: 'controls',
  1019. muted: 'muted'
  1020. }).css({
  1021. position: 'absolute',
  1022. top: '50%',
  1023. left: '50%',
  1024. transform: 'translate(-50%, -50%)',
  1025. width: 'calc(100% - 2px)',
  1026. borderRadius: '5px',
  1027. boxShadow: '0 0 10px rgba(0, 0, 0, 0.3)',
  1028. }).append(
  1029. $('<source></source>').attr({
  1030. src: url,
  1031. type: 'application/x-mpegURL'
  1032. })
  1033. ),
  1034. $('<button>X</button>').css({ // 关闭按钮
  1035. position: 'absolute',
  1036. top: '10px',
  1037. right: '60px',
  1038. border: 'none',
  1039. backgroundColor: '#757575',
  1040. color: '#ffffff',
  1041. fontSize: '16px',
  1042. fontWeight: 'bold',
  1043. cursor: 'pointer',
  1044. padding: '5px 10px',
  1045. borderRadius: '5px',
  1046. boxShadow: '0 0 5px rgba(0, 0, 0, 0.3)',
  1047. zIndex: 10000,
  1048. width: '40px',
  1049. height: '40px',
  1050. }).click(function() {
  1051. $('#floating-video-player').hide();
  1052. $('body').css('overflow', 'auto');
  1053. $('#floating-video-player').remove();
  1054. }),
  1055. $('<div></div>').css({ // 倍速和画中画按钮容器
  1056. position: 'absolute',
  1057. top: '10px',
  1058. left: '10px',
  1059. display: 'flex',
  1060. alignItems: 'center',
  1061. }).append(
  1062. $('<button>&lt;</button>').css({ // 倍速减少按钮
  1063. border: 'none',
  1064. backgroundColor: '#757575',
  1065. color: '#ffffff',
  1066. fontSize: '16px',
  1067. fontWeight: 'bold',
  1068. cursor: 'pointer',
  1069. padding: '5px 10px',
  1070. borderRadius: '5px',
  1071. boxShadow: '0 0 5px rgba(0, 0, 0, 0.3)',
  1072. width: '40px',
  1073. height: '40px',
  1074. marginRight: '10px',
  1075. }).click(function() {
  1076. video.playbackRate -= 0.25;
  1077. toastr.info("当前倍速:" + video.playbackRate.toFixed(2), '', { positionClass: 'toast-top-center', showDuration: 300, hideDuration: 1000, timeOut: 3000, extendedTimeOut: 1000, showEasing: 'swing', hideEasing: 'linear', showMethod: 'fadeIn', hideMethod: 'fadeOut' });
  1078. }),
  1079. $('<button>&gt;</button>').css({ // 倍速增加按钮
  1080. border: 'none',
  1081. backgroundColor: '#757575',
  1082. color: '#ffffff',
  1083. fontSize: '16px',
  1084. fontWeight: 'bold',
  1085. cursor: 'pointer',
  1086. padding: '5px 10px',
  1087. borderRadius: '5px',
  1088. boxShadow: '0 0 5px rgba(0, 0, 0, 0.3)',
  1089. width: '40px',
  1090. height: '40px',
  1091. marginRight: '10px',
  1092. }).click(function() {
  1093. video.playbackRate += 0.25;
  1094. toastr.info("当前倍速:" + video.playbackRate.toFixed(2), '', { positionClass: 'toast-top-center', showDuration: 300, hideDuration: 1000, timeOut: 3000, extendedTimeOut: 1000, showEasing: 'swing', hideEasing: 'linear', showMethod: 'fadeIn', hideMethod: 'fadeOut' });
  1095. }),
  1096. $('<button>&#9874; 画中画</button>').css({ // 画中画按钮
  1097. border: 'none',
  1098. backgroundColor: '#757575',
  1099. color: '#ffffff',
  1100. fontSize: '16px',
  1101. fontWeight: 'bold',
  1102. cursor: 'pointer',
  1103. padding: '5px 10px',
  1104. borderRadius: '5px',
  1105. boxShadow: '0 0 5px rgba(0, 0, 0, 0.3)',
  1106. width: '120px',
  1107. height: '40px',
  1108. }).click(function() {
  1109. if (video !== document.pictureInPictureElement) {
  1110. video.requestPictureInPicture();
  1111. } else {
  1112. document.exitPictureInPicture();
  1113. }
  1114. })
  1115. )
  1116. ).appendTo('body');
  1117.  
  1118. // 播放器相关样式
  1119. GM_addStyle('#floating-video-player video { display: block; }');
  1120.  
  1121. // 监听页面中的链接,自动播放视频
  1122. $(document).one('mousedown touchstart keydown', function() {
  1123. // 取消静音并播放视频
  1124. video.prop('muted', false)[0].play();
  1125. });
  1126.  
  1127. var videoContainer = $('#floating-video-player');
  1128. var video = videoContainer.find('video')[0];
  1129.  
  1130. // 根据视频格式选择播放器
  1131. if (videoFormat === 'm3u8') {
  1132. // 使用 HLS.js 播放 m3u8 格式的视频
  1133. if (Hls.isSupported()) {
  1134. var hls = new Hls();
  1135. hls.loadSource(url);
  1136. hls.attachMedia(video);
  1137. hls.on(Hls.Events.MANIFEST_PARSED, function() {
  1138. video.play();
  1139. videoContainer.show();
  1140. $('body').css('overflow', 'hidden');
  1141. });
  1142. } else {
  1143. toastr.error("浏览器不受支持", '', { positionClass: 'toast-top-center', showDuration: 300, hideDuration: 1000, timeOut: 3000, extendedTimeOut: 1000, showEasing: 'swing', hideEasing: 'linear', showMethod: 'fadeIn', hideMethod: 'fadeOut' });
  1144. }
  1145. } else if (videoFormat === 'mp4' && video.canPlayType && video.canPlayType('video/mp4')) {
  1146. // 使用原生视频播放器播放 mp4 格式的视频
  1147. video.setAttribute('src', url);
  1148. video.play();
  1149. videoContainer.show();
  1150. $('body').css('overflow', 'hidden');
  1151. }
  1152. }