aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMohammad Amoush <muhammedamoush@gmail.com>2019-09-13 16:43:09 -0400
committerMohammad Amoush <muhammedamoush@gmail.com>2019-09-13 16:43:09 -0400
commitee03fa6e04dd9dba3099f75154de6ffab566ff5c (patch)
treea4c79f9d871211fc3c15d443c67fdf7cc2aee04a
parent68e554cafb6107bfde9526773b3e0e667d582c88 (diff)
Trial 5(Most succesful yet)
-rw-r--r--package.json2
-rw-r--r--src/client/documents/DocumentTypes.ts1
-rw-r--r--src/client/documents/Documents.ts10
-rw-r--r--src/client/views/MainView.tsx5
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx4
-rw-r--r--src/client/views/webcam/DashWebCam.tsx302
-rw-r--r--src/new_fields/URLField.ts2
7 files changed, 323 insertions, 3 deletions
diff --git a/package.json b/package.json
index c2085fb45..d0718345f 100644
--- a/package.json
+++ b/package.json
@@ -100,6 +100,7 @@
"@types/react-color": "^2.14.1",
"@types/react-measure": "^2.0.4",
"@types/react-table": "^6.7.22",
+ "@types/react-webcam": "^3.0.0",
"@types/request": "^2.48.1",
"@types/request-promise": "^4.1.42",
"@types/sharp": "^0.22.2",
@@ -201,6 +202,7 @@
"react-simple-dropdown": "^3.2.3",
"react-split-pane": "^0.1.85",
"react-table": "^6.9.2",
+ "react-webcam": "^3.0.1",
"readline": "^1.3.0",
"request": "^2.88.0",
"request-promise": "^4.2.4",
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 381981e1b..4430ffd62 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -17,6 +17,7 @@ export enum DocumentType {
TEMPLATE = "template",
EXTENSION = "extension",
YOUTUBE = "youtube",
+ WEBCAM = "webcam",
DRAGBOX = "dragbox",
PRES = "presentation",
LINKFOLLOW = "linkfollow",
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 602a7f9ad..036cc75a0 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -22,7 +22,7 @@ import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
import { IconBox } from "../views/nodes/IconBox";
import { Field, Doc, Opt } from "../../new_fields/Doc";
import { OmitKeys, JSONUtils } from "../../Utils";
-import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField } from "../../new_fields/URLField";
+import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField, WebCamField } from "../../new_fields/URLField";
import { HtmlField } from "../../new_fields/HtmlField";
import { List } from "../../new_fields/List";
import { Cast, NumCast } from "../../new_fields/Types";
@@ -46,6 +46,7 @@ import { ComputedField } from "../../new_fields/ScriptField";
import { ProxyField } from "../../new_fields/Proxy";
import { DocumentType } from "./DocumentTypes";
import { LinkFollowBox } from "../views/linking/LinkFollowBox";
+import { DashWebCam } from "../views/webcam/DashWebCam";
//import { PresBox } from "../views/nodes/PresBox";
//import { PresField } from "../../new_fields/PresField";
var requestImageSize = require('../util/request-image-size');
@@ -176,6 +177,9 @@ export namespace Docs {
}],
[DocumentType.LINKFOLLOW, {
layout: { view: LinkFollowBox }
+ }],
+ [DocumentType.WEBCAM, {
+ layout: { view: DashWebCam }
}]
]);
@@ -357,6 +361,10 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), new YoutubeField(new URL(url)), options);
}
+ export function WebCamDocument(url: string, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.WEBCAM), "", options);
+ }
+
export function AudioDocument(url: string, options: DocumentOptions = {}) {
return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(new URL(url)), options);
}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index b64986084..62d4fb1b8 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,5 +1,5 @@
import { IconName, library } from '@fortawesome/fontawesome-svg-core';
-import { faLink, faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faUndoAlt, faTv } from '@fortawesome/free-solid-svg-icons';
+import { faLink, faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faUndoAlt, faTv, faVideo } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
@@ -195,6 +195,7 @@ export class MainView extends React.Component {
library.add(faArrowUp);
library.add(faCloudUploadAlt);
library.add(faBolt);
+ library.add(faVideo);
this.initEventListeners();
this.initAuthenticationRouters();
}
@@ -456,6 +457,7 @@ export class MainView extends React.Component {
let addImportCollectionNode = action(() => Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 }));
// let youtubeurl = "https://www.youtube.com/embed/TqcApsGRzWw";
// let addYoutubeSearcher = action(() => Docs.Create.YoutubeDocument(youtubeurl, { width: 600, height: 600, title: "youtube search" }));
+ let addWebCam = action(() => Docs.Create.WebCamDocument("", {}));
let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [
[React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode],
@@ -464,6 +466,7 @@ export class MainView extends React.Component {
[React.createRef<HTMLDivElement>(), "bolt", "Add Button", addButtonDocument],
[React.createRef<HTMLDivElement>(), "file", "Add Document Dragger", addDragboxNode],
[React.createRef<HTMLDivElement>(), "cloud-upload-alt", "Import Directory", addImportCollectionNode], //remove at some point in favor of addImportCollectionNode
+ [React.createRef<HTMLDivElement>(), "video", "Add WebCam", addWebCam]
//[React.createRef<HTMLDivElement>(), "play", "Add Youtube Searcher", addYoutubeSearcher],
];
if (!ClientUtils.RELEASE) btns.unshift([React.createRef<HTMLDivElement>(), "cat", "Add Cat Image", addImageNode]);
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index d0e117fe4..df5ff04dd 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -32,6 +32,8 @@ import { Doc } from "../../../new_fields/Doc";
import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox";
import { ScriptField } from "../../../new_fields/ScriptField";
import { fromPromise } from "mobx-utils";
+import { DashWebCam } from "../../views/webcam/DashWebCam";
+
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
type BindingProps = Without<FieldViewProps, 'fieldKey'>;
@@ -110,7 +112,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null);
return <ObserverJsxParser
blacklistedAttrs={[]}
- components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox }}
+ components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, DashWebCam }}
bindings={this.CreateBindings()}
jsx={this.finalLayout}
showWarnings={true}
diff --git a/src/client/views/webcam/DashWebCam.tsx b/src/client/views/webcam/DashWebCam.tsx
new file mode 100644
index 000000000..a158938fe
--- /dev/null
+++ b/src/client/views/webcam/DashWebCam.tsx
@@ -0,0 +1,302 @@
+import React = require("react");
+import { observer } from "mobx-react";
+import { FieldViewProps, FieldView } from "../nodes/FieldView";
+import { observable } from "mobx";
+
+
+
+function hasGetUserMedia() {
+ return !!(
+ (navigator.mediaDevices && navigator.mediaDevices.getUserMedia));
+}
+
+interface WebcamProps {
+ audio: boolean;
+ audioConstraints?: MediaStreamConstraints["audio"];
+ imageSmoothing: boolean;
+ minScreenshotHeight?: number;
+ minScreenshotWidth?: number;
+ onUserMedia: () => void;
+ onUserMediaError: (error: string) => void;
+ screenshotFormat: "image/webp" | "image/png" | "image/jpeg";
+ screenshotQuality: number;
+ videoConstraints?: MediaStreamConstraints["video"];
+}
+
+@observer
+export class DashWebCam extends React.Component<FieldViewProps & WebcamProps & React.HTMLAttributes<HTMLVideoElement>, WebcamState> {
+ static defaultProps = {
+ audio: true,
+ imageSmoothing: true,
+ onUserMedia: () => { },
+ onUserMediaError: () => { },
+ screenshotFormat: "image/webp",
+ screenshotQuality: 0.92
+ };
+
+ private static mountedInstances: DashWebCam[] = [];
+ private static userMediaRequested = false;
+ private canvas: HTMLCanvasElement | undefined;
+ private ctx: CanvasRenderingContext2D | null = null;
+ private stream: MediaStream | undefined;
+ private video: HTMLVideoElement | null | undefined;
+
+ @observable private hasUserMedia: boolean | undefined;
+ @observable private src: string | undefined;
+
+ // constructor(props: any) {
+ // super(props);
+ // this.state = {
+ // hasUserMedia: false
+ // };
+ // }
+
+ componentDidMount() {
+ if (!hasGetUserMedia()) return;
+
+ const { state } = this;
+
+ DashWebCam.mountedInstances.push(this);
+
+ if (!state.hasUserMedia && !DashWebCam.userMediaRequested) {
+ this.requestUserMedia();
+ }
+ }
+
+ componentDidUpdate(nextProps: WebcamProps) {
+ const { props } = this;
+ if (
+ JSON.stringify(nextProps.audioConstraints) !==
+ JSON.stringify(props.audioConstraints) ||
+ JSON.stringify(nextProps.videoConstraints) !==
+ JSON.stringify(props.videoConstraints)
+ ) {
+ this.requestUserMedia();
+ }
+ }
+
+ componentWillUnmount() {
+ //const { state } = this;
+ const index = DashWebCam.mountedInstances.indexOf(this);
+ DashWebCam.mountedInstances.splice(index, 1);
+
+ DashWebCam.userMediaRequested = false;
+ if (DashWebCam.mountedInstances.length === 0 && this.hasUserMedia) {
+ if (this.stream!.getVideoTracks && this.stream!.getAudioTracks) {
+ this.stream!.getVideoTracks().map(track => track.stop());
+ this.stream!.getAudioTracks().map(track => track.stop());
+ } else {
+ ((this.stream as unknown) as MediaStreamTrack).stop();
+ }
+
+ if (this.src) {
+ window.URL.revokeObjectURL(this.src);
+ }
+ }
+ }
+
+ getScreenshot() {
+ const { props } = this;
+
+ if (!this.hasUserMedia) return null;
+
+ const canvas = this.getCanvas();
+ return (
+ canvas &&
+ canvas.toDataURL(props.screenshotFormat, props.screenshotQuality)
+ );
+ }
+
+ getCanvas() {
+ const { props } = this;
+
+ if (!this.video) {
+ return null;
+ }
+
+ if (!this.hasUserMedia || !this.video.videoHeight) return null;
+
+ if (!this.ctx) {
+ const canvas = document.createElement("canvas");
+ const aspectRatio = this.video.videoWidth / this.video.videoHeight;
+
+ let canvasWidth = props.minScreenshotWidth || this.video.clientWidth;
+ let canvasHeight = canvasWidth / aspectRatio;
+
+ if (
+ props.minScreenshotHeight &&
+ canvasHeight < props.minScreenshotHeight
+ ) {
+ canvasHeight = props.minScreenshotHeight;
+ canvasWidth = canvasHeight * aspectRatio;
+ }
+
+ canvas.width = canvasWidth;
+ canvas.height = canvasHeight;
+
+ this.canvas = canvas;
+ this.ctx = canvas.getContext("2d");
+ }
+
+ const { ctx, canvas } = this;
+
+ if (ctx) {
+ ctx.imageSmoothingEnabled = props.imageSmoothing;
+ ctx.drawImage(this.video, 0, 0, canvas!.width, canvas!.height);
+ }
+
+ return canvas;
+ }
+
+ requestUserMedia() {
+ const { props } = this;
+
+ navigator.getUserMedia =
+ navigator.mediaDevices.getUserMedia;
+
+ const sourceSelected = (audioConstraints, videoConstraints) => {
+ const constraints: MediaStreamConstraints = {
+ video: typeof videoConstraints !== "undefined" ? videoConstraints : true
+ };
+
+ if (props.audio) {
+ constraints.audio =
+ typeof audioConstraints !== "undefined" ? audioConstraints : true;
+ }
+
+ navigator.mediaDevices
+ .getUserMedia(constraints)
+ .then(stream => {
+ DashWebCam.mountedInstances.forEach(instance =>
+ instance.handleUserMedia(null, stream)
+ );
+ })
+ .catch(e => {
+ DashWebCam.mountedInstances.forEach(instance =>
+ instance.handleUserMedia(e)
+ );
+ });
+ };
+
+ if ("mediaDevices" in navigator) {
+ sourceSelected(props.audioConstraints, props.videoConstraints);
+ } else {
+ const optionalSource = id => ({ optional: [{ sourceId: id }] });
+
+ const constraintToSourceId = constraint => {
+ const { deviceId } = constraint;
+
+ if (typeof deviceId === "string") {
+ return deviceId;
+ }
+
+ if (Array.isArray(deviceId) && deviceId.length > 0) {
+ return deviceId[0];
+ }
+
+ if (typeof deviceId === "object" && deviceId.ideal) {
+ return deviceId.ideal;
+ }
+
+ return null;
+ };
+
+ // @ts-ignore: deprecated api
+ MediaStreamTrack.getSources(sources => {
+ let audioSource = null;
+ let videoSource = null;
+
+ sources.forEach(source => {
+ if (source.kind === "audio") {
+ audioSource = source.id;
+ } else if (source.kind === "video") {
+ videoSource = source.id;
+ }
+ });
+
+ const audioSourceId = constraintToSourceId(props.audioConstraints);
+ if (audioSourceId) {
+ audioSource = audioSourceId;
+ }
+
+ const videoSourceId = constraintToSourceId(props.videoConstraints);
+ if (videoSourceId) {
+ videoSource = videoSourceId;
+ }
+
+ sourceSelected(
+ optionalSource(audioSource),
+ optionalSource(videoSource)
+ );
+ });
+ }
+
+ DashWebCam.userMediaRequested = true;
+ }
+
+ handleUserMedia(err, stream?: MediaStream) {
+ const { props } = this;
+
+ if (err || !stream) {
+ this.setState({ hasUserMedia: false });
+ props.onUserMediaError(err);
+
+ return;
+ }
+
+ this.stream = stream;
+
+ try {
+ if (this.video) {
+ this.video.srcObject = stream;
+ }
+ this.setState({ hasUserMedia: true });
+ } catch (error) {
+ this.setState({
+ hasUserMedia: true,
+ src: window.URL.createObjectURL(stream)
+ });
+ }
+
+ props.onUserMedia();
+ }
+
+
+
+
+
+ public static LayoutString() { return FieldView.LayoutString(DashWebCam); }
+
+
+ render() {
+ const { props } = this;
+
+ const {
+ audio,
+ onUserMedia,
+ onUserMediaError,
+ screenshotFormat,
+ screenshotQuality,
+ minScreenshotWidth,
+ minScreenshotHeight,
+ audioConstraints,
+ videoConstraints,
+ imageSmoothing,
+ ...rest
+ } = props;
+
+
+ return (
+ <video
+ autoPlay
+ src={this.src}
+ muted={audio}
+ playsInline
+ ref={ref => {
+ this.video = ref;
+ }}
+ {...rest}
+ />
+ );
+ }
+} \ No newline at end of file
diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts
index b9ad96450..eebd15ecd 100644
--- a/src/new_fields/URLField.ts
+++ b/src/new_fields/URLField.ts
@@ -44,3 +44,5 @@ export abstract class URLField extends ObjectField {
@scriptingGlobal @Deserializable("pdf") export class PdfField extends URLField { }
@scriptingGlobal @Deserializable("web") export class WebField extends URLField { }
@scriptingGlobal @Deserializable("youtube") export class YoutubeField extends URLField { }
+@scriptingGlobal @Deserializable("webcam") export class WebCamField extends URLField { }
+