@lzwme/m3u8-dl 1.5.0 → 1.6.0-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client/play.html CHANGED
@@ -2,14 +2,21 @@
2
2
  <html lang="zh">
3
3
 
4
4
  <head>
5
- <title>M3U8 在线播放</title>
5
+ <title>M3U8 Player</title>
6
6
  <meta charset="utf-8">
7
7
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
8
8
  <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0,viewport-fit=cover"
9
9
  name="viewport" />
10
+ <link rel="shortcut icon" href="logo.png">
10
11
  <style>
11
12
  body {
12
13
  margin: 0;
14
+ padding: 0;
15
+ }
16
+
17
+ #dplayer {
18
+ width: 100vw;
19
+ height: 100vh;
13
20
  }
14
21
  </style>
15
22
  </head>
@@ -19,25 +26,225 @@
19
26
  <script src="https://s4.zstatic.net/ajax/libs/hls.js/1.5.18/hls.min.js"
20
27
  integrity="sha512-hARxLWym80kd0Bzl5/93OuW1ujaKfvmJ90yTKak/RB67JuNIjtErU2H7H3bteyfzMuqiSK0tXarT7eK6lEWBBA=="
21
28
  crossorigin="anonymous" referrerpolicy="no-referrer"></script>
22
- <script src="https://s4.zstatic.net/ajax/libs/dplayer/1.26.0/DPlayer.min.js" crossorigin="anonymous"
23
- referrerpolicy="no-referrer"></script>
24
29
 
25
30
  <script>
26
- const url = location.href.split('url=')[1];
27
- if (!url) {
31
+ const playUrl = location.href.split('url=')[1];
32
+ if (!playUrl) {
28
33
  document.getElementById('dplayer').innerText = '请传入播放地址参数 url=';
29
34
  } else {
30
- const dp = new DPlayer({
31
- container: document.getElementById('dplayer'),
32
- autoplay: true,
33
- video: {
34
- url: decodeURIComponent(url),
35
- type: 'auto',
35
+ const CDN_CONFIG = {
36
+ // m3u8Demo: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8',
37
+ dplayer: 'https://s4.zstatic.net/ajax/libs/dplayer/1.26.0/DPlayer.min.js',
38
+ artplayer: [
39
+ 'https://s4.zstatic.net/ajax/libs/artplayer/5.3.0/artplayer.min.js',
40
+ // 'https://fastly.jsdelivr.net/npm/artplayer-plugin-hls-control/dist/artplayer-plugin-hls-control.min.js',
41
+ ],
42
+ }
43
+ const T = {
44
+ data: {
45
+ playType: playUrl.includes('dplayer') ? 'dplayer' : 'artplayer',
46
+ videoUrl: decodeURIComponent(playUrl),
47
+ dpInstance: null,
48
+ artInstance: null,
49
+ },
50
+ play(videoUrl, playType) {
51
+ if (videoUrl && videoUrl !== T.data.videoUrl) T.data.videoUrl = videoUrl;
52
+ if (playType && playType !== T.data.playType) T.data.playType = playType;
53
+ if (!['dplayer', 'artplayer'].includes(T.data.playType)) T.data.playType = 'artplayer';
54
+
55
+ if (T.data.playType === 'dplayer') {
56
+ return T.dplayer(T.data.videoUrl);
57
+ } else {
58
+ return T.artplayer(T.data.videoUrl);
59
+ }
36
60
  },
37
- pluginOptions: {
38
- hls: {},
61
+ async loadJS(url) {
62
+ if (Array.isArray(url)) {
63
+ for (const u of url) {
64
+ await T.loadJS(u);
65
+ }
66
+ return;
67
+ }
68
+ if (document.querySelector(`script[src="${url}"]`)) {
69
+ return;
70
+ }
71
+
72
+ return new Promise((resolve, reject) => {
73
+ const script = document.createElement('script');
74
+ script.src = url;
75
+ script.onload = resolve;
76
+ script.onerror = reject;
77
+ document.body.appendChild(script);
78
+ });
79
+ },
80
+ sleep(ms) {
81
+ return new Promise(resolve => setTimeout(resolve, ms));
82
+ },
83
+ async dplayer(videoUrl) {
84
+ if (T.data.dpInstance) {
85
+ T.data.dpInstance.destroy();
86
+ T.data.dpInstance = null;
87
+ await T.sleep(100);
88
+ } else {
89
+ await T.loadJS(CDN_CONFIG.dplayer);
90
+ }
91
+
92
+ const dp = new DPlayer({
93
+ container: document.getElementById('dplayer'),
94
+ autoplay: true,
95
+ airplay: true,
96
+ theme: '#FADFA3',
97
+ loop: true,
98
+ screenshot: true,
99
+ hotkey: true,
100
+ chromecast: true,
101
+ preload: 'auto',
102
+ playbackSpeed: [0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4, 8],
103
+ video: {
104
+ url: videoUrl,
105
+ type: 'auto',
106
+ },
107
+ pluginOptions: {
108
+ hls: {},
109
+ },
110
+ contextmenu: [
111
+ { text: '在线播放器', link: 'https://m3u8-player.lzw.me' },
112
+ { text: '在线下载器', link: 'https://m3u8-downloader.lzw.me' },
113
+ ],
114
+ });
115
+ dp.on('ended', () => {
116
+ console.log('[dplayer]播放完毕', videoUrl);
117
+ T.next_video();
118
+ });
119
+ T.data.dpInstance = dp;
120
+ return dp;
39
121
  },
40
- });
122
+ /** 使用 artplayer 播放 */
123
+ async artplayer(videoUrl) {
124
+ if (T.data.artInstance) {
125
+ T.data.artInstance.destroy();
126
+ T.data.artInstance = null;
127
+ await T.sleep(100);
128
+ } else {
129
+ await T.loadJS(CDN_CONFIG.artplayer);
130
+ }
131
+
132
+ const art = new Artplayer({
133
+ container: document.getElementById('dplayer'),
134
+ url: videoUrl, // 'https://playertest.longtailvideo.com/adaptive/elephants_dream_v4/index.m3u8',
135
+ // airplay: true,
136
+ aspectRatio: true, // 是否显示视频长宽比功能
137
+ autoplay: true,
138
+ autoOrientation: true,
139
+ // autoMini: true, // 当播放器滚动到浏览器视口以外时,自动进入 迷你播放 模式
140
+ autoPlayback: true,
141
+ // autoSize: true, // 自动调整播放器尺寸
142
+ fastForward: true,
143
+ flip: true, // 是否显示视频翻转功能
144
+ fullscreen: true, // 是否在底部控制栏里显示播放器 窗口全屏 按钮
145
+ fullscreenWeb: true, // 是否在底部控制栏里显示播放器 网页全屏 按钮
146
+ lock: true,
147
+ miniProgressBar: true,
148
+ pip: true, // 是否在底部控制栏里显示 画中画 的开关按钮
149
+ playbackRate: true, // 是否显示视频播放速度功能
150
+ screenshot: true,
151
+ setting: true,
152
+ theme: '#39f',
153
+ type: videoUrl.includes('mp4') ? 'mp4' : 'm3u8',
154
+ customType: {
155
+ m3u8: function playM3u8(video, url, art) {
156
+ if (Hls.isSupported()) {
157
+ if (art.hls) art.hls.destroy();
158
+ const hls = new Hls();
159
+ hls.loadSource(url);
160
+ hls.attachMedia(video);
161
+ art.hls = hls;
162
+ art.on('destroy', () => hls.destroy());
163
+ } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
164
+ video.src = url;
165
+ } else {
166
+ art.notice.show = 'Unsupported playback format: m3u8';
167
+ }
168
+ },
169
+ flv: function playFlv(video, url, art) {
170
+ if (flvjs.isSupported()) {
171
+ if (art.flv) art.flv.destroy();
172
+ const flv = flvjs.createPlayer({ type: 'flv', url });
173
+ flv.attachMediaElement(video);
174
+ flv.load();
175
+ art.flv = flv;
176
+ art.on('destroy', () => flv.destroy());
177
+ } else {
178
+ art.notice.show = 'Unsupported playback format: flv';
179
+ }
180
+ },
181
+ },
182
+ controls: [{
183
+ name: "previous-button",
184
+ index: 10,
185
+ position: "left",
186
+ html: '<svg fill="none" stroke-width="2" xmlns="http://www.w3.org/2000/svg" height="22" width="22" class="icon icon-tabler icon-tabler-player-track-prev-filled" width="1em" height="1em" viewBox="0 0 24 24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" style="overflow: visible; color: currentcolor;"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M20.341 4.247l-8 7a1 1 0 0 0 0 1.506l8 7c.647 .565 1.659 .106 1.659 -.753v-14c0 -.86 -1.012 -1.318 -1.659 -.753z" stroke-width="0" fill="currentColor"></path><path d="M9.341 4.247l-8 7a1 1 0 0 0 0 1.506l8 7c.647 .565 1.659 .106 1.659 -.753v-14c0 -.86 -1.012 -1.318 -1.659 -.753z" stroke-width="0" fill="currentColor"></path></svg>',
187
+ tooltip: "Previous",
188
+ click: function () {
189
+ T.previous_video()
190
+ },
191
+ },
192
+ {
193
+ name: "next-button",
194
+ index: 11,
195
+ position: "left",
196
+ html: '<svg fill="none" stroke-width="2" xmlns="http://www.w3.org/2000/svg" height="22" width="22" class="icon icon-tabler icon-tabler-player-track-next-filled" width="1em" height="1em" viewBox="0 0 24 24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" style="overflow: visible; color: currentcolor;"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M2 5v14c0 .86 1.012 1.318 1.659 .753l8 -7a1 1 0 0 0 0 -1.506l-8 -7c-.647 -.565 -1.659 -.106 -1.659 .753z" stroke-width="0" fill="currentColor"></path><path d="M13 5v14c0 .86 1.012 1.318 1.659 .753l8 -7a1 1 0 0 0 0 -1.506l-8 -7c-.647 -.565 -1.659 -.106 -1.659 .753z" stroke-width="0" fill="currentColor"></path></svg>',
197
+ tooltip: "Next",
198
+ click: function () {
199
+ T.next_video()
200
+ },
201
+ }],
202
+ contextmenu: [
203
+ { html: '在线播放器', click: () => window.open('https://m3u8-player.lzw.me', '_blank') },
204
+ { html: '在线下载器', click: () => window.open('https://m3u8-downloader.lzw.me', '_blank') },
205
+ ],
206
+ });
207
+
208
+ art.on('video:ended', () => {
209
+ console.log('[artplayer]播放完毕', videoUrl);
210
+ T.next_video();
211
+ });
212
+ // console.log('[artplayer]播放器初始化完成', art);
213
+ T.data.artInstance = art;
214
+ return art;
215
+ },
216
+ previous_video() {
217
+ T.postMessage({ action: 'previous', url: T.data.videoUrl, playType: T.data.playType });
218
+ },
219
+ next_video() {
220
+ T.postMessage({ action: 'next', url: T.data.videoUrl, playType: T.data.playType });
221
+ },
222
+ postMessage(message) {
223
+ if (window.sef === window.parent) {
224
+ return;
225
+ } else {
226
+ window.parent.postMessage(message, '*');
227
+ }
228
+ },
229
+ init() {
230
+ if (window.sef !== window.parent) {
231
+ // 监听来自父窗口的消息
232
+ window.addEventListener('message', (event) => {
233
+ const data = event.data;
234
+ if (!data) return;
235
+
236
+ if (typeof data === 'object' && data.url) {
237
+ T.data.videoUrl = data.url;
238
+ if (data.playType) T.data.playType = data.playType;
239
+ T.play();
240
+ }
241
+ });
242
+ }
243
+ return T.play();
244
+ }
245
+ };
246
+ T.init();
247
+ window.T = T;
41
248
  }
42
249
  </script>
43
250
  </body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lzwme/m3u8-dl",
3
- "version": "1.5.0",
3
+ "version": "1.6.0-0",
4
4
  "description": "Batch download of m3u8 files and convert to mp4",
5
5
  "main": "cjs/index.js",
6
6
  "types": "cjs/index.d.ts",
@@ -16,14 +16,18 @@
16
16
  "dev": "npm run watch",
17
17
  "watch": "npm run build -- -- -w",
18
18
  "lint:flh": "flh --eslint --tscheck --prettier",
19
- "lint": "biome lint src",
20
- "build": "npm run clean && npm run build:cjs",
19
+ "lint": "biome lint",
20
+ "format": "biome format --write",
21
+ "fix": "biome check --fix",
22
+ "build": "npm run clean && npm run build:cjs && npm run build:frontend",
21
23
  "build:cjs": "tsc -p tsconfig.cjs.json",
24
+ "build:frontend": "pnpm -F @lzwme/m3u8-dl-frontend build",
25
+ "download-cdn": "node scripts/download-cdn-resources.js",
22
26
  "doc": "typedoc src/ --exclude **/*.spec.ts --out docs --tsconfig tsconfig.module.json",
23
27
  "version": "standard-version",
24
28
  "dist": "npm run build",
25
29
  "release": "npm run dist && npm run version",
26
- "clean": "flh rm -f ./cjs ./esm ./docs",
30
+ "clean": "flh rm -f ./cjs ./esm ./docs ./client/assets",
27
31
  "test": "npm run lint"
28
32
  },
29
33
  "bin": {
@@ -44,32 +48,31 @@
44
48
  "registry": "https://registry.npmjs.com"
45
49
  },
46
50
  "devDependencies": {
47
- "@biomejs/biome": "^2.2.2",
48
- "@eslint/js": "^9.34.0",
51
+ "@biomejs/biome": "^2.3.4",
52
+ "@eslint/js": "^9.39.1",
49
53
  "@lzwme/fed-lint-helper": "^2.6.6",
50
- "@types/express": "^5.0.3",
51
- "@types/m3u8-parser": "^7.2.3",
52
- "@types/node": "^24.3.0",
54
+ "@types/express": "^5.0.5",
55
+ "@types/m3u8-parser": "^7.2.5",
56
+ "@types/node": "^24.10.0",
53
57
  "@types/ws": "^8.18.1",
54
- "@typescript-eslint/eslint-plugin": "^8.41.0",
55
- "@typescript-eslint/parser": "^8.41.0",
56
- "eslint": "^9.34.0",
58
+ "@typescript-eslint/eslint-plugin": "^8.46.3",
59
+ "@typescript-eslint/parser": "^8.46.3",
60
+ "eslint": "^9.39.1",
57
61
  "eslint-config-prettier": "^10.1.8",
58
62
  "eslint-plugin-prettier": "^5.5.4",
59
63
  "express": "^5.1.0",
60
64
  "husky": "^9.1.7",
61
65
  "prettier": "^3.6.2",
62
66
  "standard-version": "^9.5.0",
63
- "typescript": "^5.9.2",
64
- "typescript-eslint": "^8.41.0",
67
+ "typescript": "^5.9.3",
68
+ "typescript-eslint": "^8.46.3",
65
69
  "ws": "^8.18.3"
66
70
  },
67
71
  "dependencies": {
68
- "@lzwme/fe-utils": "^1.9.0",
69
- "commander": "^14.0.0",
72
+ "@lzwme/fe-utils": "^1.9.2",
73
+ "commander": "^14.0.2",
70
74
  "console-log-colors": "^0.5.0",
71
75
  "enquirer": "^2.4.1",
72
- "ffmpeg-static": "^5.2.0",
73
76
  "m3u8-parser": "^7.2.0"
74
77
  },
75
78
  "files": [
package/client/style.css DELETED
@@ -1,137 +0,0 @@
1
- ::placeholder {
2
- font-size: 14px;
3
- }
4
-
5
- .swal2-html-container textarea, input {
6
- font-size: 16px !important;
7
- }
8
-
9
- #app {
10
- max-width: 1400px;
11
- margin: auto;
12
- min-height: 100vh;
13
- background-color: #f5f5f5;
14
- position: relative;
15
- }
16
-
17
- .sidebar {
18
- background-color: #fff;
19
- box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
20
- transition: all 0.3s ease;
21
- position: absolute;
22
- left: 0;
23
- top: 0;
24
- bottom: 0;
25
- width: 16rem;
26
- z-index: 1;
27
- }
28
-
29
- .download-item {
30
- transition: all 0.3s ease;
31
- }
32
-
33
- .download-item:hover {
34
- background-color: #f8f9fa;
35
- }
36
-
37
- .progress-bar {
38
- height: 4px;
39
- transition: width 0.3s ease;
40
- }
41
-
42
- .nav-item {
43
- transition: all 0.3s ease;
44
- }
45
-
46
- .nav-item:hover {
47
- background-color: #f0f0f0;
48
- transform: translateX(4px);
49
- }
50
-
51
- .nav-item.active {
52
- background-color: #e6f3ff;
53
- color: #1890ff;
54
- }
55
-
56
- /* 移动端适配 */
57
- @media (max-width: 768px) {
58
- .sidebar {
59
- transform: translateX(-100%);
60
- }
61
-
62
- .sidebar.show {
63
- transform: translateX(0);
64
- }
65
-
66
- .menu-toggle {
67
- display: block !important;
68
- }
69
-
70
- .main-content {
71
- margin-left: 0 !important;
72
- }
73
- }
74
-
75
- .menu-toggle {
76
- display: none;
77
- position: fixed;
78
- top: 1rem;
79
- left: 1rem;
80
- z-index: 51;
81
- padding: 0.5rem;
82
- background: white;
83
- border-radius: 0.5rem;
84
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
85
- box-shadow: 0 2px 4px #9bdff5;
86
- width: 42px;
87
- height: 42px;
88
- }
89
-
90
- .main-content {
91
- transition: margin-left 0.3s ease;
92
- margin-left: 16rem;
93
- width: calc(100% - 16rem);
94
- }
95
-
96
- /* 自定义toast样式 */
97
- .custom-toast {
98
- position: fixed;
99
- top: 20px;
100
- right: 20px;
101
- min-width: 250px;
102
- max-width: 400px;
103
- padding: 15px 25px 15px 15px;
104
- border-radius: 4px;
105
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
106
- background: #fff;
107
- color: #333;
108
- z-index: 99999;
109
- display: flex;
110
- align-items: center;
111
- transform: translateX(150%);
112
- transition: transform 0.3s ease;
113
- }
114
-
115
- .custom-toast.show {
116
- transform: translateX(0);
117
- }
118
-
119
- .custom-toast.hide {
120
- transform: translateX(150%);
121
- }
122
-
123
- .custom-toast-success {
124
- border-left: 4px solid #28a745;
125
- }
126
-
127
- .custom-toast-error {
128
- border-left: 4px solid #dc3545;
129
- }
130
-
131
- .custom-toast-warning {
132
- border-left: 4px solid #ffc107;
133
- }
134
-
135
- .custom-toast-info {
136
- border-left: 4px solid #17a2b8;
137
- }