feat(web): single WHIP trunk publish + 4-mode watch (srs-hls/whep/cf/cdn)
This commit is contained in:
parent
91b4f01310
commit
5b7edcac5c
|
|
@ -1 +1 @@
|
||||||
import{K as e,R as t,v as n}from"./ui-CzB-gjVP.js";var r=[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`m12 5 7 7-7 7`,key:`xquz4c`}]],i=i=>t(n,e(i,{iconNode:r,name:`arrow-right`}));export{i as t};
|
import{G as e,L as t,_ as n}from"./ui-BjxGELcM.js";var r=[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`m12 5 7 7-7 7`,key:`xquz4c`}]],i=i=>t(n,e(i,{iconNode:r,name:`arrow-right`}));export{i as t};
|
||||||
|
|
@ -1 +1 @@
|
||||||
import{K as e,R as t,v as n}from"./ui-CzB-gjVP.js";var r=[[`path`,{d:`M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z`,key:`p7xjir`}]],i=i=>t(n,e(i,{iconNode:r,name:`cloud`}));export{i as t};
|
import{G as e,L as t,_ as n}from"./ui-BjxGELcM.js";var r=[[`path`,{d:`M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z`,key:`p7xjir`}]],i=i=>t(n,e(i,{iconNode:r,name:`cloud`}));export{i as t};
|
||||||
|
|
@ -1 +1 @@
|
||||||
import{K as e,R as t,v as n}from"./ui-CzB-gjVP.js";var r=[[`path`,{d:`M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49`,key:`ct8e1f`}],[`path`,{d:`M14.084 14.158a3 3 0 0 1-4.242-4.242`,key:`151rxh`}],[`path`,{d:`M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143`,key:`13bj9a`}],[`path`,{d:`m2 2 20 20`,key:`1ooewy`}]],i=i=>t(n,e(i,{iconNode:r,name:`eye-off`})),a=[[`path`,{d:`M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0`,key:`1nclc0`}],[`circle`,{cx:`12`,cy:`12`,r:`3`,key:`1v7zrd`}]],o=r=>t(n,e(r,{iconNode:a,name:`eye`}));export{i as n,o as t};
|
import{G as e,L as t,_ as n}from"./ui-BjxGELcM.js";var r=[[`path`,{d:`M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49`,key:`ct8e1f`}],[`path`,{d:`M14.084 14.158a3 3 0 0 1-4.242-4.242`,key:`151rxh`}],[`path`,{d:`M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143`,key:`13bj9a`}],[`path`,{d:`m2 2 20 20`,key:`1ooewy`}]],i=i=>t(n,e(i,{iconNode:r,name:`eye-off`})),a=[[`path`,{d:`M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0`,key:`1nclc0`}],[`circle`,{cx:`12`,cy:`12`,r:`3`,key:`1v7zrd`}]],o=r=>t(n,e(r,{iconNode:a,name:`eye`}));export{i as n,o as t};
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
import{G as e,L as t,_ as n}from"./ui-BjxGELcM.js";var r=[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20`,key:`13o1zl`}],[`path`,{d:`M2 12h20`,key:`9i4pu4`}]],i=i=>t(n,e(i,{iconNode:r,name:`globe`}));export{i as t};
|
||||||
|
|
@ -1 +1 @@
|
||||||
import{K as e,R as t,v as n}from"./ui-CzB-gjVP.js";var r=[[`line`,{x1:`4`,x2:`20`,y1:`9`,y2:`9`,key:`4lhtct`}],[`line`,{x1:`4`,x2:`20`,y1:`15`,y2:`15`,key:`vyu0kd`}],[`line`,{x1:`10`,x2:`8`,y1:`3`,y2:`21`,key:`1ggp8o`}],[`line`,{x1:`16`,x2:`14`,y1:`3`,y2:`21`,key:`weycgp`}]],i=i=>t(n,e(i,{iconNode:r,name:`hash`}));export{i as t};
|
import{G as e,L as t,_ as n}from"./ui-BjxGELcM.js";var r=[[`line`,{x1:`4`,x2:`20`,y1:`9`,y2:`9`,key:`4lhtct`}],[`line`,{x1:`4`,x2:`20`,y1:`15`,y2:`15`,key:`vyu0kd`}],[`line`,{x1:`10`,x2:`8`,y1:`3`,y2:`21`,key:`1ggp8o`}],[`line`,{x1:`16`,x2:`14`,y1:`3`,y2:`21`,key:`weycgp`}]],i=i=>t(n,e(i,{iconNode:r,name:`hash`}));export{i as t};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
||||||
import{K as e,R as t,v as n}from"./ui-CzB-gjVP.js";var r=[[`path`,{d:`m16 17 5-5-5-5`,key:`1bji2h`}],[`path`,{d:`M21 12H9`,key:`dn1m92`}],[`path`,{d:`M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4`,key:`1uf3rs`}]],i=i=>t(n,e(i,{iconNode:r,name:`log-out`}));export{i as t};
|
import{G as e,L as t,_ as n}from"./ui-BjxGELcM.js";var r=[[`path`,{d:`m16 17 5-5-5-5`,key:`1bji2h`}],[`path`,{d:`M21 12H9`,key:`dn1m92`}],[`path`,{d:`M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4`,key:`1uf3rs`}]],i=i=>t(n,e(i,{iconNode:r,name:`log-out`}));export{i as t};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
||||||
import{K as e,R as t,v as n}from"./ui-CzB-gjVP.js";var r=[[`path`,{d:`M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z`,key:`18887p`}]],i=i=>t(n,e(i,{iconNode:r,name:`message-square`}));export{i as t};
|
import{G as e,L as t,_ as n}from"./ui-BjxGELcM.js";var r=[[`path`,{d:`M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z`,key:`18887p`}]],i=i=>t(n,e(i,{iconNode:r,name:`message-square`}));export{i as t};
|
||||||
|
|
@ -1 +1 @@
|
||||||
import{K as e,R as t,v as n}from"./ui-CzB-gjVP.js";var r=[[`path`,{d:`M15.033 9.44a.647.647 0 0 1 0 1.12l-4.065 2.352a.645.645 0 0 1-.968-.56V7.648a.645.645 0 0 1 .967-.56z`,key:`vbtd3f`}],[`path`,{d:`M12 17v4`,key:`1riwvh`}],[`path`,{d:`M8 21h8`,key:`1ev6f3`}],[`rect`,{x:`2`,y:`3`,width:`20`,height:`14`,rx:`2`,key:`x3v2xh`}]],i=i=>t(n,e(i,{iconNode:r,name:`monitor-play`}));export{i as t};
|
import{G as e,L as t,_ as n}from"./ui-BjxGELcM.js";var r=[[`path`,{d:`M15.033 9.44a.647.647 0 0 1 0 1.12l-4.065 2.352a.645.645 0 0 1-.968-.56V7.648a.645.645 0 0 1 .967-.56z`,key:`vbtd3f`}],[`path`,{d:`M12 17v4`,key:`1riwvh`}],[`path`,{d:`M8 21h8`,key:`1ev6f3`}],[`rect`,{x:`2`,y:`3`,width:`20`,height:`14`,rx:`2`,key:`x3v2xh`}]],i=i=>t(n,e(i,{iconNode:r,name:`monitor-play`}));export{i as t};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
||||||
import{K as e,R as t,v as n}from"./ui-CzB-gjVP.js";var r=[[`path`,{d:`M4.9 16.1C1 12.2 1 5.8 4.9 1.9`,key:`s0qx1y`}],[`path`,{d:`M7.8 4.7a6.14 6.14 0 0 0-.8 7.5`,key:`1idnkw`}],[`circle`,{cx:`12`,cy:`9`,r:`2`,key:`1092wv`}],[`path`,{d:`M16.2 4.8c2 2 2.26 5.11.8 7.47`,key:`ojru2q`}],[`path`,{d:`M19.1 1.9a9.96 9.96 0 0 1 0 14.1`,key:`rhi7fg`}],[`path`,{d:`M9.5 18h5`,key:`mfy3pd`}],[`path`,{d:`m8 22 4-11 4 11`,key:`25yftu`}]],i=i=>t(n,e(i,{iconNode:r,name:`radio-tower`}));export{i as t};
|
import{G as e,L as t,_ as n}from"./ui-BjxGELcM.js";var r=[[`path`,{d:`M4.9 16.1C1 12.2 1 5.8 4.9 1.9`,key:`s0qx1y`}],[`path`,{d:`M7.8 4.7a6.14 6.14 0 0 0-.8 7.5`,key:`1idnkw`}],[`circle`,{cx:`12`,cy:`9`,r:`2`,key:`1092wv`}],[`path`,{d:`M16.2 4.8c2 2 2.26 5.11.8 7.47`,key:`ojru2q`}],[`path`,{d:`M19.1 1.9a9.96 9.96 0 0 1 0 14.1`,key:`rhi7fg`}],[`path`,{d:`M9.5 18h5`,key:`mfy3pd`}],[`path`,{d:`m8 22 4-11 4 11`,key:`25yftu`}]],i=i=>t(n,e(i,{iconNode:r,name:`radio-tower`}));export{i as t};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
||||||
import{K as e,R as t,v as n}from"./ui-CzB-gjVP.js";var r=[[`rect`,{width:`20`,height:`8`,x:`2`,y:`2`,rx:`2`,ry:`2`,key:`ngkwjq`}],[`rect`,{width:`20`,height:`8`,x:`2`,y:`14`,rx:`2`,ry:`2`,key:`iecqi9`}],[`line`,{x1:`6`,x2:`6.01`,y1:`6`,y2:`6`,key:`16zg32`}],[`line`,{x1:`6`,x2:`6.01`,y1:`18`,y2:`18`,key:`nzw8ys`}]],i=i=>t(n,e(i,{iconNode:r,name:`server`}));export{i as t};
|
import{G as e,L as t,_ as n}from"./ui-BjxGELcM.js";var r=[[`rect`,{width:`20`,height:`8`,x:`2`,y:`2`,rx:`2`,ry:`2`,key:`ngkwjq`}],[`rect`,{width:`20`,height:`8`,x:`2`,y:`14`,rx:`2`,ry:`2`,key:`iecqi9`}],[`line`,{x1:`6`,x2:`6.01`,y1:`6`,y2:`6`,key:`16zg32`}],[`line`,{x1:`6`,x2:`6.01`,y1:`18`,y2:`18`,key:`nzw8ys`}]],i=i=>t(n,e(i,{iconNode:r,name:`server`}));export{i as t};
|
||||||
|
|
@ -1 +1 @@
|
||||||
import{K as e,R as t,v as n}from"./ui-CzB-gjVP.js";var r=[[`path`,{d:`M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z`,key:`oel41y`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]],i=i=>t(n,e(i,{iconNode:r,name:`shield-check`}));export{i as t};
|
import{G as e,L as t,_ as n}from"./ui-BjxGELcM.js";var r=[[`path`,{d:`M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z`,key:`oel41y`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]],i=i=>t(n,e(i,{iconNode:r,name:`shield-check`}));export{i as t};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
||||||
import{K as e,R as t,v as n}from"./ui-CzB-gjVP.js";var r=[[`path`,{d:`M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z`,key:`oel41y`}],[`path`,{d:`M12 8v4`,key:`1got3b`}],[`path`,{d:`M12 16h.01`,key:`1drbdi`}]],i=i=>t(n,e(i,{iconNode:r,name:`shield-alert`})),a=[[`circle`,{cx:`12`,cy:`8`,r:`5`,key:`1hypcn`}],[`path`,{d:`M20 21a8 8 0 0 0-16 0`,key:`rfgkzh`}]],o=r=>t(n,e(r,{iconNode:a,name:`user-round`}));export{i as n,o as t};
|
import{G as e,L as t,_ as n}from"./ui-BjxGELcM.js";var r=[[`path`,{d:`M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z`,key:`oel41y`}],[`path`,{d:`M12 8v4`,key:`1got3b`}],[`path`,{d:`M12 16h.01`,key:`1drbdi`}]],i=i=>t(n,e(i,{iconNode:r,name:`shield-alert`})),a=[[`circle`,{cx:`12`,cy:`8`,r:`5`,key:`1hypcn`}],[`path`,{d:`M20 21a8 8 0 0 0-16 0`,key:`rfgkzh`}]],o=r=>t(n,e(r,{iconNode:a,name:`user-round`}));export{i as n,o as t};
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
||||||
|
import{G as e,L as t,_ as n}from"./ui-BjxGELcM.js";var r=[[`rect`,{width:`18`,height:`11`,x:`3`,y:`11`,rx:`2`,ry:`2`,key:`1w4ew1`}],[`path`,{d:`M7 11V7a5 5 0 0 1 10 0v4`,key:`fwvmzm`}]],i=i=>t(n,e(i,{iconNode:r,name:`lock`})),a=[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`M12 5v14`,key:`s699le`}]],o=r=>t(n,e(r,{iconNode:a,name:`plus`})),s=[[`path`,{d:`M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2`,key:`1yyitq`}],[`path`,{d:`M16 3.128a4 4 0 0 1 0 7.744`,key:`16gr8j`}],[`path`,{d:`M22 21v-2a4 4 0 0 0-3-3.87`,key:`kshegd`}],[`circle`,{cx:`9`,cy:`7`,r:`4`,key:`nufk8`}]],c=r=>t(n,e(r,{iconNode:s,name:`users`}));export{o as n,i as r,c as t};
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
import{K as e,R as t,v as n}from"./ui-CzB-gjVP.js";var r=[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20`,key:`13o1zl`}],[`path`,{d:`M2 12h20`,key:`9i4pu4`}]],i=i=>t(n,e(i,{iconNode:r,name:`globe`})),a=[[`rect`,{width:`18`,height:`11`,x:`3`,y:`11`,rx:`2`,ry:`2`,key:`1w4ew1`}],[`path`,{d:`M7 11V7a5 5 0 0 1 10 0v4`,key:`fwvmzm`}]],o=r=>t(n,e(r,{iconNode:a,name:`lock`})),s=[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`M12 5v14`,key:`s699le`}]],c=r=>t(n,e(r,{iconNode:s,name:`plus`})),l=[[`path`,{d:`M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2`,key:`1yyitq`}],[`path`,{d:`M16 3.128a4 4 0 0 1 0 7.744`,key:`16gr8j`}],[`path`,{d:`M22 21v-2a4 4 0 0 0-3-3.87`,key:`kshegd`}],[`circle`,{cx:`9`,cy:`7`,r:`4`,key:`nufk8`}]],u=r=>t(n,e(r,{iconNode:l,name:`users`}));export{i,c as n,o as r,u as t};
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
||||||
import{K as e,R as t,v as n}from"./ui-CzB-gjVP.js";var r=[[`path`,{d:`m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L16 10.5`,key:`ftymec`}],[`rect`,{x:`2`,y:`6`,width:`14`,height:`12`,rx:`2`,key:`158x01`}]],i=i=>t(n,e(i,{iconNode:r,name:`video`}));export{i as t};
|
import{G as e,L as t,_ as n}from"./ui-BjxGELcM.js";var r=[[`path`,{d:`m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L16 10.5`,key:`ftymec`}],[`rect`,{x:`2`,y:`6`,width:`14`,height:`12`,rx:`2`,key:`158x01`}]],i=i=>t(n,e(i,{iconNode:r,name:`video`}));export{i as t};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -19,9 +19,9 @@
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
</script>
|
</script>
|
||||||
<script type="module" crossorigin src="/static/assets/index-D9mHcT7c.js"></script>
|
<script type="module" crossorigin src="/static/assets/index-BsK239Bd.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/static/assets/ui-CzB-gjVP.js">
|
<link rel="modulepreload" crossorigin href="/static/assets/ui-BjxGELcM.js">
|
||||||
<link rel="stylesheet" crossorigin href="/static/assets/index-Cj7RYHpI.css">
|
<link rel="stylesheet" crossorigin href="/static/assets/index-Ub-SlVMy.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import {
|
||||||
CircleStop,
|
CircleStop,
|
||||||
Cloud,
|
Cloud,
|
||||||
FileVideo,
|
FileVideo,
|
||||||
|
Globe,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Server,
|
Server,
|
||||||
Video,
|
Video,
|
||||||
|
|
@ -14,30 +15,29 @@ import { Alert, Badge, Button, CheckboxRow, Corners, LiveDot, Meter, Reveal } fr
|
||||||
import { DanmakuOverlay, ChatPanel } from '../../components/danmaku'
|
import { DanmakuOverlay, ChatPanel } from '../../components/danmaku'
|
||||||
import { subscribeRoom } from '../../lib/sse'
|
import { subscribeRoom } from '../../lib/sse'
|
||||||
import { subscribeChat, sendChat, type ChatMessage } from '../../lib/chat'
|
import { subscribeChat, sendChat, type ChatMessage } from '../../lib/chat'
|
||||||
import { whipPublish, cfPublish, toRTC } from '../../lib/webrtc'
|
import { whipPublish, toRTC } from '../../lib/webrtc'
|
||||||
import type { BackendInfo, RoomEvent, StreamTarget } from '../../lib/types'
|
import type { DistributorInfo, RoomEvent, StreamTarget } from '../../lib/types'
|
||||||
|
|
||||||
function backendLabel(kind: string): string {
|
function distributorLabel(kind: string): string {
|
||||||
return kind === 'BACKEND_KIND_CLOUDFLARE'
|
if (kind === 'DISTRIBUTION_KIND_SRS_HLS') return 'SRS HLS 直推'
|
||||||
? 'Cloudflare Realtime'
|
if (kind === 'DISTRIBUTION_KIND_CF_SFU') return 'Cloudflare SFU'
|
||||||
: kind === 'BACKEND_KIND_SRS'
|
if (kind === 'DISTRIBUTION_KIND_CDN') return '第三方 CDN'
|
||||||
? 'SRS'
|
return kind
|
||||||
: kind
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function backendIcon(kind: string) {
|
function distributorIcon(kind: string) {
|
||||||
return kind === 'BACKEND_KIND_CLOUDFLARE' ? Cloud : Server
|
if (kind === 'DISTRIBUTION_KIND_SRS_HLS') return Server
|
||||||
|
if (kind === 'DISTRIBUTION_KIND_CF_SFU') return Cloud
|
||||||
|
if (kind === 'DISTRIBUTION_KIND_CDN') return Globe
|
||||||
|
return Server
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Publish: Component = () => {
|
export const Publish: Component = () => {
|
||||||
const search = useSearch({ from: '/publish' })
|
const search = useSearch({ from: '/publish' })
|
||||||
const room = () => search().room
|
const room = () => search().room
|
||||||
|
|
||||||
const [backends, setBackends] = createSignal<BackendInfo[]>([])
|
const [distributors, setDistributors] = createSignal<DistributorInfo[]>([])
|
||||||
const [selected, setSelected] = createSignal<Record<string, boolean>>({
|
const [trunkName, setTrunkName] = createSignal('SRS')
|
||||||
BACKEND_KIND_CLOUDFLARE: true,
|
|
||||||
BACKEND_KIND_SRS: true,
|
|
||||||
})
|
|
||||||
const [stream, setStream] = createSignal<MediaStream | null>(null)
|
const [stream, setStream] = createSignal<MediaStream | null>(null)
|
||||||
const [running, setRunning] = createSignal(false)
|
const [running, setRunning] = createSignal(false)
|
||||||
const [targets, setTargets] = createSignal<StreamTarget[]>([])
|
const [targets, setTargets] = createSignal<StreamTarget[]>([])
|
||||||
|
|
@ -46,11 +46,16 @@ export const Publish: Component = () => {
|
||||||
const [showDanmaku, setShowDanmaku] = createSignal(true)
|
const [showDanmaku, setShowDanmaku] = createSignal(true)
|
||||||
|
|
||||||
let videoEl: HTMLVideoElement | undefined
|
let videoEl: HTMLVideoElement | undefined
|
||||||
const pcs: Record<string, RTCPeerConnection> = {}
|
let pc: RTCPeerConnection | undefined
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
api.getConfig().then((c) => setBackends(c.backends || [])).catch(() => {})
|
api.getConfig().then((c) => {
|
||||||
const stopRoom = subscribeRoom(room(), (ev: RoomEvent) => setTargets(ev.targets || []))
|
setDistributors(c.distributors || [])
|
||||||
|
if (c.trunk) setTrunkName(c.trunk.name || 'SRS')
|
||||||
|
}).catch(() => {})
|
||||||
|
const stopRoom = subscribeRoom(room(), (ev: RoomEvent) => {
|
||||||
|
setTargets(ev.targets || [])
|
||||||
|
})
|
||||||
const stopChat = subscribeChat(room(), (m) => setMsgs((p) => [...p, m].slice(-100)))
|
const stopChat = subscribeChat(room(), (m) => setMsgs((p) => [...p, m].slice(-100)))
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
stopRoom()
|
stopRoom()
|
||||||
|
|
@ -62,8 +67,6 @@ export const Publish: Component = () => {
|
||||||
sendChat(room(), text, color).catch(() => {})
|
sendChat(room(), text, color).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggle = (kind: string) => setSelected((s) => ({ ...s, [kind]: !s[kind] }))
|
|
||||||
|
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
if (!permissions.can('room:publish')) {
|
if (!permissions.can('room:publish')) {
|
||||||
setError('当前角色无权发布(需要 room:publish)')
|
setError('当前角色无权发布(需要 room:publish)')
|
||||||
|
|
@ -78,24 +81,14 @@ export const Publish: Component = () => {
|
||||||
if (videoEl) videoEl.srcObject = ms
|
if (videoEl) videoEl.srcObject = ms
|
||||||
}
|
}
|
||||||
const ms = stream()!
|
const ms = stream()!
|
||||||
const wants = backends().filter((b) => selected()[b.kind])
|
// SRS trunk: single WHIP publish
|
||||||
for (const b of wants) {
|
const resp = await api.publish(room(), 'BACKEND_KIND_SRS', '')
|
||||||
const resp = await api.publish(room(), b.kind, '')
|
pc = await whipPublish({
|
||||||
if (b.kind === 'BACKEND_KIND_SRS') {
|
stream: resp.stream!,
|
||||||
pcs.srs = await whipPublish({
|
token: resp.publish_token!,
|
||||||
stream: resp.stream!,
|
local: ms,
|
||||||
token: resp.publish_token!,
|
iceServers: (resp.ice_servers || []).map(toRTC),
|
||||||
local: ms,
|
})
|
||||||
iceServers: (resp.ice_servers || []).map(toRTC),
|
|
||||||
})
|
|
||||||
} else if (b.kind === 'BACKEND_KIND_CLOUDFLARE') {
|
|
||||||
pcs.cloudflare = await cfPublish({
|
|
||||||
sessionId: resp.session_id!,
|
|
||||||
local: ms,
|
|
||||||
iceServers: (resp.ice_servers || []).map(toRTC),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(String(e?.message || e))
|
setError(String(e?.message || e))
|
||||||
setRunning(false)
|
setRunning(false)
|
||||||
|
|
@ -103,12 +96,8 @@ export const Publish: Component = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopAll = async () => {
|
const stopAll = async () => {
|
||||||
for (const k of Object.keys(pcs)) {
|
try { pc?.close() } catch {}
|
||||||
try {
|
pc = undefined
|
||||||
pcs[k].close()
|
|
||||||
} catch {}
|
|
||||||
delete pcs[k]
|
|
||||||
}
|
|
||||||
for (const t of targets()) {
|
for (const t of targets()) {
|
||||||
try {
|
try {
|
||||||
await api.stop(room(), t.backend)
|
await api.stop(room(), t.backend)
|
||||||
|
|
@ -132,7 +121,7 @@ export const Publish: Component = () => {
|
||||||
<p class="mono-label text-muted-foreground">publish / {room()}</p>
|
<p class="mono-label text-muted-foreground">publish / {room()}</p>
|
||||||
<h1 class="display mt-2 text-4xl sm:text-5xl">开始直播</h1>
|
<h1 class="display mt-2 text-4xl sm:text-5xl">开始直播</h1>
|
||||||
<p class="mt-3 max-w-lg text-sm text-muted-foreground">
|
<p class="mt-3 max-w-lg text-sm text-muted-foreground">
|
||||||
选择分发后端,一路推流将扇出到所选 SFU。
|
推流仅进入 SRS 主干(WHIP),分发由服务端按配置扇出到多路。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3 border border-line bg-card/60 px-4 py-2.5">
|
<div class="flex items-center gap-3 border border-line bg-card/60 px-4 py-2.5">
|
||||||
|
|
@ -161,7 +150,7 @@ export const Publish: Component = () => {
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Video class="size-4 text-acid-lime" />
|
<Video class="size-4 text-acid-lime" />
|
||||||
<h3 class="font-semibold">本地预览</h3>
|
<h3 class="font-semibold">本地预览</h3>
|
||||||
<span class="micro-label text-muted-foreground">preview</span>
|
<span class="micro-label text-muted-foreground">preview · trunk: {trunkName()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="font-mono text-[11px] text-muted-foreground">
|
<span class="font-mono text-[11px] text-muted-foreground">
|
||||||
|
|
@ -185,6 +174,9 @@ export const Publish: Component = () => {
|
||||||
<LiveDot active={running()} />
|
<LiveDot active={running()} />
|
||||||
{running() ? 'LIVE' : 'STANDBY'}
|
{running() ? 'LIVE' : 'STANDBY'}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
<Badge variant="secondary" class="border-line bg-black/50 text-white/90 shadow-sm backdrop-blur-sm">
|
||||||
|
SRS WHIP
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<Show when={!stream()}>
|
<Show when={!stream()}>
|
||||||
<div class="pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-2.5 text-white/60">
|
<div class="pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-2.5 text-white/60">
|
||||||
|
|
@ -214,24 +206,25 @@ export const Publish: Component = () => {
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Server class="size-4 text-acid-lime" />
|
<Server class="size-4 text-acid-lime" />
|
||||||
<h3 class="font-semibold">分发后端</h3>
|
<h3 class="font-semibold">分发方式</h3>
|
||||||
<span class="micro-label text-muted-foreground">routing</span>
|
<span class="micro-label text-muted-foreground">distributions</span>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="muted">{backends().length} 路</Badge>
|
<Badge variant="muted">{distributors().length} 路</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-2 text-xs leading-relaxed text-muted-foreground">由服务端按 DISTRIBUTORS 配置自动扇出,推流端无需选择后端。</p>
|
||||||
|
|
||||||
<div class="mt-4 space-y-2.5">
|
<div class="mt-4 space-y-2.5">
|
||||||
<Show
|
<Show
|
||||||
when={backends().length > 0}
|
when={distributors().length > 0}
|
||||||
fallback={<p class="text-sm text-muted-foreground">正在读取后端配置…</p>}
|
fallback={<p class="text-sm text-muted-foreground">正在读取分发配置…</p>}
|
||||||
>
|
>
|
||||||
<For each={backends()}>
|
<For each={distributors()}>
|
||||||
{(b) => {
|
{(d) => {
|
||||||
const Icon = backendIcon(b.kind)
|
const Icon = distributorIcon(d.kind)
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={`flex items-center justify-between gap-3 border p-3 transition-colors ${
|
class={`flex items-center justify-between gap-3 border p-3 transition-colors ${
|
||||||
b.configured ? 'border-line bg-background/50' : 'border-line/60 bg-background/20 opacity-70'
|
d.configured ? 'border-line bg-background/50' : 'border-line/60 bg-background/20 opacity-70'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div class="flex min-w-0 items-center gap-3">
|
<div class="flex min-w-0 items-center gap-3">
|
||||||
|
|
@ -239,19 +232,14 @@ export const Publish: Component = () => {
|
||||||
<Icon class="size-4" />
|
<Icon class="size-4" />
|
||||||
</span>
|
</span>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<CheckboxRow
|
<p class="text-sm font-medium">{d.name}</p>
|
||||||
label={b.name}
|
<p class="micro-label mt-1 text-muted-foreground">
|
||||||
checked={!!selected()[b.kind]}
|
{distributorLabel(d.kind)} · {d.enabled ? '已启用' : '未启用'}
|
||||||
onChange={() => toggle(b.kind)}
|
|
||||||
disabled={running()}
|
|
||||||
/>
|
|
||||||
<p class="micro-label mt-1.5 text-muted-foreground">
|
|
||||||
{backendLabel(b.kind)} · {b.primary ? '主 SFU' : '对照后端'}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant={b.configured ? 'success' : 'muted'}>
|
<Badge variant={d.configured ? 'success' : 'muted'}>
|
||||||
{b.configured ? '在线' : '未配置'}
|
{d.configured ? '就绪' : '未配置'}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -261,7 +249,7 @@ export const Publish: Component = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="mt-4 border-t border-line/70 pt-4 text-xs leading-relaxed text-muted-foreground">
|
<p class="mt-4 border-t border-line/70 pt-4 text-xs leading-relaxed text-muted-foreground">
|
||||||
推流凭证仅在服务端会话中生效,浏览器端不会出现 SFU 密钥。
|
主干为 SRS,推流凭证仅在服务端会话中生效。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
@ -271,8 +259,8 @@ export const Publish: Component = () => {
|
||||||
<div class="relative border border-line bg-card/50 p-4 sm:p-5">
|
<div class="relative border border-line bg-card/50 p-4 sm:p-5">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<FileVideo class="size-4 text-acid-lime" />
|
<FileVideo class="size-4 text-acid-lime" />
|
||||||
<h3 class="font-semibold">分发状态</h3>
|
<h3 class="font-semibold">主干状态</h3>
|
||||||
<span class="micro-label text-muted-foreground">fan-out status</span>
|
<span class="micro-label text-muted-foreground">trunk status</span>
|
||||||
</div>
|
</div>
|
||||||
<Show
|
<Show
|
||||||
when={targets().length > 0}
|
when={targets().length > 0}
|
||||||
|
|
@ -288,7 +276,7 @@ export const Publish: Component = () => {
|
||||||
<div class="relative border border-line/80 bg-background/40 p-4">
|
<div class="relative border border-line/80 bg-background/40 p-4">
|
||||||
<span class="absolute top-0 left-0 h-3 w-3 border-t-2 border-l-2 border-acid-lime" aria-hidden="true" />
|
<span class="absolute top-0 left-0 h-3 w-3 border-t-2 border-l-2 border-acid-lime" aria-hidden="true" />
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<span class="truncate text-sm font-medium">{backendLabel(t.backend)}</span>
|
<span class="truncate text-sm font-medium">SRS 主干</span>
|
||||||
<Badge variant="success">
|
<Badge variant="success">
|
||||||
<LiveDot active />
|
<LiveDot active />
|
||||||
直播中
|
直播中
|
||||||
|
|
@ -306,11 +294,6 @@ export const Publish: Component = () => {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={t.session_id}>
|
|
||||||
<p class="mt-2 truncate font-mono text-[10px] text-muted-foreground">
|
|
||||||
session · {t.session_id}
|
|
||||||
</p>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
FileVideo,
|
FileVideo,
|
||||||
|
Globe,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
MonitorPlay,
|
MonitorPlay,
|
||||||
Radio,
|
Radio,
|
||||||
|
|
@ -19,27 +20,32 @@ import { whepSubscribe, cfSubscribe, toRTC } from '../../lib/webrtc'
|
||||||
import { playHLS } from '../../lib/hls'
|
import { playHLS } from '../../lib/hls'
|
||||||
import { subscribeChat, sendChat, type ChatMessage } from '../../lib/chat'
|
import { subscribeChat, sendChat, type ChatMessage } from '../../lib/chat'
|
||||||
import { DanmakuOverlay, ChatPanel } from '../../components/danmaku'
|
import { DanmakuOverlay, ChatPanel } from '../../components/danmaku'
|
||||||
import type { RoomEvent } from '../../lib/types'
|
import type { DistributionTarget, RoomEvent } from '../../lib/types'
|
||||||
|
|
||||||
type Mode = 'cloudflare' | 'srs-whep' | 'srs-hls'
|
type Mode = 'srs-hls' | 'srs-whep' | 'cf-sfu' | 'cdn-hls'
|
||||||
|
|
||||||
const MODE_LABEL: Record<Mode, string> = {
|
const MODE_LABEL: Record<Mode, string> = {
|
||||||
cloudflare: 'Cloudflare',
|
|
||||||
'srs-whep': 'SRS WHEP',
|
|
||||||
'srs-hls': 'SRS HLS',
|
'srs-hls': 'SRS HLS',
|
||||||
|
'srs-whep': 'SRS WHEP',
|
||||||
|
'cf-sfu': 'CF SFU',
|
||||||
|
'cdn-hls': 'CDN HLS',
|
||||||
}
|
}
|
||||||
|
|
||||||
function backendText(kind: string) {
|
function distText(kind: string) {
|
||||||
return kind === 'BACKEND_KIND_CLOUDFLARE' ? 'Cloudflare Realtime' : 'SRS'
|
if (kind === 'DISTRIBUTION_KIND_SRS_HLS') return 'SRS HLS'
|
||||||
|
if (kind === 'DISTRIBUTION_KIND_CF_SFU') return 'CF SFU'
|
||||||
|
if (kind === 'DISTRIBUTION_KIND_CDN') return 'CDN'
|
||||||
|
return kind
|
||||||
}
|
}
|
||||||
|
|
||||||
function RoomHub(props: { room: string }) {
|
function RoomHub(props: { room: string }) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const room = () => props.room
|
const room = () => props.room
|
||||||
const [mode, setMode] = createSignal<Mode>('cloudflare')
|
const [mode, setMode] = createSignal<Mode>('srs-hls')
|
||||||
const [running, setRunning] = createSignal(false)
|
const [running, setRunning] = createSignal(false)
|
||||||
const [error, setError] = createSignal('')
|
const [error, setError] = createSignal('')
|
||||||
const [targets, setTargets] = createSignal<RoomEvent['targets']>([])
|
const [targets, setTargets] = createSignal<RoomEvent['targets']>([])
|
||||||
|
const [dists, setDists] = createSignal<DistributionTarget[]>([])
|
||||||
const [msgs, setMsgs] = createSignal<ChatMessage[]>([])
|
const [msgs, setMsgs] = createSignal<ChatMessage[]>([])
|
||||||
const [showDanmaku, setShowDanmaku] = createSignal(true)
|
const [showDanmaku, setShowDanmaku] = createSignal(true)
|
||||||
|
|
||||||
|
|
@ -48,7 +54,10 @@ function RoomHub(props: { room: string }) {
|
||||||
let stopHls: (() => void) | undefined
|
let stopHls: (() => void) | undefined
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const unsubRoom = subscribeRoom(room(), (ev) => setTargets(ev.targets || []))
|
const unsubRoom = subscribeRoom(room(), (ev) => {
|
||||||
|
setTargets(ev.targets || [])
|
||||||
|
setDists(ev.distributions || [])
|
||||||
|
})
|
||||||
const unsubChat = subscribeChat(room(), (m) => setMsgs((p) => [...p, m].slice(-100)))
|
const unsubChat = subscribeChat(room(), (m) => setMsgs((p) => [...p, m].slice(-100)))
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
unsubRoom()
|
unsubRoom()
|
||||||
|
|
@ -79,7 +88,7 @@ function RoomHub(props: { room: string }) {
|
||||||
setError('')
|
setError('')
|
||||||
setRunning(true)
|
setRunning(true)
|
||||||
try {
|
try {
|
||||||
if (mode() === 'cloudflare') {
|
if (mode() === 'cf-sfu') {
|
||||||
const resp = await api.subscribe(room(), 'BACKEND_KIND_CLOUDFLARE', '')
|
const resp = await api.subscribe(room(), 'BACKEND_KIND_CLOUDFLARE', '')
|
||||||
pc = await cfSubscribe({
|
pc = await cfSubscribe({
|
||||||
viewerSessionId: resp.session_id!,
|
viewerSessionId: resp.session_id!,
|
||||||
|
|
@ -91,6 +100,13 @@ function RoomHub(props: { room: string }) {
|
||||||
const resp = await api.subscribe(room(), 'BACKEND_KIND_SRS', '')
|
const resp = await api.subscribe(room(), 'BACKEND_KIND_SRS', '')
|
||||||
pc = await whepSubscribe({ stream: resp.stream!, iceServers: (resp.ice_servers || []).map(toRTC) })
|
pc = await whepSubscribe({ stream: resp.stream!, iceServers: (resp.ice_servers || []).map(toRTC) })
|
||||||
attach(pc)
|
attach(pc)
|
||||||
|
} else if (mode() === 'cdn-hls') {
|
||||||
|
const resp = await api.subscribe(room(), 'BACKEND_KIND_SRS', '')
|
||||||
|
if (!videoEl) throw new Error('video element missing')
|
||||||
|
const cdnDist = dists().find((d) => d.kind === 'DISTRIBUTION_KIND_CDN')
|
||||||
|
// Prefer CDN URL if available, fallback to /live/cdn path
|
||||||
|
const url = cdnDist?.url || `/live/cdn/${resp.stream}.m3u8`
|
||||||
|
stopHls = playHLS(videoEl, url)
|
||||||
} else {
|
} else {
|
||||||
const resp = await api.subscribe(room(), 'BACKEND_KIND_SRS', '')
|
const resp = await api.subscribe(room(), 'BACKEND_KIND_SRS', '')
|
||||||
if (!videoEl) throw new Error('video element missing')
|
if (!videoEl) throw new Error('video element missing')
|
||||||
|
|
@ -119,7 +135,7 @@ function RoomHub(props: { room: string }) {
|
||||||
<div class="flex items-center gap-2.5 border border-line bg-card/60 px-3.5 py-2.5">
|
<div class="flex items-center gap-2.5 border border-line bg-card/60 px-3.5 py-2.5">
|
||||||
<LiveDot active={targets().length > 0} />
|
<LiveDot active={targets().length > 0} />
|
||||||
<span class="mono-label">
|
<span class="mono-label">
|
||||||
{targets().length} targets · {targets().length > 0 ? 'live' : 'idle'}
|
{targets().length} trunk · {dists().length} 分发 · {targets().length > 0 ? 'live' : 'idle'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Show when={permissions.can('room:publish')}>
|
<Show when={permissions.can('room:publish')}>
|
||||||
|
|
@ -149,9 +165,10 @@ function RoomHub(props: { room: string }) {
|
||||||
value={mode()}
|
value={mode()}
|
||||||
onChange={(v) => setMode(v)}
|
onChange={(v) => setMode(v)}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'cloudflare', label: 'Cloudflare', icon: <Cloud class="size-3.5" /> },
|
|
||||||
{ value: 'srs-whep', label: 'SRS WHEP', icon: <Radio class="size-3.5" /> },
|
|
||||||
{ value: 'srs-hls', label: 'SRS HLS', icon: <MonitorPlay class="size-3.5" /> },
|
{ value: 'srs-hls', label: 'SRS HLS', icon: <MonitorPlay class="size-3.5" /> },
|
||||||
|
{ value: 'srs-whep', label: 'SRS WHEP', icon: <Radio class="size-3.5" /> },
|
||||||
|
{ value: 'cf-sfu', label: 'CF SFU', icon: <Cloud class="size-3.5" /> },
|
||||||
|
{ value: 'cdn-hls', label: 'CDN HLS', icon: <Globe class="size-3.5" /> },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<span class="hidden font-mono text-[11px] text-muted-foreground sm:block">
|
<span class="hidden font-mono text-[11px] text-muted-foreground sm:block">
|
||||||
|
|
@ -225,45 +242,61 @@ function RoomHub(props: { room: string }) {
|
||||||
<span class="micro-label text-muted-foreground">fan-out status</span>
|
<span class="micro-label text-muted-foreground">fan-out status</span>
|
||||||
</div>
|
</div>
|
||||||
<Show
|
<Show
|
||||||
when={targets().length > 0}
|
when={targets().length > 0 || dists().length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="mt-4 border border-dashed border-line px-5 py-8 text-center text-sm text-muted-foreground">
|
<div class="mt-4 border border-dashed border-line px-5 py-8 text-center text-sm text-muted-foreground">
|
||||||
房间暂无直播
|
房间暂无直播
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="mt-4 grid gap-3 md:grid-cols-2">
|
<div class="mt-4 space-y-3">
|
||||||
<For each={targets()}>
|
<Show when={targets().length > 0}>
|
||||||
{(t) => (
|
<div class="grid gap-3 md:grid-cols-2">
|
||||||
<div class="relative border border-line/80 bg-background/40 p-4">
|
<For each={targets()}>
|
||||||
<span class="absolute top-0 left-0 h-3 w-3 border-t-2 border-l-2 border-acid-lime" aria-hidden="true" />
|
{(t) => (
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="relative border border-line/80 bg-background/40 p-4">
|
||||||
<span class="truncate text-sm font-medium">{backendText(t.backend)}</span>
|
<span class="absolute top-0 left-0 h-3 w-3 border-t-2 border-l-2 border-acid-lime" aria-hidden="true" />
|
||||||
<Badge variant="success">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<LiveDot active />
|
<span class="truncate text-sm font-medium">SRS 主干</span>
|
||||||
直播中
|
<Badge variant="success">
|
||||||
</Badge>
|
<LiveDot active />
|
||||||
</div>
|
直播中
|
||||||
<Show when={t.stream}>
|
</Badge>
|
||||||
<div class="mt-3 flex flex-wrap gap-2">
|
</div>
|
||||||
<a class="mono-chip" href={`/live/${t.stream}.m3u8`} target="_blank" rel="noreferrer">
|
<Show when={t.stream}>
|
||||||
<FileVideo class="size-3" />
|
<div class="mt-3 flex flex-wrap gap-2">
|
||||||
HLS
|
<a class="mono-chip" href={`/live/${t.stream}.m3u8`} target="_blank" rel="noreferrer">
|
||||||
</a>
|
<FileVideo class="size-3" />
|
||||||
<a class="mono-chip" href={`/live/${t.stream}.flv`} target="_blank" rel="noreferrer">
|
HLS
|
||||||
<FileVideo class="size-3" />
|
</a>
|
||||||
FLV
|
<a class="mono-chip" href={`/live/${t.stream}.flv`} target="_blank" rel="noreferrer">
|
||||||
</a>
|
<FileVideo class="size-3" />
|
||||||
|
FLV
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
)}
|
||||||
<Show when={t.session_id}>
|
</For>
|
||||||
<p class="mt-2 truncate font-mono text-[10px] text-muted-foreground">
|
</div>
|
||||||
session · {t.session_id}
|
</Show>
|
||||||
</p>
|
<Show when={dists().length > 0}>
|
||||||
</Show>
|
<div class="grid gap-3 md:grid-cols-3">
|
||||||
</div>
|
<For each={dists()}>
|
||||||
)}
|
{(d) => (
|
||||||
</For>
|
<div class="relative border border-line/60 bg-background/30 p-3">
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<span class="truncate text-xs font-medium">{distText(d.kind)}</span>
|
||||||
|
<Badge variant={d.status === 'ready' ? 'success' : d.status === 'forwarding' ? 'warning' : 'muted'}>
|
||||||
|
{d.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p class="mt-1.5 truncate font-mono text-[10px] text-muted-foreground">{d.url}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue