aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/ScreenshotBox.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/ScreenshotBox.tsx')
-rw-r--r--src/client/views/nodes/ScreenshotBox.tsx281
1 files changed, 145 insertions, 136 deletions
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index a14d8ccae..c00c79eb9 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -1,40 +1,59 @@
import React = require("react");
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, IReactionDisposer, observable, runInAction } from "mobx";
+import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
-import * as rp from 'request-promise';
+import { DateField } from "../../../fields/DateField";
import { Doc, WidthSym } from "../../../fields/Doc";
import { documentSchema } from "../../../fields/documentSchemas";
+import { Id } from "../../../fields/FieldSymbols";
import { InkTool } from "../../../fields/InkField";
-import { listSpec, makeInterface } from "../../../fields/Schema";
+import { makeInterface } from "../../../fields/Schema";
+import { ComputedField } from "../../../fields/ScriptField";
import { Cast, NumCast } from "../../../fields/Types";
-import { VideoField } from "../../../fields/URLField";
-import { emptyFunction, returnFalse, returnOne, returnZero, Utils, OmitKeys } from "../../../Utils";
-import { Docs } from "../../documents/Documents";
+import { AudioField, VideoField } from "../../../fields/URLField";
+import { emptyFunction, OmitKeys, returnFalse, returnOne, Utils } from "../../../Utils";
+import { DocUtils } from "../../documents/Documents";
+import { DocumentType } from "../../documents/DocumentTypes";
+import { Networking } from "../../Network";
+import { CurrentUserUtils } from "../../util/CurrentUserUtils";
import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
+import { CollectionStackedTimeline } from "../collections/CollectionStackedTimeline";
import { ContextMenu } from "../ContextMenu";
-import { ContextMenuProps } from "../ContextMenuItem";
-import { ViewBoxBaseComponent } from "../DocComponent";
+import { ViewBoxAnnotatableComponent } from "../DocComponent";
import { FieldView, FieldViewProps } from './FieldView';
import "./ScreenshotBox.scss";
-import { CurrentUserUtils } from "../../util/CurrentUserUtils";
-const path = require('path');
+import { VideoBox } from "./VideoBox";
+import { TraceMobx } from "../../../fields/util";
+import { FormattedTextBox } from "./formattedText/FormattedTextBox";
+declare class MediaRecorder {
+ constructor(e: any, options?: any); // whatever MediaRecorder has
+}
type ScreenshotDocument = makeInterface<[typeof documentSchema]>;
const ScreenshotDocument = makeInterface(documentSchema);
@observer
-export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, ScreenshotDocument>(ScreenshotDocument) {
- private _reactionDisposer?: IReactionDisposer;
- private _videoRef: HTMLVideoElement | null = null;
+export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps, ScreenshotDocument>(ScreenshotDocument) {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ScreenshotBox, fieldKey); }
+ private _videoRef = React.createRef<HTMLVideoElement>();
+ private _audioRec: any;
+ private _videoRec: any;
+ @observable _screenCapture = false;
+ @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); }
- public get player(): HTMLVideoElement | null {
- return this._videoRef;
+ constructor(props: any) {
+ super(props);
+ this.setupDictation();
+ }
+ getAnchor = () => {
+ const startTime = Cast(this.layoutDoc._currentTimecode, "number", null) || (this._videoRec ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined);
+ return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow", "_timecodeToHide",
+ startTime, startTime === undefined ? undefined : startTime + 3)
+ || this.rootDoc;
}
videoLoad = () => {
- const aspect = this.player!.videoWidth / this.player!.videoHeight;
+ const aspect = this._videoRef.current!.videoWidth / this._videoRef.current!.videoHeight;
const nativeWidth = Doc.NativeWidth(this.layoutDoc);
const nativeHeight = Doc.NativeHeight(this.layoutDoc);
if (!nativeWidth || !nativeHeight) {
@@ -44,95 +63,24 @@ export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, Screensh
}
}
- @action public Snapshot() {
- const width = NumCast(this.layoutDoc._width);
- const height = NumCast(this.layoutDoc._height);
- const canvas = document.createElement('canvas');
- canvas.width = 640;
- canvas.height = 640 / (Doc.NativeAspect(this.layoutDoc) || 1);
- const ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions
- if (ctx) {
- ctx.rect(0, 0, canvas.width, canvas.height);
- ctx.fillStyle = "blue";
- ctx.fill();
- this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height);
- }
-
- if (this._videoRef) {
- //convert to desired file format
- const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
- // if you want to preview the captured image,
- const filename = path.basename(encodeURIComponent("screenshot" + Utils.GenerateGuid().replace(/\..*$/, "").replace(" ", "_")));
- ScreenshotBox.convertDataUri(dataUrl, filename).then(returnedFilename => {
- setTimeout(() => {
- if (returnedFilename) {
- const imageSummary = Docs.Create.ImageDocument(Utils.prepend(returnedFilename), {
- x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y),
- _width: 150, _height: height / width * 150, title: "--screenshot--"
- });
- if (!this.props.addDocument || this.props.addDocument === returnFalse) {
- const spt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
- imageSummary.x = spt[0];
- imageSummary.y = spt[1];
- Cast(Cast(Doc.UserDoc().myOverlayDocs, Doc, null)?.data, listSpec(Doc), []).push(imageSummary);
- } else {
- this.props.addDocument?.(imageSummary);
- }
- }
- }, 500);
- });
- }
- }
-
componentDidMount() {
+ this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = 0;
+ this.props.setContentView?.(this); // this tells the DocumentView that this ScreenshotBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
}
-
componentWillUnmount() {
- this._reactionDisposer && this._reactionDisposer();
- }
-
- @action
- setVideoRef = (vref: HTMLVideoElement | null) => {
- this._videoRef = vref;
+ const ind = DocUtils.ActiveRecordings.indexOf(this);
+ ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
}
- public static async convertDataUri(imageUri: string, returnedFilename: string) {
- try {
- const posting = Utils.prepend("/uploadURI");
- const returnedUri = await rp.post(posting, {
- body: {
- uri: imageUri,
- name: returnedFilename
- },
- json: true,
- });
- return returnedUri;
-
- } catch (e) {
- console.log("ScreenShotBox:" + e);
- }
- }
- @observable _screenCapture = false;
specificContextMenu = (e: React.MouseEvent): void => {
- const field = Cast(this.dataDoc[this.fieldKey], VideoField);
- if (field) {
- const url = field.url.href;
- const subitems: ContextMenuProps[] = [];
- subitems.push({ description: "Take Snapshot", event: () => this.Snapshot(), icon: "expand-arrows-alt" });
- subitems.push({
- description: "Screen Capture", event: (async () => {
- runInAction(() => this._screenCapture = !this._screenCapture);
- this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
- }), icon: "expand-arrows-alt"
- });
- ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" });
- }
+ const subitems = [{ description: "Screen Capture", event: this.toggleRecording, icon: "expand-arrows-alt" as any }];
+ ContextMenu.Instance.addItem({ description: "Options...", subitems, icon: "video" });
}
@computed get content() {
const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive";
- const style = "videoBox-content" + interactive;
- return <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
+ return <video className={"videoBox-content" + interactive} key="video" ref={this._videoRef}
+ autoPlay={this._screenCapture}
style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }}
onCanPlay={this.videoLoad}
controls={true}
@@ -144,53 +92,114 @@ export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, Screensh
toggleRecording = action(async () => {
this._screenCapture = !this._screenCapture;
- this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
+ if (this._screenCapture) {
+ this._audioRec = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true }));
+ const aud_chunks: any = [];
+ this._audioRec.ondataavailable = (e: any) => aud_chunks.push(e.data);
+ this._audioRec.onstop = async (e: any) => {
+ const [{ result }] = await Networking.UploadFilesToServer(aud_chunks);
+ if (!(result instanceof Error)) {
+ this.dataDoc[this.props.fieldKey + "-audio"] = new AudioField(Utils.prepend(result.accessPaths.agnostic.client));
+ }
+ };
+ this._videoRef.current!.srcObject = await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
+ this._videoRec = new MediaRecorder(this._videoRef.current!.srcObject);
+ const vid_chunks: any = [];
+ this._videoRec.onstart = () => this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField(new Date());
+ this._videoRec.ondataavailable = (e: any) => vid_chunks.push(e.data);
+ this._videoRec.onstop = async (e: any) => {
+ const file = new File(vid_chunks, `${this.rootDoc[Id]}.mkv`, { type: vid_chunks[0].type, lastModified: Date.now() });
+ const [{ result }] = await Networking.UploadFilesToServer(file);
+ this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this.recordingStart!) / 1000;
+ if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
+ this.dataDoc.type = DocumentType.VID;
+ this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey);
+ this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined;
+ this.layoutDoc._fitWidth = undefined;
+ this.dataDoc[this.props.fieldKey] = new VideoField(Utils.prepend(result.accessPaths.agnostic.client));
+ } else alert("video conversion failed");
+ };
+ this._audioRec.start();
+ this._videoRec.start();
+ this.dataDoc.mediaState = "recording";
+ DocUtils.ActiveRecordings.push(this);
+ } else {
+ this._audioRec.stop();
+ this._videoRec.stop();
+ this.dataDoc.mediaState = "paused";
+ const ind = DocUtils.ActiveRecordings.indexOf(this);
+ ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
+ }
});
- private get uIButtons() {
- return (<div className="screenshotBox-uiButtons">
- <div className="screenshotBox-recorder" key="snap" onPointerDown={this.toggleRecording} >
- <FontAwesomeIcon icon="file" size="lg" />
- </div>,
- <div className="screenshotBox-snapshot" key="snap" onPointerDown={this.onSnapshot} >
- <FontAwesomeIcon icon="camera" size="lg" />
- </div>
- </div>);
- }
-
- onSnapshot = (e: React.PointerEvent) => {
- this.Snapshot();
- e.stopPropagation();
- e.preventDefault();
+ setupDictation = () => {
+ if (this.dataDoc[this.fieldKey + "-dictation"]) return;
+ const dictationText = CurrentUserUtils.GetNewTextDoc("dictation",
+ NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10,
+ NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height));
+ dictationText._autoHeight = false;
+ const dictationTextProto = Doc.GetProto(dictationText);
+ dictationTextProto.recordingSource = this.dataDoc;
+ dictationTextProto.recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`);
+ dictationTextProto.mediaState = ComputedField.MakeFunction("self.recordingSource.mediaState");
+ this.dataDoc[this.fieldKey + "-dictation"] = dictationText;
}
-
-
contentFunc = () => [this.content];
+ videoPanelHeight = () => NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], 1) / NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], 1) * this.props.PanelWidth();
+ formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight());
render() {
- return (<div className="videoBox" onContextMenu={this.specificContextMenu}
- style={{ width: `${100}%`, height: `${100}%` }} >
+ TraceMobx();
+ return <div className="videoBox" onContextMenu={this.specificContextMenu} style={{ width: "100%", height: "100%" }} >
<div className="videoBox-viewer" >
- <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
- PanelHeight={this.props.PanelHeight}
- PanelWidth={this.props.PanelWidth}
- focus={this.props.focus}
- isSelected={this.props.isSelected}
- isAnnotationOverlay={true}
- select={emptyFunction}
- active={returnFalse}
- scaling={returnOne}
- whenActiveChanged={emptyFunction}
- removeDocument={returnFalse}
- moveDocument={returnFalse}
- addDocument={returnFalse}
- CollectionView={undefined}
- ScreenToLocalTransform={this.props.ScreenToLocalTransform}
- renderDepth={this.props.renderDepth + 1}
- ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
- {this.contentFunc}
- </CollectionFreeFormView>
+ <div style={{ position: "relative", height: this.videoPanelHeight() }}>
+ <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
+ PanelHeight={this.videoPanelHeight}
+ PanelWidth={this.props.PanelWidth}
+ focus={this.props.focus}
+ isSelected={this.props.isSelected}
+ isAnnotationOverlay={true}
+ select={emptyFunction}
+ active={returnFalse}
+ scaling={returnOne}
+ whenActiveChanged={emptyFunction}
+ removeDocument={returnFalse}
+ moveDocument={returnFalse}
+ addDocument={returnFalse}
+ CollectionView={undefined}
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+ renderDepth={this.props.renderDepth + 1}
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
+ {this.contentFunc}
+ </CollectionFreeFormView></div>
+ <div style={{ position: "relative", height: this.formattedPanelHeight() }}>
+ <FormattedTextBox {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
+ Document={this.dataDoc[this.fieldKey + "-dictation"]}
+ fieldKey={"text"}
+ PanelHeight={this.formattedPanelHeight}
+ PanelWidth={this.props.PanelWidth}
+ focus={this.props.focus}
+ isSelected={this.props.isSelected}
+ isAnnotationOverlay={true}
+ select={emptyFunction}
+ active={returnFalse}
+ scaling={returnOne}
+ xMargin={25}
+ yMargin={10}
+ whenActiveChanged={emptyFunction}
+ removeDocument={returnFalse}
+ moveDocument={returnFalse}
+ addDocument={returnFalse}
+ CollectionView={undefined}
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+ renderDepth={this.props.renderDepth + 1}
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
+ </FormattedTextBox></div>
</div>
- {this.props.isSelected() ? this.uIButtons : (null)}
- </div >);
+ {!this.props.isSelected() ? (null) : <div className="screenshotBox-uiButtons">
+ <div className="screenshotBox-recorder" key="snap" onPointerDown={this.toggleRecording} >
+ <FontAwesomeIcon icon="file" size="lg" />
+ </div>
+ </div>}
+ </div >;
}
} \ No newline at end of file