diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..12cfd80c --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +.node_modules/ +**/node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# build output +build/ +dist/ +**/build/ +**/dist/ + +# env +.env +.env.* +!.env.example + +# OS/IDE +.DS_Store +Thumbs.db +.idea/ +.vscode/ diff --git a/live-streaming/client/node_modules/.cache/.eslintcache b/live-streaming/client/node_modules/.cache/.eslintcache index afea5c48..cacc077e 100644 --- a/live-streaming/client/node_modules/.cache/.eslintcache +++ b/live-streaming/client/node_modules/.cache/.eslintcache @@ -1 +1 @@ -[{"C:\\Users\\Administrator\\Desktop\\Project\\Test\\live-streaming\\client\\src\\index.js":"1","C:\\Users\\Administrator\\Desktop\\Project\\Test\\live-streaming\\client\\src\\App.jsx":"2","C:\\Users\\Administrator\\Desktop\\Project\\Test\\live-streaming\\client\\src\\pages\\RoomPage.jsx":"3","C:\\Users\\Administrator\\Desktop\\Project\\Test\\live-streaming\\client\\src\\pages\\HomePage.jsx":"4","C:\\Users\\Administrator\\Desktop\\Project\\Test\\live-streaming\\client\\src\\services\\api.js":"5","C:\\Users\\Administrator\\Desktop\\Project\\Test\\live-streaming\\client\\src\\components\\RoomList.jsx":"6","C:\\Users\\Administrator\\Desktop\\Project\\Test\\live-streaming\\client\\src\\components\\CreateRoom.jsx":"7","C:\\Users\\Administrator\\Desktop\\Project\\Test\\live-streaming\\client\\src\\components\\LivePlayer.jsx":"8","C:\\Users\\Administrator\\Desktop\\Project\\Test\\live-streaming\\client\\src\\components\\StreamInfo.jsx":"9","C:\\Users\\Administrator\\Desktop\\Project\\Test\\live-streaming\\client\\src\\components\\RoomCard.jsx":"10"},{"size":310,"mtime":1765701583991,"results":"11","hashOfConfig":"12"},{"size":334,"mtime":1765701610578,"results":"13","hashOfConfig":"12"},{"size":2127,"mtime":1765701673204,"results":"14","hashOfConfig":"12"},{"size":1724,"mtime":1765701625879,"results":"15","hashOfConfig":"12"},{"size":533,"mtime":1765701603938,"results":"16","hashOfConfig":"12"},{"size":476,"mtime":1765701614401,"results":"17","hashOfConfig":"12"},{"size":1607,"mtime":1765701629855,"results":"18","hashOfConfig":"12"},{"size":3669,"mtime":1765701660353,"results":"19","hashOfConfig":"12"},{"size":1389,"mtime":1765701642070,"results":"20","hashOfConfig":"12"},{"size":619,"mtime":1765701612898,"results":"21","hashOfConfig":"12"},{"filePath":"22","messages":"23","suppressedMessages":"24","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"j0zk4s",{"filePath":"25","messages":"26","suppressedMessages":"27","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"28","messages":"29","suppressedMessages":"30","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"31","messages":"32","suppressedMessages":"33","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"34","messages":"35","suppressedMessages":"36","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"37","messages":"38","suppressedMessages":"39","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"40","messages":"41","suppressedMessages":"42","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"43","messages":"44","suppressedMessages":"45","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"46","messages":"47","suppressedMessages":"48","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"49","messages":"50","suppressedMessages":"51","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"C:\\Users\\Administrator\\Desktop\\Project\\Test\\live-streaming\\client\\src\\index.js",[],[],"C:\\Users\\Administrator\\Desktop\\Project\\Test\\live-streaming\\client\\src\\App.jsx",[],[],"C:\\Users\\Administrator\\Desktop\\Project\\Test\\live-streaming\\client\\src\\pages\\RoomPage.jsx",[],[],"C:\\Users\\Administrator\\Desktop\\Project\\Test\\live-streaming\\client\\src\\pages\\HomePage.jsx",[],[],"C:\\Users\\Administrator\\Desktop\\Project\\Test\\live-streaming\\client\\src\\services\\api.js",[],[],"C:\\Users\\Administrator\\Desktop\\Project\\Test\\live-streaming\\client\\src\\components\\RoomList.jsx",[],[],"C:\\Users\\Administrator\\Desktop\\Project\\Test\\live-streaming\\client\\src\\components\\CreateRoom.jsx",[],[],"C:\\Users\\Administrator\\Desktop\\Project\\Test\\live-streaming\\client\\src\\components\\LivePlayer.jsx",[],[],"C:\\Users\\Administrator\\Desktop\\Project\\Test\\live-streaming\\client\\src\\components\\StreamInfo.jsx",[],[],"C:\\Users\\Administrator\\Desktop\\Project\\Test\\live-streaming\\client\\src\\components\\RoomCard.jsx",[],[]] \ No newline at end of file +[{"C:\\Users\\Administrator\\Desktop\\zhibo_1\\live-streaming\\client\\src\\index.js":"1","C:\\Users\\Administrator\\Desktop\\zhibo_1\\live-streaming\\client\\src\\App.jsx":"2","C:\\Users\\Administrator\\Desktop\\zhibo_1\\live-streaming\\client\\src\\pages\\HomePage.jsx":"3","C:\\Users\\Administrator\\Desktop\\zhibo_1\\live-streaming\\client\\src\\pages\\RoomPage.jsx":"4","C:\\Users\\Administrator\\Desktop\\zhibo_1\\live-streaming\\client\\src\\services\\api.js":"5","C:\\Users\\Administrator\\Desktop\\zhibo_1\\live-streaming\\client\\src\\components\\CreateRoom.jsx":"6","C:\\Users\\Administrator\\Desktop\\zhibo_1\\live-streaming\\client\\src\\components\\LivePlayer.jsx":"7","C:\\Users\\Administrator\\Desktop\\zhibo_1\\live-streaming\\client\\src\\components\\RoomList.jsx":"8","C:\\Users\\Administrator\\Desktop\\zhibo_1\\live-streaming\\client\\src\\components\\StreamInfo.jsx":"9","C:\\Users\\Administrator\\Desktop\\zhibo_1\\live-streaming\\client\\src\\components\\RoomCard.jsx":"10"},{"size":310,"mtime":1765770657007,"results":"11","hashOfConfig":"12"},{"size":334,"mtime":1765770657002,"results":"13","hashOfConfig":"12"},{"size":1724,"mtime":1765770657008,"results":"14","hashOfConfig":"12"},{"size":2127,"mtime":1765770657008,"results":"15","hashOfConfig":"12"},{"size":533,"mtime":1765770657009,"results":"16","hashOfConfig":"12"},{"size":1607,"mtime":1765770657003,"results":"17","hashOfConfig":"12"},{"size":4682,"mtime":1765786910702,"results":"18","hashOfConfig":"12"},{"size":476,"mtime":1765770657005,"results":"19","hashOfConfig":"12"},{"size":1389,"mtime":1765770657005,"results":"20","hashOfConfig":"12"},{"size":619,"mtime":1765770657004,"results":"21","hashOfConfig":"12"},{"filePath":"22","messages":"23","suppressedMessages":"24","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"1w1jria",{"filePath":"25","messages":"26","suppressedMessages":"27","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"28","messages":"29","suppressedMessages":"30","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"31","messages":"32","suppressedMessages":"33","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"34","messages":"35","suppressedMessages":"36","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"37","messages":"38","suppressedMessages":"39","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"40","messages":"41","suppressedMessages":"42","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"43","messages":"44","suppressedMessages":"45","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"46","messages":"47","suppressedMessages":"48","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"49","messages":"50","suppressedMessages":"51","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"C:\\Users\\Administrator\\Desktop\\zhibo_1\\live-streaming\\client\\src\\index.js",[],[],"C:\\Users\\Administrator\\Desktop\\zhibo_1\\live-streaming\\client\\src\\App.jsx",[],[],"C:\\Users\\Administrator\\Desktop\\zhibo_1\\live-streaming\\client\\src\\pages\\HomePage.jsx",[],[],"C:\\Users\\Administrator\\Desktop\\zhibo_1\\live-streaming\\client\\src\\pages\\RoomPage.jsx",[],[],"C:\\Users\\Administrator\\Desktop\\zhibo_1\\live-streaming\\client\\src\\services\\api.js",[],[],"C:\\Users\\Administrator\\Desktop\\zhibo_1\\live-streaming\\client\\src\\components\\CreateRoom.jsx",[],[],"C:\\Users\\Administrator\\Desktop\\zhibo_1\\live-streaming\\client\\src\\components\\LivePlayer.jsx",[],[],"C:\\Users\\Administrator\\Desktop\\zhibo_1\\live-streaming\\client\\src\\components\\RoomList.jsx",[],[],"C:\\Users\\Administrator\\Desktop\\zhibo_1\\live-streaming\\client\\src\\components\\StreamInfo.jsx",[],[],"C:\\Users\\Administrator\\Desktop\\zhibo_1\\live-streaming\\client\\src\\components\\RoomCard.jsx",[],[]] \ No newline at end of file diff --git a/live-streaming/client/node_modules/.cache/default-development/0.pack b/live-streaming/client/node_modules/.cache/default-development/0.pack index 9a65f85a..c5dec054 100644 Binary files a/live-streaming/client/node_modules/.cache/default-development/0.pack and b/live-streaming/client/node_modules/.cache/default-development/0.pack differ diff --git a/live-streaming/client/node_modules/.cache/default-development/index.pack b/live-streaming/client/node_modules/.cache/default-development/index.pack index d28ecb32..e9dc1c36 100644 Binary files a/live-streaming/client/node_modules/.cache/default-development/index.pack and b/live-streaming/client/node_modules/.cache/default-development/index.pack differ diff --git a/live-streaming/client/src/components/LivePlayer.jsx b/live-streaming/client/src/components/LivePlayer.jsx index e1556270..14db7978 100644 --- a/live-streaming/client/src/components/LivePlayer.jsx +++ b/live-streaming/client/src/components/LivePlayer.jsx @@ -5,26 +5,38 @@ import Hls from 'hls.js'; function LivePlayer({ room }) { const videoRef = useRef(null); const playerRef = useRef(null); + const triedAltFlvRef = useRef(false); const [error, setError] = useState(null); const [playerType, setPlayerType] = useState(null); + const flvUrl = room?.streamUrls?.flv; + const hlsUrl = room?.streamUrls?.hls; + useEffect(() => { - if (!room?.isLive || !room?.streamUrls) return; + if (!room?.isLive || !flvUrl) return; + + triedAltFlvRef.current = false; const video = videoRef.current; - const { flv: flvUrl, hls: hlsUrl } = room.streamUrls; - // 优先使用 FLV (低延迟) - if (flvjs.isSupported()) { - console.log('使用 FLV 播放器'); - setPlayerType('FLV'); - + const getAltFlvUrl = (url) => { + try { + const u = new URL(url); + if (u.pathname.startsWith('/__defaultVhost__/')) return null; + u.pathname = `/__defaultVhost__${u.pathname}`; + return u.toString(); + } catch { + return null; + } + }; + + const createFlvPlayer = (url) => { const player = flvjs.createPlayer({ type: 'flv', - url: flvUrl, + url, isLive: true }, { - enableWorker: true, + enableWorker: false, enableStashBuffer: false, stashInitialSize: 128 }); @@ -32,9 +44,36 @@ function LivePlayer({ room }) { player.attachMediaElement(video); player.load(); player.play().catch(console.error); + return player; + }; + + // 优先使用 FLV (低延迟) + if (flvjs.isSupported()) { + console.log('使用 FLV 播放器'); + setPlayerType('FLV'); + + const altFlvUrl = getAltFlvUrl(flvUrl); + let player = createFlvPlayer(flvUrl); player.on(flvjs.Events.ERROR, (type, detail) => { console.error('FLV 错误:', type, detail); + + if (!triedAltFlvRef.current && altFlvUrl) { + triedAltFlvRef.current = true; + setError('播放出错,正在切换线路...'); + + try { + player.destroy(); + } catch (e) { + console.error(e); + } + + player = createFlvPlayer(altFlvUrl); + playerRef.current = player; + setError(null); + return; + } + setError('播放出错,正在重试...'); // 3秒后重试 setTimeout(() => { @@ -48,7 +87,7 @@ function LivePlayer({ room }) { playerRef.current = player; } // 降级到 HLS - else if (Hls.isSupported()) { + else if (hlsUrl && Hls.isSupported()) { console.log('使用 HLS 播放器'); setPlayerType('HLS'); @@ -73,7 +112,7 @@ function LivePlayer({ room }) { playerRef.current = hls; } // 原生 HLS (Safari) - else if (video.canPlayType('application/vnd.apple.mpegurl')) { + else if (hlsUrl && video.canPlayType('application/vnd.apple.mpegurl')) { console.log('使用原生 HLS'); setPlayerType('Native HLS'); video.src = hlsUrl; @@ -91,7 +130,7 @@ function LivePlayer({ room }) { playerRef.current = null; } }; - }, [room]); + }, [room?.isLive, flvUrl, hlsUrl]); if (!room?.isLive) { return ( diff --git a/live-streaming/docker/srs/srs.conf b/live-streaming/docker/srs/srs.conf index 45ad558a..e68e12be 100644 --- a/live-streaming/docker/srs/srs.conf +++ b/live-streaming/docker/srs/srs.conf @@ -30,14 +30,14 @@ vhost __defaultVhost__ { # HTTP-FLV 配置 (低延迟) http_remux { enabled on; - mount [vhost]/[app]/[stream].flv; + mount [app]/[stream].flv; } # HTTP 回调配置 http_hooks { enabled on; - on_publish http://api-server:3001/api/srs/on_publish; - on_unpublish http://api-server:3001/api/srs/on_unpublish; + on_publish http://localhost:3001/api/srs/on_publish; + on_unpublish http://localhost:3001/api/srs/on_unpublish; } # GOP 缓存,提高首屏速度 diff --git a/live-streaming/node_modules/.package-lock.json b/live-streaming/node_modules/.package-lock.json index 81e284a2..b590443d 100644 --- a/live-streaming/node_modules/.package-lock.json +++ b/live-streaming/node_modules/.package-lock.json @@ -35,7 +35,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1129,7 +1128,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -1318,6 +1316,14 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/basic-auth-connect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/basic-auth-connect/-/basic-auth-connect-1.1.0.tgz", + "integrity": "sha512-rKcWjfiRZ3p5WS9e5q6msXa07s6DaFAMXoyowV+mb2xQG+oYdw2QEUyKi0Xp95JvXzShlM+oGy5QuqSK6TfC1Q==", + "dependencies": { + "tsscmp": "^1.0.6" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1399,7 +1405,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1514,7 +1519,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -1622,7 +1626,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -1635,7 +1638,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -1768,6 +1770,14 @@ "node": ">= 8" } }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -2281,22 +2291,6 @@ "dev": true, "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "ideallyInert": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -2444,7 +2438,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2517,6 +2510,14 @@ "url": "https://opencollective.com/express" } }, + "node_modules/http2-express": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/http2-express/-/http2-express-1.1.0.tgz", + "integrity": "sha512-rEKh5J8KBh76SZ9ejs4q2SQV5X5DA7pMamuGod2ifPLCRcgm8Ru1awbyPPr56JzzMpUY+MKc2NlUxsGpaaqO4Q==", + "engines": { + "node": ">= 20.0.0" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -3511,6 +3512,11 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3673,6 +3679,28 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", + "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -3702,6 +3730,28 @@ "dev": true, "license": "MIT" }, + "node_modules/node-media-server": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/node-media-server/-/node-media-server-2.7.4.tgz", + "integrity": "sha512-4nsqfukfnI6tY7d1deqBQSj3KywOiNVlPQkY0tLIjF62rzXo1SDXJXgUU+cVs5e06uO4x/55O4SiAaRH3li/Vg==", + "dependencies": { + "basic-auth-connect": "^1.1.0", + "chalk": "^4.1.2", + "dateformat": "^4.6.3", + "express": "^4.21.1", + "http2-express": "^1.0.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "mkdirp": "^2.1.6", + "ws": "^8.18.0" + }, + "bin": { + "node-media-server": "bin/app.js" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -4791,7 +4841,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -4867,6 +4916,14 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "engines": { + "node": ">=0.6.x" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -5068,6 +5125,26 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/live-streaming/package-lock.json b/live-streaming/package-lock.json index 3c507c26..491a0fbe 100644 --- a/live-streaming/package-lock.json +++ b/live-streaming/package-lock.json @@ -11,6 +11,7 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "node-media-server": "^2.7.0", "uuid": "^9.0.1" }, "devDependencies": { @@ -51,7 +52,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1145,7 +1145,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -1334,6 +1333,14 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/basic-auth-connect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/basic-auth-connect/-/basic-auth-connect-1.1.0.tgz", + "integrity": "sha512-rKcWjfiRZ3p5WS9e5q6msXa07s6DaFAMXoyowV+mb2xQG+oYdw2QEUyKi0Xp95JvXzShlM+oGy5QuqSK6TfC1Q==", + "dependencies": { + "tsscmp": "^1.0.6" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1415,7 +1422,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1530,7 +1536,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -1638,7 +1643,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -1651,7 +1655,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -1784,6 +1787,14 @@ "node": ">= 8" } }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -2459,7 +2470,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2532,6 +2542,14 @@ "url": "https://opencollective.com/express" } }, + "node_modules/http2-express": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/http2-express/-/http2-express-1.1.0.tgz", + "integrity": "sha512-rEKh5J8KBh76SZ9ejs4q2SQV5X5DA7pMamuGod2ifPLCRcgm8Ru1awbyPPr56JzzMpUY+MKc2NlUxsGpaaqO4Q==", + "engines": { + "node": ">= 20.0.0" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -3526,6 +3544,11 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3688,6 +3711,28 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", + "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -3717,6 +3762,28 @@ "dev": true, "license": "MIT" }, + "node_modules/node-media-server": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/node-media-server/-/node-media-server-2.7.4.tgz", + "integrity": "sha512-4nsqfukfnI6tY7d1deqBQSj3KywOiNVlPQkY0tLIjF62rzXo1SDXJXgUU+cVs5e06uO4x/55O4SiAaRH3li/Vg==", + "dependencies": { + "basic-auth-connect": "^1.1.0", + "chalk": "^4.1.2", + "dateformat": "^4.6.3", + "express": "^4.21.1", + "http2-express": "^1.0.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "mkdirp": "^2.1.6", + "ws": "^8.18.0" + }, + "bin": { + "node-media-server": "bin/app.js" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -4806,7 +4873,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -4882,6 +4948,14 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "engines": { + "node": ">=0.6.x" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -5083,6 +5157,26 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/live-streaming/package.json b/live-streaming/package.json index 89903d2e..89b0de07 100644 --- a/live-streaming/package.json +++ b/live-streaming/package.json @@ -15,6 +15,7 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "node-media-server": "^2.7.0", "uuid": "^9.0.1" }, "devDependencies": { diff --git a/live-streaming/server/index.js b/live-streaming/server/index.js index fc3a1efb..38bf4f93 100644 --- a/live-streaming/server/index.js +++ b/live-streaming/server/index.js @@ -1,13 +1,59 @@ require('dotenv').config(); const express = require('express'); const cors = require('cors'); +const NodeMediaServer = require('node-media-server'); const roomsRouter = require('./routes/rooms'); const srsRouter = require('./routes/srs'); const errorHandler = require('./middleware/errorHandler'); +const roomStore = require('./store/roomStore'); const app = express(); const PORT = process.env.PORT || 3001; +const startMediaServer = () => { + const host = process.env.SRS_HOST || 'localhost'; + const rtmpPort = Number(process.env.SRS_RTMP_PORT || 1935); + const httpPort = Number(process.env.SRS_HTTP_PORT || 8080); + + try { + const nms = new NodeMediaServer({ + rtmp: { + port: rtmpPort, + chunk_size: 60000, + gop_cache: true, + ping: 30, + ping_timeout: 60 + }, + http: { + port: httpPort, + allow_origin: '*' + } + }); + + nms.on('prePublish', (id, streamPath) => { + const parts = String(streamPath || '').split('/').filter(Boolean); + const streamKey = parts[1]; + if (streamKey) roomStore.setLiveStatus(streamKey, true); + }); + + nms.on('donePublish', (id, streamPath) => { + const parts = String(streamPath || '').split('/').filter(Boolean); + const streamKey = parts[1]; + if (streamKey) roomStore.setLiveStatus(streamKey, false); + }); + + nms.run(); + + console.log(`Media Server RTMP: rtmp://${host}:${rtmpPort}/live (Stream Key = streamKey)`); + console.log(`Media Server HTTP-FLV: http://${host}:${httpPort}/live/{streamKey}.flv`); + } catch (e) { + console.error('[Media Server] Failed to start embedded media server:', e.message); + console.error('[Media Server] If ports 1935/8080 are in use, stop the occupying process or change SRS_RTMP_PORT/SRS_HTTP_PORT.'); + } +}; + +startMediaServer(); + // 中间件 app.use(cors({ origin: process.env.CLIENT_URL || 'http://localhost:3000', diff --git a/live-streaming/server/routes/rooms.js b/live-streaming/server/routes/rooms.js index 99f5361a..3f01be49 100644 --- a/live-streaming/server/routes/rooms.js +++ b/live-streaming/server/routes/rooms.js @@ -3,14 +3,18 @@ const router = express.Router(); const roomStore = require('../store/roomStore'); const { validateRoom } = require('../middleware/validate'); const { generateStreamUrls } = require('../utils/streamUrl'); +const { getActiveStreamKeys } = require('../utils/srsHttpApi'); // GET /api/rooms - 获取所有房间 -router.get('/', (req, res) => { +router.get('/', async (req, res) => { const rooms = roomStore.getAll(); + const activeStreamKeys = await getActiveStreamKeys({ app: 'live' }); + res.json({ success: true, data: rooms.map(room => ({ ...room, + isLive: activeStreamKeys.size ? activeStreamKeys.has(room.streamKey) : room.isLive, streamUrls: generateStreamUrls(room.streamKey) })) }); @@ -31,7 +35,7 @@ router.post('/', validateRoom, (req, res) => { }); // GET /api/rooms/:id - 获取单个房间 -router.get('/:id', (req, res) => { +router.get('/:id', async (req, res) => { const room = roomStore.getById(req.params.id); if (!room) { @@ -45,6 +49,7 @@ router.get('/:id', (req, res) => { success: true, data: { ...room, + isLive: (await getActiveStreamKeys({ app: 'live' })).has(room.streamKey) || room.isLive, streamUrls: generateStreamUrls(room.streamKey) } }); diff --git a/live-streaming/server/utils/srsHttpApi.js b/live-streaming/server/utils/srsHttpApi.js new file mode 100644 index 00000000..2939b80d --- /dev/null +++ b/live-streaming/server/utils/srsHttpApi.js @@ -0,0 +1,88 @@ +const http = require('http'); +const https = require('https'); + +const requestJson = (url, { timeoutMs = 2000 } = {}) => { + return new Promise((resolve, reject) => { + const u = new URL(url); + const lib = u.protocol === 'https:' ? https : http; + + const req = lib.request( + { + protocol: u.protocol, + hostname: u.hostname, + port: u.port, + path: `${u.pathname}${u.search}`, + method: 'GET' + }, + (res) => { + let raw = ''; + res.setEncoding('utf8'); + res.on('data', (chunk) => (raw += chunk)); + res.on('end', () => { + if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) { + return reject(new Error(`SRS HTTP API status ${res.statusCode}`)); + } + + try { + resolve(JSON.parse(raw || '{}')); + } catch (e) { + reject(new Error('Invalid JSON from SRS HTTP API')); + } + }); + } + ); + + req.on('error', reject); + req.setTimeout(timeoutMs, () => { + req.destroy(new Error('SRS HTTP API request timeout')); + }); + req.end(); + }); +}; + +const normalizeStreamName = (s) => { + if (!s) return null; + if (typeof s === 'string') return s; + return s.stream || s.name || s.id || null; +}; + +const isPublishActive = (streamObj) => { + const publish = streamObj && streamObj.publish; + if (!publish) return false; + if (typeof publish.active === 'boolean') return publish.active; + return Boolean(publish.cid); +}; + +let warnedOnce = false; + +const getActiveStreamKeys = async ({ app = 'live' } = {}) => { + const host = process.env.SRS_HOST || 'localhost'; + const apiPort = process.env.SRS_API_PORT || 1985; + + const url = `http://${host}:${apiPort}/api/v1/streams?count=100`; + + try { + const payload = await requestJson(url, { timeoutMs: 1500 }); + const streams = payload.streams || payload.data?.streams || []; + + const active = new Set(); + for (const s of streams) { + if (app && s.app && s.app !== app) continue; + if (!isPublishActive(s)) continue; + const name = normalizeStreamName(s); + if (name) active.add(name); + } + + return active; + } catch (e) { + if (!warnedOnce) { + warnedOnce = true; + console.warn(`[SRS] HTTP API unavailable at ${url}. Will fallback to callbacks/in-memory status.`); + } + return new Set(); + } +}; + +module.exports = { + getActiveStreamKeys +}; diff --git a/live-streaming/server/utils/streamUrl.js b/live-streaming/server/utils/streamUrl.js index fd695a80..8e9f1920 100644 --- a/live-streaming/server/utils/streamUrl.js +++ b/live-streaming/server/utils/streamUrl.js @@ -6,7 +6,7 @@ const generateStreamUrls = (streamKey) => { return { // 推流地址 (给主播用) - rtmp: `rtmp://${host}:${rtmpPort}/live/${streamKey}`, + rtmp: `rtmp://${host}:${rtmpPort}/live`, // 播放地址 (给观众用) flv: `http://${host}:${httpPort}/live/${streamKey}.flv`, diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..c94e6258 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "zhibo_1", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "zhibo_1", + "version": "1.0.0", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..4bf8ae4d --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "zhibo_1", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/query b/query new file mode 100644 index 00000000..8903c178 --- /dev/null +++ b/query @@ -0,0 +1 @@ +vmcompute