桌面原生数据面:pion 数据通道取代 WebView WebRTC,逃离 256KB rwnd 与渲染器节流
把桌面 P2P 数据面从 WebView 内 JS WebRTC 移到 Go 进程的 pion,逃离 WebKit/WebView2 写死的 256KB SCTP 接收窗与隐藏窗口的渲染器节流。后端零改动、与现有客户端互通(线协议逐字节对齐)。真机实测原生↔原生 / 原生↔iOS(经有线)稳定 7-10MB/s,对比旧 JS 路径的 1.5MB/s 还塌缩是质变。设计与阶段见 desktop/NATIVE-TRANSFER.md。
- engine 包(纯 Go,仅 pion+stdlib、零 Wails 依赖,供将来 gomobile 复用于 iOS):pion 收发,逐字节对齐 web 引擎线协议(DataChannel cdrop-file ordered、文本帧 meta/done/ack、二进制分片、信令 {type,sdp?,candidate?});SetSCTPMaxReceiveBufferSize 顶到 4MB;mDNS QueryOnly 解析对端 .local 候选实现同内网 host↔host 直连;接收端直接写盘(去 base64 桥 / OPFS);进程内端到端测试 + 诊断日志(候选/ICE 态/选中对/发送停滞)。
- app.go:Bind P2PStartOutgoing / StartIncoming / HandleSignal / Cancel + 原生取文件 PickFileForSend + 中继回退读盘片 ReadFileSlice;引擎回调经 EventsEmit p2p:* 反向通知(进度/状态/信令/落盘路径/候选对)。
- JS p2pBackend 抽象(p2pNative.ts):桌面按对端把会话委派给 Go 引擎,transfer.ts / hub.ts 零改动;HTTP 全留 JS(出站信令 POST、状态机 /p2p·接收端 /done·/fail);监听 p2p:* 事件驱动 store、phase 与 iceStats。
- 发送取文件改原生路径:selectedFile 模型 File→FileSource(桌面带绝对 path → Go 直接读盘走原生;浏览器拖放无 path → 回退 JS)。Composer dropzone 在桌面打开原生文件对话框。
- 完成语义修复:接收端完成不立即拆连接(续发最终 ack、留 SSE 终态 Cancel 拆、60s 兜底、终态不可被后续 close 翻转)——修「iOS→mac 实际成功却被误标失败」。
- 剪贴板暂存路径修复:富文本经 Universal Clipboard 暂存为 .rtfd,pasteboard 纯文本表示有时取到该暂存路径——uploadClipboard 共享守卫拦截 + mac 原生读改从 RTF / RTFD 派生纯文本。
This commit is contained in:
+115
@@ -2,14 +2,18 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||
"github.com/wailsapp/wails/v2/pkg/menu/keys"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
|
||||
"cdrop-desktop/engine"
|
||||
"cdrop-desktop/platform"
|
||||
)
|
||||
|
||||
@@ -24,6 +28,11 @@ type App struct {
|
||||
|
||||
clipSource platform.Source
|
||||
clipMonitor *platform.Monitor
|
||||
|
||||
// transfer 是原生 P2P 数据面(pion):取代 WebView 内 JS WebRTC,逃离 WebKit/WebView2
|
||||
// 的 256KB rwnd 与渲染器节流。HTTP(信令 / 状态机)仍由 WebView 侧 JS 经事件桥发起,
|
||||
// 引擎只做纯 WebRTC + 文件 I/O。见 desktop/NATIVE-TRANSFER.md。
|
||||
transfer *engine.Engine
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct.
|
||||
@@ -53,6 +62,8 @@ func (a *App) startup(ctx context.Context) {
|
||||
if platform.IsLaunchAtLoginEnabled() {
|
||||
_ = platform.SetLaunchAtLogin(true)
|
||||
}
|
||||
// 原生传输引擎:落地目录取当前配置(设置页改目录时经 SaveSettings 同步到引擎)。
|
||||
a.transfer = engine.New(engine.Config{DownloadDir: platform.ResolveDownloadDir()}, &transferEvents{app: a})
|
||||
a.startClipboardSync(ctx)
|
||||
// 触发 macOS 本地网络权限(macOS 15+ 隐私门):使本进程 WKWebView 的 WebRTC 能收集 host /
|
||||
// mDNS 候选、同内网走直连而非 prflx↔prflx 慢路径(见 platform.TriggerLocalNetwork)。其他平台空实现。
|
||||
@@ -325,6 +336,10 @@ func (a *App) SaveSettings(cfg platform.DesktopConfig) error {
|
||||
if err := platform.SaveConfig(cur); err != nil {
|
||||
return err
|
||||
}
|
||||
// 让引擎的落地目录跟随设置变化(接收端直接写盘)。
|
||||
if a.transfer != nil {
|
||||
a.transfer.SetDownloadDir(platform.ResolveDownloadDir())
|
||||
}
|
||||
return platform.SetLaunchAtLogin(cur.LaunchAtLogin)
|
||||
}
|
||||
|
||||
@@ -422,3 +437,103 @@ func (a *App) LoggedIn() bool {
|
||||
func (a *App) Greet(name string) string {
|
||||
return fmt.Sprintf("Hello %s, It's show time!", name)
|
||||
}
|
||||
|
||||
// --- 原生 P2P 数据面桥(绑定到 WebView) ---
|
||||
//
|
||||
// WebView 侧的 p2pBackend(Go-bridge 后端)调用这四个方法发起 / 接收 / 喂信令 / 取消;
|
||||
// 引擎经 transferEvents 反向 EventsEmit 进度 / 状态 / 出站信令 / 落盘路径。HTTP 全留 JS:
|
||||
// 出站信令由 JS POST /api/hub/signal,入站信令由 JS hub 转发进 P2PHandleSignal,状态机
|
||||
// POST(/p2p、/done、/fail)由 JS 据 p2p:state 事件发起。iceServersJSON 由 JS 透传
|
||||
// (与 web 同源,来自 /api/calls/credentials)。
|
||||
|
||||
// P2PStartOutgoing 作为发送端发起:filePath 为本机绝对路径(经 PickFileForSend 获得)。
|
||||
func (a *App) P2PStartOutgoing(sessionID, peerName, filePath, iceServersJSON string) error {
|
||||
return a.transfer.StartOutgoing(sessionID, peerName, filePath, iceServersJSON)
|
||||
}
|
||||
|
||||
// P2PStartIncoming 预备接收端,等对端 offer。
|
||||
func (a *App) P2PStartIncoming(sessionID, peerName, iceServersJSON string) error {
|
||||
return a.transfer.StartIncoming(sessionID, peerName, iceServersJSON)
|
||||
}
|
||||
|
||||
// P2PHandleSignal 把 JS hub 从 SSE 收到的入站信令喂给引擎(按对端设备名路由)。
|
||||
func (a *App) P2PHandleSignal(fromPeer, payloadJSON string) error {
|
||||
return a.transfer.HandleSignal(fromPeer, payloadJSON)
|
||||
}
|
||||
|
||||
// P2PCancel 拆除会话(取消 / 中继回退)。
|
||||
func (a *App) P2PCancel(sessionID string) {
|
||||
a.transfer.Cancel(sessionID)
|
||||
}
|
||||
|
||||
// PickedFile 是原生文件选择的结果,供发送端走原生数据面(Go 直接按路径读盘,不经浏览器
|
||||
// File 的沙箱无路径 + 内存约束)。
|
||||
type PickedFile struct {
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
// ReadFileSlice 读 path 的 [start,end) 字节并 base64 返回。仅供中继回退时 WebView 侧的
|
||||
// FileSource.slice 用(原生 P2P 由引擎直接按路径读盘、不走此桥);故 base64 开销只在罕见
|
||||
// 的 relay 路径上,不影响 P2P 主路径。镜像 iOS 的 readOutgoingSlice。
|
||||
func (a *App) ReadFileSlice(path string, start, end int64) (string, error) {
|
||||
if start < 0 || end < start {
|
||||
return "", fmt.Errorf("bad range [%d,%d)", start, end)
|
||||
}
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
buf := make([]byte, end-start)
|
||||
n, err := f.ReadAt(buf, start)
|
||||
if err != nil && err != io.EOF {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(buf[:n]), nil
|
||||
}
|
||||
|
||||
// PickFileForSend 打开原生文件对话框,返回选中文件的绝对路径 + 名 + 大小;用户取消返回 nil。
|
||||
func (a *App) PickFileForSend(title string) (*PickedFile, error) {
|
||||
path, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{Title: title})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if path == "" {
|
||||
return nil, nil // 用户取消
|
||||
}
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PickedFile{Path: path, Name: filepath.Base(path), Size: fi.Size()}, nil
|
||||
}
|
||||
|
||||
// transferEvents 把引擎回调转成 Wails 事件,供 WebView 侧 p2pBackend 监听。回调可能在 pion
|
||||
// 任意 goroutine 触发;Wails 的 EventsEmit / Log* 可跨线程安全调用。
|
||||
type transferEvents struct{ app *App }
|
||||
|
||||
func (e *transferEvents) OnProgress(sessionID string, bytes int64) {
|
||||
runtime.EventsEmit(e.app.ctx, "p2p:progress", map[string]any{"sessionId": sessionID, "bytes": bytes})
|
||||
}
|
||||
|
||||
func (e *transferEvents) OnState(sessionID, state string) {
|
||||
runtime.EventsEmit(e.app.ctx, "p2p:state", map[string]any{"sessionId": sessionID, "state": state})
|
||||
}
|
||||
|
||||
func (e *transferEvents) OnSignal(sessionID, toPeer, payloadJSON string) {
|
||||
runtime.EventsEmit(e.app.ctx, "p2p:signal", map[string]any{"sessionId": sessionID, "to": toPeer, "payload": payloadJSON})
|
||||
}
|
||||
|
||||
func (e *transferEvents) OnSaved(sessionID, path string) {
|
||||
runtime.EventsEmit(e.app.ctx, "p2p:saved", map[string]any{"sessionId": sessionID, "path": path})
|
||||
}
|
||||
|
||||
func (e *transferEvents) OnIcePair(sessionID, local, remote string) {
|
||||
runtime.EventsEmit(e.app.ctx, "p2p:icepair", map[string]any{"sessionId": sessionID, "local": local, "remote": remote})
|
||||
}
|
||||
|
||||
func (e *transferEvents) OnLog(line string) {
|
||||
runtime.LogInfof(e.app.ctx, "p2p: %s", line)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user