ffm3u8_open.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. /*
  2. * @File : ffm3u8_open.js.js
  3. * @Author : jade
  4. * @Date : 2024/2/5 16:06
  5. * @Email : jadehh@1ive.com
  6. * @Software : Samples
  7. * @Desc :
  8. */
  9. import { _ } from './cat.js';
  10. import * as HLS from './hls.js';
  11. let key = 'ffm3u8';
  12. let url = '';
  13. let categories = [];
  14. let siteKey = '';
  15. let siteType = 0;
  16. async function request(reqUrl, agentSp) {
  17. let res = await req(reqUrl, {
  18. method: 'get',
  19. });
  20. return JSON.parse(res.content);
  21. }
  22. async function init(cfg) {
  23. siteKey = cfg.skey;
  24. siteType = cfg.stype;
  25. url = cfg.ext.url;
  26. categories = cfg.ext.categories;
  27. }
  28. async function home(filter) {
  29. const data = await request(url);
  30. let classes = [];
  31. for (const cls of data.class) {
  32. const n = cls.type_name.toString().trim();
  33. if (categories && categories.length > 0) {
  34. if (categories.indexOf(n) < 0) continue;
  35. }
  36. classes.push({
  37. type_id: cls.type_id.toString(),
  38. type_name: n,
  39. });
  40. }
  41. if (categories && categories.length > 0) {
  42. classes = _.sortBy(classes, (p) => {
  43. return categories.indexOf(p.type_name);
  44. });
  45. }
  46. return {
  47. class: classes,
  48. };
  49. }
  50. async function homeVod() {
  51. return '{}';
  52. }
  53. async function category(tid, pg, filter, extend) {
  54. let page = pg || 1;
  55. if (page == 0) page = 1;
  56. const data = await request(url + `?ac=detail&t=${tid}&pg=${page}`);
  57. let videos = [];
  58. for (const vod of data.list) {
  59. videos.push({
  60. vod_id: vod.vod_id.toString(),
  61. vod_name: vod.vod_name.toString(),
  62. vod_pic: vod.vod_pic,
  63. vod_remarks: vod.vod_remarks,
  64. });
  65. }
  66. return {
  67. page: parseInt(data.page),
  68. pagecount: data.pagecount,
  69. total: data.total,
  70. list: videos,
  71. };
  72. }
  73. async function detail(id) {
  74. const data = (await request(url + `?ac=detail&ids=${id}`)).list[0];
  75. let vod = {
  76. vod_id: data.vod_id,
  77. vod_name: data.vod_name,
  78. vod_pic: data.vod_pic,
  79. type_name: data.type_name,
  80. vod_year: data.vod_year,
  81. vod_area: data.vod_area,
  82. vod_remarks: data.vod_remarks,
  83. vod_actor: data.vod_actor,
  84. vod_director: data.vod_director,
  85. vod_content: data.vod_content.trim(),
  86. vod_play_from: data.vod_play_from,
  87. vod_play_url: data.vod_play_url,
  88. };
  89. return {
  90. list: [vod],
  91. };
  92. }
  93. async function proxy(segments, headers, reqHeaders) {
  94. let what = segments[0];
  95. let segs = decodeURIComponent(segments[1]);
  96. if (what == 'hls') {
  97. function hlsHeader(data, hls) {
  98. let hlsHeaders = {};
  99. if (data.headers['content-length']) {
  100. Object.assign(hlsHeaders, data.headers, { 'content-length': hls.length.toString() });
  101. } else {
  102. Object.assign(hlsHeaders, data.headers);
  103. }
  104. delete hlsHeaders['transfer-encoding'];
  105. if (hlsHeaders['content-encoding'] == 'gzip') {
  106. delete hlsHeaders['content-encoding'];
  107. }
  108. return hlsHeaders;
  109. }
  110. const hlsData = await hlsCache(segs, headers);
  111. if (hlsData.variants) {
  112. // variants -> variants -> .... ignore
  113. const hls = HLS.stringify(hlsData.plist);
  114. return {
  115. code: hlsData.code,
  116. content: hls,
  117. headers: hlsHeader(hlsData, hls),
  118. };
  119. } else {
  120. const hls = HLS.stringify(hlsData.plist, (segment) => {
  121. return js2Proxy(false, siteType, siteKey, 'ts/' + encodeURIComponent(hlsData.key + '/' + segment.mediaSequenceNumber.toString()), headers);
  122. });
  123. return {
  124. code: hlsData.code,
  125. content: hls,
  126. headers: hlsHeader(hlsData, hls),
  127. };
  128. }
  129. } else if (what == 'ts') {
  130. const info = segs.split('/');
  131. const hlsKey = info[0];
  132. const segIdx = parseInt(info[1]);
  133. return await tsCache(hlsKey, segIdx, headers);
  134. }
  135. return '{}';
  136. }
  137. async function play(flag, id, flags) {
  138. try {
  139. const pUrls = await hls2Urls(id, {});
  140. for (let index = 1; index < pUrls.length; index += 2) {
  141. pUrls[index] = js2Proxy(false, siteType, siteKey, 'hls/' + encodeURIComponent(pUrls[index]), {});
  142. }
  143. pUrls.push('original');
  144. pUrls.push(id);
  145. return {
  146. parse: 0,
  147. url: pUrls,
  148. };
  149. } catch (e) {
  150. return {
  151. parse: 0,
  152. url: id,
  153. };
  154. }
  155. }
  156. async function search(wd, quick, pg) {
  157. let page = pg || 1;
  158. if (page == 0) page = 1;
  159. const data = await request(url + `?ac=detail&wd=${wd}`);
  160. let videos = [];
  161. for (const vod of data.list) {
  162. videos.push({
  163. vod_id: vod.vod_id.toString(),
  164. vod_name: vod.vod_name.toString(),
  165. vod_pic: vod.vod_pic,
  166. vod_remarks: vod.vod_remarks,
  167. });
  168. }
  169. return {
  170. page: parseInt(data.page),
  171. pagecount: data.pagecount,
  172. total: data.total,
  173. list: videos,
  174. };
  175. }
  176. const cacheRoot = 'hls_cache';
  177. const hlsKeys = [];
  178. const hlsPlistCaches = {};
  179. const interrupts = {};
  180. const downloadTask = {};
  181. let currentDownloadHlsKey = '';
  182. function hlsCacheInsert(key, data) {
  183. hlsKeys.push(key);
  184. hlsPlistCaches[key] = data;
  185. if (hlsKeys.length > 5) {
  186. const rmKey = hlsKeys.shift();
  187. hlsCacheRemove(rmKey);
  188. }
  189. }
  190. function hlsCacheRemove(key) {
  191. delete hlsPlistCaches[key];
  192. delete hlsKeys[key];
  193. new JSFile(cacheRoot + '/' + key).delete();
  194. }
  195. function plistUriResolve(baseUrl, plist) {
  196. if (plist.variants) {
  197. for (const v of plist.variants) {
  198. if (!v.uri.startsWith('http')) {
  199. v.uri = relative2Absolute(baseUrl, v.uri);
  200. }
  201. }
  202. }
  203. if (plist.segments) {
  204. for (const s of plist.segments) {
  205. if (!s.uri.startsWith('http')) {
  206. s.uri = relative2Absolute(baseUrl, s.uri);
  207. }
  208. if (s.key && s.key.uri && !s.key.uri.startsWith('http')) {
  209. s.key.uri = relative2Absolute(baseUrl, s.key.uri);
  210. }
  211. }
  212. }
  213. return plist;
  214. }
  215. async function hls2Urls(url, headers) {
  216. let urls = [];
  217. let resp = {};
  218. let tmpUrl = url;
  219. while (true) {
  220. resp = await req(tmpUrl, {
  221. headers: headers,
  222. redirect: 0,
  223. });
  224. if (resp.headers['location']) {
  225. tmpUrl = resp.headers['location'];
  226. } else {
  227. break;
  228. }
  229. }
  230. if (resp.code == 200) {
  231. var hls = resp.content;
  232. const plist = plistUriResolve(tmpUrl, HLS.parse(hls));
  233. if (plist.variants) {
  234. for (const vari of _.sortBy(plist.variants, (v) => -1 * v.bandwidth)) {
  235. urls.push(`proxy_${vari.resolution.width}x${vari.resolution.height}`);
  236. urls.push(vari.uri);
  237. }
  238. } else {
  239. urls.push('proxy');
  240. urls.push(url);
  241. const hlsKey = md5X(url);
  242. hlsCacheInsert(hlsKey, {
  243. code: resp.code,
  244. plist: plist,
  245. key: hlsKey,
  246. headers: resp.headers,
  247. });
  248. }
  249. }
  250. return urls;
  251. }
  252. async function hlsCache(url, headers) {
  253. const hlsKey = md5X(url);
  254. if (hlsPlistCaches[hlsKey]) {
  255. return hlsPlistCaches[hlsKey];
  256. }
  257. let resp = {};
  258. let tmpUrl = url;
  259. while (true) {
  260. resp = await req(tmpUrl, {
  261. headers: headers,
  262. redirect: 0,
  263. });
  264. if (resp.headers['location']) {
  265. tmpUrl = resp.headers['location'];
  266. } else {
  267. break;
  268. }
  269. }
  270. if (resp.code == 200) {
  271. var hls = resp.content;
  272. const plist = plistUriResolve(tmpUrl, HLS.parse(hls));
  273. hlsCacheInsert(hlsKey, {
  274. code: resp.code,
  275. plist: plist,
  276. key: hlsKey,
  277. headers: resp.headers,
  278. });
  279. return hlsPlistCaches[hlsKey];
  280. }
  281. return {};
  282. }
  283. async function tsCache(hlsKey, segmentIndex, headers) {
  284. if (!hlsPlistCaches[hlsKey]) {
  285. return {};
  286. }
  287. const plist = hlsPlistCaches[hlsKey].plist;
  288. const segments = plist.segments;
  289. let startFirst = !downloadTask[hlsKey];
  290. if (startFirst) {
  291. downloadTask[hlsKey] = {};
  292. for (const seg of segments) {
  293. const tk = md5X(seg.uri + seg.mediaSequenceNumber.toString());
  294. downloadTask[hlsKey][tk] = {
  295. file: cacheRoot + '/' + hlsKey + '/' + tk,
  296. uri: seg.uri,
  297. key: tk,
  298. index: seg.mediaSequenceNumber,
  299. order: seg.mediaSequenceNumber,
  300. state: -1,
  301. read: false,
  302. };
  303. }
  304. }
  305. // sort task
  306. for (const tk in downloadTask[hlsKey]) {
  307. const task = downloadTask[hlsKey][tk];
  308. if (task.index >= segmentIndex) {
  309. task.order = task.index - segmentIndex;
  310. } else {
  311. task.order = segments.length - segmentIndex + task.index;
  312. }
  313. }
  314. if (startFirst) {
  315. fixedCachePool(hlsKey, 5, headers);
  316. }
  317. const segment = segments[segmentIndex];
  318. const tsKey = md5X(segment.uri + segment.mediaSequenceNumber.toString());
  319. const task = downloadTask[hlsKey][tsKey];
  320. if (task.state == 1 || task.state == -1) {
  321. const file = new JSFile(task.file);
  322. if (await file.exist()) {
  323. task.state = 1;
  324. // download finish
  325. return {
  326. buffer: 3,
  327. code: 200,
  328. headers: {
  329. connection: 'close',
  330. 'content-type': 'video/mp2t',
  331. },
  332. content: file,
  333. };
  334. } else {
  335. // file miss?? retry
  336. task.state = -1;
  337. }
  338. }
  339. if (task.state == -1) {
  340. // start download
  341. startTsTask(hlsKey, task, headers);
  342. }
  343. // wait read dwonload
  344. if (task.state == 0) {
  345. var stream = new JSProxyStream();
  346. stream.head(200, {
  347. connection: 'close',
  348. 'content-type': 'video/mp2t',
  349. });
  350. let downloaded = 0;
  351. task.read = true;
  352. new Promise(async function (resolve, reject) {
  353. const f = new JSFile(task.file + '.dl');
  354. await f.open('r');
  355. (async function waitReadFile() {
  356. const s = await f.size();
  357. if (s > downloaded) {
  358. var downloadBuf = await f.read(s - downloaded, downloaded);
  359. await stream.write(downloadBuf);
  360. downloaded = s;
  361. }
  362. if (task.state == 1 || task.state < 0) {
  363. // finish error or done
  364. stream.done();
  365. await f.close();
  366. await f.delete();
  367. task.read = false;
  368. resolve();
  369. return;
  370. }
  371. setTimeout(waitReadFile, 5);
  372. })();
  373. });
  374. return {
  375. buffer: 3,
  376. content: stream,
  377. };
  378. }
  379. }
  380. async function startTsTask(hlsKey, task, headers) {
  381. if (task.state >= 0) return;
  382. if (!interrupts[hlsKey]) {
  383. return;
  384. }
  385. task.state = 0;
  386. if (await new JSFile(task.file).exist()) {
  387. task.state = 1;
  388. return;
  389. }
  390. const file = new JSFile(task.file + '.dl');
  391. await file.open('w');
  392. const resp = await req(task.uri, {
  393. buffer: 3,
  394. headers: headers,
  395. stream: file,
  396. timeout: [5000, 10000],
  397. });
  398. if (resp.error || resp.code >= 300) {
  399. await file.close();
  400. if (!task.read) {
  401. await file.delete();
  402. }
  403. task.state = -1;
  404. return;
  405. }
  406. await file.close();
  407. if (task.read) {
  408. await file.copy(task.file);
  409. } else {
  410. await file.move(task.file);
  411. }
  412. task.state = 1;
  413. }
  414. async function fixedCachePool(hlsKey, limit, headers) {
  415. // keep last cache task only
  416. if (currentDownloadHlsKey && currentDownloadHlsKey != hlsKey) {
  417. delete interrupts[currentDownloadHlsKey];
  418. }
  419. currentDownloadHlsKey = hlsKey;
  420. interrupts[hlsKey] = true;
  421. for (let index = 0; index < limit; index++) {
  422. if (!interrupts[hlsKey]) break;
  423. new Promise(function (resolve, reject) {
  424. (async function doTask() {
  425. if (!interrupts[hlsKey]) {
  426. resolve();
  427. return;
  428. }
  429. const tasks = _.pickBy(downloadTask[hlsKey], function (o) {
  430. return o.state == -1;
  431. });
  432. const task = _.minBy(Object.values(tasks), function (o) {
  433. return o.order;
  434. });
  435. if (!task) {
  436. resolve();
  437. return;
  438. }
  439. await startTsTask(hlsKey, task, headers);
  440. setTimeout(doTask, 5);
  441. })();
  442. });
  443. }
  444. }
  445. function relative2Absolute(base, relative) {
  446. var stack = base.split('/'),
  447. parts = relative.split('/');
  448. stack.pop();
  449. for (var i = 0; i < parts.length; i++) {
  450. if (parts[i] == '.') continue;
  451. if (parts[i] == '..') stack.pop();
  452. else stack.push(parts[i]);
  453. }
  454. return stack.join('/');
  455. }
  456. export {hls2Urls,hlsCache,tsCache}