diff --git a/entry/src/main/ets/pages/StreamPage.ets b/entry/src/main/ets/pages/StreamPage.ets index 860095e..0790888 100644 --- a/entry/src/main/ets/pages/StreamPage.ets +++ b/entry/src/main/ets/pages/StreamPage.ets @@ -918,7 +918,7 @@ struct StreamPage { }, onToggleWindowMode: async () => { await this.windowManager?.toggleWindowMode(); - this.recalculateXComponentSize(); + await this.recalculateXComponentSize(); }, onTogglePanZoom: () => { this.isTouchOverrideEnabled = !this.isTouchOverrideEnabled; @@ -1044,15 +1044,28 @@ struct StreamPage { if (this.windowManager.isFullscreen) { // 全屏:按屏幕尺寸精确计算 letterbox/pillarbox + const screenSize = await this.windowManager.updateScreenSize(); + this.viewModel.displayInfo.screenWidth = screenSize.width; + this.viewModel.displayInfo.screenHeight = screenSize.height; + this.panZoomHandler.setParentSize(screenSize.width, screenSize.height); const size = this.windowManager.calculateXComponentSize( this.viewModel.displayInfo.stretchVideo ); this.xComponentWidth = size.width; this.xComponentHeight = size.height; } else { - // 窗口模式:直接填满窗口,视频缩放由 Surface 处理 - this.xComponentWidth = '100%'; - this.xComponentHeight = '100%'; + // 窗口/浮窗:按实际窗口内容区等比适配,避免 Surface 被父容器裁剪 + const viewportSize = await this.windowManager.getCurrentWindowViewportSize(); + const size = this.windowManager.calculateXComponentSizeForViewport( + viewportSize.width, + viewportSize.height, + this.viewModel.displayInfo.stretchVideo + ); + this.viewModel.displayInfo.screenWidth = viewportSize.width; + this.viewModel.displayInfo.screenHeight = viewportSize.height; + this.panZoomHandler.setParentSize(viewportSize.width, viewportSize.height); + this.xComponentWidth = size.width; + this.xComponentHeight = size.height; } // 更新画面位置和偏移 this.updateDisplayPosition(); diff --git a/entry/src/main/ets/service/streaming/StreamLifecycleManager.ets b/entry/src/main/ets/service/streaming/StreamLifecycleManager.ets index bfb0481..983eeed 100644 --- a/entry/src/main/ets/service/streaming/StreamLifecycleManager.ets +++ b/entry/src/main/ets/service/streaming/StreamLifecycleManager.ets @@ -193,21 +193,31 @@ export class StreamLifecycleManager { */ async updateScreenSizeAfterRotation(): Promise { try { - const screenSize = await this.windowManager?.updateScreenSize(); - if (screenSize) { - if (this.viewModel) { - this.viewModel.displayInfo.screenWidth = screenSize.width; - this.viewModel.displayInfo.screenHeight = screenSize.height; - } + if (!this.windowManager) return; + + let viewportSize = await this.windowManager.updateScreenSize(); + if (!this.windowManager.isFullscreen) { + viewportSize = await this.windowManager.getCurrentWindowViewportSize(); + } + + if (this.viewModel) { + this.viewModel.displayInfo.screenWidth = viewportSize.width; + this.viewModel.displayInfo.screenHeight = viewportSize.height; + } - // 重新计算 XComponent 尺寸 - const xComponentSize = this.windowManager?.calculateXComponentSize( + // 重新计算 XComponent 尺寸 + const xComponentSize = this.windowManager.isFullscreen + ? this.windowManager.calculateXComponentSize( + this.viewModel?.displayInfo.stretchVideo ?? false + ) + : this.windowManager.calculateXComponentSizeForViewport( + viewportSize.width, + viewportSize.height, this.viewModel?.displayInfo.stretchVideo ?? false ); - if (xComponentSize) { - this.callbacks?.updateXComponentSize(xComponentSize.width, xComponentSize.height); - this.callbacks?.updatePanZoomParentSize(screenSize.width, screenSize.height); - } + if (xComponentSize) { + this.callbacks?.updateXComponentSize(xComponentSize.width, xComponentSize.height); + this.callbacks?.updatePanZoomParentSize(viewportSize.width, viewportSize.height); } } catch (err) { console.warn('[StreamPage] 更新屏幕尺寸失败:', err); @@ -220,29 +230,37 @@ export class StreamLifecycleManager { */ async handleServerResolutionChanged(width: number, height: number): Promise { console.info(`[StreamPage] 服务端分辨率变化: ${width}x${height}`); + if (!this.windowManager) return; // 更新窗口管理器的串流分辨率 - this.windowManager?.setStreamResolution(width, height); + this.windowManager.setStreamResolution(width, height); // 重新配置窗口方向(可能会触发屏幕旋转) - await this.windowManager?.configureOrientation(); + await this.windowManager.configureOrientation(); - // 更新屏幕尺寸(等待旋转完成后获取正确的屏幕尺寸) - const screenSize = await this.windowManager?.updateScreenSize(); - if (screenSize && this.viewModel) { - this.viewModel.displayInfo.screenWidth = screenSize.width; - this.viewModel.displayInfo.screenHeight = screenSize.height; + // 更新视口尺寸(浮窗模式下使用实际窗口内容区) + let viewportSize = await this.windowManager.updateScreenSize(); + if (!this.windowManager.isFullscreen) { + viewportSize = await this.windowManager.getCurrentWindowViewportSize(); + } + if (this.viewModel) { + this.viewModel.displayInfo.screenWidth = viewportSize.width; + this.viewModel.displayInfo.screenHeight = viewportSize.height; } - // 重新计算 XComponent 尺寸以避免画面拉伸 - const xComponentSize = this.windowManager?.calculateXComponentSize( - this.viewModel?.displayInfo.stretchVideo ?? false - ); + // 重新计算 XComponent 尺寸以避免画面拉伸/裁剪 + const xComponentSize = this.windowManager.isFullscreen + ? this.windowManager.calculateXComponentSize( + this.viewModel?.displayInfo.stretchVideo ?? false + ) + : this.windowManager.calculateXComponentSizeForViewport( + viewportSize.width, + viewportSize.height, + this.viewModel?.displayInfo.stretchVideo ?? false + ); if (xComponentSize) { this.callbacks?.updateXComponentSize(xComponentSize.width, xComponentSize.height); - if (screenSize) { - this.callbacks?.updatePanZoomParentSize(screenSize.width, screenSize.height); - } + this.callbacks?.updatePanZoomParentSize(viewportSize.width, viewportSize.height); console.info(`[StreamPage] XComponent 尺寸更新: ${JSON.stringify(xComponentSize)}`); } } diff --git a/entry/src/main/ets/service/streaming/StreamWindowManager.ets b/entry/src/main/ets/service/streaming/StreamWindowManager.ets index dddebc4..67cb81b 100644 --- a/entry/src/main/ets/service/streaming/StreamWindowManager.ets +++ b/entry/src/main/ets/service/streaming/StreamWindowManager.ets @@ -47,6 +47,8 @@ export class StreamWindowManager { private onWindowSizeChanged: (() => void) | null = null; /** windowSizeChange 监听器回调引用(用于 off 取消注册) */ private windowSizeChangeHandler: ((size: window.Size) => void) | null = null; + /** windowSizeChange 防抖定时器 */ + private windowSizeDebounceTimer: number = -1; /** 查询当前是否全屏 */ get isFullscreen(): boolean { @@ -161,6 +163,8 @@ export class StreamWindowManager { const win = await window.getLastWindow(this.context); if (this._isFullscreen) { + // 先注册监听,再执行 recover,避免窗口恢复过程中的首个尺寸事件丢失 + this.registerWindowSizeListener(win); await this.exitImmersiveFullscreen(win); try { await win.recover(); @@ -168,14 +172,14 @@ export class StreamWindowManager { console.info('StreamWindowManager: recover() 不可用,保持最大化'); } this._isFullscreen = false; - // 注册窗口大小变化监听(窗口模式下用户可能拖拽缩放) - this.registerWindowSizeListener(win); + this.notifyWindowSizeChanged(); console.info('StreamWindowManager: 已切换到窗口模式'); } else { // 取消窗口大小变化监听 this.unregisterWindowSizeListener(win); await this.enterImmersiveFullscreen(win); this._isFullscreen = true; + this.notifyWindowSizeChanged(); console.info('StreamWindowManager: 已切换到全屏模式'); } } catch (err) { @@ -188,16 +192,8 @@ export class StreamWindowManager { */ private registerWindowSizeListener(win: window.Window): void { this.unregisterWindowSizeListener(win); - let debounceTimer: number = -1; this.windowSizeChangeHandler = (_size: window.Size) => { - // 防抖:拖拽缩放时高频触发,200ms 后才真正回调 - if (debounceTimer !== -1) { - clearTimeout(debounceTimer); - } - debounceTimer = Number(setTimeout(() => { - debounceTimer = -1; - this.onWindowSizeChanged?.(); - }, 200)); + this.notifyWindowSizeChanged(); }; try { win.on('windowSizeChange', this.windowSizeChangeHandler); @@ -214,6 +210,51 @@ export class StreamWindowManager { } catch (_e) {} this.windowSizeChangeHandler = null; } + if (this.windowSizeDebounceTimer !== -1) { + clearTimeout(this.windowSizeDebounceTimer); + this.windowSizeDebounceTimer = -1; + } + } + + /** + * 防抖通知布局重算。窗口 recover/maximize、用户拖拽和系统布局 settle + * 都从这里汇总,避免页面层用固定延时猜窗口何时稳定。 + */ + private notifyWindowSizeChanged(): void { + if (this.windowSizeDebounceTimer !== -1) { + clearTimeout(this.windowSizeDebounceTimer); + } + this.windowSizeDebounceTimer = Number(setTimeout(() => { + this.windowSizeDebounceTimer = -1; + this.onWindowSizeChanged?.(); + }, 120)); + } + + /** + * 获取当前窗口内容区域尺寸(vp)。 + * + * 浮窗/自由窗口下默认屏幕尺寸不等于实际可绘制窗口尺寸,继续按屏幕计算会导致 + * XComponent 超出窗口后被父容器裁剪,看起来像画面被放大裁切。 + */ + async getCurrentWindowViewportSize(): Promise { + try { + const win = await window.getLastWindow(this.context); + const properties = win.getWindowProperties(); + let rect: window.Rect = properties.windowRect; + if (properties.drawableRect.width > 0 && properties.drawableRect.height > 0) { + rect = properties.drawableRect; + } + const displayInfo = display.getDefaultDisplaySync(); + const density = displayInfo.densityPixels; + + return { + width: rect.width / density, + height: rect.height / density + }; + } catch (err) { + console.warn('StreamWindowManager: 获取窗口内容区尺寸失败:', err); + return this.getScreenSize(); + } } // ==================== 沉浸式全屏辅助方法 ==================== @@ -424,6 +465,40 @@ export class StreamWindowManager { } } + /** + * 按指定视口尺寸计算 XComponent 尺寸。 + * 全屏和浮窗共用同一套等比适配逻辑,避免窗口模式下 Surface 拉伸/裁剪。 + */ + calculateXComponentSizeForViewport(viewportWidth: number, viewportHeight: number, + stretchVideo: boolean = false): XComponentSize { + if (stretchVideo) { + return { width: '100%', height: '100%' }; + } + if (viewportWidth <= 0 || viewportHeight <= 0) { + return { width: '100%', height: '100%' }; + } + + const videoAspectRatio = this.streamWidth / this.streamHeight; + const viewportAspectRatio = viewportWidth / viewportHeight; + let measuredWidth: number; + let measuredHeight: number; + + if (viewportAspectRatio > videoAspectRatio) { + measuredHeight = viewportHeight; + measuredWidth = Math.floor(measuredHeight * videoAspectRatio); + } else { + measuredWidth = viewportWidth; + measuredHeight = Math.floor(measuredWidth / videoAspectRatio); + } + + console.info('StreamWindowManager: 视口视频尺寸计算 - 视口=' + + viewportWidth.toFixed(1) + 'x' + viewportHeight.toFixed(1) + 'vp, 视频=' + + this.streamWidth.toString() + 'x' + this.streamHeight.toString() + ', XComponent=' + + measuredWidth.toFixed(1) + 'x' + measuredHeight.toFixed(1) + 'vp'); + + return { width: measuredWidth, height: measuredHeight }; + } + // ==================== 初始化 ==================== /**