aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/DocumentTypes.ts1
-rw-r--r--src/client/documents/Documents.ts21
-rw-r--r--src/client/views/MainView.tsx7
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx18
-rw-r--r--src/client/views/webcam/DashWebCam.tsx400
-rw-r--r--src/client/views/webcam/DashWebRTC.tsx138
-rw-r--r--src/new_fields/URLField.ts2
-rw-r--r--src/typings/index.d.ts3
8 files changed, 590 insertions, 0 deletions
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index e5d5885cd..be290862a 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 0d04d044e..3482033d5 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -20,8 +20,12 @@ import { AggregateFunction } from "../northstar/model/idea/idea";
import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
import { IconBox } from "../views/nodes/IconBox";
import { OmitKeys, JSONUtils } from "../../Utils";
+<<<<<<< HEAD
+import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField, WebCamField } from "../../new_fields/URLField";
+=======
import { Field, Doc, Opt, DocListCastAsync } from "../../new_fields/Doc";
import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField } from "../../new_fields/URLField";
+>>>>>>> 69e4a936c4eb0cc2e35e4e7f3258aed1f72b8da7
import { HtmlField } from "../../new_fields/HtmlField";
import { List } from "../../new_fields/List";
import { Cast, NumCast } from "../../new_fields/Types";
@@ -45,7 +49,14 @@ import { ComputedField } from "../../new_fields/ScriptField";
import { ProxyField } from "../../new_fields/Proxy";
import { DocumentType } from "./DocumentTypes";
import { LinkFollowBox } from "../views/linking/LinkFollowBox";
+<<<<<<< HEAD
+import { DashWebCam } from "../views/webcam/DashWebCam";
+import { DashWebRTC } from "../views/webcam/DashWebRTC";
+//import { PresBox } from "../views/nodes/PresBox";
+//import { PresField } from "../../new_fields/PresField";
+=======
import { PresElementBox } from "../views/presentationview/PresElementBox";
+>>>>>>> 69e4a936c4eb0cc2e35e4e7f3258aed1f72b8da7
var requestImageSize = require('../util/request-image-size');
var path = require('path');
@@ -175,9 +186,15 @@ export namespace Docs {
[DocumentType.LINKFOLLOW, {
layout: { view: LinkFollowBox }
}],
+<<<<<<< HEAD
+ [DocumentType.WEBCAM, {
+ layout: { view: DashWebRTC }
+ }]
+=======
[DocumentType.PRESELEMENT, {
layout: { view: PresElementBox }
}],
+>>>>>>> 69e4a936c4eb0cc2e35e4e7f3258aed1f72b8da7
]);
// All document prototypes are initialized with at least these values
@@ -365,6 +382,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 35d527c91..22d8171c0 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,5 +1,9 @@
import { IconName, library } from '@fortawesome/fontawesome-svg-core';
+<<<<<<< HEAD
+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 { 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, faTv, faUndoAlt } from '@fortawesome/free-solid-svg-icons';
+>>>>>>> 69e4a936c4eb0cc2e35e4e7f3258aed1f72b8da7
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
@@ -213,6 +217,7 @@ export class MainView extends React.Component {
library.add(faArrowUp);
library.add(faCloudUploadAlt);
library.add(faBolt);
+ library.add(faVideo);
this.initEventListeners();
this.initAuthenticationRouters();
}
@@ -535,6 +540,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 googlePhotosSearch = () => GooglePhotosClientUtils.CollectionFromSearch(Docs.Create.MasonryDocument, { included: [GooglePhotosClientUtils.ContentCategories.LANDSCAPES] });
@@ -546,6 +552,7 @@ export class MainView extends React.Component {
[React.createRef<HTMLDivElement>(), "file", "Add Document Dragger", addDragboxNode],
// [React.createRef<HTMLDivElement>(), "object-group", "Test Google Photos Search", googlePhotosSearch],
[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 75dd27f46..f1c10f2f2 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -30,6 +30,20 @@ import { PresElementBox } from "../presentationview/PresElementBox";
import { VideoBox } from "./VideoBox";
import { WebBox } from "./WebBox";
import React = require("react");
+<<<<<<< HEAD
+import { FieldViewProps } from "./FieldView";
+import { Without, OmitKeys } from "../../../Utils";
+import { Cast, StrCast, NumCast } from "../../../new_fields/Types";
+import { List } from "../../../new_fields/List";
+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";
+import { DashWebRTC } from "../webcam/DashWebRTC";
+
+=======
+>>>>>>> 69e4a936c4eb0cc2e35e4e7f3258aed1f72b8da7
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
type BindingProps = Without<FieldViewProps, 'fieldKey'>;
@@ -101,7 +115,11 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
if (!this.layout && this.props.layoutKey !== "overlayLayout") return (null);
return <ObserverJsxParser
blacklistedAttrs={[]}
+<<<<<<< HEAD
+ components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, DashWebCam, DashWebRTC }}
+=======
components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, PresElementBox }}
+>>>>>>> 69e4a936c4eb0cc2e35e4e7f3258aed1f72b8da7
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..175bbe595
--- /dev/null
+++ b/src/client/views/webcam/DashWebCam.tsx
@@ -0,0 +1,400 @@
+import React = require("react");
+import { observer } from "mobx-react";
+import { FieldViewProps, FieldView } from "../nodes/FieldView";
+import { observable, action } from "mobx";
+import { DocumentDecorations } from "../DocumentDecorations";
+import { InkingControl } from "../InkingControl";
+import { CollectionFreeFormDocumentViewProps } from "../nodes/CollectionFreeFormDocumentView";
+import "../../views/nodes/WebBox.scss";
+
+
+//https://github.com/mozmorris/react-webcam is the source code used for the bigger part of this implementation. It's only modified to fit our current system.
+
+
+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"];
+}
+
+interface WebcamState {
+ hasUserMedia: boolean;
+ src?: string;
+}
+
+@observer
+export class DashWebCam extends React.Component<CollectionFreeFormDocumentViewProps & FieldViewProps & WebcamProps & React.HTMLAttributes<HTMLVideoElement> & {
+ layoutKey: string,
+}, 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 && state.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 (state.src) {
+ window.URL.revokeObjectURL(state.src);
+ }
+ }
+ }
+
+ //These are for screenshot if wanted.
+
+ // getScreenshot() {
+ // const { state, props } = this;
+
+ // if (!state.hasUserMedia) return null;
+
+ // const canvas = this.getCanvas();
+ // return (
+ // canvas &&
+ // canvas.toDataURL(props.screenshotFormat, props.screenshotQuality)
+ // );
+ // }
+
+ // getCanvas() {
+ // const { state, props } = this;
+
+ // if (!this.video) {
+ // return null;
+ // }
+
+ // if (!state.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: any, videoConstraints: any) => {
+ 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: any) => ({ optional: [{ sourceId: id }] });
+
+ const constraintToSourceId = (constraint: any) => {
+ 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: { kind: string; id: any; }) => {
+ 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: string | null, stream?: MediaStream) {
+ const { props } = this;
+
+ if (err || !stream) {
+ this.setState({ hasUserMedia: false });
+ // action(() => this.hasUserMedia = false);
+ props.onUserMediaError(err!);
+
+ return;
+ }
+
+ this.stream = stream;
+
+ console.log("Stream done: ", stream);
+
+ try {
+ if (this.video) {
+ this.video.srcObject = stream;
+ console.log("Source object: ", stream);
+
+ }
+ this.setState({ hasUserMedia: true });
+ // action(() => this.hasUserMedia = true);
+
+ } catch (error) {
+ this.setState({
+ hasUserMedia: true,
+ src: window.URL.createObjectURL(stream)
+ });
+ console.log("State src set: ", this.state.src);
+
+ // action(() => this.hasUserMedia = true);
+ // action(() => this.src = window.URL.createObjectURL(stream));
+ }
+
+ props.onUserMedia();
+ }
+
+ _ignore = 0;
+ onPreWheel = (e: React.WheelEvent) => {
+ this._ignore = e.timeStamp;
+ }
+ onPrePointer = (e: React.PointerEvent) => {
+ this._ignore = e.timeStamp;
+ }
+ onPostPointer = (e: React.PointerEvent) => {
+ if (this._ignore !== e.timeStamp) {
+ e.stopPropagation();
+ }
+ }
+ onPostWheel = (e: React.WheelEvent) => {
+ if (this._ignore !== e.timeStamp) {
+ e.stopPropagation();
+ }
+ }
+
+
+
+
+ public static LayoutString() { return FieldView.LayoutString(DashWebCam); }
+
+
+ render() {
+ const { state, props } = this;
+
+
+
+ const {
+ audio,
+ onUserMedia,
+ onUserMediaError,
+ screenshotFormat,
+ screenshotQuality,
+ minScreenshotWidth,
+ minScreenshotHeight,
+ audioConstraints,
+ videoConstraints,
+ imageSmoothing,
+ fieldKey,
+ fieldExt,
+ leaveNativeSize,
+ fitToBox,
+ ContainingCollectionView,
+ Document,
+ DataDoc,
+ onClick,
+ isSelected,
+ select,
+ renderDepth,
+ addDocument,
+ addDocTab,
+ pinToPres,
+ removeDocument,
+ moveDocument,
+ ScreenToLocalTransform,
+ active,
+ whenActiveChanged,
+ focus,
+ PanelWidth,
+ PanelHeight,
+ setVideoBox,
+ setPdfBox,
+ ContentScaling,
+ ChromeHeight,
+ jitterRotation,
+ backgroundColor,
+ bringToFront,
+ zoomToScale,
+ getScale,
+ animateBetweenIcon,
+ layoutKey,
+ ...rest
+ } = props;
+
+ console.log("Source produced: ", state.src);
+
+
+
+ let content =
+ <div className="webcam-cont" style={{ width: "100%", height: "100%" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}>
+ <video
+ autoPlay
+ src={state.src}
+ muted={!audio}
+ playsInline
+ ref={ref => {
+ console.log("Source produced: ", state.src);
+ console.log("HasUser Media produced: ", state.hasUserMedia);
+
+ this.video = ref;
+ }}
+ {...rest}
+ />
+ </div>;
+
+
+ let frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
+ let classname = "webBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
+
+ return (
+ <>
+ <div className={classname} >
+ {content}
+ </div>
+ {!frozen ? (null) : <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} />}
+ </>);
+
+ }
+} \ No newline at end of file
diff --git a/src/client/views/webcam/DashWebRTC.tsx b/src/client/views/webcam/DashWebRTC.tsx
new file mode 100644
index 000000000..9c289b40f
--- /dev/null
+++ b/src/client/views/webcam/DashWebRTC.tsx
@@ -0,0 +1,138 @@
+import { observer } from "mobx-react";
+import React = require("react");
+import { CollectionFreeFormDocumentViewProps } from "../nodes/CollectionFreeFormDocumentView";
+import { FieldViewProps, FieldView } from "../nodes/FieldView";
+import { observable, trace } from "mobx";
+import { DocumentDecorations } from "../DocumentDecorations";
+import { InkingControl } from "../InkingControl";
+import "../../views/nodes/WebBox.scss";
+import adapter from 'webrtc-adapter';
+
+
+
+
+const mediaStreamConstaints = {
+ video: true,
+};
+
+const offerOptions = {
+ offerToReceiveVideo: 1,
+};
+
+
+@observer
+export class DashWebRTC extends React.Component<CollectionFreeFormDocumentViewProps & FieldViewProps> {
+
+ @observable private localVideoEl: HTMLVideoElement | undefined;
+ @observable private peerVideoEl: HTMLVideoElement | undefined;
+ @observable private localStream: MediaStream | undefined;
+ @observable private startTime = null;
+ @observable private remoteStream: MediaStream | undefined;
+ @observable private localPeerConnection: any;
+ @observable private remotePeerConnection: any;
+ private callButton: HTMLButtonElement | undefined;
+ private startButton: HTMLButtonElement | undefined;
+ private hangupButton: HTMLButtonElement | undefined;
+
+
+ componentDidMount() {
+ this.callButton!.disabled = true;
+ this.hangupButton!.disabled = true;
+ navigator.mediaDevices.getUserMedia(mediaStreamConstaints).then(this.gotLocalMediaStream).catch(this.handleLocalMediaStreamError);
+ this.localVideoEl!.addEventListener('loadedmetadata', this.logVideoLoaded);
+ this.peerVideoEl!.addEventListener('loadedmetadata', this.logVideoLoaded);
+ this.peerVideoEl!.addEventListener('onresize', this.logResizedVideo);
+ }
+
+
+ gotLocalMediaStream = (mediaStream: MediaStream) => {
+ this.localStream = mediaStream;
+ if (this.localVideoEl) {
+ this.localVideoEl.srcObject = mediaStream;
+ }
+ trace('Received local stream.');
+ this.callButton!.disabled = false;
+
+ }
+
+ gotRemoteMediaStream = (event: MediaStreamEvent) => {
+ let mediaStream = event.stream;
+ this.peerVideoEl!.srcObject = mediaStream;
+ this.remoteStream = mediaStream!;
+
+ }
+
+ handleLocalMediaStreamError = (error: string) => {
+ //console.log("navigator.getUserMedia error: ", error);
+ trace(`navigator.getUserMedia error: ${error.toString()}.`);
+
+ }
+
+ logVideoLoaded(event: any) {
+ let video = event.target;
+ trace(`${video!.id} videoWidth: ${video!.videoWidth}px, ` +
+ `videoHeight: ${video!.videoHeight}px.`);
+ }
+
+ logResizedVideo(event: any) {
+ this.logVideoLoaded(event);
+
+ if (this.startTime) {
+ let elapsedTime = window.performance.now() - this.startTime!;
+ this.startTime = null;
+ trace(`Setup time: ${elapsedTime.toFixed(3)}ms.`);
+ }
+
+ }
+
+
+
+
+ public static LayoutString() { return FieldView.LayoutString(DashWebRTC); }
+
+
+ _ignore = 0;
+ onPreWheel = (e: React.WheelEvent) => {
+ this._ignore = e.timeStamp;
+ }
+ onPrePointer = (e: React.PointerEvent) => {
+ this._ignore = e.timeStamp;
+ }
+ onPostPointer = (e: React.PointerEvent) => {
+ if (this._ignore !== e.timeStamp) {
+ e.stopPropagation();
+ }
+ }
+ onPostWheel = (e: React.WheelEvent) => {
+ if (this._ignore !== e.timeStamp) {
+ e.stopPropagation();
+ }
+ }
+
+
+
+ render() {
+ let content =
+ <div className="webcam-cont" style={{ width: "100%", height: "100%" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}>
+ <video id="localVideo" autoPlay playsInline ref={(e) => this.localVideoEl = e!}></video>
+ <video id="remoteVideo" autoPlay playsInline ref={(e) => this.peerVideoEl = e!}></video>
+ <button id="startButton" ref={(e) => this.startButton = e!}>Start</button>
+ <button id="callButton" ref={(e) => this.callButton = e!}>Call</button>
+ <button id="hangupButton" ref={(e) => this.hangupButton = e!}>Hang Up</button>
+ </div>;
+
+ let frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
+ let classname = "webBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
+
+
+ return (
+ <>
+ <div className={classname} >
+ {content}
+ </div>
+ {!frozen ? (null) : <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} />}
+ </>);
+ }
+
+
+} \ 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 { }
+
diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts
index 36d828fdb..26fc5a0b8 100644
--- a/src/typings/index.d.ts
+++ b/src/typings/index.d.ts
@@ -2,6 +2,9 @@
declare module 'googlephotos';
+declare module 'webrtc-adapter';
+
+
declare module '@react-pdf/renderer' {
import * as React from 'react';