aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Utils.ts8
-rw-r--r--src/client/DocServer.ts2
-rw-r--r--src/client/documents/DocumentTypes.ts2
-rw-r--r--src/client/documents/Documents.ts9
-rw-r--r--src/client/northstar/utils/Utils.ts6
-rw-r--r--src/client/views/DocumentDecorations.tsx14
-rw-r--r--src/client/views/MainView.tsx3
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx4
-rw-r--r--src/client/views/webcam/DashWebCam.tsx396
-rw-r--r--src/client/views/webcam/DashWebRTC.scss23
-rw-r--r--src/client/views/webcam/DashWebRTC.ts314
-rw-r--r--src/client/views/webcam/DashWebRTCVideo.scss64
-rw-r--r--src/client/views/webcam/DashWebRTCVideo.tsx404
-rw-r--r--src/client/views/webcam/WebCamLogic.js281
-rw-r--r--src/new_fields/URLField.ts2
-rw-r--r--src/server/Message.ts9
-rw-r--r--src/server/Websocket/Websocket.ts101
-rw-r--r--src/server/authentication/models/current_user_utils.ts1
-rw-r--r--src/server/index.ts48
-rw-r--r--src/typings/index.d.ts3
20 files changed, 1688 insertions, 6 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index 0fa33dcb7..b564564be 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -1,6 +1,6 @@
import v4 = require('uuid/v4');
import v5 = require("uuid/v5");
-import { Socket } from 'socket.io';
+import { Socket, Room } from 'socket.io';
import { Message } from './server/Message';
export namespace Utils {
@@ -310,6 +310,12 @@ export namespace Utils {
handler([arg, loggingCallback('S sending', fn, message.Name)]);
});
}
+ export type RoomHandler = (socket: Socket, room: string) => any;
+ export type UsedSockets = Socket | SocketIOClient.Socket;
+ export type RoomMessage = "create or join" | "created" | "joined";
+ export function AddRoomHandler(socket: Socket, message: RoomMessage, handler: RoomHandler) {
+ socket.on(message, room => handler(socket, room));
+ }
}
export function OmitKeys(obj: any, keys: string[], addKeyFunc?: (dup: any) => void): { omit: any, extract: any } {
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index d793b56af..7fbf30af8 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -21,7 +21,7 @@ import { Id, HandleUpdate } from '../new_fields/FieldSymbols';
*/
export namespace DocServer {
let _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {};
- let _socket: SocketIOClient.Socket;
+ export let _socket: SocketIOClient.Socket;
// this client's distinct GUID created at initialization
let GUID: string;
// indicates whether or not a document is currently being udpated, and, if so, its id
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 8f96b2fa6..46f27f958 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -17,6 +17,8 @@ export enum DocumentType {
TEMPLATE = "template",
EXTENSION = "extension",
YOUTUBE = "youtube",
+ WEBCAM = "webcam",
+ DRAGBOX = "dragbox",
FONTICON = "fonticonbox",
PRES = "presentation",
LINKFOLLOW = "linkfollow",
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index d647b34e6..ba0f69846 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -44,7 +44,9 @@ import { ComputedField, ScriptField } 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 { PresElementBox } from "../views/presentationview/PresElementBox";
+import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo";
import { QueryBox } from "../views/nodes/QueryBox";
import { ColorBox } from "../views/nodes/ColorBox";
import { DocuLinkBox } from "../views/nodes/DocuLinkBox";
@@ -241,6 +243,9 @@ export namespace Docs {
[DocumentType.LINKFOLLOW, {
layout: { view: LinkFollowBox, dataField: data }
}],
+ [DocumentType.WEBCAM, {
+ layout: { view: DashWebRTCVideo, dataField: data }
+ }],
[DocumentType.PRESELEMENT, {
layout: { view: PresElementBox, dataField: data }
}],
@@ -440,6 +445,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/northstar/utils/Utils.ts b/src/client/northstar/utils/Utils.ts
index d071dec62..09f83ce80 100644
--- a/src/client/northstar/utils/Utils.ts
+++ b/src/client/northstar/utils/Utils.ts
@@ -4,6 +4,12 @@ import { IBaseFilterProvider } from '../core/filter/IBaseFilterProvider';
import { AggregateFunction } from '../model/idea/idea';
export class Utils {
+ static Emit() {
+ throw new Error("Method not implemented.");
+ }
+ static EmitCallback() {
+ throw new Error("Method not implemented.");
+ }
public static EqualityHelper(a: Object, b: Object): boolean {
if (a === b) return true;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index a0ba16ea4..4ec1659cc 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -35,6 +35,8 @@ library.add(faCloudUploadAlt);
library.add(faSyncAlt);
library.add(faShare);
+export type CloseCall = (toBeDeleted: DocumentView[]) => void;
+
@observer
export class DocumentDecorations extends React.Component<{}, { value: string }> {
static Instance: DocumentDecorations;
@@ -59,6 +61,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
@observable public pullIcon: IconProp = "arrow-alt-circle-down";
@observable public pullColor: string = "white";
@observable public openHover = false;
+ @observable private addedCloseCalls: CloseCall[] = [];
+
constructor(props: Readonly<{}>) {
super(props);
@@ -69,6 +73,14 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
@action titleChanged = (event: any) => this._accumulatedTitle = event.target.value;
+ addCloseCall = (handler: CloseCall) => {
+ const currentOffset = this.addedCloseCalls.length - 1;
+ this.addedCloseCalls.push((toBeDeleted: DocumentView[]) => {
+ this.addedCloseCalls.splice(currentOffset, 1);
+ handler(toBeDeleted);
+ });
+ }
+
titleBlur = undoBatch(action((commit: boolean) => {
this._edtingTitle = false;
if (commit) {
@@ -225,6 +237,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
const recent = Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc) as Doc;
const selected = SelectionManager.SelectedDocuments().slice();
SelectionManager.DeselectAll();
+ this.addedCloseCalls.forEach(handler => handler(selected));
+
selected.map(dv => {
recent && Doc.AddDocToList(recent, "data", dv.props.Document, undefined, true, true);
dv.props.removeDocument && dv.props.removeDocument(dv.props.Document);
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index a839f9fd2..a9e81093a 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,7 +1,7 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import {
faFileAlt, faStickyNote, faArrowDown, faBullseye, faFilter, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faChevronRight, faClone, faCloudUploadAlt, faCommentAlt, faCut, faEllipsisV, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight,
- faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter, faMicrophone, faCompressArrowsAlt, faPhone, faStamp, faClipboard
+ faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter, faMicrophone, faCompressArrowsAlt, faPhone, faStamp, faClipboard, faVideo,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
@@ -139,6 +139,7 @@ export class MainView extends React.Component {
library.add(faArrowUp);
library.add(faCloudUploadAlt);
library.add(faBolt);
+ library.add(faVideo);
library.add(faChevronRight);
library.add(faEllipsisV);
library.add(faMusic);
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 3b9015994..ac80e2ec1 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -33,6 +33,8 @@ import { VideoBox } from "./VideoBox";
import { WebBox } from "./WebBox";
import { InkingStroke } from "../InkingStroke";
import React = require("react");
+import { DashWebRTCVideo } from "../webcam/DashWebRTCVideo";
+
import { TraceMobx } from "../../../new_fields/util";
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
@@ -107,7 +109,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, FontIconBox: FontIconBox, ButtonBox, FieldView,
CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox,
PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, PresElementBox, QueryBox,
- ColorBox, DocuLinkBox, InkingStroke, DocumentBox
+ ColorBox, DashWebRTCVideo, DocuLinkBox, InkingStroke, DocumentBox
}}
bindings={this.CreateBindings()}
jsx={this.layout}
diff --git a/src/client/views/webcam/DashWebCam.tsx b/src/client/views/webcam/DashWebCam.tsx
new file mode 100644
index 000000000..a9669750f
--- /dev/null
+++ b/src/client/views/webcam/DashWebCam.tsx
@@ -0,0 +1,396 @@
+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(fieldKey: string) { return FieldView.LayoutString(DashWebCam, fieldKey); }
+
+ render() {
+ const { state, props } = this;
+
+
+
+ const {
+ audio,
+ onUserMedia,
+ onUserMediaError,
+ screenshotFormat,
+ screenshotQuality,
+ minScreenshotWidth,
+ minScreenshotHeight,
+ audioConstraints,
+ videoConstraints,
+ imageSmoothing,
+ fieldKey,
+ fitToBox,
+ ContainingCollectionView,
+ Document,
+ DataDoc,
+ onClick,
+ isSelected,
+ select,
+ renderDepth,
+ addDocument,
+ addDocTab,
+ pinToPres,
+ removeDocument,
+ moveDocument,
+ ScreenToLocalTransform,
+ active,
+ whenActiveChanged,
+ focus,
+ PanelWidth,
+ PanelHeight,
+ setVideoBox,
+ 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.scss b/src/client/views/webcam/DashWebRTC.scss
new file mode 100644
index 000000000..ddf4777a8
--- /dev/null
+++ b/src/client/views/webcam/DashWebRTC.scss
@@ -0,0 +1,23 @@
+.webcam-cont {
+ button {
+ margin: 10px;
+ position: relative;
+ top: 20%;
+ left: -60%;
+ }
+
+ #localVideo {
+ margin: 10px;
+ position: relative;
+ width: 300px;
+ max-height: 300px;
+ }
+
+ #remoteVideo {
+ margin: 10px;
+ position: relative;
+ width: 300px;
+ max-height: 300px;
+
+ }
+} \ No newline at end of file
diff --git a/src/client/views/webcam/DashWebRTC.ts b/src/client/views/webcam/DashWebRTC.ts
new file mode 100644
index 000000000..ef5ecf0fc
--- /dev/null
+++ b/src/client/views/webcam/DashWebRTC.ts
@@ -0,0 +1,314 @@
+// import { DocServer } from '../../DocServer';
+// import { Utils } from '../../../Utils';
+// import { MessageStore } from '../../../server/Message';
+
+
+
+// /**
+// * This namespace will have the code required to have functionality code for the usage of webRTC.
+// */
+// export class DashWebRTC {
+
+
+// private isChannelReady = false;
+// private isInitiator = false;
+// private isStarted = false;
+// localStream: MediaStream | undefined;
+// private pc: any;
+// remoteStream: MediaStream | undefined;
+// private turnReady: boolean | undefined;
+// localVideo: HTMLVideoElement | undefined;
+// remoteVideo: HTMLVideoElement | undefined;
+// curRoom: string = "";
+
+
+// private pcConfig: any;
+// private sdpConstraints: any;
+
+// constructor() {
+// this.pcConfig = {
+// 'iceServers': [{
+// 'urls': 'stun:stun.l.google.com:19302'
+// }]
+// };
+
+// // Set up audio and video regardless of what devices are present.
+// this.sdpConstraints = {
+// offerToReceiveAudio: true,
+// offerToReceiveVideo: true
+// };
+// }
+
+
+
+// init(room: string) {
+
+// this.curRoom = room;
+// let self = this;
+
+// if (room !== '') {
+// DocServer._socket.emit('create or join', room);
+// console.log('Attempted to create or join room', room);
+
+// }
+
+// DocServer._socket.on('created', function (room: string) {
+// console.log('Created room ' + room);
+// self.isInitiator = true;
+// });
+
+// DocServer._socket.on('full', function (room: string) {
+// console.log('Room ' + room + ' is full');
+// });
+
+// DocServer._socket.on('join', function (room: string) {
+// console.log('Another peer made a request to join room ' + room);
+// console.log('This peer is the initiator of room ' + room + '!');
+// self.isChannelReady = true;
+// });
+
+
+// DocServer._socket.on('joined', function (room: string) {
+// console.log('joined: ' + room);
+// self.isChannelReady = true;
+// });
+
+
+// DocServer._socket.on('log', function (array: any) {
+// console.log.apply(console, array);
+// });
+
+// // This client receives a message
+// DocServer._socket.on('message', function (message: any) {
+// console.log('Client received message:', message);
+// if (message.message === 'got user media') {
+// self.maybeStart();
+// } else if (message.message.type === 'offer') {
+// if (!self.isInitiator && !self.isStarted) {
+// self.maybeStart();
+// }
+// self.pc.setRemoteDescription(new RTCSessionDescription(message.message));
+// self.doAnswer();
+// } else if (message.message.type === 'answer' && self.isStarted) {
+// self.pc.setRemoteDescription(new RTCSessionDescription(message.message));
+// } else if (message.message.type === 'candidate' && self.isStarted) {
+// let candidate = new RTCIceCandidate({
+// sdpMLineIndex: message.message.label,
+// candidate: message.message.candidate
+// });
+// self.pc.addIceCandidate(candidate);
+// } else if (message === 'bye' && self.isStarted) {
+// self.handleRemoteHangup();
+// }
+// });
+
+// navigator.mediaDevices.getUserMedia({
+// audio: false,
+// video: true
+// })
+// .then(this.gotStream)
+// .catch(function (e) {
+// alert('getUserMedia() error: ' + e.name);
+// });
+
+// //Trying this one out!!!
+// console.log('Getting user media with constraints', this.constraints);
+
+// if (location.hostname !== 'localhost') {
+// this.requestTurn(
+// 'https://computeengineondemand.appspot.com/turn?username=41784574&key=4080218913'
+// );
+// }
+
+
+// }
+
+
+// sendMessage(message: any) {
+// console.log('Client sending message: ', message);
+// Utils.Emit(DocServer._socket, MessageStore.NotifyRoommates, { message: message, room: this.curRoom });
+// //DocServer._socket.emit('message', message);
+// }
+
+
+
+
+
+// setVideoObjects(localVideo: HTMLVideoElement, remoteVideo: HTMLVideoElement) {
+// this.localVideo = localVideo;
+// this.remoteVideo = remoteVideo;
+// }
+
+// setLocalVideoObject(localVideoRef: HTMLVideoElement) {
+// this.localVideo = localVideoRef;
+// }
+
+// setRemoteVideoObject(remoteVideoRef: HTMLVideoElement) {
+// this.remoteVideo = remoteVideoRef;
+// }
+
+
+
+
+// gotStream(stream: any) {
+// console.log('Adding local stream.');
+// this.localStream = stream;
+// this.localVideo!.srcObject = stream;
+// this.sendMessage('got user media');
+// if (this.isInitiator) {
+// this.maybeStart();
+// }
+// }
+
+// constraints = {
+// video: true,
+// audio: true
+// };
+
+
+
+
+
+// maybeStart() {
+// console.log('>>>>>>> maybeStart() ', this.isStarted, this.localStream, this.isChannelReady);
+// if (!this.isStarted && typeof this.localStream !== 'undefined' && this.isChannelReady) {
+// console.log('>>>>>> creating peer connection');
+// this.createPeerConnection();
+// this.pc.addStream(this.localStream);
+// this.isStarted = true;
+// console.log('isInitiator', this.isInitiator);
+// if (this.isInitiator) {
+// this.doCall();
+// }
+// }
+// }
+
+
+// // //this will need to be changed to our version of hangUp
+// // window.onbeforeunload = function () {
+// // sendMessage('bye');
+// // };
+
+// createPeerConnection() {
+// try {
+// this.pc = new RTCPeerConnection(undefined);
+// this.pc.onicecandidate = this.handleIceCandidate;
+// this.pc.onaddstream = this.handleRemoteStreamAdded;
+// this.pc.onremovestream = this.handleRemoteStreamRemoved;
+// console.log('Created RTCPeerConnnection');
+// } catch (e) {
+// console.log('Failed to create PeerConnection, exception: ' + e.message);
+// alert('Cannot create RTCPeerConnection object.');
+// return;
+// }
+// }
+
+// handleIceCandidate(event: any) {
+// console.log('icecandidate event: ', event);
+// if (event.candidate) {
+// this.sendMessage({
+// type: 'candidate',
+// label: event.candidate.sdpMLineIndex,
+// id: event.candidate.sdpMid,
+// candidate: event.candidate.candidate
+// });
+// } else {
+// console.log('End of candidates.');
+// }
+// }
+
+// handleCreateOfferError(event: any) {
+// console.log('createOffer() error: ', event);
+// }
+
+// doCall() {
+// console.log('Sending offer to peer');
+// this.pc.createOffer(this.setLocalAndSendMessage, this.handleCreateOfferError);
+// }
+
+// doAnswer() {
+// console.log('Sending answer to peer.');
+// this.pc.createAnswer().then(
+// this.setLocalAndSendMessage,
+// this.onCreateSessionDescriptionError
+// );
+// }
+
+// setLocalAndSendMessage(sessionDescription: any) {
+// this.pc.setLocalDescription(sessionDescription);
+// console.log('setLocalAndSendMessage sending message', sessionDescription);
+// this.sendMessage(sessionDescription);
+// }
+
+// onCreateSessionDescriptionError(error: any) {
+// console.log('Failed to create session description: ' + error.toString());
+// }
+
+
+// requestTurn(turnURL: any) {
+// var turnExists = false;
+// let self = this;
+// for (var i in this.pcConfig.iceServers) {
+// if (this.pcConfig.iceServers[i].urls.substr(0, 5) === 'turn:') {
+// turnExists = true;
+// this.turnReady = true;
+// break;
+// }
+// }
+// if (!turnExists) {
+// console.log('Getting TURN server from ', turnURL);
+// // No TURN server. Get one from computeengineondemand.appspot.com:
+// var xhr = new XMLHttpRequest();
+// xhr.onreadystatechange = function () {
+// if (xhr.readyState === 4 && xhr.status === 200) {
+// var turnServer = JSON.parse(xhr.responseText);
+// console.log('Got TURN server: ', turnServer);
+// self.pcConfig.iceServers.push({
+// 'urls': 'turn:' + turnServer.username + '@' + turnServer.turn,
+// //'credential': turnServer.password
+// });
+// self.turnReady = true;
+// }
+// };
+// xhr.open('GET', turnURL, true);
+// xhr.send();
+// }
+// }
+
+// handleRemoteStreamAdded(event: MediaStreamEvent) {
+// console.log('Remote stream added.');
+// this.remoteStream = event.stream!;
+// this.remoteVideo!.srcObject = this.remoteStream;
+// }
+
+// handleRemoteStreamRemoved(event: MediaStreamEvent) {
+// console.log('Remote stream removed. Event: ', event);
+// }
+
+// hangup() {
+// console.log('Hanging up.');
+// if (this.pc) {
+// stop();
+// this.sendMessage('bye');
+// }
+
+// if (this.localStream) {
+// this.localStream.getTracks().forEach(track => track.stop());
+// }
+
+// }
+
+// handleRemoteHangup() {
+// console.log('Session terminated.');
+// stop();
+// this.isInitiator = false;
+// }
+
+// stop() {
+// this.isStarted = false;
+// this.pc.close();
+// this.pc = null;
+// }
+
+
+// } \ No newline at end of file
diff --git a/src/client/views/webcam/DashWebRTCVideo.scss b/src/client/views/webcam/DashWebRTCVideo.scss
new file mode 100644
index 000000000..052832db5
--- /dev/null
+++ b/src/client/views/webcam/DashWebRTCVideo.scss
@@ -0,0 +1,64 @@
+@import "../globalCssVariables";
+
+.webcam-cont {
+ // position: absolute;
+ background: whitesmoke;
+ color: grey;
+ border-radius: 15px;
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw;
+ border: solid #BBBBBBBB 5px;
+ pointer-events: all;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+
+ .webcam-header {
+ height: 50px;
+ text-align: center;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 16px;
+ width: 100%;
+ }
+
+ #roomName {
+ outline: none;
+ border-radius: inherit;
+ border: 1px solid #BBBBBBBB;
+ }
+
+ // #localVideo {
+ // width: 50%;
+ // height: 50%;
+ // position: relative;
+ // // top: 65%;
+ // // z-index: 2;
+ // // right: 5%;
+ // }
+
+ .side {
+ width: 25%;
+ height: 20%;
+ position: absolute;
+ top: 65%;
+ z-index: 2;
+ right: 5%;
+ }
+
+ .main {
+ position: absolute;
+ width: 95%;
+ height: 75%;
+ top: 20%;
+ align-self: center;
+ }
+
+ // #remoteVideo {
+ // position: relative;
+ // width: 50%;
+ // height: 50%;
+ // // top: 20%;
+ // // align-self: center;
+ // }
+
+} \ No newline at end of file
diff --git a/src/client/views/webcam/DashWebRTCVideo.tsx b/src/client/views/webcam/DashWebRTCVideo.tsx
new file mode 100644
index 000000000..f93d4a662
--- /dev/null
+++ b/src/client/views/webcam/DashWebRTCVideo.tsx
@@ -0,0 +1,404 @@
+import { observer } from "mobx-react";
+import React = require("react");
+import { CollectionFreeFormDocumentViewProps } from "../nodes/CollectionFreeFormDocumentView";
+import { FieldViewProps, FieldView } from "../nodes/FieldView";
+import { observable, action } from "mobx";
+import { DocumentDecorations, CloseCall } from "../DocumentDecorations";
+import { InkingControl } from "../InkingControl";
+import "../../views/nodes/WebBox.scss";
+import "./DashWebRTCVideo.scss";
+import adapter from 'webrtc-adapter';
+import { DocServer } from "../../DocServer";
+import { DocumentView } from "../nodes/DocumentView";
+import { Utils } from "../../../Utils";
+import { MessageStore } from "../../../server/Message";
+import { initialize, hangup } from "./WebCamLogic";
+
+const mediaStreamConstraints = {
+ video: true,
+};
+
+const offerOptions = {
+ offerToReceiveVideo: 1,
+};
+
+/**
+ * This models the component that will be rendered, that can be used as a doc that will reflect the video cams.
+ */
+@observer
+export class DashWebRTCVideo extends React.Component<CollectionFreeFormDocumentViewProps & FieldViewProps> {
+ @observable private localVideoEl: HTMLVideoElement | undefined;
+ @observable private peerVideoEl: HTMLVideoElement | undefined;
+ private roomText: HTMLInputElement | undefined;
+ // private roomOfCam: string = "";
+ // private isChannelReady = false;
+ // private isInitiator = false;
+ // private isStarted = false;
+ @observable remoteVideoAdded: boolean = false;
+ // localStream: MediaStream | undefined;
+ // private pc: any;
+ // remoteStream: MediaStream | undefined;
+ // private turnReady: boolean | undefined;
+ // //localVideo: HTMLVideoElement | undefined;
+ // //remoteVideo: HTMLVideoElement | undefined;
+ // curRoom: string = "";
+
+ // private pcConfig = {
+ // 'iceServers': [{
+ // 'urls': 'stun:stun.l.google.com:19302'
+ // }]
+ // };
+
+ // // Set up audio and video regardless of what devices are present.
+ // private sdpConstraints = {
+ // offerToReceiveAudio: true,
+ // offerToReceiveVideo: true
+ // };
+
+ componentDidMount() {
+ DocumentDecorations.Instance.addCloseCall(this.closeConnection);
+ // setTimeout(() => initialize(), 10000);
+ // let self = this;
+ // window.onbeforeunload = function () {
+ // self.sendMessage('bye');
+ // };
+ }
+
+ closeConnection: CloseCall = () => {
+ hangup();
+ }
+
+ @action
+ changeUILook = () => {
+ this.remoteVideoAdded = true;
+ }
+
+ // componentWillUnmount() {
+ // }
+
+
+ // init(room: string) {
+
+ // this.curRoom = room;
+ // let self = this;
+
+ // if (room !== '') {
+ // DocServer._socket.emit('create or join', room);
+ // console.log('Attempted to create or join room', room);
+
+ // }
+
+ // DocServer._socket.on('created', function (room: string) {
+ // console.log('Created room ' + room);
+ // self.isInitiator = true;
+ // });
+
+ // DocServer._socket.on('full', function (room: string) {
+ // console.log('Room ' + room + ' is full');
+ // });
+
+ // DocServer._socket.on('join', function (room: string) {
+ // console.log('Another peer made a request to join room ' + room);
+ // console.log('This peer is the initiator of room ' + room + '!');
+ // self.isChannelReady = true;
+ // });
+
+
+ // DocServer._socket.on('joined', function (room: string) {
+ // console.log('joined: ' + room);
+ // self.isChannelReady = true;
+ // });
+
+
+ // DocServer._socket.on('log', function (array: any) {
+ // console.log.apply(console, array);
+ // });
+
+ // // This client receives a message
+ // DocServer._socket.on('message', async function (message: any) {
+ // console.log('Client received message:', message);
+ // if (message.message === 'got user media') {
+ // self.maybeStart();
+ // } else if (message.message.type === 'offer') {
+ // if (!self.isInitiator && !self.isStarted) {
+ // self.maybeStart();
+ // }
+ // await self.pc.setRemoteDescription(new RTCSessionDescription(message.message));
+ // self.doAnswer();
+ // } else if (message.message.type === 'answer' && self.isStarted) {
+ // await self.pc.setRemoteDescription(new RTCSessionDescription(message.message));
+ // } else if (message.message.type === 'candidate' && self.isStarted) {
+ // let candidate = new RTCIceCandidate({
+ // sdpMLineIndex: message.message.label,
+ // candidate: message.message.candidate
+ // });
+ // self.pc.addIceCandidate(candidate);
+ // } else if (message.message === 'bye' && self.isStarted) {
+ // self.handleRemoteHangup();
+ // }
+ // });
+
+ // navigator.mediaDevices.getUserMedia({
+ // audio: true,
+ // video: true
+ // })
+ // .then(this.gotStream)
+ // .catch(function (e) {
+ // alert('getUserMedia() error: ' + e.name);
+ // });
+
+ // //Trying this one out!!!
+ // console.log('Getting user media with constraints', this.constraints);
+
+ // if (location.hostname !== 'localhost') {
+ // this.requestTurn(
+ // 'https://computeengineondemand.appspot.com/turn?username=41784574&key=4080218913'
+ // );
+ // }
+
+
+ // }
+
+
+ // private sendMessage = (message: any) => {
+ // console.log('Client sending message: ', message);
+ // Utils.Emit(DocServer._socket, MessageStore.NotifyRoommates, { message: message, room: this.curRoom });
+ // //DocServer._socket.emit('message', message);
+ // }
+
+
+
+ // private gotStream = (stream: any) => {
+ // console.log('Adding local stream.');
+ // this.localStream = stream;
+ // this.localVideoEl!.srcObject = stream;
+ // this.sendMessage('got user media');
+ // if (this.isInitiator) {
+ // this.maybeStart();
+ // }
+ // }
+
+ // constraints = {
+ // video: true,
+ // audio: true
+ // };
+
+
+
+
+
+ // private maybeStart = () => {
+ // console.log('>>>>>>> maybeStart() ', this.isStarted, this.localStream, this.isChannelReady);
+ // if (!this.isStarted && typeof this.localStream !== 'undefined' && this.isChannelReady) {
+ // console.log('>>>>>> creating peer connection');
+ // this.createPeerConnection();
+ // this.pc.addStream(this.localStream);
+ // this.isStarted = true;
+ // console.log('isInitiator', this.isInitiator);
+ // if (this.isInitiator) {
+ // this.doCall();
+ // }
+ // }
+ // }
+
+
+ // // //this will need to be changed to our version of hangUp
+ // // window.onbeforeunload = function () {
+ // // sendMessage('bye');
+ // // };
+
+ // private createPeerConnection = () => {
+ // try {
+ // this.pc = new RTCPeerConnection(undefined);
+ // this.pc.onicecandidate = this.handleIceCandidate;
+ // this.pc.onaddstream = this.handleRemoteStreamAdded;
+ // this.pc.onremovestream = this.handleRemoteStreamRemoved;
+ // console.log('Created RTCPeerConnnection');
+ // } catch (e) {
+ // console.log('Failed to create PeerConnection, exception: ' + e.message);
+ // alert('Cannot create RTCPeerConnection object.');
+ // return;
+ // }
+ // }
+
+ // private handleIceCandidate = (event: any) => {
+ // console.log('icecandidate event: ', event);
+ // if (event.candidate) {
+ // this.sendMessage({
+ // type: 'candidate',
+ // label: event.candidate.sdpMLineIndex,
+ // id: event.candidate.sdpMid,
+ // candidate: event.candidate.candidate
+ // });
+ // } else {
+ // console.log('End of candidates.');
+ // }
+ // }
+
+ // private handleCreateOfferError = (event: any) => {
+ // console.log('createOffer() error: ', event);
+ // }
+
+ // private doCall = () => {
+ // console.log('Sending offer to peer');
+ // this.pc.createOffer(this.setLocalAndSendMessage, this.handleCreateOfferError);
+ // }
+
+ // private doAnswer = () => {
+ // console.log('Sending answer to peer.');
+ // this.pc.createAnswer().then(
+ // this.setLocalAndSendMessage,
+ // this.onCreateSessionDescriptionError
+ // );
+ // }
+
+ // private setLocalAndSendMessage = (sessionDescription: any) => {
+ // this.pc.setLocalDescription(sessionDescription);
+ // console.log('setLocalAndSendMessage sending message', sessionDescription);
+ // this.sendMessage(sessionDescription);
+ // }
+
+ // private onCreateSessionDescriptionError = (error: any) => {
+ // console.log('Failed to create session description: ' + error.toString());
+ // }
+
+
+ // private requestTurn = (turnURL: any) => {
+ // var turnExists = false;
+ // let self = this;
+ // for (var i in this.pcConfig.iceServers) {
+ // if (this.pcConfig.iceServers[i].urls.substr(0, 5) === 'turn:') {
+ // turnExists = true;
+ // this.turnReady = true;
+ // break;
+ // }
+ // }
+ // if (!turnExists) {
+ // console.log('Getting TURN server from ', turnURL);
+ // // No TURN server. Get one from computeengineondemand.appspot.com:
+ // var xhr = new XMLHttpRequest();
+ // xhr.onreadystatechange = function () {
+ // if (xhr.readyState === 4 && xhr.status === 200) {
+ // var turnServer = JSON.parse(xhr.responseText);
+ // console.log('Got TURN server: ', turnServer);
+ // self.pcConfig.iceServers.push({
+ // 'urls': 'turn:' + turnServer.username + '@' + turnServer.turn,
+ // //'credential': turnServer.password
+ // });
+ // self.turnReady = true;
+ // }
+ // };
+ // xhr.open('GET', turnURL, true);
+ // xhr.send();
+ // }
+ // }
+ // @action
+ // private handleRemoteStreamAdded = (event: MediaStreamEvent) => {
+ // console.log('Remote stream added.');
+ // this.remoteStream = event.stream!;
+ // this.peerVideoEl!.srcObject = this.remoteStream;
+ // this.remoteVideoAdded = true;
+ // }
+
+ // private handleRemoteStreamRemoved = (event: MediaStreamEvent) => {
+ // console.log('Remote stream removed. Event: ', event);
+ // }
+
+ // private hangup = () => {
+ // console.log('Hanging up.');
+ // if (this.pc) {
+ // stop();
+ // this.sendMessage('bye');
+ // }
+
+ // if (this.localStream) {
+ // this.localStream.getTracks().forEach(track => track.stop());
+ // }
+
+ // }
+
+ // private handleRemoteHangup = () => {
+ // console.log('Session terminated.');
+ // this.stop();
+ // this.isInitiator = false;
+
+ // if (this.localStream) {
+ // this.localStream.getTracks().forEach(track => track.stop());
+ // }
+
+
+ // }
+
+ // private stop = () => {
+ // this.isStarted = false;
+ // this.pc.close();
+ // this.pc = null;
+ // }
+
+
+
+
+
+
+ /**
+ * Function that submits the title entered by user on enter press.
+ */
+ private onEnterKeyDown = (e: React.KeyboardEvent) => {
+ if (e.keyCode === 13) {
+ let submittedTitle = this.roomText!.value;
+ this.roomText!.value = "";
+ this.roomText!.blur();
+ initialize(submittedTitle, this.changeUILook);
+ }
+ }
+
+
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DashWebRTCVideo, fieldKey); }
+
+ _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}>
+ <div className="webcam-header">DashWebRTC</div>
+ <input id="roomName" type="text" placeholder="Enter room name" ref={(e) => this.roomText = e!} onKeyDown={this.onEnterKeyDown} />
+ <video id="localVideo" className={"RTCVideo" + (this.remoteVideoAdded ? " side" : " main")} autoPlay playsInline ref={(e) => {
+ this.localVideoEl = e!;
+ }}></video>
+ <video id="remoteVideo" className="RTCVideo main" autoPlay playsInline ref={(e) => {
+ this.peerVideoEl = e!;
+ }}></video>
+
+ </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/WebCamLogic.js b/src/client/views/webcam/WebCamLogic.js
new file mode 100644
index 000000000..37d152cc7
--- /dev/null
+++ b/src/client/views/webcam/WebCamLogic.js
@@ -0,0 +1,281 @@
+'use strict';
+import io from "socket.io-client";
+
+var socket;
+var isChannelReady = false;
+var isInitiator = false;
+var isStarted = false;
+var localStream;
+var pc;
+var remoteStream;
+var turnReady;
+var room;
+
+export function initialize(roomName, handlerUI) {
+
+ var pcConfig = {
+ 'iceServers': [{
+ 'urls': 'stun:stun.l.google.com:19302'
+ }]
+ };
+
+ // Set up audio and video regardless of what devices are present.
+ var sdpConstraints = {
+ offerToReceiveAudio: true,
+ offerToReceiveVideo: true
+ };
+
+ /////////////////////////////////////////////
+
+ room = roomName;
+ // Could prompt for room name:
+ // room = prompt('Enter room name:');
+
+ socket = io.connect(`${window.location.protocol}//${window.location.hostname}:${4321}`);
+
+ if (room !== '') {
+ socket.emit('create or join', room);
+ console.log('Attempted to create or join room', room);
+ }
+
+ socket.on('created', function (room) {
+ console.log('Created room ' + room);
+ isInitiator = true;
+ });
+
+ socket.on('full', function (room) {
+ console.log('Room ' + room + ' is full');
+ });
+
+ socket.on('join', function (room) {
+ console.log('Another peer made a request to join room ' + room);
+ console.log('This peer is the initiator of room ' + room + '!');
+ isChannelReady = true;
+ });
+
+ socket.on('joined', function (room) {
+ console.log('joined: ' + room);
+ isChannelReady = true;
+ });
+
+ socket.on('log', function (array) {
+ console.log.apply(console, array);
+ });
+
+ ////////////////////////////////////////////////
+
+
+ // This client receives a message
+ socket.on('message', function (message) {
+ console.log('Client received message:', message);
+ if (message === 'got user media') {
+ maybeStart();
+ } else if (message.type === 'offer') {
+ if (!isInitiator && !isStarted) {
+ maybeStart();
+ }
+ pc.setRemoteDescription(new RTCSessionDescription(message));
+ doAnswer();
+ } else if (message.type === 'answer' && isStarted) {
+ pc.setRemoteDescription(new RTCSessionDescription(message));
+ } else if (message.type === 'candidate' && isStarted) {
+ var candidate = new RTCIceCandidate({
+ sdpMLineIndex: message.label,
+ candidate: message.candidate
+ });
+ pc.addIceCandidate(candidate);
+ } else if (message === 'bye' && isStarted) {
+ handleRemoteHangup();
+ }
+ });
+
+ ////////////////////////////////////////////////////
+
+ var localVideo = document.querySelector('#localVideo');
+ var remoteVideo = document.querySelector('#remoteVideo');
+
+
+ console.log("Local Video: ", localVideo);
+ console.log("Remote Video: ", remoteVideo);
+
+ const gotStream = (stream) => {
+ console.log('Adding local stream.');
+ localStream = stream;
+ localVideo.srcObject = stream;
+ sendMessage('got user media');
+ if (isInitiator) {
+ maybeStart();
+ }
+ }
+
+
+ navigator.mediaDevices.getUserMedia({
+ audio: false,
+ video: true
+ })
+ .then(gotStream)
+ .catch(function (e) {
+ alert('getUserMedia() error: ' + e.name);
+ });
+
+
+
+ var constraints = {
+ video: true
+ };
+
+ console.log('Getting user media with constraints', constraints);
+
+ if (location.hostname !== 'localhost') {
+ requestTurn(
+ 'https://computeengineondemand.appspot.com/turn?username=41784574&key=4080218913'
+ );
+ }
+
+ const maybeStart = () => {
+ console.log('>>>>>>> maybeStart() ', isStarted, localStream, isChannelReady);
+ if (!isStarted && typeof localStream !== 'undefined' && isChannelReady) {
+ console.log('>>>>>> creating peer connection');
+ createPeerConnection();
+ pc.addStream(localStream);
+ isStarted = true;
+ console.log('isInitiator', isInitiator);
+ if (isInitiator) {
+ doCall();
+ }
+ }
+ };
+
+ window.onbeforeunload = function () {
+ sendMessage('bye');
+ };
+
+ /////////////////////////////////////////////////////////
+
+ const createPeerConnection = () => {
+ try {
+ pc = new RTCPeerConnection(null);
+ pc.onicecandidate = handleIceCandidate;
+ pc.onaddstream = handleRemoteStreamAdded;
+ pc.onremovestream = handleRemoteStreamRemoved;
+ console.log('Created RTCPeerConnnection');
+ } catch (e) {
+ console.log('Failed to create PeerConnection, exception: ' + e.message);
+ alert('Cannot create RTCPeerConnection object.');
+ return;
+ }
+ }
+
+ const handleIceCandidate = (event) => {
+ console.log('icecandidate event: ', event);
+ if (event.candidate) {
+ sendMessage({
+ type: 'candidate',
+ label: event.candidate.sdpMLineIndex,
+ id: event.candidate.sdpMid,
+ candidate: event.candidate.candidate
+ });
+ } else {
+ console.log('End of candidates.');
+ }
+ }
+
+ const handleCreateOfferError = (event) => {
+ console.log('createOffer() error: ', event);
+ }
+
+ const doCall = () => {
+ console.log('Sending offer to peer');
+ pc.createOffer(setLocalAndSendMessage, handleCreateOfferError);
+ }
+
+ const doAnswer = () => {
+ console.log('Sending answer to peer.');
+ pc.createAnswer().then(
+ setLocalAndSendMessage,
+ onCreateSessionDescriptionError
+ );
+ }
+
+ const setLocalAndSendMessage = (sessionDescription) => {
+ pc.setLocalDescription(sessionDescription);
+ console.log('setLocalAndSendMessage sending message', sessionDescription);
+ sendMessage(sessionDescription);
+ }
+
+ const onCreateSessionDescriptionError = (error) => {
+ trace('Failed to create session description: ' + error.toString());
+ }
+
+ const requestTurn = (turnURL) => {
+ var turnExists = false;
+ for (var i in pcConfig.iceServers) {
+ if (pcConfig.iceServers[i].urls.substr(0, 5) === 'turn:') {
+ turnExists = true;
+ turnReady = true;
+ break;
+ }
+ }
+ if (!turnExists) {
+ console.log('Getting TURN server from ', turnURL);
+ // No TURN server. Get one from computeengineondemand.appspot.com:
+ var xhr = new XMLHttpRequest();
+ xhr.onreadystatechange = function () {
+ if (xhr.readyState === 4 && xhr.status === 200) {
+ var turnServer = JSON.parse(xhr.responseText);
+ console.log('Got TURN server: ', turnServer);
+ pcConfig.iceServers.push({
+ 'urls': 'turn:' + turnServer.username + '@' + turnServer.turn,
+ 'credential': turnServer.password
+ });
+ turnReady = true;
+ }
+ };
+ xhr.open('GET', turnURL, true);
+ xhr.send();
+ }
+ }
+
+ const handleRemoteStreamAdded = (event) => {
+ console.log('Remote stream added.');
+ remoteStream = event.stream;
+ remoteVideo.srcObject = remoteStream;
+ handlerUI();
+
+ };
+
+ const handleRemoteStreamRemoved = (event) => {
+ console.log('Remote stream removed. Event: ', event);
+ }
+}
+
+export function hangup() {
+ console.log('Hanging up.');
+ stop();
+ sendMessage('bye');
+ if (localStream) {
+ localStream.getTracks().forEach(track => track.stop());
+ }
+}
+
+function stop() {
+ isStarted = false;
+ if (pc) {
+ pc.close();
+ }
+ pc = null;
+}
+
+function handleRemoteHangup() {
+ console.log('Session terminated.');
+ stop();
+ isInitiator = false;
+ if (localStream) {
+ localStream.getTracks().forEach(track => track.stop());
+ }
+}
+
+function sendMessage(message) {
+ console.log('Client sending message: ', message);
+ socket.emit('message', message, room);
+}; \ No newline at end of file
diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts
index cfab36906..fb71160ca 100644
--- a/src/new_fields/URLField.ts
+++ b/src/new_fields/URLField.ts
@@ -49,3 +49,5 @@ export const nullAudio = "https://actions.google.com/sounds/v1/alarms/beep_short
@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/server/Message.ts b/src/server/Message.ts
index 621abfd1e..6ce5cd96a 100644
--- a/src/server/Message.ts
+++ b/src/server/Message.ts
@@ -42,6 +42,11 @@ export interface Diff extends Reference {
readonly diff: any;
}
+export interface RoomMessage {
+ readonly message: string;
+ readonly room: string;
+}
+
export namespace MessageStore {
export const Foo = new Message<string>("Foo");
export const Bar = new Message<string>("Bar");
@@ -59,4 +64,8 @@ export namespace MessageStore {
export const YoutubeApiQuery = new Message<YoutubeQueryInput>("Youtube Api Query");
export const DeleteField = new Message<string>("Delete field");
export const DeleteFields = new Message<string[]>("Delete fields");
+ export const NotifyRoommates = new Message<RoomMessage>("message");
+ export const HangUpCall = new Message<string>("bye");
+
+
}
diff --git a/src/server/Websocket/Websocket.ts b/src/server/Websocket/Websocket.ts
index a2fdc7c89..b4cd2dbe2 100644
--- a/src/server/Websocket/Websocket.ts
+++ b/src/server/Websocket/Websocket.ts
@@ -1,5 +1,5 @@
import { Utils } from "../../Utils";
-import { MessageStore, Transferable, Types, Diff, YoutubeQueryInput, YoutubeQueryTypes } from "../Message";
+import { MessageStore, Transferable, Types, Diff, YoutubeQueryInput, YoutubeQueryTypes, RoomMessage } from "../Message";
import { Client } from "../Client";
import { Socket } from "socket.io";
import { Database } from "../database";
@@ -10,6 +10,8 @@ import { GoogleCredentialsLoader } from "../credentials/CredentialsLoader";
import { logPort } from "../ActionUtilities";
import { timeMap } from "../ApiManagers/UserManager";
import { green } from "colors";
+import { networkInterfaces, type } from "os";
+import { object } from "serializr";
export namespace WebSocket {
@@ -17,6 +19,8 @@ export namespace WebSocket {
const clients: { [key: string]: Client } = {};
export const socketMap = new Map<SocketIO.Socket, string>();
export let disconnect: Function;
+ let endpoint: io.Server;
+
export async function start(isRelease: boolean) {
await preliminaryFunctions();
@@ -25,7 +29,6 @@ export namespace WebSocket {
async function preliminaryFunctions() {
}
-
function initialize(isRelease: boolean) {
const endpoint = io();
endpoint.on("connection", function (socket: Socket) {
@@ -39,6 +42,54 @@ export namespace WebSocket {
next();
});
+ // convenience function to log server messages on the client
+ function log(message?: any, ...optionalParams: any[]) {
+ socket.emit('log', ['Message from server:', message, ...optionalParams]);
+ }
+
+ socket.on('message', function (message, room) {
+ console.log('Client said: ', message);
+ socket.in(room).emit('message', message);
+ });
+
+ socket.on('create or join', function (room) {
+ console.log('Received request to create or join room ' + room);
+
+ var clientsInRoom = socket.adapter.rooms[room];
+ var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;
+ console.log('Room ' + room + ' now has ' + numClients + ' client(s)');
+
+ if (numClients === 0) {
+ socket.join(room);
+ console.log('Client ID ' + socket.id + ' created room ' + room);
+ socket.emit('created', room, socket.id);
+
+ } else if (numClients === 1) {
+ console.log('Client ID ' + socket.id + ' joined room ' + room);
+ socket.in(room).emit('join', room);
+ socket.join(room);
+ socket.emit('joined', room, socket.id);
+ socket.in(room).emit('ready');
+ } else { // max two clients
+ socket.emit('full', room);
+ }
+ });
+
+ socket.on('ipaddr', function () {
+ var ifaces = networkInterfaces();
+ for (var dev in ifaces) {
+ ifaces[dev].forEach(function (details) {
+ if (details.family === 'IPv4' && details.address !== '127.0.0.1') {
+ socket.emit('ipaddr', details.address);
+ }
+ });
+ }
+ });
+
+ socket.on('bye', function () {
+ console.log('received bye');
+ });
+
Utils.Emit(socket, MessageStore.Foo, "handshooken");
Utils.AddServerHandler(socket, MessageStore.Bar, guid => barReceived(socket, guid));
@@ -56,6 +107,9 @@ export namespace WebSocket {
Utils.AddServerHandler(socket, MessageStore.DeleteFields, ids => DeleteFields(socket, ids));
Utils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField);
Utils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields);
+ //Utils.AddServerHandler(socket, MessageStore.NotifyRoommates, message => HandleRoommateNotification(socket, message));
+ //Utils.AddServerHandler(socket, MessageStore.HangUpCall, message => HandleHangUp(socket, message));
+ //Utils.AddRoomHandler(socket, "create or join", HandleCreateOrJoin);
disconnect = () => {
socket.broadcast.emit("connection_terminated", Date.now());
@@ -68,6 +122,49 @@ export namespace WebSocket {
logPort("websocket", socketPort);
}
+
+ function HandleRoommateNotification(socket: Socket, message: RoomMessage) {
+ //socket.broadcast.emit('message', message);
+ console.log("The room that sent this: ", message.room, " and message is : ", message.message);
+ endpoint.sockets.in(message.room).emit('message', message);
+
+ }
+
+ function HandleCreateOrJoin(socket: io.Socket, room: string) {
+ console.log("Received request to create or join room " + room);
+
+
+ let clientsInRoom = endpoint.sockets.adapter.rooms[room];
+ let numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;
+ console.log('Room ' + room + ' now has ' + numClients + ' client(s)');
+
+
+ if (numClients === 0) {
+ socket.join(room);
+ console.log('Client ID ' + socket.id + ' created room ' + room);
+ socket.emit('created', room, socket.id);
+
+ } else if (numClients === 1) {
+ console.log('Client ID ' + socket.id + ' joined room ' + room);
+ endpoint.sockets.in(room).emit('join', room);
+ socket.join(room);
+ socket.emit('joined', room, socket.id);
+ endpoint.sockets.in(room).emit('ready');
+
+ } else {
+ socket.emit('full', room);
+ }
+
+
+
+
+
+ }
+
+ function HandleHangUp(socket: io.Socket, message: string) {
+ console.log("Receive bye from someone");
+ }
+
function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any[]) => void]) {
const { ProjectCredentials } = GoogleCredentialsLoader;
switch (query.type) {
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 71775bed6..db20d10f2 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -58,6 +58,7 @@ export class CurrentUserUtils {
{ title: "todo item", icon: "check", ignoreClick: true, drag: 'getCopy(this.dragFactory, true)', dragFactory: notes[notes.length - 1] },
{ title: "web page", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", {_width: 300, _height: 300, title: "New Webpage" })' },
{ title: "cat image", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 200, title: "an image of a cat" })' },
+ { title: "webcam", icon: "video", ignoreClick: true, drag: 'Docs.Create.WebCamDocument("", { width: 400, height: 400, title: "a test cam" })' },
{ title: "buxton", icon: "faObjectGroup", ignoreClick: true, drag: "Docs.Create.Buxton()" },
{ title: "record", icon: "microphone", ignoreClick: true, drag: `Docs.Create.AudioDocument("${nullAudio}", { _width: 200, title: "ready to record audio" })` },
{ title: "clickable button", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ _width: 150, _height: 50, title: "Button" })' },
diff --git a/src/server/index.ts b/src/server/index.ts
index 313a2f0e2..72ddb5bd7 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -86,12 +86,14 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }:
secureHandler: ({ res }) => res.redirect("/home")
});
+
addSupervisedRoute({
method: Method.GET,
subscription: "/serverHeartbeat",
secureHandler: ({ res }) => res.send(true)
});
+
const serve: PublicHandler = ({ req, res }) => {
const detector = new mobileDetect(req.headers['user-agent'] || "");
const filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html';
@@ -119,6 +121,52 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }:
WebSocket.start(isRelease);
}
+// Utils.AddServerHandler(socket, MessageStore.NotifyRoommates, message => HandleRoommateNotification(socket, message));
+// // Utils.AddServerHandler(socket, MessageStore.HangUpCall, message => HandleHangUp(socket, message));
+// // Utils.AddRoomHandler(socket, "create or join", HandleCreateOrJoin);
+
+// function HandleRoommateNotification(socket: Socket, message: RoomMessage) {
+// //socket.broadcast.emit('message', message);
+// console.log("The room that sent this: ", message.room, " and message is : ", message.message);
+// server.sockets.in(message.room).emit('message', message);
+
+// }
+
+// function HandleCreateOrJoin(socket: io.Socket, room: string) {
+// console.log("Received request to create or join room " + room);
+
+
+// let clientsInRoom = server.sockets.adapter.rooms[room];
+// let numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;
+// console.log('Room ' + room + ' now has ' + numClients + ' client(s)');
+
+
+// if (numClients === 0) {
+// socket.join(room);
+// console.log('Client ID ' + socket.id + ' created room ' + room);
+// socket.emit('created', room, socket.id);
+
+// } else if (numClients === 1) {
+// console.log('Client ID ' + socket.id + ' joined room ' + room);
+// server.sockets.in(room).emit('join', room);
+// socket.join(room);
+// socket.emit('joined', room, socket.id);
+// server.sockets.in(room).emit('ready');
+
+// } else {
+// socket.emit('full', room);
+// }
+
+
+
+
+
+// }
+
+// function HandleHangUp(socket: io.Socket, message: string) {
+// console.log("Receive bye from someone");
+// }
+
/**
* This function can be used in two different ways. If not in release mode,
* this is simply the logic that is invoked to start the server. In release mode,
diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts
index 281bb3217..850c533fc 100644
--- a/src/typings/index.d.ts
+++ b/src/typings/index.d.ts
@@ -4,6 +4,9 @@ declare module 'googlephotos';
declare module 'react-image-lightbox-with-rotate';
declare module 'cors';
+declare module 'webrtc-adapter';
+
+
declare module '@react-pdf/renderer' {
import * as React from 'react';