├── .env ├── .gitignore ├── LICENSE.md ├── api ├── sessions │ └── sessions.go └── system │ ├── ping.go │ └── version.go ├── config └── constants.go ├── database └── redis.go ├── debug └── debug.go ├── deploy.sh ├── docker-compose.yml ├── go.mod ├── go.sum ├── handlers ├── cursor_websocket_handler.go ├── index_handler.go ├── screenshot_handler.go ├── session_handler.go ├── templates │ ├── index.html │ ├── session_not_found.html │ ├── template_fs.go │ └── x.html └── upload_handler.go ├── repository └── sessions.go ├── server.go └── util └── sessions.go /.env: -------------------------------------------------------------------------------- 1 | REDIS_HOST=localhost 2 | REDIS_PORT=8877 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | uploads/* 2 | server 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Functional Source License, Version 1.1, Apache 2.0 Future License 2 | 3 | ## Abbreviation 4 | 5 | FSL-1.1-Apache-2.0 6 | 7 | ## Notice 8 | 9 | Copyright 2024 Roman Pushkin 10 | 11 | ## Terms and Conditions 12 | 13 | ### Licensor ("We") 14 | 15 | The party offering the Software under these Terms and Conditions. 16 | 17 | ### The Software 18 | 19 | The "Software" is each version of the software that we make available under 20 | these Terms and Conditions, as indicated by our inclusion of these Terms and 21 | Conditions with the Software. 22 | 23 | ### License Grant 24 | 25 | Subject to your compliance with this License Grant and the Patents, 26 | Redistribution and Trademark clauses below, we hereby grant you the right to 27 | use, copy, modify, create derivative works, publicly perform, publicly display 28 | and redistribute the Software for any Permitted Purpose identified below. 29 | 30 | ### Permitted Purpose 31 | 32 | A Permitted Purpose is any purpose other than a Competing Use. A Competing Use 33 | means making the Software available to others in a commercial product or 34 | service that: 35 | 36 | 1. substitutes for the Software; 37 | 38 | 2. substitutes for any other product or service we offer using the Software 39 | that exists as of the date we make the Software available; or 40 | 41 | 3. offers the same or substantially similar functionality as the Software. 42 | 43 | Permitted Purposes specifically include using the Software: 44 | 45 | 1. for your internal use and access; 46 | 47 | 2. for non-commercial education; 48 | 49 | 3. for non-commercial research; and 50 | 51 | 4. in connection with professional services that you provide to a licensee 52 | using the Software in accordance with these Terms and Conditions. 53 | 54 | ### Patents 55 | 56 | To the extent your use for a Permitted Purpose would necessarily infringe our 57 | patents, the license grant above includes a license under our patents. If you 58 | make a claim against any party that the Software infringes or contributes to 59 | the infringement of any patent, then your patent license to the Software ends 60 | immediately. 61 | 62 | ### Redistribution 63 | 64 | The Terms and Conditions apply to all copies, modifications and derivatives of 65 | the Software. 66 | 67 | If you redistribute any copies, modifications or derivatives of the Software, 68 | you must include a copy of or a link to these Terms and Conditions and not 69 | remove any copyright notices provided in or with the Software. 70 | 71 | ### Disclaimer 72 | 73 | THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR 74 | IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR 75 | PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT. 76 | 77 | IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE 78 | SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES, 79 | EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE. 80 | 81 | ### Trademarks 82 | 83 | Except for displaying the License Details and identifying us as the origin of 84 | the Software, you have no right under these Terms and Conditions to use our 85 | trademarks, trade names, service marks or product names. 86 | 87 | ## Grant of Future License 88 | 89 | We hereby irrevocably grant you an additional license to use the Software under 90 | the Apache License, Version 2.0 that is effective on the second anniversary of 91 | the date we make the Software available. On or after that date, you may use the 92 | Software under the Apache License, Version 2.0, in which case the following 93 | will apply: 94 | 95 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 96 | this file except in compliance with the License. 97 | 98 | You may obtain a copy of the License at 99 | 100 | http://www.apache.org/licenses/LICENSE-2.0 101 | 102 | Unless required by applicable law or agreed to in writing, software distributed 103 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 104 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 105 | specific language governing permissions and limitations under the License. 106 | 107 | -------------------------------------------------------------------------------- /api/sessions/sessions.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/json" 6 | "math/big" 7 | "net/http" 8 | "time" 9 | 10 | "server/database" 11 | "server/repository" 12 | ) 13 | 14 | const ( 15 | SESSION_ID_LENGTH = 10 16 | SESSION_EXPIRY = 48 * time.Hour 17 | ) 18 | 19 | type ApiSessions struct { 20 | repoSessions *repository.RepoSessions 21 | } 22 | 23 | func NewApiSessions() *ApiSessions { 24 | return &ApiSessions{ 25 | repoSessions: repository.NewRepoSessions(database.GetRedisClient()), 26 | } 27 | } 28 | 29 | func (api *ApiSessions) CreateSession(w http.ResponseWriter, r *http.Request) { 30 | sessionID := generateRandomID(SESSION_ID_LENGTH) 31 | 32 | err := api.repoSessions.Create(sessionID, SESSION_EXPIRY) 33 | if err != nil { 34 | http.Error(w, "Failed to create session", http.StatusInternalServerError) 35 | return 36 | } 37 | 38 | w.Header().Set("Content-Type", "application/json") 39 | json.NewEncoder(w).Encode(map[string]string{ 40 | "status": "ok", 41 | "session_id": sessionID, 42 | }) 43 | } 44 | 45 | func generateRandomID(length int) string { 46 | const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" 47 | b := make([]byte, length) 48 | for i := range b { 49 | n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) 50 | b[i] = charset[n.Int64()] 51 | } 52 | return string(b) 53 | } 54 | -------------------------------------------------------------------------------- /api/system/ping.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | func PingHandler(w http.ResponseWriter, r *http.Request) { 9 | w.Header().Set("Content-Type", "application/json") 10 | json.NewEncoder(w).Encode(map[string]string{"status": "pong"}) 11 | } 12 | -------------------------------------------------------------------------------- /api/system/version.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | func VersionHandler(w http.ResponseWriter, r *http.Request) { 9 | w.Header().Set("Content-Type", "application/json") 10 | json.NewEncoder(w).Encode(map[string]string{"version": "0.1"}) 11 | } 12 | -------------------------------------------------------------------------------- /config/constants.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | const ( 8 | UPLOAD_DIR = "uploads" 9 | SCREENSHOT_FILE = "screenshot.jpeg" 10 | SCREENSHOT_HASH_FILE = "screenshot_hash.txt" 11 | ) 12 | 13 | func PrintDebug(message string) { 14 | // Implement the printDebug function here or import it from the appropriate package 15 | fmt.Println(message) 16 | } 17 | -------------------------------------------------------------------------------- /database/redis.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sync" 7 | 8 | "github.com/go-redis/redis" 9 | "github.com/joho/godotenv" 10 | ) 11 | 12 | var ( 13 | redisClient *redis.Client 14 | once sync.Once 15 | ) 16 | 17 | func GetRedisClient() *redis.Client { 18 | once.Do(func() { 19 | // Load .env file 20 | err := godotenv.Load() 21 | if err != nil { 22 | fmt.Println("Error loading .env file") 23 | } 24 | 25 | // Read Redis host and port from environment variables 26 | redisHost := os.Getenv("REDIS_HOST") 27 | redisPort := os.Getenv("REDIS_PORT") 28 | 29 | if redisHost == "" { 30 | redisHost = "localhost" 31 | } 32 | if redisPort == "" { 33 | redisPort = "6379" 34 | } 35 | 36 | redisAddr := fmt.Sprintf("%s:%s", redisHost, redisPort) 37 | 38 | redisClient = redis.NewClient(&redis.Options{ 39 | Addr: redisAddr, 40 | }) 41 | }) 42 | return redisClient 43 | } 44 | -------------------------------------------------------------------------------- /debug/debug.go: -------------------------------------------------------------------------------- 1 | package debug 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func PrintDebug(message string) { 8 | // Implement the printDebug function here or import it from the appropriate package 9 | fmt.Println(message) 10 | } 11 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 1fps Deployment Script 3 | 4 | # This script deploys the 1fps server to the remote machine. 5 | # It builds the server locally, stops the service on the remote machine, 6 | # updates Docker components if necessary, copies the binary, and restarts the service. 7 | 8 | # To set up the Ubuntu service (do this manually, once): 9 | # 1. Create a systemd service file: sudo nano /etc/systemd/system/1fps.service 10 | # 2. Add the following content: 11 | # [Unit] 12 | # Description=1fps Server 13 | # After=network.target 14 | # 15 | # [Service] 16 | # ExecStart=/home/ubuntu/work/1fps/server 17 | # WorkingDirectory=/home/ubuntu/work/1fps 18 | # User=ubuntu 19 | # Restart=always 20 | # 21 | # [Install] 22 | # WantedBy=multi-user.target 23 | # 24 | # 3. Save the file and exit 25 | # 4. Reload systemd: sudo systemctl daemon-reload 26 | # 5. Enable the service: sudo systemctl enable 1fps.service 27 | 28 | # Remote server details 29 | REMOTE_HOST="51.81.245.182" 30 | REMOTE_DIR="/home/ubuntu/work/1fps" 31 | 32 | # Build the server locally for Linux 33 | echo "Building server locally for Linux..." 34 | GOOS=linux GOARCH=amd64 go build -o server.linux server.go 35 | if [ $? -ne 0 ]; then 36 | echo "Build failed. Aborting deployment." 37 | exit 1 38 | fi 39 | 40 | # Stop the service on the remote machine 41 | echo "Stopping 1fps service on remote machine..." 42 | ssh $REMOTE_HOST "sudo systemctl stop 1fps.service && sudo systemctl status 1fps.service" 43 | 44 | # Check if docker-compose.yml needs to be updated 45 | echo "Checking if docker-compose.yml needs to be updated..." 46 | if ! ssh $REMOTE_HOST "test -e $REMOTE_DIR/docker-compose.yml && diff <(cat $REMOTE_DIR/docker-compose.yml) <(cat -)" < docker-compose.yml; then 47 | echo "docker-compose.yml needs updating. Copying file and restarting containers..." 48 | scp docker-compose.yml $REMOTE_HOST:$REMOTE_DIR/ 49 | ssh $REMOTE_HOST "cd $REMOTE_DIR && docker-compose down && docker-compose up -d" 50 | else 51 | echo "docker-compose.yml is up to date." 52 | fi 53 | 54 | # Copy .env file to remote host 55 | echo "Copying .env file to remote machine..." 56 | scp .env $REMOTE_HOST:$REMOTE_DIR/ 57 | 58 | # Parse .env file and test Redis connection on remote server 59 | REDIS_HOST=$(grep REDIS_HOST .env | cut -d '=' -f2) 60 | REDIS_PORT=$(grep REDIS_PORT .env | cut -d '=' -f2) 61 | echo "Testing Redis connection on remote server (${REDIS_HOST}:${REDIS_PORT})..." 62 | ssh $REMOTE_HOST " 63 | while ! nc -z ${REDIS_HOST} ${REDIS_PORT}; do 64 | echo 'Redis is unavailable - sleeping for 5 seconds' 65 | sleep 5 66 | done 67 | echo 'Redis is available' 68 | " 69 | 70 | # Copy the server binary 71 | echo "Copying server binary to remote machine..." 72 | scp server.linux $REMOTE_HOST:$REMOTE_DIR/server 73 | 74 | # Remove the local Linux binary 75 | echo "Removing local Linux binary..." 76 | rm server.linux 77 | 78 | # Start the service on the remote machine 79 | echo "Starting 1fps service on remote machine..." 80 | ssh $REMOTE_HOST "sudo systemctl start 1fps.service && sudo systemctl status 1fps.service" 81 | 82 | echo "Deployment completed successfully." 83 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | redis: 5 | container_name: 1fps_redis 6 | image: redis:latest 7 | command: redis-server --protected-mode no 8 | ports: 9 | - "127.0.0.1:8877:6379" 10 | volumes: 11 | - redis_data:/data 12 | 13 | volumes: 14 | redis_data: 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module server 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/gorilla/mux v1.8.1 7 | github.com/gorilla/websocket v1.5.1 8 | ) 9 | 10 | require ( 11 | github.com/go-redis/redis v6.15.9+incompatible // indirect 12 | github.com/joho/godotenv v1.5.1 // indirect 13 | golang.org/x/net v0.21.0 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= 2 | github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= 3 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 4 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 5 | github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= 6 | github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= 7 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 8 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 9 | golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= 10 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 11 | -------------------------------------------------------------------------------- /handlers/cursor_websocket_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "server/debug" 8 | 9 | "github.com/gorilla/mux" 10 | "github.com/gorilla/websocket" 11 | ) 12 | 13 | var ( 14 | upgrader = websocket.Upgrader{ 15 | ReadBufferSize: 1024, 16 | WriteBufferSize: 1024, 17 | } 18 | 19 | // clients is a nested map that stores WebSocket connections for each session. 20 | // The outer map uses session IDs as keys, and the inner map uses WebSocket connections as keys. 21 | // This structure allows for efficient management of multiple clients across different sessions. 22 | // 23 | // Ruby equivalent: 24 | // clients = { 25 | // "session1" => { 26 | // websocket_conn1 => true, 27 | // websocket_conn2 => true 28 | // }, 29 | // "session2" => { 30 | // websocket_conn3 => true 31 | // } 32 | // } 33 | clients = make(map[string]map[*websocket.Conn]bool) 34 | ) 35 | 36 | func CursorWebSocketHandler(w http.ResponseWriter, r *http.Request) { 37 | vars := mux.Vars(r) 38 | sessionID := vars["sessionID"] 39 | 40 | conn, err := upgrader.Upgrade(w, r, nil) 41 | if err != nil { 42 | debug.PrintDebug(fmt.Sprintf("WebSocket upgrade failed: %v", err)) 43 | return 44 | } 45 | defer conn.Close() 46 | 47 | if clients[sessionID] == nil { 48 | clients[sessionID] = make(map[*websocket.Conn]bool) 49 | } 50 | clients[sessionID][conn] = true 51 | 52 | for { 53 | _, message, err := conn.ReadMessage() 54 | if err != nil { 55 | debug.PrintDebug(fmt.Sprintf("WebSocket read failed: %v", err)) 56 | delete(clients[sessionID], conn) 57 | if len(clients[sessionID]) == 0 { 58 | delete(clients, sessionID) 59 | } 60 | break 61 | } 62 | 63 | var data struct { 64 | X int `json:"x"` 65 | Y int `json:"y"` 66 | RW *int `json:"rw"` // must be nil if not specified 67 | RH *int `json:"rh"` // because of fallbacks in client code 68 | } 69 | err = json.Unmarshal(message, &data) 70 | if err != nil { 71 | debug.PrintDebug(fmt.Sprintf("JSON unmarshal failed: %v", err)) 72 | continue 73 | } 74 | 75 | // Broadcast the cursor position to all connected clients in the same session 76 | for client := range clients[sessionID] { 77 | err := client.WriteJSON(data) 78 | if err != nil { 79 | debug.PrintDebug(fmt.Sprintf("WebSocket write failed: %v", err)) 80 | client.Close() 81 | delete(clients[sessionID], client) 82 | if len(clients[sessionID]) == 0 { 83 | delete(clients, sessionID) 84 | } 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /handlers/index_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "html/template" 5 | "net/http" 6 | "server/debug" 7 | "server/handlers/templates" 8 | ) 9 | 10 | func IndexHandler(w http.ResponseWriter, r *http.Request) { 11 | debug.PrintDebug("Received index request") 12 | 13 | // Parse the index template 14 | tmpl, err := template.ParseFS(templates.TemplateFS, "index.html") 15 | if err != nil { 16 | debug.PrintDebug("Error parsing index template: " + err.Error()) 17 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) 18 | return 19 | } 20 | 21 | // Execute the template 22 | err = tmpl.Execute(w, nil) 23 | if err != nil { 24 | debug.PrintDebug("Error executing index template: " + err.Error()) 25 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) 26 | return 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /handlers/screenshot_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | "server/config" 9 | "server/debug" 10 | "server/util" 11 | 12 | "github.com/gorilla/mux" 13 | ) 14 | 15 | func ScreenshotHandler(w http.ResponseWriter, r *http.Request) { 16 | debug.PrintDebug("Received screenshot request") 17 | 18 | // Extract sessionID from the URL parameters 19 | vars := mux.Vars(r) 20 | sessionID := vars["sessionID"] 21 | 22 | if !util.IsValidSessionID(sessionID) { 23 | debug.PrintDebug("Invalid session ID format") 24 | http.Error(w, "Invalid session ID format", http.StatusBadRequest) 25 | return 26 | } 27 | 28 | screenshotPath := filepath.Join(config.UPLOAD_DIR, sessionID, config.SCREENSHOT_FILE) 29 | if _, err := os.Stat(screenshotPath); os.IsNotExist(err) { 30 | http.NotFound(w, r) 31 | return 32 | } 33 | 34 | hashPath := filepath.Join(config.UPLOAD_DIR, sessionID, config.SCREENSHOT_HASH_FILE) 35 | hash, err := os.ReadFile(hashPath) 36 | if err != nil { 37 | http.Error(w, err.Error(), http.StatusInternalServerError) 38 | return 39 | } 40 | 41 | w.Header().Set("ETag", string(hash)) 42 | w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") 43 | w.Header().Set("Pragma", "no-cache") 44 | w.Header().Set("Expires", "0") 45 | http.ServeFile(w, r, screenshotPath) 46 | 47 | debug.PrintDebug(fmt.Sprintf("Served screenshot file %s", screenshotPath)) 48 | } 49 | -------------------------------------------------------------------------------- /handlers/session_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "net/http" 7 | "server/debug" 8 | 9 | "server/database" 10 | "server/handlers/templates" 11 | "server/repository" 12 | 13 | "github.com/gorilla/mux" 14 | ) 15 | 16 | func SessionHandler(w http.ResponseWriter, r *http.Request) { 17 | debug.PrintDebug("Received index request") 18 | 19 | // Get the session ID from the URL parameters 20 | vars := mux.Vars(r) 21 | sessionID := vars["sessionID"] 22 | 23 | debug.PrintDebug(fmt.Sprintf("Session ID: %s", sessionID)) 24 | 25 | // Create a new RepoSessions instance 26 | repoSessions := repository.NewRepoSessions(database.GetRedisClient()) 27 | 28 | // Check if the session exists in Redis 29 | sessionValue, err := repoSessions.Get(sessionID) 30 | if err != nil { 31 | debug.PrintDebug(fmt.Sprintf("Error getting session: %v", err)) 32 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) 33 | return 34 | } 35 | 36 | if sessionValue == nil { 37 | // Session not found, render the session_not_found template 38 | tmpl, err := template.ParseFS(templates.TemplateFS, "session_not_found.html") 39 | if err != nil { 40 | debug.PrintDebug(fmt.Sprintf("Error parsing template: %v", err)) 41 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) 42 | return 43 | } 44 | 45 | err = tmpl.Execute(w, nil) 46 | if err != nil { 47 | debug.PrintDebug(fmt.Sprintf("Error executing template: %v", err)) 48 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) 49 | } 50 | return 51 | } 52 | 53 | // Session exists, render the share template 54 | tmpl, err := template.ParseFS(templates.TemplateFS, "x.html") 55 | if err != nil { 56 | debug.PrintDebug(fmt.Sprintf("Error parsing template: %v", err)) 57 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) 58 | return 59 | } 60 | 61 | // Pass the session ID to the template 62 | data := struct { 63 | SessionID string 64 | }{ 65 | SessionID: sessionID, 66 | } 67 | 68 | err = tmpl.Execute(w, data) 69 | if err != nil { 70 | debug.PrintDebug(fmt.Sprintf("Error executing template: %v", err)) 71 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) 72 | return 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /handlers/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |You need Golang installed for this command to work. If you don't have Golang, you can install it using one of these methods:
84 |go run github.com/1fpsvideo/1fps@v0.1.11
90 |
91 | 93 | 94 | 🔒 Check out our secure, open-source client app 95 | 96 |
97 | 98 |We've discovered that low FPS video sharing is often sufficient for most collaborative tasks, especially for developers who spend most of their time writing code. Our approach offers several benefits:
108 |For most coding and development work, absolutely! Plus, we use WebSocket-based cursor tracking, providing smooth, near 30 FPS pointer movement for precise demonstrations.
121 | 122 |1fps.video is perfect for introverts and remote workers who prefer sharing their screen without the pressure of audio or video calls. It's a versatile solution that works alongside any team chat application you're already using. Our multi-monitor support, adjustable image quality, and screen resizing options allow you to tailor the sharing experience to your specific needs and network conditions.
124 | 125 |We take your privacy seriously:
127 |Note: We're currently working on encrypting cursor coordinates for even greater privacy.
135 | 136 |1fps.video addresses the growing need for secure, easy screen sharing and monitoring across various sectors. It's designed for 138 | long-running sessions, offering a simple link-sharing solution without software installation requirements.
139 | 140 |Unlike current options that often need specialized software or lack simple URL sharing, 1fps.video aims to streamline screen 141 | sharing globally. Our goal is to provide a secure, efficient solution for long-term streaming needs, making screen 142 | sharing as easy as sharing a link, while offering advanced features like multi-monitor support and traffic optimization.
143 | 144 |Start sharing your screen effortlessly and securely with 1fps.video today!
145 | 146 | 161 | 164 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /handlers/templates/session_not_found.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |The requested session does not exist or has expired.
36 |Please check the URL and try again, or create a new session.
37 |