diff --git a/src/plugins/media/media.less b/src/plugins/media/media.less
new file mode 100644
index 00000000..8b3b929a
--- /dev/null
+++ b/src/plugins/media/media.less
@@ -0,0 +1,7 @@
+.media-renderer {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ padding-top: var(--termpad);
+}
diff --git a/src/plugins/media/media.tsx b/src/plugins/media/media.tsx
new file mode 100644
index 00000000..7a904761
--- /dev/null
+++ b/src/plugins/media/media.tsx
@@ -0,0 +1,49 @@
+// Copyright 2024, Command Line Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+import * as React from "react";
+import * as mobx from "mobx";
+import * as mobxReact from "mobx-react";
+import { GlobalModel } from "@/models";
+
+import "./media.less";
+
+@mobxReact.observer
+class SimpleMediaRenderer extends React.Component<
+ { data: ExtBlob; context: RendererContext; opts: RendererOpts; savedHeight: number; lineState: LineStateType },
+ {}
+> {
+ objUrl: string = null;
+
+ componentWillUnmount() {
+ if (this.objUrl != null) {
+ URL.revokeObjectURL(this.objUrl);
+ }
+ }
+
+ render() {
+ let dataBlob = this.props.data;
+ if (dataBlob == null || dataBlob.notFound) {
+ return (
+
+
+ ERROR: file {dataBlob && dataBlob.name ? JSON.stringify(dataBlob.name) : ""} not found
+
+
+ );
+ }
+ let videoUrl = GlobalModel.getBaseHostPort() + this.props.lineState["wave:fileurl"];
+ const opts = this.props.opts;
+ const maxHeight = opts.maxSize.height - 10;
+ const maxWidth = opts.maxSize.width - 10;
+ return (
+
+
+
+ );
+ }
+}
+
+export { SimpleMediaRenderer };
diff --git a/src/plugins/pdf/pdf.tsx b/src/plugins/pdf/pdf.tsx
index dda6208e..5e21d715 100644
--- a/src/plugins/pdf/pdf.tsx
+++ b/src/plugins/pdf/pdf.tsx
@@ -32,7 +32,7 @@ class SimplePdfRenderer extends React.Component<
);
}
if (this.objUrl == null) {
- const pdfBlob = new File([dataBlob], "test.pdf", { type: "application/pdf" });
+ const pdfBlob = new File([dataBlob], dataBlob.name ?? "file.pdf", { type: "application/pdf" });
this.objUrl = URL.createObjectURL(pdfBlob);
}
const opts = this.props.opts;
diff --git a/src/plugins/plugins.ts b/src/plugins/plugins.ts
index e55b0161..4a55a8cd 100644
--- a/src/plugins/plugins.ts
+++ b/src/plugins/plugins.ts
@@ -8,6 +8,7 @@ import { SimpleMustacheRenderer } from "./mustache/mustache";
import { CSVRenderer } from "./csv/csv";
import { OpenAIRenderer, OpenAIRendererModel } from "./openai/openai";
import { SimplePdfRenderer } from "./pdf/pdf";
+import { SimpleMediaRenderer } from "./media/media";
import { isBlank } from "@/util/util";
import { sprintf } from "sprintf-js";
@@ -89,6 +90,16 @@ const PluginConfigs: RendererPluginType[] = [
mimeTypes: ["application/pdf"],
simpleComponent: SimplePdfRenderer,
},
+ {
+ name: "media",
+ rendererType: "simple",
+ heightType: "pixels",
+ dataType: "blob",
+ collapseType: "hide",
+ globalCss: null,
+ mimeTypes: ["video/*", "audio/*"],
+ simpleComponent: SimpleMediaRenderer,
+ },
];
class PluginModelClass {
diff --git a/wavesrv/pkg/cmdrunner/cmdrunner.go b/wavesrv/pkg/cmdrunner/cmdrunner.go
index fcb22b8c..84999d1b 100644
--- a/wavesrv/pkg/cmdrunner/cmdrunner.go
+++ b/wavesrv/pkg/cmdrunner/cmdrunner.go
@@ -37,6 +37,7 @@ import (
"github.com/wavetermdev/waveterm/wavesrv/pkg/comp"
"github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil"
"github.com/wavetermdev/waveterm/wavesrv/pkg/pcloud"
+ "github.com/wavetermdev/waveterm/wavesrv/pkg/promptenc"
"github.com/wavetermdev/waveterm/wavesrv/pkg/releasechecker"
"github.com/wavetermdev/waveterm/wavesrv/pkg/remote"
"github.com/wavetermdev/waveterm/wavesrv/pkg/remote/openai"
@@ -280,6 +281,7 @@ func init() {
registerCmdFn("mdview", MarkdownViewCommand)
registerCmdFn("markdownview", MarkdownViewCommand)
registerCmdFn("pdfview", PdfViewCommand)
+ registerCmdFn("mediaview", MediaViewCommand)
registerCmdFn("csvview", CSVViewCommand)
}
@@ -4916,6 +4918,58 @@ func PdfViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbu
return update, nil
}
+func MakeReadFileUrl(screenId string, lineId string, filePath string) (string, error) {
+ qvals := make(url.Values)
+ qvals.Set("screenid", screenId)
+ qvals.Set("lineid", lineId)
+ qvals.Set("path", filePath)
+ qvals.Set("nonce", uuid.New().String())
+ hmacStr, err := promptenc.ComputeUrlHmac([]byte(scbase.WaveAuthKey), "/api/read-file", qvals)
+ if err != nil {
+ return "", fmt.Errorf("error computing hmac-url: %v", err)
+ }
+ qvals.Set("hmac", hmacStr)
+ return "/api/read-file?" + qvals.Encode(), nil
+}
+
+func MediaViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
+ if len(pk.Args) == 0 {
+ return nil, fmt.Errorf("%s requires an argument (file name)", GetCmdStr(pk))
+ }
+ // TODO more error checking on filename format?
+ if pk.Args[0] == "" {
+ return nil, fmt.Errorf("%s argument cannot be empty", GetCmdStr(pk))
+ }
+ fileName := pk.Args[0]
+ ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected)
+ if err != nil {
+ return nil, err
+ }
+ outputStr := fmt.Sprintf("%s %q", GetCmdStr(pk), fileName)
+ cmd, err := makeStaticCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), []byte(outputStr))
+ if err != nil {
+ // TODO tricky error since the command was a success, but we can't show the output
+ return nil, err
+ }
+ // compute hmac read-file URL
+ readFileUrl, err := MakeReadFileUrl(ids.ScreenId, cmd.LineId, fileName)
+ if err != nil {
+ // TODO tricky error since the command was a success, but we can't show the output
+ return nil, fmt.Errorf("error making read-file url: %v", err)
+ }
+ // set the line state
+ lineState := make(map[string]any)
+ lineState[sstore.LineState_FileUrl] = readFileUrl
+ lineState[sstore.LineState_File] = fileName
+ update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "media", lineState)
+ if err != nil {
+ // TODO tricky error since the command was a success, but we can't show the output
+ return nil, err
+ }
+ update.AddUpdate(sstore.InteractiveUpdate(pk.Interactive))
+ return update, nil
+}
+
func MarkdownViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
if len(pk.Args) == 0 {
return nil, fmt.Errorf("%s requires an argument (file name)", GetCmdStr(pk))
diff --git a/wavesrv/pkg/cmdrunner/shparse.go b/wavesrv/pkg/cmdrunner/shparse.go
index 3cb043da..500ffd73 100644
--- a/wavesrv/pkg/cmdrunner/shparse.go
+++ b/wavesrv/pkg/cmdrunner/shparse.go
@@ -36,6 +36,7 @@ var BareMetaCmds = []BareMetaCmdDecl{
{"mdview", "markdownview"},
{"csvview", "csvview"},
{"pdfview", "pdfview"},
+ {"mediaview", "mediaview"},
}
const (
diff --git a/wavesrv/pkg/sstore/sstore.go b/wavesrv/pkg/sstore/sstore.go
index 29459435..e7643d5a 100644
--- a/wavesrv/pkg/sstore/sstore.go
+++ b/wavesrv/pkg/sstore/sstore.go
@@ -63,6 +63,7 @@ const (
const (
LineState_Source = "prompt:source"
LineState_File = "prompt:file"
+ LineState_FileUrl = "wave:fileurl"
LineState_Min = "wave:min"
LineState_Template = "template"
LineState_Mode = "mode"