Files
Commilitia-Drop/desktop/engine/wire.go
T
admin f1a00d128e 原生数据面协议层:桌面 engine 进度节流 / pion 日志 + iOS 数据面路由
- 桌面 engine(pion 数据面,承 ab57afd 之后的精修):
  - 新增 logging.go——把 pion 内部日志路由到宿主 OnLog(真机无 stderr,连接失败无从查);仅 ice/mdns 作用域放 Debug、余 Info+、Trace 丢弃,按 session 聚合
  - wire.go / session.go / engine.go:进度回调 ~10Hz 节流(progressEmitThrottleMs,高吞吐下每片一回调打满宿主主线程;终态由 emitProgressNow 强发最终值),与 web store push 同量级;engine_test.go 跟进
- web 数据面路由:p2p.ts 翻 IOS_NATIVE=true(iOS 走原生 libwebrtc 引擎)+ 新增 p2pIos.ts(iOS p2p 后端)+ p2pNative.ts / net/ios.ts 跟进
- 数据面设计文档 NATIVE-TRANSFER.md:U1(gomobile+pion)真机证伪 → 翻案 U2(libwebrtc)的依据与实测证据(§7/§8)
- 线协议三端一致(cdrop-file ordered / meta+chunk(64KB)+done+ack / 16MB-4MB 水位 / ack 追平完成),桌面 pion ↔ iOS libwebrtc ↔ web JS 互通
2026-06-28 19:32:37 +08:00

125 lines
4.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package engine 是 cdrop 的原生数据面:用 pion/webrtc 在 Go 进程内完成 P2P 文件收发,
// 取代 WebView 内 JS 引擎跑 WebRTC 的旧路径。动机见 desktop/NATIVE-TRANSFER.md——逃离
// WebKit 写死的 256KB SCTP 接收窗 + 渲染器节流,并让桌面与 iOS(经 gomobile)共用同一份实现。
//
// 本包刻意只依赖 pion + 标准库,不碰 Wails / 平台代码:① 将来 `gomobile bind` 出 iOS
// xcframework 时不会拖入不可编译的桌面依赖;② 一切 HTTP(信令 / 状态机 POST / ICE 凭据)
// 留给宿主(桌面 JS / iOS Swift),引擎只经回调收发不透明信令串——纯数据面。
//
// 线协议与 web 引擎 web/src/features/transfer/p2p.ts 逐字节一致,故 Go 端可与浏览器 /
// iOS 的 JS 引擎互通:DataChannel "cdrop-file"ordered),控制帧 meta/done/ack 走文本帧、
// 文件分片走二进制帧;信令 payload 形如 {type, sdp?, candidate?}。
package engine
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/pion/webrtc/v4"
)
const (
channelName = "cdrop-file"
chunkSize = 64 * 1024
highWatermark = 16 * 1024 * 1024
lowWatermark = 4 * 1024 * 1024
// 接收端把已收字节回传发送端的节流间隔(毫秒),与 web 引擎 ACK_INTERVAL_MS 一致。
ackIntervalMs = 200
// 进度回调上报的节流间隔(毫秒):高吞吐下每片一回调会把宿主主线程打满——iOS 上每片
// 都经桥 evaluateJavaScript,数百次/秒会令 UI 无响应(gomobile 集成实测)。限到 ~10Hz
// 终态另由 emitProgressNow 强发最终值。与 web 引擎 store push 的 10Hz 节流同量级。
progressEmitThrottleMs = 100
// 发送端等接收端 ack 追平总量的上限(秒):超时仍照常收尾,避免 ack 丢失致悬挂。
ackCompleteTimeoutSec = 30
// pion 接收窗口:远高于 WebKit 写死的 256KB,使 pion 作接收端不被 rwnd × RTT 掐死
// (吞吐专项的核心修复,见 SetSCTPMaxReceiveBufferSize)。
sctpReceiveBuffer = 4 * 1024 * 1024
)
// 控制帧(文本帧),与 p2p.ts 的 meta/done/ack 同形。发送端 meta 仅含 name/sizesha256
// 在 streamFile 路径上 web 端亦不发),故此处不带。
type metaFrame struct {
Type string `json:"type"`
Name string `json:"name"`
Size int64 `json:"size"`
}
type doneFrame struct {
Type string `json:"type"`
}
type ackFrame struct {
Type string `json:"type"`
Bytes int64 `json:"bytes"`
}
// signalPayload 与 p2p.ts 的 SignalPayload 同形:offer/answer 带 sdpRTCSessionDescriptionInit),
// ice 带 candidateRTCIceCandidateInit)。pion 的 SessionDescription / ICECandidateInit 的 JSON
// 标签与浏览器侧一致,故双向可直接 (un)marshal。
type signalPayload struct {
Type string `json:"type"`
SDP *webrtc.SessionDescription `json:"sdp,omitempty"`
Candidate *webrtc.ICECandidateInit `json:"candidate,omitempty"`
}
// iceServerJSON 解析 web getICEServers() 的输出:urls 既可能是单串也可能是数组。
type iceServerJSON struct {
URLs json.RawMessage `json:"urls"`
Username string `json:"username,omitempty"`
Credential string `json:"credential,omitempty"`
}
// parseICEServers 把宿主传来的 ICE 服务器 JSON(与 web 同源,来自 /api/calls/credentials
// 解析成 pion 的配置。urls 兼容字符串与字符串数组两种形态。
func parseICEServers(raw string) []webrtc.ICEServer {
if strings.TrimSpace(raw) == "" {
return nil
}
var arr []iceServerJSON
if err := json.Unmarshal([]byte(raw), &arr); err != nil {
return nil
}
out := make([]webrtc.ICEServer, 0, len(arr))
for _, s := range arr {
var urls []string
var one string
if json.Unmarshal(s.URLs, &one) == nil {
urls = []string{one}
} else {
_ = json.Unmarshal(s.URLs, &urls)
}
if len(urls) == 0 {
continue
}
srv := webrtc.ICEServer{URLs: urls}
if s.Username != "" {
srv.Username = s.Username
}
if s.Credential != "" {
srv.Credential = s.Credential
}
out = append(out, srv)
}
return out
}
// nonCollidingPath 返回 dir 下不与现有文件冲突的目标路径:name、name (1)、name (2)……
// 接收端落盘时用,避免覆盖既有文件(对齐 SaveDownload 的不覆盖语义)。
func nonCollidingPath(dir, name string) string {
p := filepath.Join(dir, name)
if _, err := os.Stat(p); os.IsNotExist(err) {
return p
}
ext := filepath.Ext(name)
base := strings.TrimSuffix(name, ext)
for i := 1; ; i += 1 {
c := filepath.Join(dir, fmt.Sprintf("%s (%d)%s", base, i, ext))
if _, err := os.Stat(c); os.IsNotExist(err) {
return c
}
}
}