aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2023-08-17 22:31:42 -0400
committerbobzel <zzzman@gmail.com>2023-08-17 22:31:42 -0400
commit9dfca50488241998fa1a99f660b12f43ad803d38 (patch)
tree8ce177ee908343f9becf96cd0523d3e848df951b /src
parentf0cca82b98e766474f1df49bacec2258b67aae4e (diff)
Added the beginning of an A/V recording button
Diffstat (limited to 'src')
-rw-r--r--src/client/util/CurrentUserUtils.ts9
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingBox.tsx63
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingView.tsx44
-rw-r--r--src/client/views/nodes/ScreenshotBox.tsx2
-rw-r--r--src/client/views/nodes/VideoBox.tsx6
-rw-r--r--src/server/DashUploadUtils.ts2
6 files changed, 95 insertions, 31 deletions
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<FieldViewProps>() {
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(RecordingBox, fieldKey);
}
private _ref: React.RefObject<HTMLDivElement> = 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 (
<div className="recordingBox" ref={this._ref}>
- {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} id={DocCast(this.rootDoc.proto)?.[Id] || ''} />}
+ {!this.result && (
+ <RecordingView
+ forceTrackScreen={BoolCast(this.layoutDoc[this.fieldKey + '_trackScreen'])}
+ getControls={this.getControls}
+ setResult={this.setResult}
+ setDuration={this.setVideoDuration}
+ id={DocCast(this.rootDoc.proto)?.[Id] || ''}
+ />
+ )}
</div>
);
}
+ 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 (
<div className="recording-container">
<div className="video-wrapper">
@@ -227,7 +230,19 @@ export function RecordingView(props: IRecordingViewProps) {
</div>
<div className="controls">
<div className="controls-inner-container">
- <div className="record-button-wrapper">{recording ? <button className="stop-button" onPointerDown={pause} /> : <button className="record-button" onPointerDown={start} />}</div>
+ <div className="record-button-wrapper">
+ {recording ? (
+ <button
+ className="stop-button"
+ onPointerDown={e => {
+ e.stopPropagation();
+ pause();
+ }}
+ />
+ ) : (
+ <button className="record-button" onPointerDown={start} />
+ )}
+ </div>
{!recording &&
(videos.length > 0 ? (
@@ -236,7 +251,12 @@ export function RecordingView(props: IRecordingViewProps) {
<MdBackspace onPointerDown={undoPrevious} />
</IconContext.Provider>
<IconContext.Provider value={{ color: '#cc1c08', className: 'video-edit-buttons' }}>
- <FaCheckCircle onPointerDown={finish} />
+ <FaCheckCircle
+ onPointerDown={e => {
+ e.stopPropagation();
+ finish();
+ }}
+ />
</IconContext.Provider>
</div>
) : (
@@ -244,7 +264,7 @@ export function RecordingView(props: IRecordingViewProps) {
<label className="track-screen">
<input
type="checkbox"
- checked={trackScreen}
+ checked={trackScreen || props.forceTrackScreen}
onChange={e => {
setTrackScreen(e.target.checked);
}}
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 0a33a336f..83a29f071 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -17,6 +17,7 @@ import { DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
import { Networking } from '../../Network';
import { CaptureManager } from '../../util/CaptureManager';
+import { SettingsManager } from '../../util/SettingsManager';
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import { CollectionStackedTimeline } from '../collections/CollectionStackedTimeline';
import { ContextMenu } from '../ContextMenu';
@@ -25,7 +26,6 @@ import { FieldView, FieldViewProps } from './FieldView';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
import './ScreenshotBox.scss';
import { VideoBox } from './VideoBox';
-import { SettingsManager } from '../../util/SettingsManager';
declare class MediaRecorder {
constructor(e: any, options?: any); // whatever MediaRecorder has
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 0f470347f..2177adeff 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -110,7 +110,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// returns the presentation data if it exists, null otherwise
@computed get presentation() {
- const data = this.dataDoc[this.fieldKey + '-presentation'];
+ const data = this.dataDoc[this.fieldKey + '_presentation'];
return data ? JSON.parse(StrCast(data)) : null;
}
@@ -524,7 +524,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
icon: 'expand-arrows-alt',
});
// if the videobox was turned from a recording box
- if (this.dataDoc[this.fieldKey + '-recorded'] === true) {
+ if (this.dataDoc[this.fieldKey + '_recorded'] === true) {
subitems.push({
description: 'Recreate recording',
event: () => {
@@ -533,7 +533,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this.dataDoc[this.props.fieldKey] = '';
this.dataDoc[this.fieldKey + '_duration'] = '';
// delete assoicated presentation data
- this.dataDoc[this.fieldKey + '-presentation'] = '';
+ this.dataDoc[this.fieldKey + '_presentation'] = '';
},
icon: 'expand-arrows-alt',
});
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 337bb812f..117981a7c 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -99,7 +99,7 @@ export namespace DashUploadUtils {
merge
.input(textFilePath)
.inputOptions(['-f concat', '-safe 0'])
- .outputOptions('-c copy')
+ // .outputOptions('-c copy')
//.videoCodec("copy")
.save(outputFilePath)
.on('error', (err: any) => {