tag(功能):添加编码选项
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 × 720(720p)</option>
|
||||||
|
<option value="1920x1080">1920 × 1080(1080p)</option>
|
||||||
|
<option value="2560x1440">2560 × 1440(2K)</option>
|
||||||
|
<option value="3840x2160">3840 × 2160(4K)</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" />
|
||||||
|
|||||||
Reference in New Issue
Block a user