diff options
Diffstat (limited to 'src/client/views/nodes/ScreenshotBox.tsx')
-rw-r--r-- | src/client/views/nodes/ScreenshotBox.tsx | 281 |
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 |