diff --git a/src/client/lib/signaling.ts b/src/client/lib/signaling.ts index 328f548..c7485fb 100644 --- a/src/client/lib/signaling.ts +++ b/src/client/lib/signaling.ts @@ -154,11 +154,11 @@ export class SignalingClient { return this.setDisplayName(displayName); } - pauseSharing(): boolean { + setSharingPaused(paused: boolean): boolean { if (this.identity.role !== "host") { return false; } - this.identity.sharingPaused = true; + this.identity.sharingPaused = paused; if (!this.identity.shareGeneration) { return false; } @@ -166,7 +166,7 @@ export class SignalingClient { return this.send({ type: "set-sharing-paused", shareGeneration: this.identity.shareGeneration, - paused: true, + paused, }); } catch { return false; @@ -179,47 +179,6 @@ export class SignalingClient { } } - requestSharingResume(): boolean { - if (this.identity.role !== "host" || !this.identity.shareGeneration) { - return false; - } - try { - return this.send({ - type: "request-sharing-resume", - shareGeneration: this.identity.shareGeneration, - }); - } catch { - return false; - } - } - - confirmSharingResumed( - authorization: Extract< - ServerMessage, - { type: "sharing-resume-authorized" } - >, - ): boolean { - if ( - this.identity.role !== "host" || - !this.identity.shareGeneration || - authorization.shareGeneration !== this.identity.shareGeneration - ) { - return false; - } - let sent = false; - try { - sent = this.send({ - type: "sharing-source-enabled", - shareGeneration: authorization.shareGeneration, - codecGeneration: authorization.codecGeneration, - resumeAttempt: authorization.resumeAttempt, - }); - } catch { - sent = false; - } - return sent; - } - sendThenStop(message: ClientMessage): void { if (this.stopped) { return; diff --git a/src/client/media/codec-proof.ts b/src/client/media/codec-proof.ts deleted file mode 100644 index bcb1516..0000000 --- a/src/client/media/codec-proof.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { - codecProofEvidenceSchema, - type ClientMessage, - type CodecProofBinding, - type ServerMessage, -} from "../../shared/protocol"; -import type { ConnectionMetrics } from "../types"; - -type ProofRequest = Extract< - ServerMessage, - { type: "video-codec-proof-request" } ->; - -export interface CodecProofRouteSample { - routeRevision: number; - binding: CodecProofBinding; - metrics: ConnectionMetrics | null; -} - -interface DecodedBaseline { - sampleTimestampMs: number; - rtpStatsId: string; - rtpSsrc: number | null; - rtpMid: string | null; - rtpRid: string | null; - trackIdentifier: string | null; -} - -interface PendingProof { - request: ProofRequest; - baseline: DecodedBaseline | null; -} - -export class CodecProofReporter { - private pending: PendingProof | null = null; - - constructor( - private readonly send: (message: ClientMessage) => boolean, - ) {} - - request(request: ProofRequest): boolean { - this.pending = { - request, - baseline: null, - }; - return true; - } - - offer(sample: CodecProofRouteSample): void { - const pending = this.pending; - if (!pending) return; - if (!sameRequestRoute(pending.request, sample)) { - return; - } - const current = decodedBaseline(sample.metrics); - if (!current) return; - if (!pending.baseline) { - pending.baseline = current; - return; - } - if (current.sampleTimestampMs <= pending.baseline.sampleTimestampMs) { - return; - } - if (!sameRtpIdentity(pending.baseline, current)) { - pending.baseline = current; - return; - } - const framesDecodedDelta = sample.metrics?.intervalFramesDecoded; - if ( - framesDecodedDelta === null || - framesDecodedDelta === undefined || - !Number.isSafeInteger(framesDecodedDelta) || - framesDecodedDelta <= 0 - ) { - return; - } - const evidence = codecProofEvidenceSchema.safeParse({ - baselineSampleTimestampMs: pending.baseline.sampleTimestampMs, - sampleTimestampMs: current.sampleTimestampMs, - rtpStatsId: current.rtpStatsId, - rtpSsrc: current.rtpSsrc, - rtpMid: current.rtpMid, - rtpRid: current.rtpRid, - trackIdentifier: current.trackIdentifier, - framesDecodedDelta, - actualCodec: actualVideoCodec(sample.metrics?.codec ?? null), - }); - if (!evidence.success) return; - if ( - this.send({ - type: "video-codec-proof", - shareGeneration: pending.request.shareGeneration, - generation: pending.request.generation, - resumeAttempt: pending.request.resumeAttempt, - routeRevision: pending.request.routeRevision, - binding: pending.request.binding, - evidence: evidence.data, - }) - ) { - this.pending = null; - } - } - - clear(): void { - this.pending = null; - } -} - -function sameRequestRoute( - request: ProofRequest, - sample: CodecProofRouteSample, -): boolean { - return ( - request.routeRevision === sample.routeRevision && - sameProofBinding(request.binding, sample.binding) - ); -} - -function sameProofBinding( - left: CodecProofBinding, - right: CodecProofBinding, -): boolean { - return ( - left.kind === right.kind && - left.connectionId === right.connectionId && - (left.kind === "peer" || - (right.kind === "sfu" && - left.publicationGeneration === right.publicationGeneration)) - ); -} - -function decodedBaseline( - metrics: ConnectionMetrics | null, -): DecodedBaseline | null { - if ( - !metrics || - !Number.isFinite(metrics.sampleTimestampMs) || - metrics.sampleTimestampMs === null || - metrics.sampleTimestampMs < 0 || - !validIdentityString(metrics.rtpStatsId) || - !validOptionalIdentityString(metrics.rtpMid) || - !validOptionalIdentityString(metrics.rtpRid) || - !validOptionalIdentityString(metrics.trackIdentifier) || - (metrics.rtpSsrc !== null && - (!Number.isSafeInteger(metrics.rtpSsrc) || - metrics.rtpSsrc < 0 || - metrics.rtpSsrc > 0xffff_ffff)) - ) { - return null; - } - return { - sampleTimestampMs: metrics.sampleTimestampMs, - rtpStatsId: metrics.rtpStatsId, - rtpSsrc: metrics.rtpSsrc, - rtpMid: metrics.rtpMid, - rtpRid: metrics.rtpRid, - trackIdentifier: metrics.trackIdentifier, - }; -} - -function validIdentityString(value: string | null): value is string { - return value !== null && value.length >= 1 && value.length <= 512; -} - -function validOptionalIdentityString(value: string | null): boolean { - return value === null || validIdentityString(value); -} - -function sameRtpIdentity( - left: DecodedBaseline, - right: DecodedBaseline, -): boolean { - return ( - left.rtpStatsId === right.rtpStatsId && - left.rtpSsrc === right.rtpSsrc && - left.rtpMid === right.rtpMid && - left.rtpRid === right.rtpRid && - left.trackIdentifier === right.trackIdentifier - ); -} - -function actualVideoCodec(codec: string | null): "h264" | "vp8" | "other" { - const normalized = codec?.toLowerCase() ?? ""; - if (normalized.includes("h264")) return "h264"; - if (normalized.includes("vp8")) return "vp8"; - return "other"; -} diff --git a/src/client/media/host-sfu-route.ts b/src/client/media/host-sfu-route.ts index 3223572..5f38d62 100644 --- a/src/client/media/host-sfu-route.ts +++ b/src/client/media/host-sfu-route.ts @@ -1,12 +1,8 @@ import type { ClientMessage, - CodecPreparationBinding, - CodecTransitionGeneration, MediaRoutePhase, ParticipantRouteAssignment, - PreparedRouteCandidate, ServerMessage, - VideoCodecPreference, } from "../../shared/protocol"; import { SfuPublisher, @@ -47,8 +43,6 @@ interface HostPublisherSlot { connected: boolean; active: boolean; failed: boolean; - codecTransition: PreparedRouteCandidate["codecTransition"]; - codecPreparationReported: boolean; } export interface HostSfuPublisherSnapshot { @@ -57,22 +51,12 @@ export interface HostSfuPublisherSnapshot { audioSenderParameters: AudioSenderParameterReadback | null; } -export interface HostSfuCodecPreparationResult { - generation: CodecTransitionGeneration; - videoCodec: VideoCodecPreference; - binding: Extract; - accepted: boolean; -} - interface HostSfuRouteEvents { getStream: () => MediaStream | null; getProfile: () => QualityProfile; reconcileChildren: (childPeerIds: string[]) => void; send: (message: ClientMessage) => boolean; onPublisherUpdate?: (snapshot: HostSfuPublisherSnapshot | null) => void; - onCodecPublisherPrepared?: ( - result: HostSfuCodecPreparationResult, - ) => void; createPublisher?: ( onDisconnected: () => void, onStats: (metrics: ConnectionMetrics | null) => void, @@ -112,11 +96,7 @@ export class HostSfuRoute { if (pausedActiveReconciliation) { this.events.reconcileChildren(update.assignment.childPeerIds); } - if ( - this.paused && - update.phase === "prepare" && - !isPausedCodecSfuCandidate(update) - ) { + if (this.paused && update.phase === "prepare") { this.clearPending(); return result; } @@ -219,7 +199,7 @@ export class HostSfuRoute { if ( this.closed || !this.route.acceptsConfig(message.revision) || - (this.paused && !this.hasPausedCodecSfuCandidate()) + this.paused ) { return; } @@ -228,8 +208,6 @@ export class HostSfuRoute { const phase = this.route.getPhase(); const candidate = this.route.getPreparedCandidate(); const publicationGeneration = assignment?.sfuPublicationGeneration; - const codecTransition = - phase === "prepare" ? candidate?.codecTransition ?? null : null; if ( !token || !phase || @@ -239,18 +217,6 @@ export class HostSfuRoute { ) { return; } - if ( - codecTransition !== null && - this.active?.publicationGeneration === publicationGeneration - ) { - this.events.onCodecPublisherPrepared?.({ - ...codecTransition, - binding: { kind: "sfu", publicationGeneration }, - accepted: false, - }); - this.routeFailed(message.revision, "prepare", null); - return; - } if (this.active?.publicationGeneration === publicationGeneration) { this.active.revision = message.revision; if (phase === "active" && this.active.active) { @@ -284,8 +250,6 @@ export class HostSfuRoute { connected: false, active: false, failed: false, - codecTransition: codecTransition ? { ...codecTransition } : null, - codecPreparationReported: false, }; this.pending = slot; try { @@ -330,7 +294,7 @@ export class HostSfuRoute { setPaused(paused: boolean): void { this.paused = paused; - if (paused && !this.hasPausedCodecSfuCandidate()) { + if (paused) { this.clearPending(); } } @@ -342,7 +306,7 @@ export class HostSfuRoute { : Promise.all( slots.map((slot) => slot.publisher - .updateProfile(this.profileForSlot(slot, profile)) + .updateProfile(profile) .catch(() => false), ), ).then((results) => results.every(Boolean)); @@ -516,7 +480,7 @@ export class HostSfuRoute { private async preparePublisher(slot: HostPublisherSlot): Promise { const stream = this.events.getStream(); - const profile = this.profileForSlot(slot, this.events.getProfile()); + const profile = this.events.getProfile(); if (!stream) { this.handleFailure(slot); return false; @@ -542,7 +506,7 @@ export class HostSfuRoute { await disconnectPublisher(slot.publisher); return false; } - const latestProfile = this.profileForSlot(slot, this.events.getProfile()); + const latestProfile = this.events.getProfile(); if ( !qualitySettingsEqual(latestProfile, profile) && !(await slot.publisher.updateProfile(latestProfile)) @@ -551,11 +515,7 @@ export class HostSfuRoute { await disconnectPublisher(slot.publisher); return false; } - const prepared = this.pending === slot && this.ownsPublisherSlot(slot); - if (prepared) { - this.reportCodecPreparation(slot, true); - } - return prepared; + return this.pending === slot && this.ownsPublisherSlot(slot); } catch { this.handleFailure(slot); await disconnectPublisher(slot.publisher); @@ -571,42 +531,6 @@ export class HostSfuRoute { ); } - private hasPausedCodecSfuCandidate(): boolean { - const candidate = this.route.getPreparedCandidate(); - return ( - this.route.getPhase() === "prepare" && - candidate?.transport === "sfu" && - candidate.codecTransition !== null - ); - } - - private profileForSlot( - slot: HostPublisherSlot, - profile: QualityProfile, - ): QualityProfile { - return slot.codecTransition - ? { ...profile, videoCodec: slot.codecTransition.videoCodec } - : profile; - } - - private reportCodecPreparation( - slot: HostPublisherSlot, - accepted: boolean, - ): void { - if (!slot.codecTransition || slot.codecPreparationReported) { - return; - } - slot.codecPreparationReported = true; - this.events.onCodecPublisherPrepared?.({ - ...slot.codecTransition, - binding: { - kind: "sfu", - publicationGeneration: slot.publicationGeneration, - }, - accepted, - }); - } - private currentActiveToken( slot: HostPublisherSlot, ): RouteOperationToken | null { @@ -699,9 +623,6 @@ export class HostSfuRoute { this.active = null; } - if (matchesPlannedRoute) { - this.reportCodecPreparation(slot, false); - } if (revision === null || phase === null) { return; } @@ -769,14 +690,6 @@ export class HostSfuRoute { } } -function isPausedCodecSfuCandidate(update: RouteUpdateInput): boolean { - return ( - update.phase === "prepare" && - update.candidate.transport === "sfu" && - update.candidate.codecTransition !== null - ); -} - async function disconnectPublisher( publisher: HostPublisherTransport, ): Promise { diff --git a/src/client/media/host-source-authority.ts b/src/client/media/host-source-authority.ts deleted file mode 100644 index 76acc49..0000000 --- a/src/client/media/host-source-authority.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { - CodecTransitionGeneration, - ResumeAttempt, - ServerMessage, -} from "../../shared/protocol"; -import { setMediaPaused } from "./quality"; - -type PauseSharingSourceMessage = Extract< - ServerMessage, - { type: "pause-sharing-source" } ->; - -interface AuthoritativeHostPauseInput { - message: PauseSharingSourceMessage; - currentShareGeneration: string | null; - activeResumeAttempt: ResumeAttempt | null; - activeCodecGeneration: CodecTransitionGeneration | null; - stream: MediaStream | null; - pauseSfuRoute: () => void; - markHostPaused: () => void; -} - -export interface AuthoritativeHostPauseResult { - localCodecContextMatches: boolean; -} - -export function applyAuthoritativeHostPause({ - message, - currentShareGeneration, - activeResumeAttempt, - activeCodecGeneration, - stream, - pauseSfuRoute, - markHostPaused, -}: AuthoritativeHostPauseInput): AuthoritativeHostPauseResult | null { - if (message.shareGeneration !== currentShareGeneration) { - return null; - } - - if (stream) { - setMediaPaused(stream, true); - } - pauseSfuRoute(); - markHostPaused(); - - return { - localCodecContextMatches: - (message.resumeAttempt === null || - message.resumeAttempt === activeResumeAttempt) && - (message.codecGeneration === null || - message.codecGeneration === activeCodecGeneration), - }; -} diff --git a/src/client/media/quality.ts b/src/client/media/quality.ts index 9645d60..cb87139 100644 --- a/src/client/media/quality.ts +++ b/src/client/media/quality.ts @@ -26,7 +26,7 @@ export const QUALITY_PROFILES = { maxFramerate: 60, maxBitrate: 8_000_000, degradationPreference: "balanced", - videoCodec: "automatic", + videoCodec: "vp8", screenAudioQuality: "music", }, "1080p30": DEFAULT_QUALITY_SETTINGS, @@ -35,7 +35,7 @@ export const QUALITY_PROFILES = { maxFramerate: 30, maxBitrate: 3_000_000, degradationPreference: "balanced", - videoCodec: "automatic", + videoCodec: "vp8", screenAudioQuality: "music", }, } as const satisfies Record; @@ -145,7 +145,7 @@ export function videoQualitySettingsEqual( left.maxFramerate === right.maxFramerate && left.maxBitrate === right.maxBitrate && left.degradationPreference === right.degradationPreference && - (left.videoCodec ?? "automatic") === (right.videoCodec ?? "automatic") + (left.videoCodec ?? "vp8") === (right.videoCodec ?? "vp8") ); } diff --git a/src/client/media/route-transition.ts b/src/client/media/route-transition.ts index d45aa38..6935e7f 100644 --- a/src/client/media/route-transition.ts +++ b/src/client/media/route-transition.ts @@ -151,34 +151,14 @@ function sameCandidate( return ( left?.childPeerId === right.childPeerId && left.connectionId === right.connectionId && - left.transport === right.transport && - sameCodecTransition(left.codecTransition, right.codecTransition) - ); -} - -function sameCodecTransition( - left: PreparedRouteCandidate["codecTransition"] | undefined, - right: PreparedRouteCandidate["codecTransition"], -): boolean { - return ( - left === right || - (left !== null && - left !== undefined && - right !== null && - left.generation === right.generation && - left.videoCodec === right.videoCodec) + left.transport === right.transport ); } function cloneCandidate( candidate: PreparedRouteCandidate, ): PreparedRouteCandidate { - return { - ...candidate, - codecTransition: candidate.codecTransition - ? { ...candidate.codecTransition } - : null, - }; + return { ...candidate }; } export function reportActivePeerRouteFailure( diff --git a/src/client/media/viewer-sfu-route.ts b/src/client/media/viewer-sfu-route.ts index f82762c..5eebcf7 100644 --- a/src/client/media/viewer-sfu-route.ts +++ b/src/client/media/viewer-sfu-route.ts @@ -119,11 +119,7 @@ export class ViewerSfuRoute { if (pausedActiveReconciliation) { this.activateChildren(update.assignment.childPeerIds, update.revision); } - if ( - this.paused && - update.phase === "prepare" && - !isPausedCodecSfuCandidate(update) - ) { + if (this.paused && update.phase === "prepare") { this.discardPending(); return result; } @@ -203,21 +199,9 @@ export class ViewerSfuRoute { } setPaused(paused: boolean): void { - const wasPaused = this.paused; this.paused = paused; - if (paused && !this.hasPausedCodecSfuCandidate()) { + if (paused) { this.discardPending(); - } else if (paused && this.pending) { - this.pending.decodedFrame = false; - this.pending.readySent = false; - this.pending.subscriber.stopDecodedFrameProof(); - } else if ( - wasPaused && - !paused && - this.pending && - this.hasPausedCodecSfuCandidate() - ) { - this.pending.subscriber.armDecodedFrameProof(true); } } @@ -294,7 +278,7 @@ export class ViewerSfuRoute { if ( this.closed || !this.route.acceptsConfig(message.revision) || - (this.paused && !this.hasPausedCodecSfuCandidate()) + this.paused ) { return; } @@ -461,16 +445,6 @@ export class ViewerSfuRoute { ); } - private hasPausedCodecSfuCandidate(): boolean { - const candidate = this.route.getPreparedCandidate(); - return ( - this.route.getPhase() === "prepare" && - candidate?.childPeerId === this.viewerPeerId && - candidate.transport === "sfu" && - candidate.codecTransition !== null - ); - } - private queueActiveRoute( token: RouteOperationToken, acknowledge: boolean, @@ -804,14 +778,6 @@ export class ViewerSfuRoute { } -function isPausedCodecSfuCandidate(update: RouteUpdateInput): boolean { - return ( - update.phase === "prepare" && - update.candidate.transport === "sfu" && - update.candidate.codecTransition !== null - ); -} - async function disconnectSubscriber( subscriber: ViewerSubscriberTransport, ): Promise { diff --git a/src/client/pages/HostPage.tsx b/src/client/pages/HostPage.tsx index ae8f46d..47f89fa 100644 --- a/src/client/pages/HostPage.tsx +++ b/src/client/pages/HostPage.tsx @@ -28,8 +28,6 @@ import { type PreparedRouteCandidate, type ServerMessage, type CodeEntryPolicy, - type CodecTransitionGeneration, - type ResumeAttempt, } from "../../shared/protocol"; import { AppHeader } from "../components/AppHeader"; import { ConnectionSelfCheck } from "../components/ConnectionSelfCheck"; @@ -98,7 +96,6 @@ import { HostSfuRoute, type HostSfuPublisherSnapshot, } from "../media/host-sfu-route"; -import { applyAuthoritativeHostPause } from "../media/host-source-authority"; import { HostProvisionalChild, } from "../media/host-provisional-child"; @@ -128,24 +125,10 @@ import { shouldPauseLocalPreview, sourceSwitchNotice, type HostAction, - VIDEO_CODEC_TRANSITION_FAILED_NOTICE, videoCodecLockNotice, } from "./host-page-notices"; type HostPhase = "idle" | "starting" | "live" | "ended" | "error"; -type CodecTransitionPhase = - | "preparing" - | "prepared" - | "proving" - | "rollback-preparing" - | "rollback-prepared" - | "failed"; - -interface CodecTransitionState { - generation: CodecTransitionGeneration | null; - requested: VideoCodecPreference; - rollback: boolean; -} type ViewerQualityEvidence = Extract< ServerMessage, @@ -291,9 +274,6 @@ export function HostPage({ onAuthorizationRequired }: HostPageProps = {}) { const [switchingSource, setSwitchingSource] = useState(false); const [changingQuality, setChangingQuality] = useState(false); const [sharingPaused, setSharingPaused] = useState(false); - const [resumePending, setResumePending] = useState(false); - const [codecTransitionPhase, setCodecTransitionPhase] = - useState(null); const [localPreviewPaused, setLocalPreviewPaused] = useState(false); const [showConnectionDetails, setShowConnectionDetails] = useState(false); const [showTopology, setShowTopology] = useState(false); @@ -329,10 +309,6 @@ export function HostPage({ onAuthorizationRequired }: HostPageProps = {}) { const qualityChangeRef = useRef(null); const qualitySettingsRef = useRef(DEFAULT_QUALITY_SETTINGS); const sharingPausedRef = useRef(false); - const resumePendingRef = useRef(false); - const activeResumeAttemptRef = useRef(null); - const codecTransitionRef = useRef(null); - const codecTransitionPhaseRef = useRef(null); const retiringStreamRef = useRef(null); const hostSfuRouteRef = useRef(null); const sfuStandbyPrewarmerRef = useRef(null); @@ -510,31 +486,6 @@ export function HostPage({ onAuthorizationRequired }: HostPageProps = {}) { setSfuPublisherSnapshot(snapshot); } }, - onCodecPublisherPrepared: (result) => { - const shareGeneration = shareGenerationRef.current; - if ( - !shareGeneration || - !isCurrentGeneration(generation) || - hostSfuRouteRef.current !== route - ) { - return; - } - const rollback = - codecTransitionRef.current?.rollback === true || - codecTransitionPhaseRef.current === "rollback-preparing"; - codecTransitionRef.current = { - generation: result.generation, - requested: result.videoCodec, - rollback, - }; - signalRef.current?.send({ - type: "video-codec-prepared", - shareGeneration, - generation: result.generation, - binding: result.binding, - accepted: result.accepted, - }); - }, }); hostSfuRouteRef.current = route; return route; @@ -625,12 +576,6 @@ export function HostPage({ onAuthorizationRequired }: HostPageProps = {}) { setChangingQuality(false); sharingPausedRef.current = false; setSharingPaused(false); - resumePendingRef.current = false; - setResumePending(false); - activeResumeAttemptRef.current = null; - codecTransitionRef.current = null; - codecTransitionPhaseRef.current = null; - setCodecTransitionPhase(null); } function forgetRoom(): void { @@ -794,45 +739,7 @@ export function HostPage({ onAuthorizationRequired }: HostPageProps = {}) { } function changeVideoCodec(videoCodec: VideoCodecPreference): void { - if (phase === "starting") { - return; - } - if (phase === "live") { - const shareGeneration = shareGenerationRef.current; - if ( - !sharingPausedRef.current || - !peerAssistedRef.current || - !shareGeneration || - changingQuality || - switchingSource || - codecTransitionRef.current - ) { - return; - } - if ( - (qualitySettingsRef.current.videoCodec ?? "automatic") === videoCodec - ) { - return; - } - if ( - signalRef.current?.send({ - type: "request-video-codec-transition", - shareGeneration, - videoCodec, - }) !== true - ) { - setNotice("信令正在恢复,暂时无法切换视频编码"); - return; - } - codecTransitionRef.current = { - generation: null, - requested: videoCodec, - rollback: false, - }; - codecTransitionPhaseRef.current = "preparing"; - setCodecTransitionPhase("preparing"); - setAdvancedQuality((current) => ({ ...current, videoCodec })); - setNotice("正在准备视频编码"); + if (phase === "starting" || phase === "live") { return; } const next = { ...qualitySettingsRef.current, videoCodec }; @@ -856,24 +763,10 @@ export function HostPage({ onAuthorizationRequired }: HostPageProps = {}) { preserveAdvancedDraft = false, ): Promise { const previousProfile = qualitySettingsRef.current; - const codecTransition = codecTransitionRef.current; if ( phase === "live" && - (nextProfile.videoCodec ?? "automatic") !== - (previousProfile.videoCodec ?? "automatic") - ) { - return; - } - if ( - phase === "live" && - codecTransition && - !videoQualitySettingsEqual(previousProfile, nextProfile) - ) { - return; - } - if ( - phase === "live" && - codecTransitionPhaseRef.current === "failed" + (nextProfile.videoCodec ?? "vp8") !== + (previousProfile.videoCodec ?? "vp8") ) { return; } @@ -905,10 +798,6 @@ export function HostPage({ onAuthorizationRequired }: HostPageProps = {}) { const audioChanged = resolveScreenAudioQuality(previousProfile.screenAudioQuality) !== resolveScreenAudioQuality(nextProfile.screenAudioQuality); - const endpointProfile = codecTransition - ? { ...nextProfile, videoCodec: codecTransition.requested } - : nextProfile; - try { if (videoChanged) { await applyCaptureProfile(activeStream, nextProfile); @@ -936,14 +825,14 @@ export function HostPage({ onAuthorizationRequired }: HostPageProps = {}) { Promise.all( [ ...[...peersRef.current.values()].map((peer) => - peer.updateProfile(endpointProfile), + peer.updateProfile(nextProfile), ), ...(hostProvisionalChildRef.current - ? [hostProvisionalChildRef.current.updateProfile(endpointProfile)] + ? [hostProvisionalChildRef.current.updateProfile(nextProfile)] : []), ], ), - activeSfuRoute?.updateProfile(endpointProfile) ?? + activeSfuRoute?.updateProfile(nextProfile) ?? Promise.resolve(true), ]); if ( @@ -994,16 +883,21 @@ export function HostPage({ onAuthorizationRequired }: HostPageProps = {}) { return; } if (sharingPausedRef.current) { - if (resumePendingRef.current) { + if (!setMediaPaused(activeStream, false)) { + setNotice("当前分享没有可恢复的媒体轨道"); return; } - if (signalRef.current?.requestSharingResume() !== true) { + hostSfuRouteRef.current?.setPaused(false); + if (signalRef.current?.setSharingPaused(false) !== true) { + setMediaPaused(activeStream, true); + hostSfuRouteRef.current?.setPaused(true); + signalRef.current?.confirmSharingPaused(); setNotice("信令正在恢复,分享仍保持暂停"); return; } - resumePendingRef.current = true; - setResumePending(true); - setNotice("正在恢复分享"); + sharingPausedRef.current = false; + setSharingPaused(false); + setNotice("音视频分享已恢复"); return; } if (!setMediaPaused(activeStream, true)) { @@ -1014,17 +908,11 @@ export function HostPage({ onAuthorizationRequired }: HostPageProps = {}) { setSharingPaused(true); hostSfuRouteRef.current?.setPaused(true); discardPreparedHostChild(); - signalRef.current?.pauseSharing(); - activeResumeAttemptRef.current = null; - const codecState = codecTransitionRef.current; - if (codecState && codecTransitionPhaseRef.current === "proving") { - const nextPhase = codecState.rollback - ? "rollback-prepared" - : "prepared"; - codecTransitionPhaseRef.current = nextPhase; - setCodecTransitionPhase(nextPhase); - } - setNotice("音视频分享已暂停"); + setNotice( + signalRef.current?.setSharingPaused(true) === true + ? "音视频分享已暂停" + : "信令正在恢复,分享保持暂停", + ); } async function enterPreviewFullscreen(): Promise { @@ -1280,55 +1168,6 @@ export function HostPage({ onAuthorizationRequired }: HostPageProps = {}) { ); } - async function prepareVideoCodec( - message: Extract, - generation: number, - ): Promise { - const shareGeneration = shareGenerationRef.current; - if ( - !shareGeneration || - message.shareGeneration !== shareGeneration || - !isCurrentGeneration(generation) - ) { - return; - } - - const rollback = - codecTransitionRef.current?.rollback === true || - codecTransitionPhaseRef.current === "rollback-preparing"; - codecTransitionRef.current = { - generation: message.generation, - requested: message.videoCodec, - rollback, - }; - - let accepted = false; - if (message.binding.kind === "peer") { - const peer = peersRef.current.get(message.binding.childPeerId); - if (peer?.connectionId === message.binding.connectionId) { - peer.cancelVideoCodecPreparation(); - accepted = await peer.prepareVideoCodec( - message.generation, - message.videoCodec, - ); - } - } - - if ( - !isCurrentGeneration(generation) || - shareGenerationRef.current !== shareGeneration - ) { - return; - } - signalRef.current?.send({ - type: "video-codec-prepared", - shareGeneration, - generation: message.generation, - binding: message.binding, - accepted, - }); - } - function handleSignalMessage( message: ServerMessage, generation: number, @@ -1339,15 +1178,6 @@ export function HostPage({ onAuthorizationRequired }: HostPageProps = {}) { return; } if (message.type === "authenticated" && message.role === "host") { - peersRef.current.forEach((peer) => - peer.cancelVideoCodecPreparation(), - ); - codecTransitionRef.current = null; - codecTransitionPhaseRef.current = null; - setCodecTransitionPhase(null); - resumePendingRef.current = false; - setResumePending(false); - activeResumeAttemptRef.current = null; discardPreparedHostChild(); hostPeerIdRef.current = message.peerId; endpointMediaCopyCapacityRef.current = message.endpointMediaCopyCapacity; @@ -1374,32 +1204,21 @@ export function HostPage({ onAuthorizationRequired }: HostPageProps = {}) { ...message.routeAssignment.childPeerIds, ]; peerAssistedRef.current = true; - const activeCodecTransition = codecTransitionRef.current; if (reauthenticated) { - commitQuality( - message.qualitySettings, - activeCodecTransition !== null, - ); - if (activeCodecTransition === null) { - const endpointUpdates = [ - ...[...peersRef.current.values()].map((peer) => - peer.updateProfile(message.qualitySettings), - ), - ...(hostProvisionalChildRef.current - ? [ - hostProvisionalChildRef.current.updateProfile( - message.qualitySettings, - ), - ] - : []), - ]; - void Promise.allSettled(endpointUpdates); - } - } else { - signalRef.current?.send({ - type: "set-quality-settings", - qualitySettings: qualitySettingsRef.current, - }); + commitQuality(message.qualitySettings); + const endpointUpdates = [ + ...[...peersRef.current.values()].map((peer) => + peer.updateProfile(message.qualitySettings), + ), + ...(hostProvisionalChildRef.current + ? [ + hostProvisionalChildRef.current.updateProfile( + message.qualitySettings, + ), + ] + : []), + ]; + void Promise.allSettled(endpointUpdates); } const route = ensureHostSfuRoute(generation); void route @@ -1409,11 +1228,7 @@ export function HostPage({ onAuthorizationRequired }: HostPageProps = {}) { assignment: message.routeAssignment, }) .then(async () => { - if ( - reauthenticated && - codecTransitionRef.current === null && - hostSfuRouteRef.current === route - ) { + if (reauthenticated && hostSfuRouteRef.current === route) { await route.updateProfile(message.qualitySettings); } showHostSfuQualityWarning(route, generation); @@ -1503,166 +1318,19 @@ export function HostPage({ onAuthorizationRequired }: HostPageProps = {}) { downloadDiagnosticReport("host", connections, message.snapshot); return; } - if (message.type === "video-codec-prepare") { - void prepareVideoCodec(message, generation); - return; - } - if (message.type === "video-codec-result") { - let transition = codecTransitionRef.current; - if ( - !transition && - message.status === "failed" && - message.failure === "stale-binding" - ) { - transition = { - generation: message.generation, - requested: message.videoCodec, - rollback: true, - }; - codecTransitionRef.current = transition; - } - if ( - !transition || - (transition.generation !== null && - transition.generation !== message.generation) - ) { - return; - } - transition.generation = message.generation; - if (message.status === "prepared") { - codecTransitionPhaseRef.current = "prepared"; - setCodecTransitionPhase("prepared"); - setNotice("视频编码已准备,恢复分享后完成验证"); - return; - } - if (message.status === "rollback-prepared") { - transition.rollback = true; - transition.requested = message.videoCodec; - codecTransitionPhaseRef.current = "rollback-prepared"; - setCodecTransitionPhase("rollback-prepared"); - setAdvancedQuality((current) => ({ - ...current, - videoCodec: message.videoCodec, - })); - setNotice("新编码未通过验证;原编码已准备,恢复分享后完成恢复"); - return; - } - if (message.status === "committed") { - activeResumeAttemptRef.current = null; - codecTransitionRef.current = null; - codecTransitionPhaseRef.current = null; - setCodecTransitionPhase(null); - commitQuality( - { ...qualitySettingsRef.current, videoCodec: message.videoCodec }, - true, - ); - setAdvancedQuality((current) => ({ - ...current, - videoCodec: message.videoCodec, - })); - setNotice( - message.failure - ? "已恢复原视频编码" - : `视频编码已切换为 ${VIDEO_CODEC_PREFERENCE_LABELS[message.videoCodec]}`, - ); - } else { - transition.requested = message.videoCodec; - transition.rollback = true; - codecTransitionPhaseRef.current = "failed"; - setCodecTransitionPhase("failed"); - resumePendingRef.current = false; - setResumePending(false); - activeResumeAttemptRef.current = null; - setAdvancedQuality((current) => ({ - ...current, - videoCodec: message.videoCodec, - })); - setNotice(VIDEO_CODEC_TRANSITION_FAILED_NOTICE); - } - return; - } - if (message.type === "sharing-resume-authorized") { + if (message.type === "pause-sharing-source") { if (message.shareGeneration !== shareGenerationRef.current) { return; } - const transition = codecTransitionRef.current; - if ( - (message.codecGeneration === null && transition !== null) || - (message.codecGeneration !== null && - (transition?.generation !== message.codecGeneration || - (codecTransitionPhaseRef.current !== "prepared" && - codecTransitionPhaseRef.current !== "rollback-prepared"))) - ) { - activeResumeAttemptRef.current = null; - signalRef.current?.pauseSharing(); - resumePendingRef.current = false; - setResumePending(false); - setNotice("恢复授权已失效,分享仍保持暂停"); - return; - } - activeResumeAttemptRef.current = message.resumeAttempt; const activeStream = streamRef.current; - if (!activeStream || !setMediaPaused(activeStream, false)) { - activeResumeAttemptRef.current = null; - signalRef.current?.pauseSharing(); - resumePendingRef.current = false; - setResumePending(false); - setNotice("当前分享没有可恢复的媒体轨道"); - return; - } - hostSfuRouteRef.current?.setPaused(false); - if (signalRef.current?.confirmSharingResumed(message) !== true) { + if (activeStream) { setMediaPaused(activeStream, true); - hostSfuRouteRef.current?.setPaused(true); - signalRef.current?.pauseSharing(); - activeResumeAttemptRef.current = null; - resumePendingRef.current = false; - setResumePending(false); - setNotice("信令正在恢复,分享仍保持暂停"); - return; - } - sharingPausedRef.current = false; - setSharingPaused(false); - resumePendingRef.current = false; - setResumePending(false); - if (message.codecGeneration !== null) { - codecTransitionPhaseRef.current = "proving"; - setCodecTransitionPhase("proving"); - setNotice("正在验证视频编码"); - } else { - setNotice("音视频分享已恢复"); - } - return; - } - if (message.type === "pause-sharing-source") { - const transition = codecTransitionRef.current; - const pause = applyAuthoritativeHostPause({ - message, - currentShareGeneration: shareGenerationRef.current, - activeResumeAttempt: activeResumeAttemptRef.current, - activeCodecGeneration: transition?.generation ?? null, - stream: streamRef.current, - pauseSfuRoute: () => hostSfuRouteRef.current?.setPaused(true), - markHostPaused: () => { - sharingPausedRef.current = true; - setSharingPaused(true); - signalRef.current?.confirmSharingPaused(); - resumePendingRef.current = false; - setResumePending(false); - activeResumeAttemptRef.current = null; - }, - }); - if (!pause) { - return; - } - if (transition && pause.localCodecContextMatches) { - transition.rollback = true; - codecTransitionPhaseRef.current = "rollback-preparing"; - setCodecTransitionPhase("rollback-preparing"); - setNotice("新编码未通过验证,正在恢复原编码"); - } else { - setNotice("分享仍保持暂停"); } + hostSfuRouteRef.current?.setPaused(true); + sharingPausedRef.current = true; + setSharingPaused(true); + signalRef.current?.confirmSharingPaused(); + setNotice("分享仍保持暂停"); return; } if (message.type === "route-update") { @@ -1779,20 +1447,6 @@ export function HostPage({ onAuthorizationRequired }: HostPageProps = {}) { return; } if (message.type === "error") { - if ( - codecTransitionRef.current?.generation === null && - codecTransitionPhaseRef.current === "preparing" - ) { - codecTransitionRef.current = null; - codecTransitionPhaseRef.current = null; - setCodecTransitionPhase(null); - setAdvancedQuality((current) => ({ - ...current, - videoCodec: qualitySettingsRef.current.videoCodec ?? "automatic", - })); - } - resumePendingRef.current = false; - setResumePending(false); setViewerGrantUpdating(false); setViewerPasswordUpdating(false); viewerPasswordActionRef.current = null; @@ -1896,6 +1550,7 @@ export function HostPage({ onAuthorizationRequired }: HostPageProps = {}) { clientId: hostClientId, shareGeneration, sharingPaused: false, + qualitySettings: qualitySettingsRef.current, viewerPresence: true, viewerPasswordSettings: true, displayName: initialDisplayName, @@ -2298,14 +1953,7 @@ export function HostPage({ onAuthorizationRequired }: HostPageProps = {}) { : null; const activeCodeEntryPolicy = room?.codeEntryPolicy ?? creationProfile.codeEntryPolicy; - const codecLockNotice = videoCodecLockNotice( - phase, - sharingPaused, - codecTransitionPhase, - ); - const codecResumeReady = - codecTransitionPhase === "prepared" || - codecTransitionPhase === "rollback-prepared"; + const codecLockNotice = videoCodecLockNotice(phase); return (
@@ -2347,11 +1995,7 @@ export function HostPage({ onAuthorizationRequired }: HostPageProps = {}) { type="button" disabled={ switchingSource || - changingQuality || - resumePending || - (sharingPaused && - codecTransitionPhase !== null && - !codecResumeReady) + changingQuality } onClick={toggleSharingPause} > @@ -2360,11 +2004,7 @@ export function HostPage({ onAuthorizationRequired }: HostPageProps = {}) { ) : (