diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/Utils.ts | 8 | ||||
-rw-r--r-- | src/client/DocServer.ts | 2 | ||||
-rw-r--r-- | src/client/documents/DocumentTypes.ts | 2 | ||||
-rw-r--r-- | src/client/documents/Documents.ts | 9 | ||||
-rw-r--r-- | src/client/northstar/utils/Utils.ts | 6 | ||||
-rw-r--r-- | src/client/views/DocumentDecorations.tsx | 14 | ||||
-rw-r--r-- | src/client/views/MainView.tsx | 3 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentContentsView.tsx | 4 | ||||
-rw-r--r-- | src/client/views/webcam/DashWebCam.tsx | 396 | ||||
-rw-r--r-- | src/client/views/webcam/DashWebRTC.scss | 23 | ||||
-rw-r--r-- | src/client/views/webcam/DashWebRTC.ts | 314 | ||||
-rw-r--r-- | src/client/views/webcam/DashWebRTCVideo.scss | 64 | ||||
-rw-r--r-- | src/client/views/webcam/DashWebRTCVideo.tsx | 404 | ||||
-rw-r--r-- | src/client/views/webcam/WebCamLogic.js | 281 | ||||
-rw-r--r-- | src/new_fields/URLField.ts | 2 | ||||
-rw-r--r-- | src/server/Message.ts | 9 | ||||
-rw-r--r-- | src/server/Websocket/Websocket.ts | 101 | ||||
-rw-r--r-- | src/server/authentication/models/current_user_utils.ts | 1 | ||||
-rw-r--r-- | src/server/index.ts | 48 | ||||
-rw-r--r-- | src/typings/index.d.ts | 3 |
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'; |