diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f9e87a5 --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ + +ifndef encoders +encoders = h264 +endif + +tags = +ifneq (,$(findstring h264,$(encoders))) +tags = h264enc +endif + +ifneq (,$(findstring vp8,$(encoders))) +tags := $(tags) vp8enc +endif + +tags := $(strip $(tags)) + +main.tar.gz: clean main + @tar zcf main.tar.gz frontend main + +main.zip: clean main + @zip -r main.zip frontend main + +main: + go build -tags "$(tags)" cmd/main.go + +.PHONY: clean +clean: + @if [ -f main ]; then rm main; fi + @if [ -f main.tar.gz ]; then rm main.tar.gz ; fi + @if [ -f main.zip ]; then rm main.zip ; fi \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..10588e8 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,74 @@ +package main + +import ( + "flag" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" + + "github.com/krishpranav/remote-desktop/api" + "github.com/krishpranav/remote-desktop/encoders" + "github.com/krishpranav/remote-desktop/rdisplay" + "github.com/krishpranav/remote-desktop/rtc" +) + +const ( + httpDefaultPort = "8080" + defaultStunServer = "stun:stun.l.google.com:19302" +) + +func main() { + + httpPort := flag.String("http.port", httpDefaultPort, "HTTP listen port") + stunServer := flag.String("stun.server", defaultStunServer, "STUN server URL (stun:)") + flag.Parse() + + var video rdisplay.Service + video, err := rdisplay.NewVideoProvider() + if err != nil { + log.Fatalf("Can't init video: %v", err) + } + _, err = video.Screens() + if err != nil { + log.Fatalf("Can't get screens: %v", err) + } + + var enc encoders.Service = &encoders.EncoderService{} + if err != nil { + log.Fatalf("Can't create encoder service: %v", err) + } + + var webrtc rtc.Service + webrtc = rtc.NewRemoteScreenService(*stunServer, video, enc) + + mux := http.NewServeMux() + + mux.Handle("/api/", http.StripPrefix("/api", api.MakeHandler(webrtc, video))) + + mux.Handle("/static/", http.StripPrefix("/static", http.FileServer(http.Dir("./frontend")))) + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + w.WriteHeader(http.StatusNotFound) + return + } + http.ServeFile(w, r, "./frontend/index.html") + }) + + errors := make(chan error, 2) + go func() { + log.Printf("Starting signaling server on port %s", *httpPort) + errors <- http.ListenAndServe(fmt.Sprintf(":%s", *httpPort), mux) + }() + + go func() { + interrupt := make(chan os.Signal) + signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) + errors <- fmt.Errorf("Received %v signal", <-interrupt) + }() + + err = <-errors + log.Printf("%s, exiting.", err) +} diff --git a/frontend/css/style.css b/frontend/css/style.css index 3b06960..91c8fea 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -16,7 +16,7 @@ html, body { display: flex; justify-content: center; align-items: center; - background-color: #303030; + background-color: #141313; } #controls { @@ -36,11 +36,11 @@ button, select, option { font-size: 2.5rem; font-weight: 300; color: white; - background-color:white; + background-color:rgba(59, 40, 40, 0.225); background-image: linear-gradient(to bottom, #ffffff 0%,#efefef 100%); padding: 4px 12px; - border: 2px solid #20639b; - color:#20639b; + border: 2px solid #810d68; + color:#a40f9d; margin-right: 20px; cursor: pointer; } @@ -50,7 +50,7 @@ select { -moz-appearance: none; -webkit-appearance: none; appearance: none; - background-color: #fff; + background-color: #0d16bc; background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007CB2%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E'), linear-gradient(to bottom, #ffffff 0%,#efefef 100%); background-repeat: no-repeat, repeat; @@ -86,7 +86,7 @@ select:invalid { align-items: center; text-shadow: 1px 1px 3px black; justify-content: center; - color: #acacac; + color: #ff00e1; font-size: 3rem; z-index: 0; } \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index eaedb74..8e0d0da 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,11 +4,11 @@ - + - WebRTC remote viewer + Remote Desktop
diff --git a/frontend/js/app.js b/frontend/js/app.js index 24c5194..81bb376 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -1,12 +1,155 @@ + function showError(error) { const errorNode = document.querySelector('#error'); if (errorNode.firstChild) { - errorNode.remoteChild(errorNode.firstChild); + errorNode.removeChild(errorNode.firstChild); } - errorNode.appendChild(document.createTextNode(error.message || error)); -} - -function loadSession() { - // fetch functionality -} \ No newline at end of file + } + + function loadScreens() { + return fetch('/api/screens', { + method: 'GET', + headers: { + 'Accepts': 'application/json' + } + }).then(res => { + return res.json(); + }).catch(showError); + } + + function startSession(offer, screen) { + return fetch('/api/session', { + method: 'POST', + body: JSON.stringify({ + offer, + screen + }), + headers: { + 'Content-Type': 'application/json' + } + }).then(res => { + return res.json(); + }).then(msg => { + return msg.answer; + }); + } + + function createOffer(pc, { audio, video }) { + return new Promise((accept, reject) => { + pc.onicecandidate = evt => { + if (!evt.candidate) { + + // ICE Gathering finished + const { sdp: offer } = pc.localDescription; + accept(offer); + } + }; + pc.createOffer({ + offerToReceiveAudio: audio, + offerToReceiveVideo: video + }).then(ld => { + pc.setLocalDescription(ld) + }).catch(reject) + }); + } + + function startRemoteSession(screen, remoteVideoNode, stream) { + let pc; + + return Promise.resolve().then(() => { + pc = new RTCPeerConnection({ + iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] + }); + pc.ontrack = (evt) => { + console.info('ontrack triggered'); + + remoteVideoNode.srcObject = evt.streams[0]; + remoteVideoNode.play(); + }; + + stream && stream.getTracks().forEach(track => { + pc.addTrack(track, stream); + }) + return createOffer(pc, { audio: false, video: true }); + }).then(offer => { + console.info(offer); + return startSession(offer, screen); + }).then(answer => { + console.info(answer); + return pc.setRemoteDescription(new RTCSessionDescription({ + sdp: answer, + type: 'answer' + })); + }).then(() => pc); + } + + let peerConnection = null; + document.addEventListener('DOMContentLoaded', () => { + + let selectedScreen = 0; + const remoteVideo = document.querySelector('#remote-video'); + const screenSelect = document.querySelector('#screen-select'); + const startStop = document.querySelector('#start-stop'); + + loadScreens().then(response => { + while (screenSelect.firstChild) { + screenSelect.removeChild(screenSelect.firstChild); + } + screenSelect.appendChild(document.createElement('option')); + response.screens.forEach(screen => { + const option = document.createElement('option'); + option.appendChild(document.createTextNode('Screen ' + (screen.index + 1))); + option.setAttribute('value', screen.index); + screenSelect.appendChild(option); + }); + }).catch(showError); + + screenSelect.addEventListener('change', evt => { + selectedScreen = parseInt(evt.currentTarget.value, 10); + }); + + const enableStartStop = (enabled) => { + if (enabled) { + startStop.removeAttribute('disabled'); + } else { + startStop.setAttribute('disabled', ''); + } + } + + const setStartStopTitle = (title) => { + startStop.removeChild(startStop.firstChild); + startStop.appendChild(document.createTextNode(title)); + } + + startStop.addEventListener('click', () => { + enableStartStop(false); + + const userMediaPromise = (adapter.browserDetails.browser === 'safari') ? + navigator.mediaDevices.getUserMedia({ video: true }) : + Promise.resolve(null); + if (!peerConnection) { + userMediaPromise.then(stream => { + return startRemoteSession(selectedScreen, remoteVideo, stream).then(pc => { + remoteVideo.style.setProperty('visibility', 'visible'); + peerConnection = pc; + }).catch(showError).then(() => { + enableStartStop(true); + setStartStopTitle('Stop'); + }); + }) + } else { + peerConnection.close(); + peerConnection = null; + enableStartStop(true); + setStartStopTitle('Start'); + remoteVideo.style.setProperty('visibility', 'collapse'); + } + }); + }); + + window.addEventListener('beforeunload', () => { + if (peerConnection) { + peerConnection.close(); + } + }) \ No newline at end of file diff --git a/main b/main new file mode 100755 index 0000000..c722580 Binary files /dev/null and b/main differ