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"