From 9dfca50488241998fa1a99f660b12f43ad803d38 Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 17 Aug 2023 22:31:42 -0400 Subject: Added the beginning of an A/V recording button --- src/client/util/CurrentUserUtils.ts | 9 ++-- .../views/nodes/RecordingBox/RecordingBox.tsx | 63 ++++++++++++++++++---- .../views/nodes/RecordingBox/RecordingView.tsx | 44 ++++++++++----- src/client/views/nodes/ScreenshotBox.tsx | 2 +- src/client/views/nodes/VideoBox.tsx | 6 +-- src/server/DashUploadUtils.ts | 2 +- 6 files changed, 95 insertions(+), 31 deletions(-) (limited to 'src') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 114249b24..b148a5e26 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -701,10 +701,10 @@ export class CurrentUserUtils { CollectionViewType.Carousel3D, CollectionViewType.Linear, CollectionViewType.Map, CollectionViewType.Grid, CollectionViewType.NoteTaking]), title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}}, - { title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, expertMode: false, width: 30, scripts: { onClick: 'pinWithView(altKey)'}, funcs: {hidden: "IsNoneSelected()"}}, + { title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, expertMode: false, width: 30, scripts: { onClick: 'pinWithView(altKey)'}, funcs: {hidden: "IsNoneSelected()"}}, { title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, width: 30, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}, funcs: {hidden: "IsNoneSelected()"}}, // Only when a document is selected - { title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, expertMode: true, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}, funcs: {hidden: "IsNoneSelected()"}}, - { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)'}, scripts: { onClick: 'return { toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform + { title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, expertMode: true, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}, funcs: {hidden: "IsNoneSelected()"}}, + { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)'}, scripts: { onClick: 'return { toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 30, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}}, { title: "Num", icon:"", toolTip: "Frame Number (click to toggle edit mode)", btnType: ButtonType.TextButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}}, { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 30, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}}, @@ -713,7 +713,8 @@ export class CurrentUserUtils { { title: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: CurrentUserUtils.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available { title: "View", icon: "View", toolTip: "View tools", subMenu: CurrentUserUtils.viewTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available { title: "Web", icon: "Web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), expertMode: false, toolType:DocumentType.WEB, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Only when Web is selected - { title: "Schema", icon: "Schema",linearBtnWidth:58,toolTip: "Schema functions",subMenu: CurrentUserUtils.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} } // Only when Schema is selected + { title: "Schema", icon: "Schema",linearBtnWidth:58,toolTip: "Schema functions",subMenu: CurrentUserUtils.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Only when Schema is selected + { title: "Audio", icon: "microphone", toolTip: "Dictate", btnType: ButtonType.ToggleButton, expertMode: false, ignoreClick: true, scripts: { onClick: 'return toggleRecording(_readOnly_)'}, funcs: { }} ]; } diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx index 04f11a5df..8fa2861b6 100644 --- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -4,28 +4,28 @@ import * as React from 'react'; import { VideoField } from '../../../../fields/URLField'; import { Upload } from '../../../../server/SharedMediaTypes'; import { ViewBoxBaseComponent } from '../../DocComponent'; -import { FieldView } from '../FieldView'; +import { FieldView, FieldViewProps } from '../FieldView'; import { VideoBox } from '../VideoBox'; import { RecordingView } from './RecordingView'; import { DocumentType } from '../../../documents/DocumentTypes'; import { Presentation } from '../../../util/TrackMovements'; import { Doc } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; -import { DocCast } from '../../../../fields/Types'; +import { BoolCast, DocCast } from '../../../../fields/Types'; +import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; +import { DocumentManager } from '../../../util/DocumentManager'; +import { Docs } from '../../../documents/Documents'; @observer -export class RecordingBox extends ViewBoxBaseComponent() { +export class RecordingBox extends ViewBoxBaseComponent() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(RecordingBox, fieldKey); } private _ref: React.RefObject = React.createRef(); - constructor(props: any) { - super(props); - } - componentDidMount() { + this.props.setContentView?.(this); Doc.SetNativeWidth(this.dataDoc, 1280); Doc.SetNativeHeight(this.dataDoc, 720); } @@ -46,20 +46,63 @@ export class RecordingBox extends ViewBoxBaseComponent() { this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey); this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.client); - this.dataDoc[this.fieldKey + '-recorded'] = true; + this.dataDoc[this.fieldKey + '_recorded'] = true; // stringify the presentation and store it if (presentation?.movements) { const presCopy = { ...presentation }; presCopy.movements = presentation.movements.map(movement => ({ ...movement, doc: movement.doc[Id] })) as any; - this.dataDoc[this.fieldKey + '-presentation'] = JSON.stringify(presCopy); + this.dataDoc[this.fieldKey + '_presentation'] = JSON.stringify(presCopy); } }; + Record: undefined | (() => void); + Pause: undefined | (() => void); + Finish: undefined | (() => void); + getControls = (record: () => void, pause: () => void, finish: () => void) => { + this.Record = record; + this.Pause = pause; + this.Finish = finish; + }; + render() { return (
- {!this.result && } + {!this.result && ( + + )}
); } + static screengrabber: RecordingBox | undefined; } +ScriptingGlobals.add(function toggleRecording(_readOnly_: boolean) { + if (_readOnly_) return RecordingBox.screengrabber ? true : false; + if (RecordingBox.screengrabber) { + RecordingBox.screengrabber.Pause?.(); + setTimeout(() => { + RecordingBox.screengrabber?.Finish?.(); + RecordingBox.screengrabber!.rootDoc.overlayX = 100; + RecordingBox.screengrabber!.rootDoc.overlayY = 100; + RecordingBox.screengrabber = undefined; + }, 100); + } else { + const screengrabber = Docs.Create.WebCamDocument('', { + _width: 384, + _height: 216, + }); + screengrabber.overlayX = -400; + screengrabber.overlayY = 0; + screengrabber[Doc.LayoutFieldKey(screengrabber) + '_trackScreen'] = true; + Doc.AddToMyOverlay(screengrabber); + DocumentManager.Instance.AddViewRenderedCb(screengrabber, docView => { + RecordingBox.screengrabber = docView.ComponentView as RecordingBox; + RecordingBox.screengrabber.Record?.(); + }); + } +}, 'toggle recording'); diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index 51eb774e2..0e386b093 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -21,6 +21,8 @@ interface IRecordingViewProps { setResult: (info: Upload.AccessPathInfo, presentation?: Presentation) => void; setDuration: (seconds: number) => void; id: string; + getControls: (record: () => void, pause: () => void, finish: () => void) => void; + forceTrackScreen: boolean; } const MAXTIME = 100000; @@ -60,14 +62,14 @@ export function RecordingView(props: IRecordingViewProps) { useEffect(() => { if (finished) { // make the total presentation that'll match the concatted video - let concatPres = trackScreen && TrackMovements.Instance.concatPresentations(videos.map(v => v.presentation as Presentation)); + let concatPres = (trackScreen || props.forceTrackScreen) && TrackMovements.Instance.concatPresentations(videos.map(v => v.presentation as Presentation)); // this async function uses the server to create the concatted video and then sets the result to it's accessPaths (async () => { const videoFiles = videos.map((vid, i) => new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() })); // upload the segments to the server and get their server access paths - const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles.map(file => ({file})))).map(res => (res.result instanceof Error ? '' : res.result.accessPaths.agnostic.server)); + const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles.map(file => ({ file })))).map(res => (res.result instanceof Error ? '' : res.result.accessPaths.agnostic.server)); // concat the segments together using post call const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths); @@ -132,7 +134,7 @@ export function RecordingView(props: IRecordingViewProps) { videoRecorder.current.onstart = (event: any) => { setRecording(true); // start the recording api when the video recorder starts - trackScreen && TrackMovements.Instance.start(); + (trackScreen || props.forceTrackScreen) && TrackMovements.Instance.start(); }; videoRecorder.current.onstop = () => { @@ -147,7 +149,7 @@ export function RecordingView(props: IRecordingViewProps) { // depending on if a presenation exists, add it to the video const presentation = TrackMovements.Instance.yieldPresentation(); - setVideos(videos => [...videos, presentation != null && trackScreen ? { ...nextVideo, presentation } : nextVideo]); + setVideos(videos => [...videos, presentation != null && (trackScreen || props.forceTrackScreen) ? { ...nextVideo, presentation } : nextVideo]); } // reset the temporary chunks @@ -159,9 +161,7 @@ export function RecordingView(props: IRecordingViewProps) { }; // if this is called, then we're done recording all the segments - const finish = (e: React.PointerEvent) => { - e.stopPropagation(); - + const finish = () => { // call stop on the video recorder if active videoRecorder.current?.state !== 'inactive' && videoRecorder.current?.stop(); @@ -176,8 +176,7 @@ export function RecordingView(props: IRecordingViewProps) { setFinished(true); }; - const pause = (e: React.PointerEvent) => { - e.stopPropagation(); + const pause = () => { // if recording, then this is just a new segment videoRecorder.current?.state === 'recording' && videoRecorder.current.stop(); }; @@ -217,6 +216,10 @@ export function RecordingView(props: IRecordingViewProps) { return toTwoDigit(minutes) + ' : ' + toTwoDigit(seconds); }; + useEffect(() => { + props.getControls(record, pause, finish); + }, []); + return (
@@ -227,7 +230,19 @@ export function RecordingView(props: IRecordingViewProps) {
-
{recording ?
+
+ {recording ? ( +
{!recording && (videos.length > 0 ? ( @@ -236,7 +251,12 @@ export function RecordingView(props: IRecordingViewProps) { - + { + e.stopPropagation(); + finish(); + }} + />
) : ( @@ -244,7 +264,7 @@ export function RecordingView(props: IRecordingViewProps) {