Skip to content

Commit 3028c2a

Browse files
authored
switch /wave/stream-file to use new modern streams (w/ flow control) and fix big ttfb streaming bug (#3084)
1 parent 4ee003a commit 3028c2a

File tree

11 files changed

+315
-127
lines changed

11 files changed

+315
-127
lines changed

frontend/app/store/wshclientapi.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,12 @@ export class RpcApiType {
378378
return client.wshRpcCall("filerestorebackup", data, opts);
379379
}
380380

381+
// command "filestream" [call]
382+
FileStreamCommand(client: WshClient, data: CommandFileStreamData, opts?: RpcOpts): Promise<FileInfo> {
383+
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "filestream", data, opts);
384+
return client.wshRpcCall("filestream", data, opts);
385+
}
386+
381387
// command "filewrite" [call]
382388
FileWriteCommand(client: WshClient, data: FileData, opts?: RpcOpts): Promise<void> {
383389
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "filewrite", data, opts);
@@ -720,6 +726,12 @@ export class RpcApiType {
720726
return client.wshRpcCall("remotefilemultiinfo", data, opts);
721727
}
722728

729+
// command "remotefilestream" [call]
730+
RemoteFileStreamCommand(client: WshClient, data: CommandRemoteFileStreamData, opts?: RpcOpts): Promise<FileInfo> {
731+
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "remotefilestream", data, opts);
732+
return client.wshRpcCall("remotefilestream", data, opts);
733+
}
734+
723735
// command "remotefiletouch" [call]
724736
RemoteFileTouchCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<void> {
725737
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "remotefiletouch", data, opts);

frontend/app/view/preview/preview-streaming.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,18 +72,14 @@ function StreamingPreview({ model }: SpecializedViewProps) {
7272
if (fileInfo.mimetype.startsWith("video/")) {
7373
return (
7474
<div className="flex flex-row h-full overflow-hidden items-center justify-center">
75-
<video controls className="w-full h-full p-[10px] object-contain">
76-
<source src={streamingUrl} />
77-
</video>
75+
<video controls src={streamingUrl} className="w-full h-full p-[10px] object-contain" />
7876
</div>
7977
);
8078
}
8179
if (fileInfo.mimetype.startsWith("audio/")) {
8280
return (
8381
<div className="flex flex-row h-full overflow-hidden items-center justify-center">
84-
<audio controls className="w-full h-full p-[10px] object-contain">
85-
<source src={streamingUrl} />
86-
</audio>
82+
<audio controls src={streamingUrl} className="w-full h-full p-[10px] object-contain" />
8783
</div>
8884
);
8985
}

frontend/app/view/term/termwrap.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,7 @@ export class TermWrap {
531531
}
532532
}
533533
let resolve: () => void = null;
534-
let prtn = new Promise<void>((presolve, _) => {
534+
const prtn = new Promise<void>((presolve, _) => {
535535
resolve = presolve;
536536
});
537537
this.terminal.write(data, () => {

frontend/types/gotypes.d.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,13 @@ declare global {
380380
restoretofilename: string;
381381
};
382382

383+
// wshrpc.CommandFileStreamData
384+
type CommandFileStreamData = {
385+
info: FileInfo;
386+
byterange?: string;
387+
streammeta: StreamMeta;
388+
};
389+
383390
// wshrpc.CommandGetMetaData
384391
type CommandGetMetaData = {
385392
oref: ORef;
@@ -521,6 +528,13 @@ declare global {
521528
paths: string[];
522529
};
523530

531+
// wshrpc.CommandRemoteFileStreamData
532+
type CommandRemoteFileStreamData = {
533+
path: string;
534+
byterange?: string;
535+
streammeta: StreamMeta;
536+
};
537+
524538
// wshrpc.CommandRemoteListEntriesData
525539
type CommandRemoteListEntriesData = {
526540
path: string;

pkg/remote/fileshare/wshfs/wshfs.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,37 @@ func ReadStream(ctx context.Context, data wshrpc.FileData) <-chan wshrpc.RespOrE
6161
func readStream(conn *connparse.Connection, data wshrpc.FileData) <-chan wshrpc.RespOrErrorUnion[wshrpc.FileData] {
6262
byteRange := ""
6363
if data.At != nil && data.At.Size > 0 {
64-
byteRange = fmt.Sprintf("%d-%d", data.At.Offset, data.At.Offset+int64(data.At.Size))
64+
byteRange = fmt.Sprintf("%d-%d", data.At.Offset, data.At.Offset+int64(data.At.Size)-1)
6565
}
6666
streamFileData := wshrpc.CommandRemoteStreamFileData{Path: conn.Path, ByteRange: byteRange}
6767
return wshclient.RemoteStreamFileCommand(RpcClient, streamFileData, &wshrpc.RpcOpts{Route: wshutil.MakeConnectionRouteId(conn.Host)})
6868
}
6969

70+
func GetConnectionRouteId(ctx context.Context, path string) (string, error) {
71+
conn, err := parseConnection(ctx, path)
72+
if err != nil {
73+
return "", err
74+
}
75+
return wshutil.MakeConnectionRouteId(conn.Host), nil
76+
}
77+
78+
func FileStream(ctx context.Context, data wshrpc.CommandFileStreamData) (*wshrpc.FileInfo, error) {
79+
if data.Info == nil {
80+
return nil, fmt.Errorf("file info is required")
81+
}
82+
log.Printf("FileStream: %v", data.Info.Path)
83+
conn, err := parseConnection(ctx, data.Info.Path)
84+
if err != nil {
85+
return nil, err
86+
}
87+
remoteData := wshrpc.CommandRemoteFileStreamData{
88+
Path: conn.Path,
89+
ByteRange: data.ByteRange,
90+
StreamMeta: data.StreamMeta,
91+
}
92+
return wshclient.RemoteFileStreamCommand(RpcClient, remoteData, &wshrpc.RpcOpts{Route: wshutil.MakeConnectionRouteId(conn.Host)})
93+
}
94+
7095
func ListEntries(ctx context.Context, path string, opts *wshrpc.FileListOpts) ([]*wshrpc.FileInfo, error) {
7196
log.Printf("ListEntries: %v", path)
7297
conn, err := parseConnection(ctx, path)

pkg/util/fileutil/fileutil.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
// Copyright 2025, Command Line Inc.
1+
// Copyright 2026, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

44
package fileutil
55

66
import (
77
"bytes"
8+
"errors"
89
"fmt"
910
"io"
1011
"io/fs"
@@ -18,6 +19,38 @@ import (
1819
"github.com/wavetermdev/waveterm/pkg/wavebase"
1920
)
2021

22+
type ByteRangeType struct {
23+
All bool
24+
Start int64
25+
End int64 // inclusive; only valid when OpenEnd is false
26+
OpenEnd bool // true when range is "N-" (read from Start to EOF)
27+
}
28+
29+
func ParseByteRange(rangeStr string) (ByteRangeType, error) {
30+
if rangeStr == "" {
31+
return ByteRangeType{All: true}, nil
32+
}
33+
// handle open-ended range "N-"
34+
if len(rangeStr) > 0 && rangeStr[len(rangeStr)-1] == '-' {
35+
var start int64
36+
_, err := fmt.Sscanf(rangeStr, "%d-", &start)
37+
if err != nil || start < 0 {
38+
return ByteRangeType{}, errors.New("invalid byte range")
39+
}
40+
return ByteRangeType{Start: start, OpenEnd: true}, nil
41+
}
42+
var start, end int64
43+
_, err := fmt.Sscanf(rangeStr, "%d-%d", &start, &end)
44+
if err != nil {
45+
return ByteRangeType{}, errors.New("invalid byte range")
46+
}
47+
if start < 0 || end < 0 || start > end {
48+
return ByteRangeType{}, errors.New("invalid byte range")
49+
}
50+
// End is inclusive (HTTP byte range semantics: bytes=0-999 means 1000 bytes)
51+
return ByteRangeType{Start: start, End: end}, nil
52+
}
53+
2154
func FixPath(path string) (string, error) {
2255
origPath := path
2356
var err error

0 commit comments

Comments
 (0)