tag(功能):添加编码选项

This commit is contained in:
OrangeRoll
2026-08-07 15:26:48 +08:00
parent 03c608f3c9
commit 895dc39ce9
2 changed files with 222 additions and 2 deletions
+83
View File
@@ -269,6 +269,85 @@ button:disabled {
margin-left: 0; 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 { .screen-stage {
display: grid; display: grid;
min-height: min(72vh, 760px); min-height: min(72vh, 760px);
@@ -378,4 +457,8 @@ button:disabled {
.screen-thumbnail { .screen-thumbnail {
flex-basis: 170px; flex-basis: 170px;
} }
.settings-grid {
grid-template-columns: 1fr;
}
} }
+139 -2
View File
@@ -24,8 +24,54 @@ type TokenResponse = {
type ScreenFeed = { id: string; track: Track; label: string; isLocal: boolean } type ScreenFeed = { id: string; track: Track; label: string; isLocal: boolean }
type RoomHistoryItem = { id: string; title: string; visitedAt: string } 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 ROOM_HISTORY_KEY = 'screen-share-room-history'
const SHARE_SETTINGS_KEY = 'screen-share-share-settings'
const SHARE_RESOLUTIONS: Record<ShareResolution, { width: number; height: number }> = {
'1280x720': { width: 1280, height: 720 },
'1920x1080': { width: 1920, height: 1080 },
'2560x1440': { width: 2560, height: 1440 },
'3840x2160': { width: 3840, height: 2160 },
}
const VIDEO_CODECS: Record<VideoCodec, { label: string; description: string }> = {
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' }) { function ScreenTrack({ track, label, variant = 'main' }: ScreenFeed & { variant?: 'main' | 'thumbnail' }) {
const mediaContainerRef = useRef<HTMLDivElement>(null) const mediaContainerRef = useRef<HTMLDivElement>(null)
@@ -95,6 +141,20 @@ function App() {
const [remoteScreens, setRemoteScreens] = useState<ScreenFeed[]>([]) const [remoteScreens, setRemoteScreens] = useState<ScreenFeed[]>([])
const [activeScreenId, setActiveScreenId] = useState<string | null>(null) const [activeScreenId, setActiveScreenId] = useState<string | null>(null)
const [fullscreenMode, setFullscreenMode] = useState<'none' | 'page' | 'stage'>('none') const [fullscreenMode, setFullscreenMode] = useState<'none' | 'page' | 'stage'>('none')
const [showShareSettings, setShowShareSettings] = useState(false)
const [shareResolution, setShareResolution] = useState<ShareResolution>(() => {
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<VideoCodec>(() => {
const value = readShareSettings().codec
return value && value in VIDEO_CODECS ? value : 'vp8'
})
const [shareAudio, setShareAudio] = useState(() => readShareSettings().audio ?? false)
const [createdInvite, setCreatedInvite] = useState('') const [createdInvite, setCreatedInvite] = useState('')
const [error, setError] = useState('') const [error, setError] = useState('')
@@ -111,6 +171,18 @@ function App() {
void roomRef.current?.disconnect() 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(() => { useEffect(() => {
function syncFullscreenMode() { function syncFullscreenMode() {
if (document.fullscreenElement === screenStageRef.current) { if (document.fullscreenElement === screenStageRef.current) {
@@ -294,7 +366,20 @@ function App() {
async function toggleScreenShare() { async function toggleScreenShare() {
if (!roomRef.current) return if (!roomRef.current) return
try { 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) { } catch (cause) {
setError(cause instanceof Error ? cause.message : '无法切换屏幕共享') setError(cause instanceof Error ? cause.message : '无法切换屏幕共享')
} }
@@ -429,10 +514,62 @@ function App() {
<button type="button" className="secondary" onClick={() => void toggleStageFullscreen()} disabled={!activeScreen}> <button type="button" className="secondary" onClick={() => void toggleStageFullscreen()} disabled={!activeScreen}>
{fullscreenMode === 'stage' ? '退出全屏显示' : '全屏显示'} {fullscreenMode === 'stage' ? '退出全屏显示' : '全屏显示'}
</button> </button>
{canShare && <button type="button" className="share-button" onClick={() => void toggleScreenShare()}>{isSharing ? '停止共享屏幕' : '共享屏幕'}</button>} {canShare && (
<>
<button type="button" className="secondary" onClick={() => setShowShareSettings((visible) => !visible)}>
</button>
<button type="button" className="share-button" onClick={() => void toggleScreenShare()}>{isSharing ? '停止共享屏幕' : '共享屏幕'}</button>
</>
)}
<button type="button" className="secondary" onClick={leaveRoom}></button> <button type="button" className="secondary" onClick={leaveRoom}></button>
</section> </section>
{canShare && showShareSettings && (
<div className="modal-backdrop" role="presentation" onMouseDown={() => setShowShareSettings(false)}>
<section className="share-settings-modal" role="dialog" aria-modal="true" aria-labelledby="share-settings-title" onMouseDown={(event) => event.stopPropagation()}>
<div className="modal-heading">
<h2 id="share-settings-title"></h2>
<button type="button" className="icon-button" aria-label="关闭" onClick={() => setShowShareSettings(false)}>×</button>
</div>
<div className="settings-grid">
<label className="codec-setting">
/
<select value={shareCodec} onChange={(event) => setShareCodec(event.target.value as VideoCodec)} disabled={isSharing}>
{Object.entries(VIDEO_CODECS).map(([codec, details]) => <option key={codec} value={codec}>{details.label}</option>)}
</select>
<span className="codec-description">{VIDEO_CODECS[shareCodec].description}</span>
</label>
<label>
<select value={shareResolution} onChange={(event) => setShareResolution(event.target.value as ShareResolution)} disabled={isSharing}>
<option value="1280x720">1280 × 720720p</option>
<option value="1920x1080">1920 × 10801080p</option>
<option value="2560x1440">2560 × 14402K</option>
<option value="3840x2160">3840 × 21604K</option>
</select>
</label>
<label>
<select value={shareFrameRate} onChange={(event) => setShareFrameRate(Number(event.target.value))} disabled={isSharing}>
<option value={15}>15 FPS</option>
<option value={24}>24 FPS</option>
<option value={30}>30 FPS</option>
<option value={48}>48 FPS</option>
<option value={60}>60 FPS</option>
</select>
</label>
</div>
<label className="audio-setting">
<input type="checkbox" checked={shareAudio} onChange={(event) => setShareAudio(event.target.checked)} disabled={isSharing} />
</label>
<p className="settings-note">{isSharing ? '停止共享后可修改,设置会在下次共享时生效。' : '高级 Codec 会附带 VP8 兼容备份流;最终能否使用仍取决于发布端和观看端的浏览器解码能力。'}</p>
<button type="button" className="primary modal-confirm" onClick={() => setShowShareSettings(false)}></button>
</section>
</div>
)}
<section className="screen-stage" ref={screenStageRef}> <section className="screen-stage" ref={screenStageRef}>
{activeScreen {activeScreen
? <ScreenTrack {...activeScreen} variant="main" /> ? <ScreenTrack {...activeScreen} variant="main" />