From 895dc39ce9f1a38a5bb215ff08aff67f135a1f82 Mon Sep 17 00:00:00 2001 From: OrangeRoll Date: Fri, 7 Aug 2026 15:26:48 +0800 Subject: [PATCH] =?UTF-8?q?tag(=E5=8A=9F=E8=83=BD):=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E7=BC=96=E7=A0=81=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/ScreenShareWeb/src/App.css | 83 +++++++++++++++++++ apps/ScreenShareWeb/src/App.tsx | 141 +++++++++++++++++++++++++++++++- 2 files changed, 222 insertions(+), 2 deletions(-) diff --git a/apps/ScreenShareWeb/src/App.css b/apps/ScreenShareWeb/src/App.css index bf2403e..c1c7669 100644 --- a/apps/ScreenShareWeb/src/App.css +++ b/apps/ScreenShareWeb/src/App.css @@ -269,6 +269,85 @@ button:disabled { margin-left: 0; } +.modal-backdrop { + position: fixed; + z-index: 20; + inset: 0; + display: grid; + place-items: center; + padding: 20px; + background: rgb(16 17 27 / 58%); +} + +.share-settings-modal { + width: min(520px, 100%); + padding: 22px; + border-radius: 16px; + background: #fff; + box-shadow: 0 22px 70px rgb(0 0 0 / 30%); +} + +.modal-heading { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; +} + +.modal-heading h2 { + margin: 0; +} + +.icon-button { + min-height: 32px; + padding: 0 10px; + background: transparent; + color: #4c4e65; + font-size: 1.6rem; + font-weight: 400; +} + +.settings-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 14px; +} + +.codec-setting { + grid-column: 1 / -1; +} + +.codec-description { + min-height: 2.8em; + color: #686b82; + font-size: 0.79rem; + font-weight: 400; + line-height: 1.45; +} + +.audio-setting { + display: flex; + align-items: center; + gap: 8px; + margin-top: 18px; + font-size: 0.88rem; +} + +.audio-setting input[type='checkbox'] { + width: 17px; + min-height: 17px; +} + +.settings-note { + margin: 16px 0; + color: #686b82; + font-size: 0.83rem; +} + +.modal-confirm { + width: 100%; +} + .screen-stage { display: grid; min-height: min(72vh, 760px); @@ -378,4 +457,8 @@ button:disabled { .screen-thumbnail { flex-basis: 170px; } + + .settings-grid { + grid-template-columns: 1fr; + } } diff --git a/apps/ScreenShareWeb/src/App.tsx b/apps/ScreenShareWeb/src/App.tsx index 9602d47..89df8b9 100644 --- a/apps/ScreenShareWeb/src/App.tsx +++ b/apps/ScreenShareWeb/src/App.tsx @@ -24,8 +24,54 @@ type TokenResponse = { type ScreenFeed = { id: string; track: Track; label: string; isLocal: boolean } type RoomHistoryItem = { id: string; title: string; visitedAt: string } +type ShareResolution = '1280x720' | '1920x1080' | '2560x1440' | '3840x2160' +type VideoCodec = 'vp8' | 'h264' | 'vp9' | 'av1' | 'h265' const ROOM_HISTORY_KEY = 'screen-share-room-history' +const SHARE_SETTINGS_KEY = 'screen-share-share-settings' +const SHARE_RESOLUTIONS: Record = { + '1280x720': { width: 1280, height: 720 }, + '1920x1080': { width: 1920, height: 1080 }, + '2560x1440': { width: 2560, height: 1440 }, + '3840x2160': { width: 3840, height: 2160 }, +} +const VIDEO_CODECS: Record = { + vp8: { + label: 'VP8(推荐兼容性)', + description: '编解码兼容性最好、CPU 压力较低;压缩效率一般,适合跨浏览器和稳定内网场景。', + }, + h264: { + label: 'H.264(硬件支持优先)', + description: '多数设备具备硬件编解码,企业设备和 Safari 兼容性较好;文字/桌面内容压缩效率通常不如 VP9、AV1。', + }, + vp9: { + label: 'VP9(清晰度与带宽平衡)', + description: '对文字和静态桌面内容压缩更高效,可降低带宽;编码/解码 CPU 消耗更高,老旧设备兼容性较弱。', + }, + av1: { + label: 'AV1(最高压缩效率)', + description: '低带宽下画质通常最好;编解码计算开销最高,需要较新的浏览器与硬件支持。', + }, + h265: { + label: 'H.265 / HEVC(实验性)', + description: '压缩效率高,部分 Apple/硬件环境支持良好;浏览器和 WebRTC 互通性有限,建议只在受控设备环境使用。', + }, +} + +type SavedShareSettings = { + resolution?: ShareResolution + frameRate?: number + codec?: VideoCodec + audio?: boolean +} + +function readShareSettings(): SavedShareSettings { + try { + return JSON.parse(localStorage.getItem(SHARE_SETTINGS_KEY) ?? '{}') as SavedShareSettings + } catch { + return {} + } +} function ScreenTrack({ track, label, variant = 'main' }: ScreenFeed & { variant?: 'main' | 'thumbnail' }) { const mediaContainerRef = useRef(null) @@ -95,6 +141,20 @@ function App() { const [remoteScreens, setRemoteScreens] = useState([]) const [activeScreenId, setActiveScreenId] = useState(null) const [fullscreenMode, setFullscreenMode] = useState<'none' | 'page' | 'stage'>('none') + const [showShareSettings, setShowShareSettings] = useState(false) + const [shareResolution, setShareResolution] = useState(() => { + const value = readShareSettings().resolution + return value && value in SHARE_RESOLUTIONS ? value : '1920x1080' + }) + const [shareFrameRate, setShareFrameRate] = useState(() => { + const value = readShareSettings().frameRate + return value && [15, 24, 30, 48, 60].includes(value) ? value : 30 + }) + const [shareCodec, setShareCodec] = useState(() => { + const value = readShareSettings().codec + return value && value in VIDEO_CODECS ? value : 'vp8' + }) + const [shareAudio, setShareAudio] = useState(() => readShareSettings().audio ?? false) const [createdInvite, setCreatedInvite] = useState('') const [error, setError] = useState('') @@ -111,6 +171,18 @@ function App() { void roomRef.current?.disconnect() }, []) + useEffect(() => { + localStorage.setItem( + SHARE_SETTINGS_KEY, + JSON.stringify({ + resolution: shareResolution, + frameRate: shareFrameRate, + codec: shareCodec, + audio: shareAudio, + }), + ) + }, [shareResolution, shareFrameRate, shareCodec, shareAudio]) + useEffect(() => { function syncFullscreenMode() { if (document.fullscreenElement === screenStageRef.current) { @@ -294,7 +366,20 @@ function App() { async function toggleScreenShare() { if (!roomRef.current) return try { - await roomRef.current.localParticipant.setScreenShareEnabled(!isSharing) + if (isSharing) { + await roomRef.current.localParticipant.setScreenShareEnabled(false) + } else { + const options = { + audio: shareAudio, + resolution: { ...SHARE_RESOLUTIONS[shareResolution], frameRate: shareFrameRate }, + } + const needsVp8Backup = shareCodec === 'vp9' || shareCodec === 'av1' || shareCodec === 'h265' + await roomRef.current.localParticipant.setScreenShareEnabled(true, options, { + videoCodec: shareCodec, + backupCodec: needsVp8Backup ? { codec: 'vp8' } : false, + degradationPreference: 'maintain-resolution', + }) + } } catch (cause) { setError(cause instanceof Error ? cause.message : '无法切换屏幕共享') } @@ -429,10 +514,62 @@ function App() { - {canShare && } + {canShare && ( + <> + + + + )} + {canShare && showShareSettings && ( +
setShowShareSettings(false)}> +
event.stopPropagation()}> +
+

共享屏幕设置

+ +
+
+ + + +
+ +

{isSharing ? '停止共享后可修改,设置会在下次共享时生效。' : '高级 Codec 会附带 VP8 兼容备份流;最终能否使用仍取决于发布端和观看端的浏览器解码能力。'}

+ +
+
+ )} +
{activeScreen ?