commit laplace repo
This commit is contained in:
3
laplace/.dockerignore
Normal file
3
laplace/.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
.idea
|
||||
.DS_Store
|
||||
Dockerfile
|
||||
2
laplace/.gitignore
vendored
Normal file
2
laplace/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.idea
|
||||
.DS_Store
|
||||
15
laplace/Dockerfile
Normal file
15
laplace/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM golang:latest as builder
|
||||
LABEL maintainer="Adam Jordan <adamyordan@gmail.com>"
|
||||
WORKDIR /build
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o laplace .
|
||||
|
||||
FROM alpine:latest
|
||||
RUN apk --no-cache add ca-certificates
|
||||
WORKDIR /root/
|
||||
COPY --from=builder /build/laplace .
|
||||
COPY files files
|
||||
EXPOSE 443
|
||||
CMD ["./laplace"]
|
||||
21
laplace/LICENSE
Normal file
21
laplace/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Adam Jordan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
102
laplace/README.md
Normal file
102
laplace/README.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# Laplace
|
||||
|
||||
Laplace is an open-source project to enable screen sharing directly via browser.
|
||||
Made possible using WebRTC for low latency peer-to-peer connections, and WebSocket implemented in golang for WebRTC signaling.
|
||||
|
||||
> Demo video: https://youtu.be/E8cUaPrAlzE
|
||||
|
||||
[](https://youtu.be/E8cUaPrAlzE)
|
||||
|
||||
|
||||
## Try Demo
|
||||
|
||||
For demo, you can visit https://laplace.madeby.monster/
|
||||
|
||||

|
||||
|
||||
|
||||
## Motivation
|
||||
|
||||
There are already possible solutions to share your computer screen, e.g. TeamViewer.
|
||||
But most of them require installations of software or plugins.
|
||||
What Laplace provides is a simple solution to this problem.
|
||||
For users wanting to share their screen, all they need to do is to open a website page with their browsers, clicking some buttons, then share some session ID with their peers.
|
||||
No installation or registration required.
|
||||
|
||||
#### Solving the latency problem
|
||||
|
||||
This project also serves as a proof-of-concept (PoC) for screen sharing capability directly in browsers based on WebRTC.
|
||||
Using WebRTC, real-time communication is made possible through peer-to-peer connections.
|
||||
This proves to be very useful in solving one of the biggest problems is screen streaming: **Latency**.
|
||||
The latency represents how long the delay is from the source to transmit to the remote client.
|
||||
If you notice, this latency problem is usually highlighted by game streaming services, since gameplay relies heavily on the interactivity of inputs and outputs.
|
||||
|
||||
|
||||
#### Low server cost
|
||||
This solution also solves the server cost problem, since the expensive operations (encoding and transmission) are done on client browsers.
|
||||
The server is only needed for serving frontends and for WebRTC signaling.
|
||||
|
||||
|
||||
#### Possible Use Cases
|
||||
|
||||
- Game streaming from PC to mobile devices.
|
||||
- Collaborative work where you need to share your screen with remote coworkers.
|
||||
- Mirroring presentation slides and demonstrations.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
Build from source
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/adamyordan/laplace.git
|
||||
$ cd laplace && go build -o laplace main.go
|
||||
$ ./laplace --help
|
||||
```
|
||||
|
||||
OR, pull the pre-built docker image
|
||||
|
||||
```bash
|
||||
$ docker pull adamyordan/laplace
|
||||
$ docker run adamyordan/laplace ./laplace --help
|
||||
```
|
||||
|
||||
|
||||
## Program Execution
|
||||
|
||||
Executing this project basically serves an HTTP server that will host the frontend and the WebSocket implementation.
|
||||
Note that you sometimes need to run HTTPs in order for browser to connect to websocket.
|
||||
|
||||
```bash
|
||||
$ ./laplace --help
|
||||
-addr string
|
||||
Listen address (default "0.0.0.0:443")
|
||||
-certFile string
|
||||
TLS cert file (default "files/server.crt")
|
||||
-keyFile string
|
||||
TLS key file (default "files/server.key")
|
||||
-tls
|
||||
Use TLS (default true)
|
||||
```
|
||||
|
||||
By default, you can run the executable without any argument to listen to TLS port 443.
|
||||
A self-signed certificate files are provided to ease up development.
|
||||
|
||||
```bash
|
||||
$ ./laplace
|
||||
2020/03/25 01:01:10 Listening on TLS: 0.0.0.0:443
|
||||
```
|
||||
|
||||
You can then open https://localhost:443/ to view Laplace page.
|
||||
You may need to add certificate exceptions. In Chrome, you can type `thisisunsafe`.
|
||||
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
[MIT](https://choosealicense.com/licenses/mit/)
|
||||
497
laplace/core/names-generator.go
Normal file
497
laplace/core/names-generator.go
Normal file
@@ -0,0 +1,497 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
var (
|
||||
left = [...]string{
|
||||
"admiring",
|
||||
"adoring",
|
||||
"affectionate",
|
||||
"agitated",
|
||||
"amazing",
|
||||
"angry",
|
||||
"awesome",
|
||||
"beautiful",
|
||||
"blissful",
|
||||
"bold",
|
||||
"boring",
|
||||
"brave",
|
||||
"busy",
|
||||
"charming",
|
||||
"clever",
|
||||
"cool",
|
||||
"compassionate",
|
||||
"competent",
|
||||
"condescending",
|
||||
"confident",
|
||||
"cranky",
|
||||
"crazy",
|
||||
"dazzling",
|
||||
"determined",
|
||||
"distracted",
|
||||
"dreamy",
|
||||
"eager",
|
||||
"ecstatic",
|
||||
"elastic",
|
||||
"elated",
|
||||
"elegant",
|
||||
"eloquent",
|
||||
"epic",
|
||||
"exciting",
|
||||
"fervent",
|
||||
"festive",
|
||||
"flamboyant",
|
||||
"focused",
|
||||
"friendly",
|
||||
"frosty",
|
||||
"funny",
|
||||
"gallant",
|
||||
"gifted",
|
||||
"goofy",
|
||||
"gracious",
|
||||
"great",
|
||||
"happy",
|
||||
"hardcore",
|
||||
"heuristic",
|
||||
"hopeful",
|
||||
"hungry",
|
||||
"infallible",
|
||||
"inspiring",
|
||||
"interesting",
|
||||
"intelligent",
|
||||
"jolly",
|
||||
"jovial",
|
||||
"keen",
|
||||
"kind",
|
||||
"laughing",
|
||||
"loving",
|
||||
"lucid",
|
||||
"magical",
|
||||
"mystifying",
|
||||
"modest",
|
||||
"musing",
|
||||
"naughty",
|
||||
"nervous",
|
||||
"nice",
|
||||
"nifty",
|
||||
"nostalgic",
|
||||
"objective",
|
||||
"optimistic",
|
||||
"peaceful",
|
||||
"pedantic",
|
||||
"pensive",
|
||||
"practical",
|
||||
"priceless",
|
||||
"quirky",
|
||||
"quizzical",
|
||||
"recursing",
|
||||
"relaxed",
|
||||
"reverent",
|
||||
"romantic",
|
||||
"sad",
|
||||
"serene",
|
||||
"sharp",
|
||||
"silly",
|
||||
"sleepy",
|
||||
"stoic",
|
||||
"strange",
|
||||
"stupefied",
|
||||
"suspicious",
|
||||
"sweet",
|
||||
"tender",
|
||||
"thirsty",
|
||||
"trusting",
|
||||
"unruffled",
|
||||
"upbeat",
|
||||
"vibrant",
|
||||
"vigilant",
|
||||
"vigorous",
|
||||
"wizardly",
|
||||
"wonderful",
|
||||
"xenodochial",
|
||||
"youthful",
|
||||
"zealous",
|
||||
"zen",
|
||||
}
|
||||
|
||||
// Docker, starting from 0.7.x, generates names from notable scientists and hackers.
|
||||
// Please, for any amazing man that you add to the list, consider adding an equally amazing woman to it, and vice versa.
|
||||
right = [...]string{
|
||||
"aardvark",
|
||||
"abyssinian",
|
||||
"affenpinscher",
|
||||
"akbash",
|
||||
"akita",
|
||||
"albatross",
|
||||
"alligator",
|
||||
"alpaca",
|
||||
"angelfish",
|
||||
"ant",
|
||||
"anteater",
|
||||
"antelope",
|
||||
"ape",
|
||||
"armadillo",
|
||||
"ass",
|
||||
"avocet",
|
||||
"axolotl",
|
||||
"baboon",
|
||||
"badger",
|
||||
"balinese",
|
||||
"bandicoot",
|
||||
"barb",
|
||||
"barnacle",
|
||||
"barracuda",
|
||||
"bat",
|
||||
"beagle",
|
||||
"bear",
|
||||
"beaver",
|
||||
"bee",
|
||||
"beetle",
|
||||
"binturong",
|
||||
"bird",
|
||||
"birman",
|
||||
"bison",
|
||||
"bloodhound",
|
||||
"boar",
|
||||
"bobcat",
|
||||
"bombay",
|
||||
"bongo",
|
||||
"bonobo",
|
||||
"booby",
|
||||
"budgerigar",
|
||||
"buffalo",
|
||||
"bulldog",
|
||||
"bullfrog",
|
||||
"burmese",
|
||||
"butterfly",
|
||||
"caiman",
|
||||
"camel",
|
||||
"capybara",
|
||||
"caracal",
|
||||
"caribou",
|
||||
"cassowary",
|
||||
"cat",
|
||||
"caterpillar",
|
||||
"catfish",
|
||||
"cattle",
|
||||
"centipede",
|
||||
"chameleon",
|
||||
"chamois",
|
||||
"cheetah",
|
||||
"chicken",
|
||||
"chihuahua",
|
||||
"chimpanzee",
|
||||
"chinchilla",
|
||||
"chinook",
|
||||
"chipmunk",
|
||||
"chough",
|
||||
"cichlid",
|
||||
"clam",
|
||||
"coati",
|
||||
"cobra",
|
||||
"cockroach",
|
||||
"cod",
|
||||
"collie",
|
||||
"coral",
|
||||
"cormorant",
|
||||
"cougar",
|
||||
"cow",
|
||||
"coyote",
|
||||
"crab",
|
||||
"crane",
|
||||
"crocodile",
|
||||
"crow",
|
||||
"curlew",
|
||||
"cuscus",
|
||||
"cuttlefish",
|
||||
"dachshund",
|
||||
"dalmatian",
|
||||
"deer",
|
||||
"dhole",
|
||||
"dingo",
|
||||
"dinosaur",
|
||||
"discus",
|
||||
"dodo",
|
||||
"dog",
|
||||
"dogfish",
|
||||
"dolphin",
|
||||
"donkey",
|
||||
"dormouse",
|
||||
"dotterel",
|
||||
"dove",
|
||||
"dragonfly",
|
||||
"drever",
|
||||
"duck",
|
||||
"dugong",
|
||||
"dunker",
|
||||
"dunlin",
|
||||
"eagle",
|
||||
"earwig",
|
||||
"echidna",
|
||||
"eel",
|
||||
"eland",
|
||||
"elephant",
|
||||
"elk",
|
||||
"emu",
|
||||
"falcon",
|
||||
"ferret",
|
||||
"finch",
|
||||
"fish",
|
||||
"flamingo",
|
||||
"flounder",
|
||||
"fly",
|
||||
"fossa",
|
||||
"fox",
|
||||
"frigatebird",
|
||||
"frog",
|
||||
"galago",
|
||||
"gar",
|
||||
"gaur",
|
||||
"gazelle",
|
||||
"gecko",
|
||||
"gerbil",
|
||||
"gharial",
|
||||
"gibbon",
|
||||
"giraffe",
|
||||
"gnat",
|
||||
"gnu",
|
||||
"goat",
|
||||
"goldfinch",
|
||||
"goldfish",
|
||||
"goose",
|
||||
"gopher",
|
||||
"gorilla",
|
||||
"goshawk",
|
||||
"grasshopper",
|
||||
"greyhound",
|
||||
"grouse",
|
||||
"guanaco",
|
||||
"gull",
|
||||
"guppy",
|
||||
"hamster",
|
||||
"hare",
|
||||
"harrier",
|
||||
"havanese",
|
||||
"hawk",
|
||||
"hedgehog",
|
||||
"heron",
|
||||
"herring",
|
||||
"himalayan",
|
||||
"hippopotamus",
|
||||
"hornet",
|
||||
"horse",
|
||||
"human",
|
||||
"hummingbird",
|
||||
"hyena",
|
||||
"ibis",
|
||||
"iguana",
|
||||
"impala",
|
||||
"indri",
|
||||
"insect",
|
||||
"jackal",
|
||||
"jaguar",
|
||||
"javanese",
|
||||
"jay",
|
||||
"jellyfish",
|
||||
"kakapo",
|
||||
"kangaroo",
|
||||
"kingfisher",
|
||||
"kiwi",
|
||||
"koala",
|
||||
"kouprey",
|
||||
"kudu",
|
||||
"labradoodle",
|
||||
"ladybird",
|
||||
"lapwing",
|
||||
"lark",
|
||||
"lemming",
|
||||
"lemur",
|
||||
"leopard",
|
||||
"liger",
|
||||
"lion",
|
||||
"lionfish",
|
||||
"lizard",
|
||||
"llama",
|
||||
"lobster",
|
||||
"locust",
|
||||
"loris",
|
||||
"louse",
|
||||
"lynx",
|
||||
"lyrebird",
|
||||
"macaw",
|
||||
"magpie",
|
||||
"mallard",
|
||||
"maltese",
|
||||
"manatee",
|
||||
"mandrill",
|
||||
"markhor",
|
||||
"marten",
|
||||
"mastiff",
|
||||
"mayfly",
|
||||
"meerkat",
|
||||
"millipede",
|
||||
"mink",
|
||||
"mole",
|
||||
"molly",
|
||||
"mongoose",
|
||||
"mongrel",
|
||||
"monkey",
|
||||
"moorhen",
|
||||
"moose",
|
||||
"mosquito",
|
||||
"moth",
|
||||
"mouse",
|
||||
"mule",
|
||||
"narwhal",
|
||||
"neanderthal",
|
||||
"newfoundland",
|
||||
"newt",
|
||||
"nightingale",
|
||||
"numbat",
|
||||
"ocelot",
|
||||
"octopus",
|
||||
"okapi",
|
||||
"olm",
|
||||
"opossum",
|
||||
"orangutan",
|
||||
"oryx",
|
||||
"ostrich",
|
||||
"otter",
|
||||
"owl",
|
||||
"ox",
|
||||
"oyster",
|
||||
"pademelon",
|
||||
"panther",
|
||||
"parrot",
|
||||
"partridge",
|
||||
"peacock",
|
||||
"peafowl",
|
||||
"pekingese",
|
||||
"pelican",
|
||||
"penguin",
|
||||
"persian",
|
||||
"pheasant",
|
||||
"pig",
|
||||
"pigeon",
|
||||
"pika",
|
||||
"pike",
|
||||
"piranha",
|
||||
"platypus",
|
||||
"pointer",
|
||||
"pony",
|
||||
"poodle",
|
||||
"porcupine",
|
||||
"porpoise",
|
||||
"possum",
|
||||
"prawn",
|
||||
"puffin",
|
||||
"pug",
|
||||
"puma",
|
||||
"quail",
|
||||
"quelea",
|
||||
"quetzal",
|
||||
"quokka",
|
||||
"quoll",
|
||||
"rabbit",
|
||||
"raccoon",
|
||||
"ragdoll",
|
||||
"rail",
|
||||
"ram",
|
||||
"rat",
|
||||
"rattlesnake",
|
||||
"raven",
|
||||
"reindeer",
|
||||
"rhinoceros",
|
||||
"robin",
|
||||
"rook",
|
||||
"rottweiler",
|
||||
"ruff",
|
||||
"salamander",
|
||||
"salmon",
|
||||
"sandpiper",
|
||||
"saola",
|
||||
"sardine",
|
||||
"scorpion",
|
||||
"seahorse",
|
||||
"seal",
|
||||
"serval",
|
||||
"shark",
|
||||
"sheep",
|
||||
"shrew",
|
||||
"shrimp",
|
||||
"siamese",
|
||||
"siberian",
|
||||
"skunk",
|
||||
"sloth",
|
||||
"snail",
|
||||
"snake",
|
||||
"snowshoe",
|
||||
"somali",
|
||||
"sparrow",
|
||||
"spider",
|
||||
"sponge",
|
||||
"squid",
|
||||
"squirrel",
|
||||
"starfish",
|
||||
"starling",
|
||||
"stingray",
|
||||
"stinkbug",
|
||||
"stoat",
|
||||
"stork",
|
||||
"swallow",
|
||||
"swan",
|
||||
"tang",
|
||||
"tapir",
|
||||
"tarsier",
|
||||
"termite",
|
||||
"tetra",
|
||||
"tiffany",
|
||||
"tiger",
|
||||
"toad",
|
||||
"tortoise",
|
||||
"toucan",
|
||||
"tropicbird",
|
||||
"trout",
|
||||
"tuatara",
|
||||
"turkey",
|
||||
"turtle",
|
||||
"uakari",
|
||||
"uguisu",
|
||||
"umbrellabird",
|
||||
"viper",
|
||||
"vulture",
|
||||
"wallaby",
|
||||
"walrus",
|
||||
"warthog",
|
||||
"wasp",
|
||||
"weasel",
|
||||
"whale",
|
||||
"whippet",
|
||||
"wildebeest",
|
||||
"wolf",
|
||||
"wolverine",
|
||||
"wombat",
|
||||
"woodcock",
|
||||
"woodlouse",
|
||||
"woodpecker",
|
||||
"worm",
|
||||
"wrasse",
|
||||
"wren",
|
||||
"yak",
|
||||
"zebra",
|
||||
"zebu",
|
||||
"zonkey",
|
||||
"zorse",
|
||||
}
|
||||
)
|
||||
|
||||
func GetRandomName(retry int) string {
|
||||
name := fmt.Sprintf("%s_%s_%s", left[rand.Intn(len(left))], left[rand.Intn(len(left))], right[rand.Intn(len(right))])
|
||||
if retry > 0 {
|
||||
name = fmt.Sprintf("%s_%d", name, rand.Intn(10))
|
||||
}
|
||||
return name
|
||||
}
|
||||
74
laplace/core/room.go
Normal file
74
laplace/core/room.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type Room struct {
|
||||
ID string
|
||||
Sessions map[string]*StreamSession
|
||||
CallerConn *websocket.Conn
|
||||
}
|
||||
|
||||
type StreamSession struct {
|
||||
ID string
|
||||
Offer string
|
||||
Answer string
|
||||
CallerIceCandidates []string
|
||||
CalleeIceCandidates []string
|
||||
CallerConn *websocket.Conn
|
||||
CalleeConn *websocket.Conn
|
||||
}
|
||||
|
||||
var roomMap = make(map[string]*Room)
|
||||
|
||||
func GetRoom(id string) *Room {
|
||||
return roomMap[id]
|
||||
}
|
||||
|
||||
func NewRoom(callerConn *websocket.Conn) *Room {
|
||||
room := Room{
|
||||
ID: newRoomID(),
|
||||
Sessions: make(map[string]*StreamSession),
|
||||
CallerConn: callerConn,
|
||||
}
|
||||
roomMap[room.ID] = &room
|
||||
return &room
|
||||
}
|
||||
|
||||
func newRoomID() string {
|
||||
id := GetRandomName(0)
|
||||
for GetRoom(id) != nil {
|
||||
id = GetRandomName(0)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func RemoveRoom(id string) {
|
||||
roomMap[id] = nil
|
||||
}
|
||||
|
||||
func (room *Room) GetSession(id string) *StreamSession {
|
||||
return room.Sessions[id]
|
||||
}
|
||||
|
||||
func (room *Room) NewSession(calleeConn *websocket.Conn) *StreamSession {
|
||||
session := StreamSession{
|
||||
ID: room.newSessionID(),
|
||||
CallerIceCandidates: []string{},
|
||||
CalleeIceCandidates: []string{},
|
||||
CallerConn: room.CallerConn,
|
||||
CalleeConn: calleeConn,
|
||||
}
|
||||
room.Sessions[session.ID] = &session
|
||||
return &session
|
||||
}
|
||||
|
||||
func (room *Room) newSessionID() string {
|
||||
id := fmt.Sprintf("%s$%s", room.ID, GetRandomName(0))
|
||||
for GetRoom(id) != nil {
|
||||
id = fmt.Sprintf("%s$%s", room.ID, GetRandomName(0))
|
||||
}
|
||||
return id
|
||||
}
|
||||
159
laplace/core/signal.go
Normal file
159
laplace/core/signal.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"github.com/gorilla/websocket"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type WSMessage struct {
|
||||
SessionID string
|
||||
Type string
|
||||
Value string
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
func sendHeartBeatWS(ticker *time.Ticker, conn *websocket.Conn, quit chan struct{}) {
|
||||
for {
|
||||
select {
|
||||
case <- ticker.C:
|
||||
_ = conn.WriteJSON(WSMessage{
|
||||
Type: "beat",
|
||||
})
|
||||
case <- quit:
|
||||
log.Println("heartbeat stopped")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetHttp() *http.ServeMux {
|
||||
server := http.NewServeMux()
|
||||
|
||||
server.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("files/static"))))
|
||||
|
||||
server.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, "files/main.html")
|
||||
})
|
||||
|
||||
server.HandleFunc("/ws_serve", func(writer http.ResponseWriter, request *http.Request) {
|
||||
conn, _ := upgrader.Upgrade(writer, request, nil)
|
||||
room := NewRoom(conn)
|
||||
if err := conn.WriteJSON(WSMessage{
|
||||
SessionID: "",
|
||||
Type: "newRoom",
|
||||
Value: room.ID,
|
||||
}); err != nil {
|
||||
log.Println("newSessionWriteJsonError.", err)
|
||||
return
|
||||
}
|
||||
|
||||
go func(r *Room) {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
quit := make(chan struct{})
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
_ = room.CallerConn.Close()
|
||||
close(quit)
|
||||
RemoveRoom(r.ID)
|
||||
for sID, s := range r.Sessions {
|
||||
_ = s.CalleeConn.WriteJSON(WSMessage{
|
||||
Type: "roomClosed",
|
||||
SessionID: sID,
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
go sendHeartBeatWS(ticker, conn, quit)
|
||||
|
||||
//noinspection ALL
|
||||
defer room.CallerConn.Close()
|
||||
for {
|
||||
var msg WSMessage
|
||||
if err := room.CallerConn.ReadJSON(&msg); err != nil {
|
||||
log.Println("websocketError.", err)
|
||||
return
|
||||
}
|
||||
//log.Println(msg)
|
||||
s := room.GetSession(msg.SessionID)
|
||||
if s == nil {
|
||||
log.Println("session nil.", msg.SessionID)
|
||||
}
|
||||
if msg.Type == "addCallerIceCandidate" {
|
||||
s.CallerIceCandidates = append(s.CallerIceCandidates, msg.Value)
|
||||
} else if msg.Type == "gotOffer" {
|
||||
s.Offer = msg.Value
|
||||
}
|
||||
if err := s.CalleeConn.WriteJSON(msg); err != nil {
|
||||
log.Println("serveEchoWriteJsonError.", err)
|
||||
}
|
||||
}
|
||||
}(room)
|
||||
})
|
||||
|
||||
server.HandleFunc("/ws_connect", func(writer http.ResponseWriter, request *http.Request) {
|
||||
conn, _ := upgrader.Upgrade(writer, request, nil)
|
||||
|
||||
ids, ok := request.URL.Query()["id"]
|
||||
if !ok || ids[0] == "" {
|
||||
return
|
||||
}
|
||||
|
||||
room := GetRoom(ids[0])
|
||||
if room == nil {
|
||||
_ = conn.WriteJSON(WSMessage{
|
||||
Type: "roomNotFound",
|
||||
})
|
||||
return
|
||||
}
|
||||
session := room.NewSession(conn)
|
||||
|
||||
if err := room.CallerConn.WriteJSON(WSMessage{
|
||||
SessionID: session.ID,
|
||||
Type: "newSession",
|
||||
Value: session.ID,
|
||||
}); err != nil {
|
||||
log.Println("callerWriteJsonError.", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := conn.WriteJSON(WSMessage{
|
||||
SessionID: session.ID,
|
||||
Type: "newSession",
|
||||
Value: session.ID,
|
||||
}); err != nil {
|
||||
log.Println("calleeWriteJsonError.", err)
|
||||
return
|
||||
}
|
||||
|
||||
go func(s *StreamSession) {
|
||||
//noinspection ALL
|
||||
defer s.CalleeConn.Close()
|
||||
for {
|
||||
var msg WSMessage
|
||||
if err := conn.ReadJSON(&msg); err != nil {
|
||||
log.Println("websocketError.", err)
|
||||
return
|
||||
}
|
||||
//log.Println(msg)
|
||||
if msg.SessionID == s.ID {
|
||||
if msg.Type == "addCalleeIceCandidate" {
|
||||
s.CalleeIceCandidates = append(s.CalleeIceCandidates, msg.Value)
|
||||
} else if msg.Type == "gotAnswer" {
|
||||
s.Answer = msg.Value
|
||||
}
|
||||
if err := s.CallerConn.WriteJSON(msg); err != nil {
|
||||
log.Println("connectEchoWriteJsonError.", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}(session)
|
||||
})
|
||||
|
||||
return server
|
||||
}
|
||||
BIN
laplace/doc/laplace-for-gaming.png
Normal file
BIN
laplace/doc/laplace-for-gaming.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 155 KiB |
BIN
laplace/doc/screenshot-website.png
Normal file
BIN
laplace/doc/screenshot-website.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 256 KiB |
167
laplace/files/main.html
Normal file
167
laplace/files/main.html
Normal file
@@ -0,0 +1,167 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Laplace</title>
|
||||
<link rel="stylesheet" href="/static/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/static/main.css?v=0.0.5">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar">
|
||||
<a class="navbar-brand text-dark" href="/">Laplace</a>
|
||||
<span class="navbar-brand header-room" id="room-text"></span>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
<div class="panel" id="panel">
|
||||
<p class="help-text">
|
||||
<b>Laplace</b> is an open-source project to enable screen sharing directly via browser.
|
||||
Made possible using WebRTC for low latency peer-to-peer connections,
|
||||
and websocket implemented in golang for WebRTC signaling.
|
||||
<a href="https://github.com/adamyordan/laplace">Read more</a>
|
||||
</p>
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
<h4>Start sharing your screen</h4>
|
||||
<p class="help-text">
|
||||
Click the button below to create a new streaming room,
|
||||
where you can share your screen with your peers.
|
||||
A RoomID will be generated, and you can share the RoomID to your peers.
|
||||
</p>
|
||||
<button type="button" class="btn btn-dark btn-block" id="btnStream">Start sharing</button>
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
<h4>Join streaming room</h4>
|
||||
<p class="help-text">
|
||||
Enter a RoomID to join the streaming room.
|
||||
You can get the RoomID from your peer that is doing a screen sharing.
|
||||
</p>
|
||||
<form id="joinForm">
|
||||
<div class="form-group">
|
||||
<input id="inputRoomID" type="text" class="form-control form-control-lg text-center" placeholder="Enter room ID" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-dark btn-block" value="submit">Join</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container container-small" id="stream-serve-page-ui">
|
||||
<div class="config-panel">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h4>Screen sharing configuration</h4>
|
||||
<p class="help-text">
|
||||
For most people, using the default "balanced" preset is enough,
|
||||
then click the "Start sharing" button to start.
|
||||
For the curious, You may also choose configuration preset that will affect the performance and quality of the streaming.
|
||||
For advance user, you can modify the options inside the textbox directly, please note that it may incur errors.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label for=optionPreset>Preset</label>
|
||||
<br>
|
||||
<select name="optionPreset" id="inputOptionPreset" class="form-control"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for=displayMediaOption>DisplayMedia option</label>
|
||||
<textarea name="displayMediaOption" id="inputDisplayMediaOption" cols="30" rows="5" class="form-control"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for=RTPPeerConnection>RTPPeerConnection option</label>
|
||||
<textarea name="RTPPeerConnectionOption" id="inputRTPPeerConnectionOption" cols="30" rows="5" class="form-control"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<button type="button" class="btn btn-dark btn-block-xs-only" id="btnStartStream">Start stream</button>
|
||||
<span> </span>
|
||||
<button type="button" class="btn btn-outline-dark btn-block-xs-only" id="btnStream" onclick="leaveRoom()">Cancel</button>
|
||||
</div>
|
||||
|
||||
<br><br>
|
||||
<p class="help-text">
|
||||
<b>Some notes</b><br>
|
||||
After clicking the "Start stream" button, if your device is supported, you may be asked to choose which screen you want to share.
|
||||
<br>
|
||||
As of now, mobile devices (Android & iOS) are usually not supported for screen sharing.
|
||||
<br>
|
||||
Sharing system audio is tested to be working only with latest Google Chrome running in Windows OS.
|
||||
<br>
|
||||
Remember to tick the "Share audio" checkbox.
|
||||
<br>
|
||||
In case the streaming does not work, reload the page, the WebRTC maybe unstable.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid" id="video-container">
|
||||
<div id="video-wrapper">
|
||||
<video id="mainVideo" autoplay playsinline controls>
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br><br>
|
||||
|
||||
<div class="container container-small" id="stream-page-ui">
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<h4>How to Join this Room</h4>
|
||||
<p class="help-text">
|
||||
You can share the RoomID to your peers (it is on the top-right position on this page).
|
||||
They can also join with the following link or QRCode:
|
||||
</p>
|
||||
<a href="#" id="join-link" target="_blank"></a>
|
||||
<br><br>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div id="qrcode"></div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div>
|
||||
<h6>Status</h6>
|
||||
<table class="table table-borderless table-sm table-meta">
|
||||
<tr>
|
||||
<td>Number of connections:</td>
|
||||
<td id="statusNumConn">0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connected peers:</td>
|
||||
<td id="statusPeers"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Ping:</td>
|
||||
<td id="statusPing">0 ms</td>
|
||||
</tr>
|
||||
</table>
|
||||
<button type="button" class="btn btn-dark btn-block-xs-only" id="btnStream" onclick="leaveRoom()">Leave Room</button>
|
||||
</div>
|
||||
<hr>
|
||||
<pre id="output"></pre>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<hr>
|
||||
<div class="text-center small" id="footer">
|
||||
<span>Laplace Project by Adam Jordan.</span>
|
||||
<a href="https://github.com/adamyordan/laplace">Source Code</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/qrcode.min.js"></script>
|
||||
<script src="/static/main.js?v=0.0.5"></script>
|
||||
</body>
|
||||
</html>
|
||||
9
laplace/files/server.crt
Normal file
9
laplace/files/server.crt
Normal file
@@ -0,0 +1,9 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBRjCBzQIJAO9zOPn7NhXdMAoGCCqGSM49BAMCMA0xCzAJBgNVBAYTAlNHMB4X
|
||||
DTIwMDMyMjA3MTAwNFoXDTMwMDMyMDA3MTAwNFowDTELMAkGA1UEBhMCU0cwdjAQ
|
||||
BgcqhkjOPQIBBgUrgQQAIgNiAARK0vXzgCXCkFXMhkNyW3bCPmVkpdmzhmKvh/5l
|
||||
zVFXBX20bD5Nd/Ja6bmZ5tfpabAAJv9U9i+xh5zIMyENIu/WoYZqjbIfffYK2xkA
|
||||
VoZLQraKM8yWCGJE7k7cw+cUYaEwCgYIKoZIzj0EAwIDaAAwZQIwUwmVA+sCyJ8d
|
||||
tfxK/Yh7BMCduSLw1dPqyr2lF90b6OL6oetKa/xtTcKLPomgHEFuAjEA9Pv23QD6
|
||||
xqwwedsfrxNA3Z8WDWA+rWOEmDcsxtrTSuW1t1HsCAqzFLxQn9mIoX+b
|
||||
-----END CERTIFICATE-----
|
||||
9
laplace/files/server.key
Normal file
9
laplace/files/server.key
Normal file
@@ -0,0 +1,9 @@
|
||||
-----BEGIN EC PARAMETERS-----
|
||||
BgUrgQQAIg==
|
||||
-----END EC PARAMETERS-----
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MIGkAgEBBDDVijLZH3+6qNVMntFz5xTbqiyp8IBqqS1+TidkLS0ZVqX4u6WTRhst
|
||||
1yP67FyQ9HCgBwYFK4EEACKhZANiAARK0vXzgCXCkFXMhkNyW3bCPmVkpdmzhmKv
|
||||
h/5lzVFXBX20bD5Nd/Ja6bmZ5tfpabAAJv9U9i+xh5zIMyENIu/WoYZqjbIfffYK
|
||||
2xkAVoZLQraKM8yWCGJE7k7cw+cUYaE=
|
||||
-----END EC PRIVATE KEY-----
|
||||
7
laplace/files/static/bootstrap.min.css
vendored
Normal file
7
laplace/files/static/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
105
laplace/files/static/main.css
Normal file
105
laplace/files/static/main.css
Normal file
@@ -0,0 +1,105 @@
|
||||
#video-wrapper {
|
||||
text-align: center;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
#video-wrapper video {
|
||||
max-width: 90vw;
|
||||
max-height: 70vh;
|
||||
box-shadow: 0 10px 25px 0 rgba(0, 0, 0, 0.50);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#stream-page-ui {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#stream-serve-page-ui {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#video-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#output {
|
||||
padding: 20px 0;
|
||||
overflow: scroll;
|
||||
font-size: 10px;
|
||||
margin: 0;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
#output::-webkit-scrollbar {
|
||||
width: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#qrcode {
|
||||
width: 128px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#footer {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-room {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.panel {
|
||||
max-width: 400px;
|
||||
padding: 20px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.panel .separator {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.container-small {
|
||||
max-width: 760px;
|
||||
}
|
||||
|
||||
.config-panel {
|
||||
border: 1px solid #e2e3e5;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.config-panel label {
|
||||
font-size: 0.8rem;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.config-panel select {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.config-panel input, .config-panel textarea {
|
||||
font-size: 0.8rem;
|
||||
font-family: Consolas, monospace;
|
||||
}
|
||||
|
||||
.table-meta {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.table-meta td:first-child {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.btn-block-xs-only {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
524
laplace/files/static/main.js
Normal file
524
laplace/files/static/main.js
Normal file
@@ -0,0 +1,524 @@
|
||||
"use strict";
|
||||
|
||||
// todo: deprecate
|
||||
const iceConfig = {
|
||||
iceServers: [{
|
||||
urls: [
|
||||
'stun:stun1.l.google.com:19302',
|
||||
'stun:stun2.l.google.com:19302',
|
||||
],
|
||||
}],
|
||||
iceCandidatePoolSize: 10,
|
||||
};
|
||||
|
||||
const displayMediaOptions = {
|
||||
noConstraint: {
|
||||
video: true,
|
||||
audio: true,
|
||||
},
|
||||
v720p30: {
|
||||
video: {
|
||||
height: 720,
|
||||
frameRate: 30,
|
||||
},
|
||||
audio: true,
|
||||
},
|
||||
v480p60: {
|
||||
video: {
|
||||
height: 480,
|
||||
frameRate: 60,
|
||||
},
|
||||
audio: true,
|
||||
},
|
||||
};
|
||||
|
||||
const rtpPeerConnectionOptions = {
|
||||
stunGoogle: {
|
||||
iceServers: [{
|
||||
urls: [
|
||||
'stun:stun1.l.google.com:19302',
|
||||
'stun:stun2.l.google.com:19302',
|
||||
],
|
||||
}],
|
||||
iceCandidatePoolSize: 10,
|
||||
},
|
||||
noStun: {
|
||||
iceServers: [],
|
||||
}
|
||||
};
|
||||
|
||||
const preset = {
|
||||
balanced: {
|
||||
displayMediaOption: displayMediaOptions.v720p30,
|
||||
rtpPeerConnectionOption: rtpPeerConnectionOptions.stunGoogle,
|
||||
},
|
||||
performance: {
|
||||
displayMediaOption: displayMediaOptions.v480p60,
|
||||
rtpPeerConnectionOption: rtpPeerConnectionOptions.stunGoogle,
|
||||
},
|
||||
highQuality: {
|
||||
displayMediaOption: displayMediaOptions.noConstraint,
|
||||
rtpPeerConnectionOption: rtpPeerConnectionOptions.stunGoogle,
|
||||
},
|
||||
balancedLanOnly: {
|
||||
displayMediaOption: displayMediaOptions.v720p30,
|
||||
rtpPeerConnectionOption: rtpPeerConnectionOptions.noStun,
|
||||
},
|
||||
performanceLanOnly: {
|
||||
displayMediaOption: displayMediaOptions.v480p60,
|
||||
rtpPeerConnectionOption: rtpPeerConnectionOptions.noStun,
|
||||
},
|
||||
highQualityLanOnly: {
|
||||
displayMediaOption: displayMediaOptions.noConstraint,
|
||||
rtpPeerConnectionOption: rtpPeerConnectionOptions.noStun,
|
||||
},
|
||||
};
|
||||
|
||||
const LaplaceVar = {
|
||||
ui: {},
|
||||
};
|
||||
|
||||
function avg(arr) {
|
||||
return (arr.reduce((a, b) => a + b, 0) / arr.length) | 0
|
||||
}
|
||||
|
||||
function print(s) {
|
||||
LaplaceVar.ui.output.innerHTML += s + '\n';
|
||||
}
|
||||
|
||||
function getBaseUrl() {
|
||||
return `${window.location.protocol}//${window.location.host}`
|
||||
}
|
||||
|
||||
function getStreamUrl() {
|
||||
return `${getBaseUrl()}/?stream=1`;
|
||||
}
|
||||
|
||||
function getJoinUrl(roomID) {
|
||||
return `${getBaseUrl()}/?id=${roomID}`;
|
||||
}
|
||||
|
||||
function updateRoomUI() {
|
||||
LaplaceVar.ui.panel.style.display = 'none';
|
||||
LaplaceVar.ui.videoContainer.style.display = 'block';
|
||||
LaplaceVar.ui.streamPageUI.style.display = 'block';
|
||||
|
||||
if (LaplaceVar.roomID) {
|
||||
const joinUrl = getJoinUrl(LaplaceVar.roomID);
|
||||
LaplaceVar.ui.qrcodeObj = new QRCode(LaplaceVar.ui.qrcode, {
|
||||
text: joinUrl,
|
||||
width: 128,
|
||||
height: 128,
|
||||
});
|
||||
LaplaceVar.ui.roomText.innerHTML = '<b>RoomID:</b> #' + LaplaceVar.roomID;
|
||||
LaplaceVar.ui.joinLinkText.innerHTML = joinUrl;
|
||||
LaplaceVar.ui.joinLinkText.href = joinUrl;
|
||||
}
|
||||
}
|
||||
|
||||
function initUI() {
|
||||
LaplaceVar.ui.btnStream = document.getElementById('btnStream');
|
||||
LaplaceVar.ui.btnStartStream = document.getElementById('btnStartStream');
|
||||
LaplaceVar.ui.inputRoomID = document.getElementById('inputRoomID');
|
||||
LaplaceVar.ui.inputDisplayMediaOption = document.getElementById('inputDisplayMediaOption');
|
||||
LaplaceVar.ui.inputRTPPeerConnectionOption = document.getElementById('inputRTPPeerConnectionOption');
|
||||
LaplaceVar.ui.joinLinkText = document.getElementById("join-link");
|
||||
LaplaceVar.ui.joinForm = document.getElementById('joinForm');
|
||||
LaplaceVar.ui.output = document.getElementById('output');
|
||||
LaplaceVar.ui.qrcode = document.getElementById("qrcode");
|
||||
LaplaceVar.ui.panel = document.getElementById('panel');
|
||||
LaplaceVar.ui.roomText = document.getElementById('room-text');
|
||||
LaplaceVar.ui.statusNumConn = document.getElementById('statusNumConn');
|
||||
LaplaceVar.ui.statusPeers = document.getElementById('statusPeers');
|
||||
LaplaceVar.ui.statusPing = document.getElementById('statusPing');
|
||||
LaplaceVar.ui.selectOptionPreset = document.getElementById('inputOptionPreset');
|
||||
LaplaceVar.ui.streamPageUI = document.getElementById('stream-page-ui');
|
||||
LaplaceVar.ui.streamServePageUI = document.getElementById('stream-serve-page-ui');
|
||||
LaplaceVar.ui.video = document.getElementById('mainVideo');
|
||||
LaplaceVar.ui.videoContainer = document.getElementById('video-container');
|
||||
|
||||
LaplaceVar.ui.joinForm.onsubmit = async e => {
|
||||
e.preventDefault();
|
||||
LaplaceVar.roomID = LaplaceVar.ui.inputRoomID.value;
|
||||
window.history.pushState('', '', getJoinUrl(LaplaceVar.roomID));
|
||||
await doJoin(LaplaceVar.roomID);
|
||||
};
|
||||
LaplaceVar.ui.btnStream.onclick = async () => {
|
||||
window.history.pushState('', '', getStreamUrl());
|
||||
await doStream();
|
||||
};
|
||||
LaplaceVar.ui.btnStartStream.onclick = () => {
|
||||
LaplaceVar.ui.streamServePageUI.style.display = 'none';
|
||||
const mediaOption = JSON.parse(LaplaceVar.ui.inputDisplayMediaOption.value);
|
||||
const pcOption = JSON.parse(LaplaceVar.ui.inputRTPPeerConnectionOption.value);
|
||||
return startStream(mediaOption, pcOption);
|
||||
};
|
||||
|
||||
for (const presetName of Object.keys(preset)) {
|
||||
const optionElement = document.createElement('option');
|
||||
optionElement.appendChild(document.createTextNode(presetName));
|
||||
optionElement.value = presetName;
|
||||
LaplaceVar.ui.selectOptionPreset.appendChild(optionElement);
|
||||
}
|
||||
LaplaceVar.ui.selectOptionPreset.onchange = () => {
|
||||
const v = LaplaceVar.ui.selectOptionPreset.value;
|
||||
if (preset[v] != null) {
|
||||
LaplaceVar.ui.inputDisplayMediaOption.value = JSON.stringify(preset[v].displayMediaOption, null, 1);
|
||||
LaplaceVar.ui.inputRTPPeerConnectionOption.value = JSON.stringify(preset[v].rtpPeerConnectionOption, null, 1);
|
||||
}
|
||||
};
|
||||
const defaultPresetValue = Object.keys(preset)[0];
|
||||
LaplaceVar.ui.inputDisplayMediaOption.value = JSON.stringify(preset[defaultPresetValue].displayMediaOption, null, 1);
|
||||
LaplaceVar.ui.inputRTPPeerConnectionOption.value = JSON.stringify(preset[defaultPresetValue].rtpPeerConnectionOption, null, 1);
|
||||
|
||||
|
||||
print("Logs:");
|
||||
print("[+] Page loaded");
|
||||
}
|
||||
|
||||
function updateStatusUIStream() {
|
||||
LaplaceVar.status.peers = Object.keys(LaplaceVar.pcs).map(s => s.split('$')[1]);
|
||||
LaplaceVar.status.numConn = LaplaceVar.status.peers.length;
|
||||
LaplaceVar.ui.statusPeers.innerHTML = LaplaceVar.status.peers
|
||||
.map(s => `${s} (${LaplaceVar.pings[LaplaceVar.roomID + '$' + s]} ms)`).join(', ');
|
||||
LaplaceVar.ui.statusNumConn.innerHTML = LaplaceVar.status.numConn;
|
||||
}
|
||||
|
||||
function updateStatusUIJoin() {
|
||||
LaplaceVar.ui.statusNumConn.innerHTML = LaplaceVar.status.numConn;
|
||||
LaplaceVar.ui.statusPeers.innerHTML = LaplaceVar.status.peers.map(s => LaplaceVar.sessionID.endsWith(s) ? s + ' (you)' : s).join(', ');
|
||||
}
|
||||
|
||||
function getWebsocketUrl() {
|
||||
if (window.location.protocol === "https:") {
|
||||
return `wss://${window.location.host}`
|
||||
} else {
|
||||
return `ws://${window.location.host}`
|
||||
}
|
||||
}
|
||||
|
||||
async function newRoom(rID) {
|
||||
print("[+] Get room ID: " + rID);
|
||||
LaplaceVar.roomID = rID;
|
||||
updateRoomUI();
|
||||
}
|
||||
|
||||
async function newSessionStream(sessionID, pcOption) {
|
||||
print('[+] New session: ' + sessionID);
|
||||
LaplaceVar.pcs[sessionID] = new RTCPeerConnection(pcOption);
|
||||
LaplaceVar.pcs[sessionID].onicecandidate = e => {
|
||||
print('[+] Debug onicecandidate: ' + JSON.stringify(e.candidate));
|
||||
if (!e.candidate) {
|
||||
print('[+] Debug onicecandidate: got final candidate!');
|
||||
return;
|
||||
}
|
||||
print('[+] Send addCallerIceCandidate to websocket: ' + JSON.stringify(e.candidate));
|
||||
LaplaceVar.socket.send(JSON.stringify({
|
||||
Type: "addCallerIceCandidate",
|
||||
SessionID: sessionID,
|
||||
Value: JSON.stringify(e.candidate),
|
||||
}))
|
||||
};
|
||||
LaplaceVar.pcs[sessionID].oniceconnectionstatechange = () => {
|
||||
print('[+] Debug oniceconnectionstatechange ' + LaplaceVar.pcs[sessionID].iceConnectionState);
|
||||
if (LaplaceVar.pcs[sessionID].iceConnectionState === 'disconnected') {
|
||||
print("[-] Disconnected with a Peer " + sessionID);
|
||||
LaplaceVar.pcs[sessionID].close();
|
||||
delete LaplaceVar.pcs[sessionID];
|
||||
delete LaplaceVar.dataChannels[sessionID];
|
||||
delete LaplaceVar.pings[sessionID];
|
||||
delete LaplaceVar.pingHistories[sessionID];
|
||||
updateStatusUIStream();
|
||||
}
|
||||
};
|
||||
updateStatusUIStream();
|
||||
LaplaceVar.dataChannels[sessionID] = LaplaceVar.pcs[sessionID].createDataChannel('ping');
|
||||
LaplaceVar.dataChannels[sessionID].addEventListener('open', () => {
|
||||
print('[+] Start ping interval: ', sessionID);
|
||||
LaplaceVar.pingHistories[sessionID] = [];
|
||||
LaplaceVar.pingIntervals[sessionID] = window.setInterval(() => {
|
||||
const now = new Date().getTime();
|
||||
LaplaceVar.dataChannels[sessionID].send('ping ' + now.toString());
|
||||
LaplaceVar.dataChannels[sessionID].send('status ' + JSON.stringify(LaplaceVar.status));
|
||||
}, 5000);
|
||||
});
|
||||
LaplaceVar.dataChannels[sessionID].addEventListener('close', () => {
|
||||
print('[+] Clear ping interval: ', sessionID);
|
||||
window.clearInterval(LaplaceVar.pingIntervals[sessionID]);
|
||||
});
|
||||
LaplaceVar.dataChannels[sessionID].addEventListener('message', e => {
|
||||
if (e.data.startsWith('ping')) {
|
||||
LaplaceVar.dataChannels[sessionID].send('pong' + e.data.slice(4));
|
||||
} else if (e.data.startsWith('pong')) {
|
||||
const now = new Date().getTime();
|
||||
const then = parseInt(e.data.slice(4));
|
||||
if (!isNaN(then)) {
|
||||
LaplaceVar.pingHistories[sessionID].push(now - then);
|
||||
if (LaplaceVar.pingHistories[sessionID].length > 3) {
|
||||
LaplaceVar.pingHistories[sessionID].shift()
|
||||
}
|
||||
LaplaceVar.pings[sessionID] = avg(LaplaceVar.pingHistories[sessionID]);
|
||||
updateStatusUIStream();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
LaplaceVar.mediaStream.getTracks().forEach(track => {
|
||||
LaplaceVar.pcs[sessionID].addTrack(track, LaplaceVar.mediaStream);
|
||||
});
|
||||
|
||||
|
||||
print('[+] Creating offer');
|
||||
const offer = await LaplaceVar.pcs[sessionID].createOffer({
|
||||
offerToReceiveAudio: true,
|
||||
offerToReceiveVideo: true,
|
||||
});
|
||||
await LaplaceVar.pcs[sessionID].setLocalDescription(offer);
|
||||
|
||||
print('[+] Send offer to websocket: ' + JSON.stringify(offer));
|
||||
LaplaceVar.socket.send(JSON.stringify({
|
||||
Type: "gotOffer",
|
||||
SessionID: sessionID,
|
||||
Value: JSON.stringify(offer),
|
||||
}));
|
||||
}
|
||||
|
||||
async function addCalleeIceCandidate(sessionID, v) {
|
||||
print('[+] Debug addCalleeIceCandidate ' + sessionID + ' ' + JSON.stringify(v));
|
||||
return LaplaceVar.pcs[sessionID].addIceCandidate(v);
|
||||
}
|
||||
|
||||
async function gotAnswer(sessionID, v) {
|
||||
print('[+] Debug gotAnswer ' + sessionID + ' ' + JSON.stringify(v));
|
||||
return LaplaceVar.pcs[sessionID].setRemoteDescription(new RTCSessionDescription(v));
|
||||
}
|
||||
|
||||
async function doStream() {
|
||||
LaplaceVar.ui.panel.style.display = 'none';
|
||||
LaplaceVar.ui.streamServePageUI.style.display = 'block';
|
||||
}
|
||||
|
||||
|
||||
async function startStream(displayMediaOption, pcOption) {
|
||||
LaplaceVar.pcs = {}; // contains RTCPeerConnections
|
||||
LaplaceVar.dataChannels = {};
|
||||
LaplaceVar.pings = {};
|
||||
LaplaceVar.pingHistories = {};
|
||||
LaplaceVar.pingIntervals = {};
|
||||
LaplaceVar.status = {
|
||||
numConn: 0,
|
||||
peers: [],
|
||||
};
|
||||
|
||||
updateRoomUI();
|
||||
|
||||
print('[+] Initiate media: capture display media');
|
||||
try {
|
||||
// noinspection JSUnresolvedFunction
|
||||
LaplaceVar.mediaStream = await navigator.mediaDevices.getDisplayMedia(displayMediaOption);
|
||||
} catch {
|
||||
alert('Streaming from this device is not supported. \n\nGoogle reference: getDisplayMedia');
|
||||
leaveRoom()
|
||||
}
|
||||
LaplaceVar.ui.video.srcObject = LaplaceVar.mediaStream;
|
||||
LaplaceVar.ui.video.muted = true; // prevent duplicate sound played
|
||||
|
||||
print('[+] Initiate websocket');
|
||||
LaplaceVar.socket = new WebSocket(getWebsocketUrl() + '/ws_serve');
|
||||
LaplaceVar.socket.onerror = () => {
|
||||
alert('WebSocket error');
|
||||
leaveRoom();
|
||||
};
|
||||
LaplaceVar.socket.onopen = async function () {
|
||||
print("[+] Connected to websocket");
|
||||
};
|
||||
LaplaceVar.socket.onmessage = async function (e) {
|
||||
try {
|
||||
const jsonData = JSON.parse(e.data);
|
||||
if (jsonData.Type !== 'beat') {
|
||||
print("[+] Received websocket message: " + JSON.stringify(e.data));
|
||||
}
|
||||
if (jsonData.Type === "newRoom") {
|
||||
await newRoom(jsonData.Value);
|
||||
} else if (jsonData.Type === "newSession") {
|
||||
await newSessionStream(jsonData.SessionID, pcOption);
|
||||
} else if (jsonData.Type === "addCalleeIceCandidate") {
|
||||
await addCalleeIceCandidate(jsonData.SessionID, JSON.parse(jsonData.Value));
|
||||
} else if (jsonData.Type === "gotAnswer") {
|
||||
await gotAnswer(jsonData.SessionID, JSON.parse(jsonData.Value));
|
||||
}
|
||||
} catch (e) {
|
||||
print("[!] ERROR: " + e);
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function newSessionJoin(sID) {
|
||||
print('[+] New session: ' + sID);
|
||||
LaplaceVar.sessionID = sID;
|
||||
LaplaceVar.pc = new RTCPeerConnection(iceConfig);
|
||||
LaplaceVar.pc.onicecandidate = e => {
|
||||
print('[+] Debug onicecandidate: ' + JSON.stringify(e.candidate));
|
||||
if (!e.candidate) {
|
||||
print('[+] Debug onicecandidate: got final candidate!');
|
||||
return;
|
||||
}
|
||||
print('[+] Send addCalleeIceCandidate to websocket: ' + JSON.stringify(e.candidate));
|
||||
LaplaceVar.socket.send(JSON.stringify({
|
||||
Type: "addCalleeIceCandidate",
|
||||
SessionID: LaplaceVar.sessionID,
|
||||
Value: JSON.stringify(e.candidate),
|
||||
}))
|
||||
};
|
||||
LaplaceVar.pc.oniceconnectionstatechange = () => {
|
||||
print('[+] pc.oniceconnectionstatechange ' + LaplaceVar.pc.iceConnectionState);
|
||||
if (LaplaceVar.pc.iceConnectionState === 'disconnected') {
|
||||
print("[-] Disconnected with Peer");
|
||||
LaplaceVar.pc.close();
|
||||
LaplaceVar.pc = null;
|
||||
}
|
||||
};
|
||||
LaplaceVar.pc.ontrack = event => {
|
||||
// print('[+] Debug pc.ontrack: ' + JSON.stringify(event.track));
|
||||
// /* does not work on safari */
|
||||
// event.streams[0].getTracks().forEach(track => {
|
||||
// print('[+] Debug addTrack ' + JSON.stringify(track));
|
||||
// LaplaceVar.mediaStream.addTrack(track)
|
||||
// });
|
||||
LaplaceVar.mediaStream.addTrack(event.track);
|
||||
LaplaceVar.ui.video.srcObject = LaplaceVar.mediaStream;
|
||||
try {
|
||||
LaplaceVar.ui.video.play();
|
||||
} catch {}
|
||||
};
|
||||
LaplaceVar.pc.addEventListener('datachannel', e => {
|
||||
LaplaceVar.dataChannel = e.channel;
|
||||
LaplaceVar.dataChannel.addEventListener('open', () => {
|
||||
print('[+] Start ping interval');
|
||||
LaplaceVar.pingHistory = [];
|
||||
LaplaceVar.pingInterval = window.setInterval(() => {
|
||||
const now = new Date().getTime();
|
||||
LaplaceVar.dataChannel.send('ping ' + now.toString());
|
||||
}, 1000);
|
||||
});
|
||||
LaplaceVar.dataChannel.addEventListener('close', () => {
|
||||
print('[+] Clear ping interval');
|
||||
clearInterval(LaplaceVar.pingInterval);
|
||||
});
|
||||
LaplaceVar.dataChannel.addEventListener('message', e => {
|
||||
if (e.data.startsWith('ping')) {
|
||||
LaplaceVar.dataChannel.send('pong' + e.data.slice(4));
|
||||
} else if (e.data.startsWith('pong')) {
|
||||
const now = new Date().getTime();
|
||||
const then = parseInt(e.data.slice(4));
|
||||
if (!isNaN(then)) {
|
||||
LaplaceVar.pingHistory.push(now - then);
|
||||
if (LaplaceVar.pingHistory.length > 3) {
|
||||
LaplaceVar.pingHistory.shift()
|
||||
}
|
||||
LaplaceVar.ping = avg(LaplaceVar.pingHistory);
|
||||
LaplaceVar.ui.statusPing.innerHTML = LaplaceVar.ping + ' ms';
|
||||
}
|
||||
} else if (e.data.startsWith('status')) {
|
||||
LaplaceVar.status = JSON.parse(e.data.slice(7));
|
||||
updateStatusUIJoin();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function addCallerIceCandidate(sID, v) {
|
||||
print('[+] Debug addCallerIceCandidate ' + sID + ' ' + JSON.stringify(v));
|
||||
if (LaplaceVar.sessionID !== sID) return;
|
||||
return LaplaceVar.pc.addIceCandidate(v);
|
||||
}
|
||||
|
||||
async function gotOffer(sID, v) {
|
||||
print('[+] Debug gotOffer ' + sID + ' ' + JSON.stringify(v));
|
||||
if (LaplaceVar.sessionID !== sID) return;
|
||||
await LaplaceVar.pc.setRemoteDescription(new RTCSessionDescription(v));
|
||||
|
||||
print('[+] Create answer');
|
||||
const answer = await LaplaceVar.pc.createAnswer();
|
||||
await LaplaceVar.pc.setLocalDescription(answer);
|
||||
|
||||
print('[+] Send answer to websocket: ' + JSON.stringify(answer));
|
||||
LaplaceVar.socket.send(JSON.stringify({
|
||||
Type: "gotAnswer",
|
||||
SessionID: LaplaceVar.sessionID,
|
||||
Value: JSON.stringify(answer),
|
||||
}))
|
||||
}
|
||||
|
||||
async function doJoin(roomID) {
|
||||
if (roomID) {
|
||||
LaplaceVar.roomID = roomID;
|
||||
} else {
|
||||
return alert('roomID is not provided');
|
||||
}
|
||||
// normalize roomID starting with #
|
||||
LaplaceVar.roomID = LaplaceVar.roomID.startsWith('#') ? LaplaceVar.roomID.slice(1) : LaplaceVar.roomID;
|
||||
LaplaceVar.status = {
|
||||
numConn: 0,
|
||||
peers: [],
|
||||
};
|
||||
|
||||
updateRoomUI();
|
||||
|
||||
print('[+] Initiate media: set remote source');
|
||||
LaplaceVar.mediaStream = new MediaStream();
|
||||
LaplaceVar.ui.video.srcObject = LaplaceVar.mediaStream;
|
||||
|
||||
print('[+] Initiate websocket');
|
||||
LaplaceVar.socket = new WebSocket(getWebsocketUrl() + "/ws_connect?id=" + LaplaceVar.roomID);
|
||||
LaplaceVar.socket.onerror = () => {
|
||||
alert('WebSocket error');
|
||||
leaveRoom();
|
||||
};
|
||||
LaplaceVar.socket.onopen = async function () {
|
||||
print("[+] Connected to websocket");
|
||||
};
|
||||
LaplaceVar.socket.onmessage = async function (e) {
|
||||
try {
|
||||
const jsonData = JSON.parse(e.data);
|
||||
if (jsonData.Type !== 'beat') {
|
||||
print("[+] Received websocket message: " + JSON.stringify(e.data));
|
||||
}
|
||||
if (jsonData.Type === "newSession") {
|
||||
await newSessionJoin(jsonData.SessionID);
|
||||
} else if (jsonData.Type === "addCallerIceCandidate") {
|
||||
await addCallerIceCandidate(jsonData.SessionID, JSON.parse(jsonData.Value));
|
||||
} else if (jsonData.Type === "gotOffer") {
|
||||
await gotOffer(jsonData.SessionID, JSON.parse(jsonData.Value));
|
||||
} else if (jsonData.Type === 'roomNotFound') {
|
||||
alert('Room not found');
|
||||
leaveRoom();
|
||||
} else if (jsonData.Type === 'roomClosed') {
|
||||
alert('Room closed');
|
||||
}
|
||||
} catch (e) {
|
||||
print("[!] ERROR: " + e);
|
||||
console.error(e)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function routeByUrl() {
|
||||
const u = new URL(window.location);
|
||||
const paramId = u.searchParams.get('id');
|
||||
if (paramId && paramId.length > 0) {
|
||||
return doJoin(paramId);
|
||||
}
|
||||
const paramStream = u.searchParams.get('stream');
|
||||
if (paramStream && paramStream.length > 0) {
|
||||
return doStream();
|
||||
}
|
||||
}
|
||||
|
||||
function leaveRoom() {
|
||||
window.location.href = getBaseUrl();
|
||||
}
|
||||
|
||||
initUI();
|
||||
routeByUrl();
|
||||
1
laplace/files/static/qrcode.min.js
vendored
Normal file
1
laplace/files/static/qrcode.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
laplace/go.mod
Normal file
5
laplace/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module laplace
|
||||
|
||||
go 1.12
|
||||
|
||||
require github.com/gorilla/websocket v1.4.2
|
||||
2
laplace/go.sum
Normal file
2
laplace/go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
33
laplace/main.go
Normal file
33
laplace/main.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"laplace/core"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
addr := flag.String("addr", "0.0.0.0:443", "Listen address")
|
||||
tls := flag.Bool("tls", true, "Use TLS")
|
||||
certFile := flag.String("certFile", "files/server.crt", "TLS cert file")
|
||||
keyFile := flag.String("keyFile", "files/server.key", "TLS key file")
|
||||
flag.Parse()
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
server := core.GetHttp()
|
||||
|
||||
if *tls {
|
||||
log.Println("Listening on TLS:", *addr)
|
||||
if err := http.ListenAndServeTLS(*addr, *certFile, *keyFile, server); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
} else {
|
||||
log.Println("Listening:", *addr)
|
||||
if err := http.ListenAndServe(*addr, server); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user