From a1c29246d70a26589c958d6f99e8337464c979d1 Mon Sep 17 00:00:00 2001 From: tongtongstudio Date: Thu, 23 Jul 2026 09:26:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Windows=E5=A2=9E=E5=8A=A0=E5=88=86?= =?UTF-8?q?=E8=BE=A8=E7=8E=87=E9=80=82=E5=BA=94=EF=BC=8C=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E5=88=86=E8=BE=A8=E7=8E=87=E6=8C=89=E9=92=AE=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- webrtc_controller_flutter/lib/main.dart | 161 +++++++++++++----- .../windows/runner/flutter_window.cpp | 87 ++++++++++ .../windows/runner/flutter_window.h | 11 ++ .../windows/runner/main.cpp | 3 +- 4 files changed, 218 insertions(+), 44 deletions(-) diff --git a/webrtc_controller_flutter/lib/main.dart b/webrtc_controller_flutter/lib/main.dart index 0e67fe8..3bb0474 100644 --- a/webrtc_controller_flutter/lib/main.dart +++ b/webrtc_controller_flutter/lib/main.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'config/ice_servers.dart'; @@ -33,6 +35,12 @@ class ControllerHome extends StatefulWidget { } class _ControllerHomeState extends State { + /// 与 Windows 原生窗口通信的通道(用于按视频比例调整窗口高度)。 + static const MethodChannel _windowChannel = MethodChannel('app/window'); + + /// 记录上一次已应用的视频宽高比,避免重复调整窗口。 + double _lastResizedAspect = 0; + final _serverUrlController = TextEditingController( text: kDefaultSignalServer, ); @@ -217,10 +225,22 @@ class _ControllerHomeState extends State { final w = _renderer?.value.width ?? 0; final h = _renderer?.value.height ?? 0; if (w > 0 && h > 0 && mounted) { - setState(() => _videoAspect = w / h); + final aspect = w / h; + setState(() => _videoAspect = aspect); + _resizeWindowToAspect(aspect); } } + /// Windows 端:保持窗口宽度不变,按视频宽高比调整窗口高度; + /// 若高度超出屏幕,由原生端改为合适的窗口大小。 + void _resizeWindowToAspect(double aspect) { + if (kIsWeb || defaultTargetPlatform != TargetPlatform.windows) return; + if (aspect <= 0) return; + if ((aspect - _lastResizedAspect).abs() < 0.001) return; + _lastResizedAspect = aspect; + _windowChannel.invokeMethod('resizeToVideoAspect', aspect); + } + /// ICE 断开:提示用户并自动返回连接设置面板。 Future _onIceDisconnected(String message) async { _showAlert(message); @@ -239,10 +259,52 @@ class _ControllerHomeState extends State { setState(() => _connected = false); } + /// 弹出分辨率选择菜单(iOS 风格 ActionSheet)。 + void _showResolutionMenu() { + showCupertinoModalPopup( + context: context, + builder: (ctx) => CupertinoActionSheet( + title: const Text('切换分辨率'), + actions: [ + for (int i = 0; i < _resolutionOptions.length; i++) + CupertinoActionSheetAction( + onPressed: () { + Navigator.of(ctx).pop(); + setState(() => _selectedResolution = i); + final o = _resolutionOptions[i]; + _controller?.sendResolutionChange( + o['width'] as int, + o['height'] as int, + o['fps'] as int, + ); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (i == _selectedResolution) ...[ + const Icon(CupertinoIcons.check_mark, + size: 18, color: CupertinoColors.activeBlue), + const SizedBox(width: 8), + ], + Text(_resolutionOptions[i]['label'] as String), + ], + ), + ), + ], + cancelButton: CupertinoActionSheetAction( + isDefaultAction: true, + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('取消'), + ), + ), + ); + } + Future _disconnect() async { await _controller?.disconnect(); _controller = null; _renderer?.removeListener(_onRendererUpdate); + _lastResizedAspect = 0; setState(() { _connected = false; _renderer = null; @@ -319,6 +381,58 @@ class _ControllerHomeState extends State { ); } + /// 右上角浮动顶部菜单栏:整合「分辨率切换」与「断开连接」。 + Widget _buildTopMenuBar() { + return Container( + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(22), + ), + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // 分辨率菜单按钮:显示当前分辨率标签 + CupertinoButton( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + onPressed: _showResolutionMenu, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(CupertinoIcons.slider_horizontal_3, + color: CupertinoColors.white, size: 20), + const SizedBox(width: 6), + Text( + _resolutionOptions[_selectedResolution]['label'] as String, + style: const TextStyle( + color: CupertinoColors.white, + fontSize: 14, + ), + ), + ], + ), + ), + // 分隔线 + Container( + width: 1, + height: 22, + color: CupertinoColors.white.withValues(alpha: 0.3), + ), + // 断开连接按钮 + CupertinoButton( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + onPressed: _disconnect, + child: const Icon( + CupertinoIcons.xmark_circle_fill, + color: CupertinoColors.destructiveRed, + size: 24, + ), + ), + ], + ), + ); + } + Widget _buildControlPanel() { return SafeArea( // 仅保留顶部安全区(避开系统状态栏),底部/左右保持全屏, @@ -378,48 +492,9 @@ class _ControllerHomeState extends State { ), ), Positioned( - bottom: 16, - right: 16, - child: CupertinoButton( - padding: EdgeInsets.zero, - onPressed: _disconnect, - child: Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: CupertinoColors.destructiveRed, - borderRadius: BorderRadius.circular(24), - ), - alignment: Alignment.center, - child: const Icon( - CupertinoIcons.clear, - color: CupertinoColors.white, - ), - ), - ), - ), - Positioned( - bottom: 16, - left: 16, - child: CupertinoSegmentedControl( - groupValue: _selectedResolution, - children: { - for (int i = 0; i < _resolutionOptions.length; i++) - i: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - child: Text(_resolutionOptions[i]['label'] as String), - ), - }, - onValueChanged: (int idx) { - setState(() => _selectedResolution = idx); - final o = _resolutionOptions[idx]; - _controller?.sendResolutionChange( - o['width'] as int, - o['height'] as int, - o['fps'] as int, - ); - }, - ), + top: 8, + right: 8, + child: _buildTopMenuBar(), ), ], ), diff --git a/webrtc_controller_flutter/windows/runner/flutter_window.cpp b/webrtc_controller_flutter/windows/runner/flutter_window.cpp index 955ee30..df7f08c 100644 --- a/webrtc_controller_flutter/windows/runner/flutter_window.cpp +++ b/webrtc_controller_flutter/windows/runner/flutter_window.cpp @@ -1,6 +1,7 @@ #include "flutter_window.h" #include +#include #include "flutter/generated_plugin_registrant.h" @@ -27,6 +28,29 @@ bool FlutterWindow::OnCreate() { RegisterPlugins(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); + // 注册窗口控制通道:Dart 端可请求按视频比例调整窗口尺寸。 + window_channel_ = + std::make_unique>( + flutter_controller_->engine()->messenger(), "app/window", + &flutter::StandardMethodCodec::GetInstance()); + window_channel_->SetMethodCallHandler( + [this](const flutter::MethodCall& call, + std::unique_ptr> + result) { + if (call.method_name() == "resizeToVideoAspect") { + double aspect = 0.0; + if (const auto* value = std::get_if(call.arguments())) { + aspect = *value; + } else if (const auto* ivalue = std::get_if(call.arguments())) { + aspect = static_cast(*ivalue); + } + ResizeToVideoAspect(aspect); + result->Success(); + } else { + result->NotImplemented(); + } + }); + flutter_controller_->engine()->SetNextFrameCallback([&]() { this->Show(); }); @@ -47,6 +71,69 @@ void FlutterWindow::OnDestroy() { Win32Window::OnDestroy(); } +void FlutterWindow::ResizeToVideoAspect(double aspect) { + if (aspect <= 0.0) { + return; + } + HWND hwnd = GetHandle(); + if (!hwnd) { + return; + } + + RECT window_rect; + RECT client_rect; + GetWindowRect(hwnd, &window_rect); + GetClientRect(hwnd, &client_rect); + + const int window_w = window_rect.right - window_rect.left; + const int window_h = window_rect.bottom - window_rect.top; + const int client_w = client_rect.right - client_rect.left; + const int client_h = client_rect.bottom - client_rect.top; + // 边框(含标题栏)占用的额外像素。 + const int border_w = window_w - client_w; + const int border_h = window_h - client_h; + + // 保持窗口宽度不变,按比例计算客户区高度。 + int target_client_w = client_w; + int target_client_h = static_cast(target_client_w / aspect + 0.5); + int target_window_w = window_w; + int target_window_h = target_client_h + border_h; + + // 获取窗口所在显示器的可用工作区。 + HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + MONITORINFO monitor_info; + monitor_info.cbSize = sizeof(MONITORINFO); + GetMonitorInfo(monitor, &monitor_info); + const int work_h = monitor_info.rcWork.bottom - monitor_info.rcWork.top; + + // 高度超出屏幕:改为以屏幕高度为准的合适窗口大小。 + if (target_window_h > work_h) { + target_window_h = work_h; + target_client_h = target_window_h - border_h; + target_client_w = static_cast(target_client_h * aspect + 0.5); + target_window_w = target_client_w + border_w; + } + + // 计算位置:尽量保持当前左上角,同时确保窗口落在工作区内。 + int new_left = window_rect.left; + int new_top = window_rect.top; + if (new_left + target_window_w > monitor_info.rcWork.right) { + new_left = monitor_info.rcWork.right - target_window_w; + } + if (new_top + target_window_h > monitor_info.rcWork.bottom) { + new_top = monitor_info.rcWork.bottom - target_window_h; + } + if (new_left < monitor_info.rcWork.left) { + new_left = monitor_info.rcWork.left; + } + if (new_top < monitor_info.rcWork.top) { + new_top = monitor_info.rcWork.top; + } + + SetWindowPos(hwnd, nullptr, new_left, new_top, target_window_w, + target_window_h, SWP_NOZORDER | SWP_NOACTIVATE); +} + LRESULT FlutterWindow::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, diff --git a/webrtc_controller_flutter/windows/runner/flutter_window.h b/webrtc_controller_flutter/windows/runner/flutter_window.h index 6da0652..9f2780c 100644 --- a/webrtc_controller_flutter/windows/runner/flutter_window.h +++ b/webrtc_controller_flutter/windows/runner/flutter_window.h @@ -3,6 +3,8 @@ #include #include +#include +#include #include @@ -23,11 +25,20 @@ class FlutterWindow : public Win32Window { LPARAM const lparam) noexcept override; private: + // 按给定的视频宽高比(宽/高)调整窗口:保持窗口宽度不变, + // 高度按比例计算;若高度超出屏幕工作区,则以屏幕高度为准 + // 反推出合适的窗口宽高。 + void ResizeToVideoAspect(double aspect); + // The project to run. flutter::DartProject project_; // The Flutter instance hosted by this window. std::unique_ptr flutter_controller_; + + // 与 Dart 层通信的窗口控制通道。 + std::unique_ptr> + window_channel_; }; #endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/webrtc_controller_flutter/windows/runner/main.cpp b/webrtc_controller_flutter/windows/runner/main.cpp index 340e201..b03ce5a 100644 --- a/webrtc_controller_flutter/windows/runner/main.cpp +++ b/webrtc_controller_flutter/windows/runner/main.cpp @@ -26,7 +26,8 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); - Win32Window::Size size(1280, 720); + // 竖屏 16:9 窗口(宽:高 = 9:16),适配控制端竖屏画面。 + Win32Window::Size size(450, 800); if (!window.Create(L"webrtc_controller_flutter", origin, size)) { return EXIT_FAILURE; }