├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ ├── goreleaser.yml │ └── lint.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── Dockerfile ├── LICENSE ├── README.md ├── api.go ├── controllers ├── myStreamer.go ├── sessions.go └── streamers.go ├── go.mod ├── go.sum ├── helpers └── newUUID.go ├── main.go ├── models ├── sessions.go └── streamer.go ├── rtmp └── RTMPConnection.go ├── rtmpConnectionHandler.go ├── screenshots ├── admin.png ├── admin_medium.png ├── admin_small.png ├── streamer.png ├── streamer_medium.png └── streamer_small.png ├── sessions └── session.go ├── store ├── boltstore │ ├── datastore.go │ └── streamers.go └── store.go ├── streamers └── streamers.go └── web ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.png ├── fonts │ └── jura │ │ ├── Jura-VariableFont_wght.ttf │ │ ├── OFL.txt │ │ ├── README.txt │ │ └── static │ │ ├── Jura-Bold.ttf │ │ ├── Jura-Light.ttf │ │ ├── Jura-Medium.ttf │ │ ├── Jura-Regular.ttf │ │ └── Jura-SemiBold.ttf ├── global.css ├── img │ ├── PrismPlusLogo_LinePrism.png │ ├── PrismPlusLogo_LinePrismWhite.png │ ├── PrismPlus_PlatformColors.png │ ├── PrismPlus_PlatformColorsTrans.png │ ├── PrismPlus_PlatformColorsTrans_128x.png │ └── PrismPlus_PlatformColorsTrans_256x.png └── index.html ├── rollup.config.js ├── src ├── App.svelte ├── admin.svelte ├── global.d.ts ├── main.ts └── streamer.svelte └── tsconfig.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: geekgonecrazy 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | strategy: 7 | matrix: 8 | go-version: [~1.17, ^1] 9 | os: [ubuntu-latest, macos-latest, windows-latest] 10 | runs-on: ${{ matrix.os }} 11 | env: 12 | GO111MODULE: "on" 13 | steps: 14 | - name: Install Go 15 | uses: actions/setup-go@v2 16 | with: 17 | go-version: ${{ matrix.go-version }} 18 | 19 | - name: Checkout code 20 | uses: actions/checkout@v2 21 | 22 | - name: Download Go modules 23 | run: go mod download 24 | 25 | - name: Build 26 | run: go build -v ./... 27 | 28 | - name: Test 29 | run: go test ./... 30 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | goreleaser: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - 12 | name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | - 17 | name: Set up Go 18 | uses: actions/setup-go@v2 19 | - 20 | name: Run GoReleaser 21 | uses: goreleaser/goreleaser-action@v2 22 | with: 23 | version: latest 24 | args: release --snapshot --skip-publish --skip-sign --rm-dist 25 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: 3 | push: 4 | pull_request: 5 | 6 | jobs: 7 | golangci: 8 | name: lint 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: golangci-lint 13 | uses: golangci/golangci-lint-action@v2 14 | with: 15 | # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. 16 | version: v1.31 17 | # Optional: golangci-lint command line arguments. 18 | args: --issues-exit-code=0 19 | # Optional: working directory, useful for monorepos 20 | # working-directory: somedir 21 | # Optional: show only new issues if it's a pull request. The default value is `false`. 22 | only-new-issues: true 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | vendor/ 16 | 17 | prism 18 | dist/ 19 | .envrc 20 | data.bbolt 21 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | tests: false 3 | 4 | issues: 5 | max-issues-per-linter: 0 6 | max-same-issues: 0 7 | 8 | linters: 9 | enable: 10 | - bodyclose 11 | - dupl 12 | - exportloopref 13 | - goconst 14 | - godot 15 | - godox 16 | - goimports 17 | - goprintffuncname 18 | - gosec 19 | - misspell 20 | - prealloc 21 | - rowserrcheck 22 | - sqlclosecheck 23 | - unconvert 24 | - unparam 25 | - whitespace 26 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | env: 2 | - GO111MODULE=on 3 | before: 4 | hooks: 5 | - go mod download 6 | builds: 7 | - id: "prism" 8 | env: 9 | - CGO_ENABLED=0 10 | binary: prism 11 | flags: 12 | - -trimpath 13 | ldflags: -s -w -X main.Version={{ .Version }} -X main.CommitSHA={{ .Commit }} 14 | goos: 15 | - linux 16 | - freebsd 17 | - openbsd 18 | - darwin 19 | - windows 20 | goarch: 21 | - amd64 22 | - arm64 23 | - 386 24 | - arm 25 | goarm: 26 | - 6 27 | - 7 28 | 29 | archives: 30 | - id: default 31 | builds: 32 | - prism 33 | format_overrides: 34 | - goos: windows 35 | format: zip 36 | replacements: 37 | windows: Windows 38 | darwin: Darwin 39 | 386: i386 40 | amd64: x86_64 41 | 42 | nfpms: 43 | - builds: 44 | - prism 45 | vendor: muesli 46 | homepage: "https://fribbledom.com/" 47 | maintainer: "Christian Muehlhaeuser " 48 | description: "An RTMP stream recaster / splitter" 49 | license: MIT 50 | formats: 51 | - apk 52 | - deb 53 | - rpm 54 | bindir: /usr/bin 55 | 56 | brews: 57 | - goarm: 6 58 | tap: 59 | owner: muesli 60 | name: homebrew-tap 61 | commit_author: 62 | name: "Christian Muehlhaeuser" 63 | email: "muesli@gmail.com" 64 | homepage: "https://fribbledom.com/" 65 | description: "Disk Usage/Free Utility" 66 | # skip_upload: true 67 | 68 | signs: 69 | - artifacts: checksum 70 | 71 | checksum: 72 | name_template: "checksums.txt" 73 | snapshot: 74 | name_template: "{{ .Tag }}-next" 75 | changelog: 76 | sort: asc 77 | filters: 78 | exclude: 79 | - "^docs:" 80 | - "^test:" 81 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-buster as frontend 2 | 3 | WORKDIR /app 4 | COPY web . 5 | RUN npm install --quiet && npm run build 6 | 7 | FROM golang:1.15-alpine AS backend 8 | 9 | RUN apk add --no-cache ca-certificates git 10 | WORKDIR /go/src/github.com/geekgonecrazy/prismplus/ 11 | COPY go.mod . 12 | COPY go.sum . 13 | RUN go mod download 14 | COPY . . 15 | RUN go build 16 | 17 | FROM alpine 18 | 19 | RUN mkdir /app 20 | WORKDIR /app 21 | 22 | COPY --from=frontend /app /app/web 23 | 24 | COPY --from=backend /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 25 | COPY --from=backend /go/src/github.com/geekgonecrazy/prismplus/prismplus /app/prismplus 26 | 27 | EXPOSE 5383 28 | EXPOSE 1935 29 | 30 | CMD ["/app/prismplus"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Christian Muehlhaeuser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # prism+ 2 | 3 | ![image](web/public/img/PrismPlus_PlatformColorsTrans_256x.png) 4 | 5 | **Use at your own risk! It has worked for us.. but very much alpha quality!** 6 | 7 | Based on [prism by muesli](https://github.com/muesli/prism). 8 | 9 | Prism+ lets you multicast your rtmp stream to multiple destinations. You point your OBS at prism+ and then add your destinations to prism+. It will relay your stream to the destinations. Allowing you to stream to multiple platforms at the same time. 10 | 11 | Prism+ main Features: 12 | * Admin web interface to login and manage streamers and sessions 13 | * Ability to add streamers with their streamkeys 14 | * Ability for streamers to login and add/remove destinations 15 | 16 | #### Verified working Destinations 17 | * Owncast 18 | * Twitch 19 | * Youtube - only seems to work with variable bitrate setting in OBS. Your results may vary 20 | 21 | ## Starting prism+ 22 | 23 | 1. Clone the repo locally and enter the folder 24 | 25 | 2. Build frontend. The frontend is written in svelte 26 | 27 | ``` 28 | cd web 29 | npm install 30 | npm run build 31 | ``` 32 | 33 | 3. Build backend. The backend is written in golang. 34 | 35 | ``` 36 | go get 37 | go build 38 | ``` 39 | 40 | 4. Run 41 | 42 | ``` 43 | ./prismplus 44 | ``` 45 | 46 | On startup it will automatically generate an adminKey. If you want to define your own adminKey so that it will be the same every startup you can start up with: 47 | 48 | ``` 49 | ./prismplus --adminKey=your-super-secure-key 50 | ``` 51 | 52 | Prism+ will now be listening on: 53 | * Web interface and API - http://localhost:5383 54 | * RTMP - localhost:1935 55 | 56 | ## Using Prism+ 57 | 58 | To add your first streamer goto: http://localhost:5383/admin 59 | 60 | Enter the adminKey you provided or that was auto generated. 61 | 62 | ![image](screenshots/admin_medium.png) 63 | 64 | Once the streamer is created they can login to: http://localhost:5383 with their streamKey 65 | 66 | From here they can add their destinations 67 | 68 | ![image](screenshots/streamer_medium.png) 69 | 70 | Now the streamer just needs to point OBS (or their software of choice) to rtmp://localhost:1935/live with the streamKey. 71 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/geekgonecrazy/prismplus/controllers" 5 | "github.com/geekgonecrazy/prismplus/sessions" 6 | "github.com/labstack/echo/v4" 7 | "github.com/labstack/echo/v4/middleware" 8 | ) 9 | 10 | func apiServer() { 11 | sessions.InitializeSessionStore() 12 | 13 | router := echo.New() 14 | 15 | router.Use(middleware.Logger()) 16 | router.Use(middleware.Recover()) 17 | router.Use(middleware.CORS()) 18 | 19 | router.Use(middleware.StaticWithConfig(middleware.StaticConfig{ 20 | Root: "web/public", 21 | Index: "index.html", 22 | HTML5: true, 23 | })) 24 | 25 | keyAuthConfig := middleware.KeyAuthConfig{KeyLookup: "header:Authorization", Validator: validateAdminKey} 26 | 27 | router.GET("/api/v1/streamers", controllers.GetStreamersHandler, middleware.KeyAuthWithConfig(keyAuthConfig)) 28 | router.POST("/api/v1/streamers", controllers.CreateStreamerHandler, middleware.KeyAuthWithConfig(keyAuthConfig)) 29 | router.GET("/api/v1/streamers/:streamer", controllers.GetStreamerHandler, middleware.KeyAuthWithConfig(keyAuthConfig)) 30 | router.DELETE("/api/v1/streamers/:streamer", controllers.DeleteStreamerHandler, middleware.KeyAuthWithConfig(keyAuthConfig)) 31 | 32 | router.GET("/api/v1/streamer", controllers.GetMyStreamerHandler) 33 | router.GET("/api/v1/streamer/destinations", controllers.GetMyStreamerDestinationsHandler) 34 | router.POST("/api/v1/streamer/destinations", controllers.CreateMyStreamerDestinationHandler) 35 | router.DELETE("/api/v1/streamer/destinations/:destination", controllers.RemoveMyStreamerDestinationHandler) 36 | 37 | router.GET("/api/v1/sessions", controllers.GetSessionsHandler, middleware.KeyAuthWithConfig(keyAuthConfig)) 38 | router.POST("/api/v1/sessions", controllers.CreateSessionHandler, middleware.KeyAuthWithConfig(keyAuthConfig)) 39 | router.GET("/api/v1/sessions/:session", controllers.GetSessionHandler) 40 | router.POST("/api/v1/sessions/:session/destinations", controllers.AddDestinationHandler) 41 | router.GET("/api/v1/sessions/:session/destinations", controllers.GetDestinationsHandler) 42 | router.DELETE("/api/v1/sessions/:session/destinations/:destination", controllers.RemoveDestinationHandler) 43 | router.DELETE("/api/v1/sessions/:session", controllers.DeleteSessionHandler) 44 | 45 | router.Logger.Fatal(router.Start(":5383")) 46 | } 47 | 48 | func validateAdminKey(key string, c echo.Context) (bool, error) { 49 | if key == *adminKey { 50 | return true, nil 51 | } 52 | 53 | return false, nil 54 | } 55 | -------------------------------------------------------------------------------- /controllers/myStreamer.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "net/http" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/geekgonecrazy/prismplus/models" 11 | "github.com/geekgonecrazy/prismplus/sessions" 12 | "github.com/geekgonecrazy/prismplus/store" 13 | "github.com/geekgonecrazy/prismplus/streamers" 14 | "github.com/labstack/echo/v4" 15 | ) 16 | 17 | func getStreamKeyFromHeader(c echo.Context) (string, bool) { 18 | authorizationHeader := c.Request().Header.Get("Authorization") 19 | 20 | split := strings.Split(authorizationHeader, " ") 21 | 22 | if len(split) < 2 { 23 | return "", false 24 | } 25 | 26 | if len(split[1]) == 0 { 27 | return "", false 28 | } 29 | 30 | return split[1], true 31 | } 32 | 33 | func GetMyStreamerHandler(c echo.Context) error { 34 | streamKey, ok := getStreamKeyFromHeader(c) 35 | if !ok { 36 | return c.NoContent(http.StatusUnauthorized) 37 | } 38 | 39 | streamer, err := streamers.GetStreamerByStreamKey(streamKey) 40 | if err != nil { 41 | if errors.Is(err, store.ErrNotFound) { 42 | return c.NoContent(http.StatusNotFound) 43 | } 44 | 45 | return c.NoContent(http.StatusInternalServerError) 46 | } 47 | 48 | myStreamer := models.MyStreamer{ 49 | Streamer: streamer, 50 | } 51 | 52 | session, _ := sessions.GetSession(streamKey) 53 | 54 | if session != nil && session.Active { 55 | myStreamer.Live = true 56 | } 57 | 58 | return c.JSON(http.StatusOK, myStreamer) 59 | } 60 | 61 | func CreateMyStreamerDestinationHandler(c echo.Context) error { 62 | streamKey, ok := getStreamKeyFromHeader(c) 63 | if !ok { 64 | return c.NoContent(http.StatusUnauthorized) 65 | } 66 | 67 | myStreamer, err := streamers.GetStreamerByStreamKey(streamKey) 68 | if err != nil { 69 | if errors.Is(err, store.ErrNotFound) { 70 | return c.NoContent(http.StatusNotFound) 71 | } 72 | 73 | return c.NoContent(http.StatusInternalServerError) 74 | } 75 | 76 | destinationPayload := models.Destination{} 77 | 78 | if err := c.Bind(&destinationPayload); err != nil { 79 | return err 80 | } 81 | 82 | if err := streamers.AddDestination(myStreamer, destinationPayload); err != nil { 83 | log.Println(err) 84 | return c.NoContent(http.StatusInternalServerError) 85 | } 86 | 87 | return c.NoContent(http.StatusCreated) 88 | } 89 | 90 | func RemoveMyStreamerDestinationHandler(c echo.Context) error { 91 | streamKey, ok := getStreamKeyFromHeader(c) 92 | if !ok { 93 | return c.NoContent(http.StatusUnauthorized) 94 | } 95 | 96 | myStreamer, err := streamers.GetStreamerByStreamKey(streamKey) 97 | if err != nil { 98 | if errors.Is(err, store.ErrNotFound) { 99 | return c.NoContent(http.StatusNotFound) 100 | } 101 | 102 | return c.NoContent(http.StatusInternalServerError) 103 | } 104 | 105 | destination := c.Param("destination") 106 | id, err := strconv.Atoi(destination) 107 | if err != nil { 108 | return c.String(http.StatusBadRequest, "Not Found") 109 | } 110 | 111 | if err := streamers.RemoveDestination(myStreamer, id); err != nil { 112 | if errors.Is(err, sessions.ErrNotFound) { 113 | return c.NoContent(http.StatusNotFound) 114 | } 115 | 116 | return c.NoContent(http.StatusInternalServerError) 117 | } 118 | 119 | return c.NoContent(http.StatusAccepted) 120 | } 121 | 122 | func GetMyStreamerDestinationsHandler(c echo.Context) error { 123 | streamKey, ok := getStreamKeyFromHeader(c) 124 | if !ok { 125 | return c.NoContent(http.StatusUnauthorized) 126 | } 127 | 128 | myStreamer, err := streamers.GetStreamerByStreamKey(streamKey) 129 | if err != nil { 130 | if errors.Is(err, store.ErrNotFound) { 131 | return c.NoContent(http.StatusNotFound) 132 | } 133 | 134 | return c.NoContent(http.StatusInternalServerError) 135 | } 136 | 137 | return c.JSON(http.StatusOK, myStreamer.Destinations) 138 | } 139 | -------------------------------------------------------------------------------- /controllers/sessions.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/geekgonecrazy/prismplus/models" 10 | "github.com/geekgonecrazy/prismplus/sessions" 11 | "github.com/labstack/echo/v4" 12 | ) 13 | 14 | func GetSessionsHandler(c echo.Context) error { 15 | return c.JSON(http.StatusOK, sessions.GetSessions()) 16 | } 17 | 18 | func CreateSessionHandler(c echo.Context) error { 19 | sessionPayload := models.SessionPayload{} 20 | 21 | if err := c.Bind(&sessionPayload); err != nil { 22 | return err 23 | } 24 | 25 | if err := sessions.CreateSession(sessionPayload); err != nil { 26 | if err.Error() == "Already Exists" { 27 | return c.NoContent(http.StatusConflict) 28 | } 29 | 30 | return c.NoContent(http.StatusInternalServerError) 31 | } 32 | 33 | return c.JSON(http.StatusCreated, sessionPayload) 34 | } 35 | 36 | func GetSessionHandler(c echo.Context) error { 37 | key := c.Param("session") 38 | 39 | session, err := sessions.GetSession(key) 40 | if err != nil { 41 | if errors.Is(err, sessions.ErrNotFound) { 42 | return c.NoContent(http.StatusNotFound) 43 | } 44 | 45 | return c.NoContent(http.StatusInternalServerError) 46 | } 47 | 48 | return c.JSON(http.StatusOK, session) 49 | } 50 | 51 | func GetDestinationsHandler(c echo.Context) error { 52 | key := c.Param("session") 53 | 54 | session, err := sessions.GetSession(key) 55 | if err != nil { 56 | if errors.Is(err, sessions.ErrNotFound) { 57 | return c.NoContent(http.StatusNotFound) 58 | } 59 | 60 | return c.NoContent(http.StatusInternalServerError) 61 | } 62 | 63 | destinations := session.GetDestinations() 64 | 65 | return c.JSON(http.StatusOK, destinations) 66 | } 67 | 68 | func AddDestinationHandler(c echo.Context) error { 69 | key := c.Param("session") 70 | 71 | session, err := sessions.GetSession(key) 72 | if err != nil { 73 | if errors.Is(err, sessions.ErrNotFound) { 74 | return c.NoContent(http.StatusNotFound) 75 | } 76 | 77 | return c.NoContent(http.StatusInternalServerError) 78 | } 79 | 80 | destinationPayload := models.Destination{} 81 | 82 | if err := c.Bind(&destinationPayload); err != nil { 83 | return err 84 | } 85 | 86 | if err := session.AddDestination(destinationPayload); err != nil { 87 | log.Println(err) 88 | return c.NoContent(http.StatusInternalServerError) 89 | } 90 | 91 | return c.NoContent(http.StatusCreated) 92 | } 93 | 94 | func RemoveDestinationHandler(c echo.Context) error { 95 | key := c.Param("session") 96 | destination := c.Param("destination") 97 | 98 | id, err := strconv.Atoi(destination) 99 | if err != nil { 100 | return c.String(http.StatusBadRequest, "Not Found") 101 | } 102 | 103 | session, err := sessions.GetSession(key) 104 | if err != nil { 105 | if errors.Is(err, sessions.ErrNotFound) { 106 | return c.NoContent(http.StatusNotFound) 107 | } 108 | 109 | return c.NoContent(http.StatusInternalServerError) 110 | } 111 | 112 | if err := session.RemoveDestination(id); err != nil { 113 | if errors.Is(err, sessions.ErrNotFound) { 114 | return c.NoContent(http.StatusNotFound) 115 | } 116 | 117 | return c.NoContent(http.StatusInternalServerError) 118 | } 119 | 120 | return c.NoContent(http.StatusAccepted) 121 | } 122 | 123 | func DeleteSessionHandler(c echo.Context) error { 124 | key := c.Param("session") 125 | 126 | session, err := sessions.GetSession(key) 127 | if err != nil { 128 | if errors.Is(err, sessions.ErrNotFound) { 129 | return c.NoContent(http.StatusNotFound) 130 | } 131 | 132 | return c.NoContent(http.StatusInternalServerError) 133 | } 134 | 135 | session.EndSession() 136 | 137 | return c.NoContent(http.StatusAccepted) 138 | } 139 | -------------------------------------------------------------------------------- /controllers/streamers.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/geekgonecrazy/prismplus/models" 10 | "github.com/geekgonecrazy/prismplus/store" 11 | "github.com/geekgonecrazy/prismplus/streamers" 12 | "github.com/labstack/echo/v4" 13 | ) 14 | 15 | func GetStreamersHandler(c echo.Context) error { 16 | s, err := streamers.GetStreamers() 17 | if err != nil { 18 | log.Println("Error:", err) 19 | return c.NoContent(http.StatusInternalServerError) 20 | } 21 | 22 | return c.JSON(http.StatusOK, s) 23 | } 24 | 25 | func CreateStreamerHandler(c echo.Context) error { 26 | streamerPayload := models.StreamerCreatePayload{} 27 | 28 | if err := c.Bind(&streamerPayload); err != nil { 29 | return err 30 | } 31 | 32 | if streamerPayload.Name == "" { 33 | return c.NoContent(http.StatusBadRequest) 34 | } 35 | 36 | streamer, err := streamers.CreateStreamer(streamerPayload) 37 | if err != nil { 38 | if err.Error() == "Already Exists" { 39 | return c.NoContent(http.StatusConflict) 40 | } 41 | 42 | log.Println("Error:", err) 43 | 44 | return c.NoContent(http.StatusInternalServerError) 45 | } 46 | 47 | return c.JSON(http.StatusCreated, streamer) 48 | } 49 | 50 | func GetStreamerHandler(c echo.Context) error { 51 | key := c.Param("streamer") 52 | 53 | id, err := strconv.Atoi(key) 54 | if err != nil { 55 | return c.String(http.StatusBadRequest, "Not Found") 56 | } 57 | 58 | streamer, err := streamers.GetStreamer(id) 59 | if err != nil { 60 | if errors.Is(err, store.ErrNotFound) { 61 | return c.NoContent(http.StatusNotFound) 62 | } 63 | 64 | return c.NoContent(http.StatusInternalServerError) 65 | } 66 | 67 | return c.JSON(http.StatusOK, streamer) 68 | } 69 | 70 | func DeleteStreamerHandler(c echo.Context) error { 71 | key := c.Param("streamer") 72 | 73 | id, err := strconv.Atoi(key) 74 | if err != nil { 75 | return c.String(http.StatusBadRequest, "Not Found") 76 | } 77 | 78 | if err := streamers.DeleteStreamer(id); err != nil { 79 | if errors.Is(err, store.ErrNotFound) { 80 | return c.NoContent(http.StatusNotFound) 81 | } 82 | 83 | return c.NoContent(http.StatusInternalServerError) 84 | } 85 | 86 | return c.NoContent(http.StatusAccepted) 87 | } 88 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/geekgonecrazy/prismplus 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/geekgonecrazy/rtmp-lib v0.0.0-20220117215435-0613a79061b3 7 | github.com/kr/pretty v0.3.0 // indirect 8 | github.com/labstack/echo/v4 v4.6.3 9 | github.com/mattn/go-colorable v0.1.12 // indirect 10 | github.com/rogpeppe/go-internal v1.8.1 // indirect 11 | go.etcd.io/bbolt v1.3.6 12 | golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce // indirect 13 | golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d // indirect 14 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect 15 | golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect 16 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/geekgonecrazy/rtmp-lib v0.0.0-20220117215435-0613a79061b3 h1:K/5mz195EELdOx+O7M2IW4gD1gDLM5rg2X1ab/p2Umk= 6 | github.com/geekgonecrazy/rtmp-lib v0.0.0-20220117215435-0613a79061b3/go.mod h1:8S42yR8Bh2uOOhbkA9w8zIMBY7Aq6NfpVdXh53m3bKw= 7 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 8 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 9 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 10 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 11 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 12 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 13 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 14 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 15 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 16 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 17 | github.com/labstack/echo/v4 v4.6.3 h1:VhPuIZYxsbPmo4m9KAkMU/el2442eB7EBFFhNTTT9ac= 18 | github.com/labstack/echo/v4 v4.6.3/go.mod h1:Hk5OiHj0kDqmFq7aHe7eDqI7CUhuCrfpupQtLGGLm7A= 19 | github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o= 20 | github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= 21 | github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 22 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 23 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 24 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 25 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 26 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 27 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 28 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 29 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 30 | github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= 31 | github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= 32 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 33 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 34 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 35 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 36 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 37 | github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= 38 | github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 39 | go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= 40 | go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= 41 | golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 42 | golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce h1:Roh6XWxHFKrPgC/EQhVubSAGQ6Ozk6IdxHSzt1mR0EI= 43 | golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 44 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 45 | golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 46 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 47 | golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d h1:1n1fc535VhN8SYtD4cDUyNlfpAF2ROMM9+11equK3hs= 48 | golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 49 | golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 50 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 51 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 52 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 53 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 54 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 55 | golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 56 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= 57 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 58 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 59 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 60 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 61 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 62 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 63 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 64 | golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M= 65 | golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 66 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 67 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 68 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 69 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 70 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 71 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 72 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 73 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 74 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 75 | -------------------------------------------------------------------------------- /helpers/newUUID.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // NewUUID generates a random UUID according to the RFC 4122, https://play.golang.org/p/4FkNSiUDMg 10 | func NewUUID() (string, error) { 11 | uuid := make([]byte, 16) 12 | n, err := io.ReadFull(rand.Reader, uuid) 13 | 14 | if n != len(uuid) || err != nil { 15 | return "", err 16 | } 17 | 18 | // variant bits; see section 4.1.1 19 | uuid[8] = uuid[8]&^0xc0 | 0x80 20 | // version 4 (pseudo-random); see section 4.1.3 21 | uuid[6] = uuid[6]&^0xf0 | 0x40 22 | return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]), nil 23 | } 24 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "log" 9 | "os" 10 | 11 | // TODO: switch to joy5? 12 | 13 | "github.com/geekgonecrazy/prismplus/helpers" 14 | "github.com/geekgonecrazy/prismplus/streamers" 15 | rtmp "github.com/geekgonecrazy/rtmp-lib" 16 | ) 17 | 18 | var ( 19 | bind = flag.String("bind", ":1935", "bind address") 20 | adminKey = flag.String("adminKey", "", "Admin key. If none passed one will be created") 21 | dataPath = flag.String("dataPath", "", "Path for data") 22 | ) 23 | 24 | func main() { 25 | flag.Parse() 26 | 27 | if *adminKey == "" { 28 | uuid, err := helpers.NewUUID() 29 | if err != nil { 30 | fmt.Println("Can't generate admin authorization key:", err) 31 | os.Exit(1) 32 | } 33 | 34 | *adminKey = uuid 35 | 36 | log.Println("Admin Authorization Key Generated:", *adminKey) 37 | } 38 | 39 | streamers.Setup(*dataPath) 40 | 41 | fmt.Println("Starting RTMP server...") 42 | config := &rtmp.Config{ 43 | ChunkSize: 128, 44 | BufferSize: 0, 45 | } 46 | 47 | server := rtmp.NewServer(config) 48 | server.Addr = *bind 49 | 50 | server.HandlePublish = rtmpConnectionHandler 51 | 52 | go apiServer() 53 | 54 | fmt.Println("Waiting for incoming connection...") 55 | err := server.ListenAndServe() 56 | if err != nil { 57 | fmt.Println(err) 58 | os.Exit(1) 59 | } 60 | } 61 | 62 | // newUUID generates a random UUID according to the RFC 4122, https://play.golang.org/p/4FkNSiUDMg 63 | func newUUID() (string, error) { 64 | uuid := make([]byte, 16) 65 | n, err := io.ReadFull(rand.Reader, uuid) 66 | 67 | if n != len(uuid) || err != nil { 68 | return "", err 69 | } 70 | 71 | // variant bits; see section 4.1.1 72 | uuid[8] = uuid[8]&^0xc0 | 0x80 73 | // version 4 (pseudo-random); see section 4.1.3 74 | uuid[6] = uuid[6]&^0xf0 | 0x40 75 | return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]), nil 76 | } 77 | -------------------------------------------------------------------------------- /models/sessions.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type SessionPayload struct { 4 | StreamerID int `json:"streamerId"` 5 | Key string `json:"key"` 6 | Destinations []Destination `json:"destinations"` 7 | } 8 | -------------------------------------------------------------------------------- /models/streamer.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type StreamerCreatePayload struct { 6 | Name string `json:"name"` 7 | StreamKey string `json:"streamKey"` 8 | } 9 | 10 | type Streamer struct { 11 | ID int `json:"id"` 12 | Name string `json:"name"` 13 | StreamKey string `json:"streamKey"` 14 | 15 | NextDestinationID int `json:"nextDestinationId"` 16 | Destinations []Destination `json:"destinations"` 17 | 18 | CreatedAt time.Time `json:"createdAt"` 19 | UpdatedAt time.Time `json:"updatedAt"` 20 | } 21 | 22 | type Destination struct { 23 | ID int `json:"id"` 24 | Name string `json:"name"` 25 | Server string `json:"server"` 26 | Key string `json:"key"` 27 | } 28 | 29 | type MyStreamer struct { 30 | Streamer 31 | Live bool `json:"live"` 32 | } 33 | -------------------------------------------------------------------------------- /rtmp/RTMPConnection.go: -------------------------------------------------------------------------------- 1 | package rtmp 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | rtmp "github.com/geekgonecrazy/rtmp-lib" 8 | "github.com/geekgonecrazy/rtmp-lib/av" 9 | ) 10 | 11 | /* 12 | 13 | This is all almost as is from original prism 14 | 15 | */ 16 | 17 | type RTMPConnection struct { 18 | url string 19 | conn *rtmp.Conn 20 | 21 | header []av.CodecData 22 | packets chan av.Packet 23 | } 24 | 25 | func NewRTMPConnection(u string) *RTMPConnection { 26 | r := &RTMPConnection{ 27 | url: u, 28 | } 29 | r.reset() 30 | 31 | return r 32 | } 33 | 34 | func (r *RTMPConnection) reset() { 35 | r.packets = make(chan av.Packet, 2) 36 | r.conn = nil 37 | r.header = nil 38 | } 39 | 40 | func (r *RTMPConnection) Dial() error { 41 | c, err := rtmp.Dial(r.url) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | if len(r.header) > 0 { 47 | err = c.WriteHeader(r.header) 48 | if err != nil { 49 | fmt.Println("can't write header:", err) 50 | return err 51 | } 52 | } 53 | 54 | fmt.Println("connection established:", r.url) 55 | r.conn = c 56 | return nil 57 | } 58 | 59 | func (r *RTMPConnection) Disconnect() error { 60 | if r.conn != nil { 61 | err := r.conn.Close() 62 | if err != nil { 63 | return err 64 | } 65 | } 66 | 67 | close(r.packets) 68 | r.reset() 69 | 70 | fmt.Println("connection closed:", r.url) 71 | return nil 72 | } 73 | 74 | func (r *RTMPConnection) WriteHeader(h []av.CodecData) error { 75 | r.header = h 76 | if r.conn == nil { 77 | return r.Dial() 78 | } 79 | 80 | return r.conn.WriteHeader(h) 81 | } 82 | 83 | func (r *RTMPConnection) WritePacket(p av.Packet) { 84 | if r.conn == nil { 85 | return 86 | } 87 | 88 | defer func() { 89 | // recover from panic caused by writing to a closed channel 90 | if r := recover(); r != nil { 91 | err := fmt.Errorf("%v", r) 92 | fmt.Printf("write: error writing on rtmp channel: %v\n", err) 93 | return 94 | } 95 | }() 96 | 97 | r.packets <- p 98 | } 99 | 100 | func (r *RTMPConnection) Loop() error { 101 | defer func() { 102 | // recover from panic caused by trying to operate on closed socket or channel 103 | if r := recover(); r != nil { 104 | err := fmt.Errorf("%v", r) 105 | fmt.Printf("write: error writing on rtmp channel: %v\n", err) 106 | return 107 | } 108 | }() 109 | 110 | for p := range r.packets { 111 | if err := r.conn.WritePacket(p); err != nil { 112 | r.conn = nil 113 | fmt.Println(err) 114 | 115 | for { 116 | time.Sleep(time.Second) 117 | 118 | err := r.Dial() 119 | if err != nil { 120 | fmt.Println("can't re-connect:", err) 121 | continue 122 | } 123 | 124 | // successful re-connect 125 | break 126 | } 127 | } 128 | } 129 | 130 | return nil 131 | } 132 | -------------------------------------------------------------------------------- /rtmpConnectionHandler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strings" 9 | "time" 10 | 11 | "github.com/geekgonecrazy/prismplus/sessions" 12 | "github.com/geekgonecrazy/prismplus/streamers" 13 | rtmp "github.com/geekgonecrazy/rtmp-lib" 14 | ) 15 | 16 | func rtmpConnectionHandler(conn *rtmp.Conn) { 17 | urlSegments := strings.Split(conn.URL.Path, "/") 18 | key := urlSegments[len(urlSegments)-1:][0] 19 | 20 | fmt.Println("Incoming rtmp connection", key) 21 | 22 | // TODO: This could probably be more efficient 23 | session, err := sessions.GetSession(key) 24 | if err != nil { 25 | if errors.Is(err, sessions.ErrNotFound) { 26 | streamer, err := streamers.GetStreamerByStreamKey(key) 27 | if err == nil { 28 | session, _ = sessions.CreateSessionFromStreamer(streamer) 29 | } 30 | } 31 | } 32 | 33 | if session == nil { 34 | conn.Close() 35 | return 36 | } 37 | 38 | streams, err := conn.Streams() 39 | if err != nil { 40 | fmt.Println("can't retrieve streams:", err) 41 | os.Exit(1) 42 | } 43 | 44 | // Mark session as active and stash headers for replay on new destinations 45 | session.ChangeState(true) // Mark active 46 | session.SetHeaders(streams) 47 | 48 | log.Println("RTMP connection now active for session", key) 49 | 50 | for _, destination := range session.Destinations { 51 | if err := destination.RTMP.WriteHeader(streams); err != nil { 52 | fmt.Println("can't write header to destination stream:", err) 53 | // os.Exit(1) 54 | } 55 | go destination.RTMP.Loop() 56 | } 57 | 58 | lastTime := time.Now() 59 | for { 60 | if session.End { 61 | fmt.Printf("Ending session %s\n", key) 62 | break 63 | } 64 | 65 | packet, err := conn.ReadPacket() 66 | if err != nil { 67 | fmt.Println("can't read packet:", err) 68 | break 69 | } 70 | 71 | if time.Since(lastTime) > time.Second { 72 | fmt.Println("Duration:", packet.Time) 73 | lastTime = time.Now() 74 | } 75 | 76 | for _, destination := range session.Destinations { 77 | destination.RTMP.WritePacket(packet) 78 | } 79 | } 80 | 81 | session.ChangeState(false) // Mark inactive 82 | 83 | for _, destination := range session.Destinations { 84 | err := destination.RTMP.Disconnect() 85 | if err != nil { 86 | fmt.Println(err) 87 | os.Exit(1) 88 | } 89 | } 90 | 91 | if session.End { 92 | fmt.Printf("Session %s ended\n", key) 93 | // Make sure we are closed 94 | if err := conn.Close(); err != nil { 95 | log.Println(err) 96 | } 97 | 98 | if err := sessions.DeleteSession(key); err != nil { 99 | log.Println(err) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /screenshots/admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekgonecrazy/prismplus/511285ab1fa31f078d13a211fef760ebdf2869c5/screenshots/admin.png -------------------------------------------------------------------------------- /screenshots/admin_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekgonecrazy/prismplus/511285ab1fa31f078d13a211fef760ebdf2869c5/screenshots/admin_medium.png -------------------------------------------------------------------------------- /screenshots/admin_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekgonecrazy/prismplus/511285ab1fa31f078d13a211fef760ebdf2869c5/screenshots/admin_small.png -------------------------------------------------------------------------------- /screenshots/streamer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekgonecrazy/prismplus/511285ab1fa31f078d13a211fef760ebdf2869c5/screenshots/streamer.png -------------------------------------------------------------------------------- /screenshots/streamer_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekgonecrazy/prismplus/511285ab1fa31f078d13a211fef760ebdf2869c5/screenshots/streamer_medium.png -------------------------------------------------------------------------------- /screenshots/streamer_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekgonecrazy/prismplus/511285ab1fa31f078d13a211fef760ebdf2869c5/screenshots/streamer_small.png -------------------------------------------------------------------------------- /sessions/session.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "strings" 8 | "sync" 9 | 10 | "github.com/geekgonecrazy/prismplus/models" 11 | "github.com/geekgonecrazy/prismplus/rtmp" 12 | "github.com/geekgonecrazy/rtmp-lib/av" 13 | ) 14 | 15 | var ( 16 | _sessions map[string]*Session 17 | ErrNotFound = errors.New("not found") 18 | ) 19 | 20 | type Session struct { 21 | StreamerID int `json:"streamerId"` 22 | Key string `json:"key"` 23 | Destinations map[int]*Destination `json:"destinations"` 24 | NextDestinationID int `json:"nextDestinationId"` 25 | Active bool `json:"active"` 26 | End bool `json:"end"` 27 | StreamHeaders []av.CodecData `json:"streamHeaders"` 28 | _lock sync.Mutex // Might need if we allow modify 29 | } 30 | 31 | type Destination struct { 32 | ID int `json:"id"` 33 | Name string `json:"name"` 34 | Server string `json:"server"` 35 | Key string `json:"key"` 36 | RTMP *rtmp.RTMPConnection 37 | } 38 | 39 | func (s *Session) AddDestination(destinationPayload models.Destination) error { 40 | 41 | destinationPayload.Server = strings.TrimRight(destinationPayload.Server, "/") 42 | 43 | url := fmt.Sprintf("%s/%s", destinationPayload.Server, destinationPayload.Key) 44 | 45 | conn := rtmp.NewRTMPConnection(url) 46 | 47 | // If streamerID is 0 then we need to track the IDs 48 | if s.StreamerID == 0 { 49 | destinationPayload.ID = s.NextDestinationID 50 | s.NextDestinationID++ 51 | } 52 | 53 | s.Destinations[destinationPayload.ID] = &Destination{ 54 | ID: destinationPayload.ID, 55 | Name: destinationPayload.Name, 56 | Server: destinationPayload.Server, 57 | Key: destinationPayload.Key, 58 | RTMP: conn, 59 | } 60 | 61 | if s.Active { 62 | if err := conn.WriteHeader(s.StreamHeaders); err != nil { 63 | fmt.Println("can't write header:", err) 64 | // os.Exit(1) 65 | } 66 | 67 | go conn.Loop() 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func (s *Session) GetDestinations() []Destination { 74 | destinations := []Destination{} 75 | for _, destination := range s.Destinations { 76 | destinations = append(destinations, *destination) 77 | } 78 | 79 | return destinations 80 | } 81 | 82 | func (s *Session) GetDestination(id int) (*Destination, error) { 83 | if s.Destinations[id] == nil { 84 | return nil, ErrNotFound 85 | } 86 | 87 | return s.Destinations[id], nil 88 | } 89 | 90 | func (s *Session) RemoveDestination(id int) error { 91 | destination, err := s.GetDestination(id) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | if err := destination.RTMP.Disconnect(); err != nil { 97 | log.Println(err) 98 | } 99 | 100 | delete(s.Destinations, id) 101 | 102 | return nil 103 | } 104 | 105 | func (s *Session) ChangeState(active bool) { 106 | s.Active = active 107 | } 108 | 109 | func (s *Session) SetHeaders(streams []av.CodecData) { 110 | s.StreamHeaders = streams 111 | } 112 | 113 | func (s *Session) EndSession() { 114 | s.End = true 115 | } 116 | 117 | func InitializeSessionStore() { 118 | _sessions = make(map[string]*Session) 119 | } 120 | 121 | func CreateSession(sessionPayload models.SessionPayload) error { 122 | 123 | existingSession, err := GetSession(sessionPayload.Key) 124 | if err != nil && err != ErrNotFound { 125 | return err 126 | } 127 | 128 | if existingSession != nil { 129 | return errors.New("Already Exists") 130 | } 131 | 132 | session := &Session{ 133 | StreamerID: sessionPayload.StreamerID, 134 | Key: sessionPayload.Key, 135 | Destinations: map[int]*Destination{}, 136 | NextDestinationID: 0, 137 | Active: false, 138 | End: false, 139 | } 140 | 141 | _sessions[sessionPayload.Key] = session 142 | 143 | for _, destination := range sessionPayload.Destinations { 144 | session.AddDestination(destination) 145 | } 146 | 147 | return nil 148 | } 149 | 150 | func CreateSessionFromStreamer(streamer models.Streamer) (*Session, error) { 151 | log.Println("Creating session from streamer", streamer.Name) 152 | 153 | sessionPayload := models.SessionPayload{ 154 | StreamerID: streamer.ID, 155 | Key: streamer.StreamKey, 156 | Destinations: streamer.Destinations, 157 | } 158 | 159 | if err := CreateSession(sessionPayload); err != nil { 160 | return nil, err 161 | } 162 | 163 | return GetSession(streamer.StreamKey) 164 | } 165 | 166 | func GetSessions() []Session { 167 | sessions := []Session{} 168 | for _, session := range _sessions { 169 | sessions = append(sessions, *session) 170 | } 171 | 172 | return sessions 173 | } 174 | 175 | func GetSession(key string) (*Session, error) { 176 | if _sessions[key] == nil { 177 | return nil, ErrNotFound 178 | } 179 | 180 | return _sessions[key], nil 181 | } 182 | 183 | func DeleteSession(key string) error { 184 | if _sessions[key] == nil { 185 | return ErrNotFound 186 | } 187 | 188 | delete(_sessions, key) 189 | 190 | return nil 191 | } 192 | -------------------------------------------------------------------------------- /store/boltstore/datastore.go: -------------------------------------------------------------------------------- 1 | package boltstore 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/geekgonecrazy/prismplus/store" 9 | bolt "go.etcd.io/bbolt" 10 | ) 11 | 12 | type boltStore struct { 13 | *bolt.DB 14 | } 15 | 16 | var ( 17 | streamersBucket = []byte("streamers") 18 | ) 19 | 20 | //New creates a new bolt store 21 | func New(dataPath string) (store.Store, error) { 22 | db, err := bolt.Open(fmt.Sprintf("%s%s", dataPath, "data.bbolt"), 0600, &bolt.Options{Timeout: 15 * time.Second}) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | tx, err := db.Begin(true) 28 | if err != nil { 29 | return nil, err 30 | } 31 | defer tx.Rollback() 32 | 33 | if _, err := tx.CreateBucketIfNotExists(streamersBucket); err != nil { 34 | return nil, err 35 | } 36 | 37 | if err := tx.Commit(); err != nil { 38 | return nil, err 39 | } 40 | 41 | return &boltStore{db}, nil 42 | } 43 | 44 | func (s *boltStore) CheckDb() error { 45 | tx, err := s.Begin(false) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | return tx.Rollback() 51 | } 52 | 53 | //itob returns an 8-byte big endian representation of v. 54 | func itob(v int) []byte { 55 | b := make([]byte, 8) 56 | binary.BigEndian.PutUint64(b, uint64(v)) 57 | return b 58 | } 59 | -------------------------------------------------------------------------------- /store/boltstore/streamers.go: -------------------------------------------------------------------------------- 1 | package boltstore 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "time" 7 | 8 | "github.com/geekgonecrazy/prismplus/models" 9 | "github.com/geekgonecrazy/prismplus/store" 10 | bolt "go.etcd.io/bbolt" 11 | ) 12 | 13 | func (s *boltStore) GetStreamers() ([]models.Streamer, error) { 14 | tx, err := s.Begin(false) 15 | if err != nil { 16 | return nil, err 17 | } 18 | defer tx.Rollback() 19 | 20 | cursor := tx.Bucket(streamersBucket).Cursor() 21 | 22 | streamers := make([]models.Streamer, 0) 23 | for k, data := cursor.First(); k != nil; k, data = cursor.Next() { 24 | var i models.Streamer 25 | if err := json.Unmarshal(data, &i); err != nil { 26 | return nil, err 27 | } 28 | 29 | streamers = append(streamers, i) 30 | } 31 | 32 | return streamers, nil 33 | } 34 | 35 | func (s *boltStore) GetStreamerByID(id int) (streamer models.Streamer, err error) { 36 | tx, err := s.Begin(false) 37 | if err != nil { 38 | return streamer, err 39 | } 40 | defer tx.Rollback() 41 | 42 | bytes := tx.Bucket(streamersBucket).Get(itob(id)) 43 | if bytes == nil { 44 | return streamer, store.ErrNotFound 45 | } 46 | 47 | var i models.Streamer 48 | if err := json.Unmarshal(bytes, &i); err != nil { 49 | return streamer, err 50 | } 51 | 52 | return i, nil 53 | } 54 | 55 | func (s *boltStore) GetStreamerByStreamKey(key string) (streamer models.Streamer, err error) { 56 | tx, err := s.Begin(false) 57 | if err != nil { 58 | return streamer, err 59 | } 60 | defer tx.Rollback() 61 | 62 | cursor := tx.Bucket(streamersBucket).Cursor() 63 | 64 | for k, data := cursor.First(); k != nil; k, data = cursor.Next() { 65 | var i models.Streamer 66 | if err := json.Unmarshal(data, &i); err != nil { 67 | return streamer, err 68 | } 69 | 70 | if i.StreamKey == key { 71 | return i, nil 72 | } 73 | } 74 | 75 | return streamer, store.ErrNotFound 76 | } 77 | 78 | func (s *boltStore) CreateStreamer(streamer *models.Streamer) error { 79 | tx, err := s.Begin(true) 80 | if err != nil { 81 | return err 82 | } 83 | defer tx.Rollback() 84 | 85 | bucket := tx.Bucket(streamersBucket) 86 | 87 | seq, _ := bucket.NextSequence() 88 | streamer.ID = int(seq) 89 | streamer.CreatedAt = time.Now() 90 | streamer.UpdatedAt = time.Now() 91 | 92 | buf, err := json.Marshal(streamer) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | if err := bucket.Put(itob(streamer.ID), buf); err != nil { 98 | return err 99 | } 100 | 101 | return tx.Commit() 102 | } 103 | 104 | func (s *boltStore) UpdateStreamer(streamer *models.Streamer) error { 105 | if streamer.ID <= 0 { 106 | return errors.New("invalid service id") 107 | } 108 | 109 | tx, err := s.Begin(true) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | defer tx.Rollback() 115 | 116 | bucket := tx.Bucket(streamersBucket) 117 | 118 | streamer.UpdatedAt = time.Now() 119 | 120 | buf, err := json.Marshal(streamer) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | if err := bucket.Put(itob(streamer.ID), buf); err != nil { 126 | return err 127 | } 128 | 129 | return tx.Commit() 130 | } 131 | 132 | func (s *boltStore) DeleteStreamer(id int) error { 133 | return s.Update(func(tx *bolt.Tx) error { 134 | return tx.Bucket(streamersBucket).Delete(itob(id)) 135 | }) 136 | } 137 | -------------------------------------------------------------------------------- /store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/geekgonecrazy/prismplus/models" 7 | ) 8 | 9 | //Store is an interface that the storage implementers should implement 10 | type Store interface { 11 | CreateStreamer(streamer *models.Streamer) error 12 | GetStreamers() ([]models.Streamer, error) 13 | GetStreamerByID(id int) (models.Streamer, error) 14 | GetStreamerByStreamKey(key string) (models.Streamer, error) 15 | UpdateStreamer(streamer *models.Streamer) error 16 | DeleteStreamer(id int) error 17 | 18 | CheckDb() error 19 | } 20 | 21 | var ErrNotFound = errors.New("record not found") 22 | -------------------------------------------------------------------------------- /streamers/streamers.go: -------------------------------------------------------------------------------- 1 | package streamers 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/geekgonecrazy/prismplus/helpers" 8 | "github.com/geekgonecrazy/prismplus/models" 9 | "github.com/geekgonecrazy/prismplus/sessions" 10 | "github.com/geekgonecrazy/prismplus/store" 11 | "github.com/geekgonecrazy/prismplus/store/boltstore" 12 | ) 13 | 14 | var _dataStore store.Store 15 | 16 | func Setup(dataPath string) { 17 | if dataPath == "" { 18 | dataPath = "./" 19 | } 20 | 21 | store, err := boltstore.New(dataPath) 22 | if err != nil { 23 | log.Fatalln(err) 24 | } 25 | 26 | _dataStore = store 27 | } 28 | 29 | func GetStreamers() ([]models.Streamer, error) { 30 | streamers, err := _dataStore.GetStreamers() 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | return streamers, nil 36 | } 37 | 38 | func CreateStreamer(streamerPayload models.StreamerCreatePayload) (*models.Streamer, error) { 39 | // If no StreamKey is provided generate one 40 | if streamerPayload.StreamKey == "" { 41 | uuid, err := helpers.NewUUID() 42 | if err != nil { 43 | fmt.Println("Can't generate streamer key:", err) 44 | return nil, err 45 | } 46 | 47 | streamerPayload.StreamKey = uuid 48 | } 49 | 50 | streamer := models.Streamer{ 51 | Name: streamerPayload.Name, 52 | StreamKey: streamerPayload.StreamKey, 53 | Destinations: []models.Destination{}, 54 | 55 | NextDestinationID: 1, 56 | } 57 | 58 | if err := _dataStore.CreateStreamer(&streamer); err != nil { 59 | return nil, err 60 | } 61 | 62 | return &streamer, nil 63 | } 64 | 65 | func GetStreamer(id int) (models.Streamer, error) { 66 | streamer, err := _dataStore.GetStreamerByID(id) 67 | if err != nil { 68 | return streamer, err 69 | } 70 | 71 | //TODO: Maybe for privacy we should translate to an object that doesn't have destination creds in it? 72 | 73 | return streamer, nil 74 | } 75 | 76 | func UpdateStreamer(streamer models.Streamer) error { 77 | 78 | return nil 79 | } 80 | 81 | func DeleteStreamer(id int) error { 82 | streamer, err := _dataStore.GetStreamerByID(id) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | if err := _dataStore.DeleteStreamer(id); err != nil { 88 | return err 89 | } 90 | 91 | session, _ := sessions.GetSession(streamer.StreamKey) 92 | if session == nil { 93 | return nil 94 | } 95 | 96 | if err := sessions.DeleteSession(session.Key); err != nil { 97 | return err 98 | } 99 | 100 | return nil 101 | } 102 | 103 | func GetStreamerByStreamKey(streamKey string) (models.Streamer, error) { 104 | streamer, err := _dataStore.GetStreamerByStreamKey(streamKey) 105 | if err != nil { 106 | return streamer, err 107 | } 108 | 109 | return streamer, nil 110 | } 111 | 112 | func AddDestination(streamer models.Streamer, destination models.Destination) error { 113 | 114 | destination.ID = streamer.NextDestinationID 115 | streamer.NextDestinationID++ 116 | 117 | streamer.Destinations = append(streamer.Destinations, destination) 118 | 119 | if err := _dataStore.UpdateStreamer(&streamer); err != nil { 120 | return err 121 | } 122 | 123 | session, _ := sessions.GetSession(streamer.StreamKey) 124 | if session == nil { 125 | return nil 126 | } 127 | 128 | if err := session.AddDestination(destination); err != nil { 129 | return err 130 | } 131 | 132 | return nil 133 | } 134 | 135 | func RemoveDestination(streamer models.Streamer, id int) error { 136 | // This is kind of ugly.. 137 | newDestinations := []models.Destination{} 138 | 139 | for _, destination := range streamer.Destinations { 140 | if destination.ID != id { 141 | newDestinations = append(newDestinations, destination) 142 | } 143 | } 144 | 145 | if len(streamer.Destinations) == len(newDestinations) { 146 | return store.ErrNotFound 147 | } 148 | 149 | streamer.Destinations = newDestinations 150 | 151 | if err := _dataStore.UpdateStreamer(&streamer); err != nil { 152 | return err 153 | } 154 | 155 | session, _ := sessions.GetSession(streamer.StreamKey) 156 | if session == nil { 157 | return nil 158 | } 159 | 160 | if err := session.RemoveDestination(id); err != nil { 161 | return err 162 | } 163 | 164 | return nil 165 | } 166 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /public/build/ 3 | 4 | /.vscode/ 5 | 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | *Psst — looking for a more complete solution? Check out [SvelteKit](https://kit.svelte.dev), the official framework for building web applications of all sizes, with a beautiful development experience and flexible filesystem-based routing.* 2 | 3 | *Looking for a shareable component template instead? You can [use SvelteKit for that as well](https://kit.svelte.dev/docs#packaging) or the older [sveltejs/component-template](https://github.com/sveltejs/component-template)* 4 | 5 | --- 6 | 7 | # svelte app 8 | 9 | This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template. 10 | 11 | To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit): 12 | 13 | ```bash 14 | npx degit sveltejs/template svelte-app 15 | cd svelte-app 16 | ``` 17 | 18 | *Note that you will need to have [Node.js](https://nodejs.org) installed.* 19 | 20 | 21 | ## Get started 22 | 23 | Install the dependencies... 24 | 25 | ```bash 26 | cd svelte-app 27 | npm install 28 | ``` 29 | 30 | ...then start [Rollup](https://rollupjs.org): 31 | 32 | ```bash 33 | npm run dev 34 | ``` 35 | 36 | Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes. 37 | 38 | By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`. 39 | 40 | If you're using [Visual Studio Code](https://code.visualstudio.com/) we recommend installing the official extension [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). If you are using other editors you may need to install a plugin in order to get syntax highlighting and intellisense. 41 | 42 | ## Building and running in production mode 43 | 44 | To create an optimised version of the app: 45 | 46 | ```bash 47 | npm run build 48 | ``` 49 | 50 | You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com). 51 | 52 | 53 | ## Single-page app mode 54 | 55 | By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere. 56 | 57 | If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json: 58 | 59 | ```js 60 | "start": "sirv public --single" 61 | ``` 62 | 63 | ## Using TypeScript 64 | 65 | This template comes with a script to set up a TypeScript development environment, you can run it immediately after cloning the template with: 66 | 67 | ```bash 68 | node scripts/setupTypeScript.js 69 | ``` 70 | 71 | Or remove the script via: 72 | 73 | ```bash 74 | rm scripts/setupTypeScript.js 75 | ``` 76 | 77 | If you want to use `baseUrl` or `path` aliases within your `tsconfig`, you need to set up `@rollup/plugin-alias` to tell Rollup to resolve the aliases. For more info, see [this StackOverflow question](https://stackoverflow.com/questions/63427935/setup-tsconfig-path-in-svelte). 78 | 79 | ## Deploying to the web 80 | 81 | ### With [Vercel](https://vercel.com) 82 | 83 | Install `vercel` if you haven't already: 84 | 85 | ```bash 86 | npm install -g vercel 87 | ``` 88 | 89 | Then, from within your project folder: 90 | 91 | ```bash 92 | cd public 93 | vercel deploy --name my-project 94 | ``` 95 | 96 | ### With [surge](https://surge.sh/) 97 | 98 | Install `surge` if you haven't already: 99 | 100 | ```bash 101 | npm install -g surge 102 | ``` 103 | 104 | Then, from within your project folder: 105 | 106 | ```bash 107 | npm run build 108 | surge public my-project.surge.sh 109 | ``` 110 | -------------------------------------------------------------------------------- /web/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-app", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/code-frame": { 8 | "version": "7.16.7", 9 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", 10 | "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", 11 | "dev": true, 12 | "requires": { 13 | "@babel/highlight": "^7.16.7" 14 | } 15 | }, 16 | "@babel/helper-validator-identifier": { 17 | "version": "7.16.7", 18 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", 19 | "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", 20 | "dev": true 21 | }, 22 | "@babel/highlight": { 23 | "version": "7.16.7", 24 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.7.tgz", 25 | "integrity": "sha512-aKpPMfLvGO3Q97V0qhw/V2SWNWlwfJknuwAunU7wZLSfrM4xTBvg7E5opUVi1kJTBKihE38CPg4nBiqX83PWYw==", 26 | "dev": true, 27 | "requires": { 28 | "@babel/helper-validator-identifier": "^7.16.7", 29 | "chalk": "^2.0.0", 30 | "js-tokens": "^4.0.0" 31 | } 32 | }, 33 | "@nodelib/fs.scandir": { 34 | "version": "2.1.5", 35 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", 36 | "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 37 | "dev": true, 38 | "requires": { 39 | "@nodelib/fs.stat": "2.0.5", 40 | "run-parallel": "^1.1.9" 41 | } 42 | }, 43 | "@nodelib/fs.stat": { 44 | "version": "2.0.5", 45 | "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", 46 | "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", 47 | "dev": true 48 | }, 49 | "@nodelib/fs.walk": { 50 | "version": "1.2.8", 51 | "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", 52 | "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 53 | "dev": true, 54 | "requires": { 55 | "@nodelib/fs.scandir": "2.1.5", 56 | "fastq": "^1.6.0" 57 | } 58 | }, 59 | "@polka/url": { 60 | "version": "1.0.0-next.21", 61 | "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", 62 | "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==" 63 | }, 64 | "@rollup/plugin-commonjs": { 65 | "version": "17.1.0", 66 | "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-17.1.0.tgz", 67 | "integrity": "sha512-PoMdXCw0ZyvjpCMT5aV4nkL0QywxP29sODQsSGeDpr/oI49Qq9tRtAsb/LbYbDzFlOydVEqHmmZWFtXJEAX9ew==", 68 | "dev": true, 69 | "requires": { 70 | "@rollup/pluginutils": "^3.1.0", 71 | "commondir": "^1.0.1", 72 | "estree-walker": "^2.0.1", 73 | "glob": "^7.1.6", 74 | "is-reference": "^1.2.1", 75 | "magic-string": "^0.25.7", 76 | "resolve": "^1.17.0" 77 | } 78 | }, 79 | "@rollup/plugin-node-resolve": { 80 | "version": "11.2.1", 81 | "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", 82 | "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", 83 | "dev": true, 84 | "requires": { 85 | "@rollup/pluginutils": "^3.1.0", 86 | "@types/resolve": "1.17.1", 87 | "builtin-modules": "^3.1.0", 88 | "deepmerge": "^4.2.2", 89 | "is-module": "^1.0.0", 90 | "resolve": "^1.19.0" 91 | } 92 | }, 93 | "@rollup/plugin-typescript": { 94 | "version": "8.3.0", 95 | "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.3.0.tgz", 96 | "integrity": "sha512-I5FpSvLbtAdwJ+naznv+B4sjXZUcIvLLceYpITAn7wAP8W0wqc5noLdGIp9HGVntNhRWXctwPYrSSFQxtl0FPA==", 97 | "dev": true, 98 | "requires": { 99 | "@rollup/pluginutils": "^3.1.0", 100 | "resolve": "^1.17.0" 101 | } 102 | }, 103 | "@rollup/pluginutils": { 104 | "version": "3.1.0", 105 | "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", 106 | "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", 107 | "dev": true, 108 | "requires": { 109 | "@types/estree": "0.0.39", 110 | "estree-walker": "^1.0.1", 111 | "picomatch": "^2.2.2" 112 | }, 113 | "dependencies": { 114 | "estree-walker": { 115 | "version": "1.0.1", 116 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", 117 | "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", 118 | "dev": true 119 | } 120 | } 121 | }, 122 | "@tsconfig/svelte": { 123 | "version": "2.0.1", 124 | "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-2.0.1.tgz", 125 | "integrity": "sha512-aqkICXbM1oX5FfgZd2qSSAGdyo/NRxjWCamxoyi3T8iVQnzGge19HhDYzZ6NrVOW7bhcWNSq9XexWFtMzbB24A==", 126 | "dev": true 127 | }, 128 | "@types/estree": { 129 | "version": "0.0.39", 130 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", 131 | "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", 132 | "dev": true 133 | }, 134 | "@types/node": { 135 | "version": "17.0.8", 136 | "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz", 137 | "integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==", 138 | "dev": true 139 | }, 140 | "@types/pug": { 141 | "version": "2.0.6", 142 | "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.6.tgz", 143 | "integrity": "sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==", 144 | "dev": true 145 | }, 146 | "@types/resolve": { 147 | "version": "1.17.1", 148 | "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", 149 | "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", 150 | "dev": true, 151 | "requires": { 152 | "@types/node": "*" 153 | } 154 | }, 155 | "@types/sass": { 156 | "version": "1.43.1", 157 | "resolved": "https://registry.npmjs.org/@types/sass/-/sass-1.43.1.tgz", 158 | "integrity": "sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g==", 159 | "dev": true, 160 | "requires": { 161 | "@types/node": "*" 162 | } 163 | }, 164 | "ansi-styles": { 165 | "version": "3.2.1", 166 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 167 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 168 | "dev": true, 169 | "requires": { 170 | "color-convert": "^1.9.0" 171 | } 172 | }, 173 | "anymatch": { 174 | "version": "3.1.2", 175 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", 176 | "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", 177 | "dev": true, 178 | "requires": { 179 | "normalize-path": "^3.0.0", 180 | "picomatch": "^2.0.4" 181 | } 182 | }, 183 | "balanced-match": { 184 | "version": "1.0.2", 185 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 186 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 187 | "dev": true 188 | }, 189 | "binary-extensions": { 190 | "version": "2.2.0", 191 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 192 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 193 | "dev": true 194 | }, 195 | "brace-expansion": { 196 | "version": "1.1.11", 197 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 198 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 199 | "dev": true, 200 | "requires": { 201 | "balanced-match": "^1.0.0", 202 | "concat-map": "0.0.1" 203 | } 204 | }, 205 | "braces": { 206 | "version": "3.0.2", 207 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 208 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 209 | "dev": true, 210 | "requires": { 211 | "fill-range": "^7.0.1" 212 | } 213 | }, 214 | "buffer-crc32": { 215 | "version": "0.2.13", 216 | "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", 217 | "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", 218 | "dev": true 219 | }, 220 | "buffer-from": { 221 | "version": "1.1.2", 222 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", 223 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", 224 | "dev": true 225 | }, 226 | "builtin-modules": { 227 | "version": "3.2.0", 228 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", 229 | "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", 230 | "dev": true 231 | }, 232 | "callsites": { 233 | "version": "3.1.0", 234 | "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 235 | "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 236 | "dev": true 237 | }, 238 | "chalk": { 239 | "version": "2.4.2", 240 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 241 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 242 | "dev": true, 243 | "requires": { 244 | "ansi-styles": "^3.2.1", 245 | "escape-string-regexp": "^1.0.5", 246 | "supports-color": "^5.3.0" 247 | } 248 | }, 249 | "chokidar": { 250 | "version": "3.5.2", 251 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", 252 | "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", 253 | "dev": true, 254 | "requires": { 255 | "anymatch": "~3.1.2", 256 | "braces": "~3.0.2", 257 | "fsevents": "~2.3.2", 258 | "glob-parent": "~5.1.2", 259 | "is-binary-path": "~2.1.0", 260 | "is-glob": "~4.0.1", 261 | "normalize-path": "~3.0.0", 262 | "readdirp": "~3.6.0" 263 | } 264 | }, 265 | "color-convert": { 266 | "version": "1.9.3", 267 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 268 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 269 | "dev": true, 270 | "requires": { 271 | "color-name": "1.1.3" 272 | } 273 | }, 274 | "color-name": { 275 | "version": "1.1.3", 276 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 277 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 278 | "dev": true 279 | }, 280 | "commander": { 281 | "version": "2.20.3", 282 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 283 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", 284 | "dev": true 285 | }, 286 | "commondir": { 287 | "version": "1.0.1", 288 | "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", 289 | "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", 290 | "dev": true 291 | }, 292 | "concat-map": { 293 | "version": "0.0.1", 294 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 295 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 296 | "dev": true 297 | }, 298 | "console-clear": { 299 | "version": "1.1.1", 300 | "resolved": "https://registry.npmjs.org/console-clear/-/console-clear-1.1.1.tgz", 301 | "integrity": "sha512-pMD+MVR538ipqkG5JXeOEbKWS5um1H4LUUccUQG68qpeqBYbzYy79Gh55jkd2TtPdRfUaLWdv6LPP//5Zt0aPQ==" 302 | }, 303 | "dedent-js": { 304 | "version": "1.0.1", 305 | "resolved": "https://registry.npmjs.org/dedent-js/-/dedent-js-1.0.1.tgz", 306 | "integrity": "sha1-vuX7fJ5yfYXf+iRZDRDsGrElUwU=", 307 | "dev": true 308 | }, 309 | "deepmerge": { 310 | "version": "4.2.2", 311 | "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", 312 | "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", 313 | "dev": true 314 | }, 315 | "detect-indent": { 316 | "version": "6.1.0", 317 | "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", 318 | "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", 319 | "dev": true 320 | }, 321 | "es6-promise": { 322 | "version": "3.3.1", 323 | "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", 324 | "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=", 325 | "dev": true 326 | }, 327 | "escape-string-regexp": { 328 | "version": "1.0.5", 329 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 330 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 331 | "dev": true 332 | }, 333 | "estree-walker": { 334 | "version": "2.0.2", 335 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", 336 | "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", 337 | "dev": true 338 | }, 339 | "fast-glob": { 340 | "version": "3.2.7", 341 | "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", 342 | "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", 343 | "dev": true, 344 | "requires": { 345 | "@nodelib/fs.stat": "^2.0.2", 346 | "@nodelib/fs.walk": "^1.2.3", 347 | "glob-parent": "^5.1.2", 348 | "merge2": "^1.3.0", 349 | "micromatch": "^4.0.4" 350 | } 351 | }, 352 | "fastq": { 353 | "version": "1.13.0", 354 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", 355 | "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", 356 | "dev": true, 357 | "requires": { 358 | "reusify": "^1.0.4" 359 | } 360 | }, 361 | "fill-range": { 362 | "version": "7.0.1", 363 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 364 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 365 | "dev": true, 366 | "requires": { 367 | "to-regex-range": "^5.0.1" 368 | } 369 | }, 370 | "fs.realpath": { 371 | "version": "1.0.0", 372 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 373 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 374 | "dev": true 375 | }, 376 | "fsevents": { 377 | "version": "2.3.2", 378 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 379 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 380 | "dev": true, 381 | "optional": true 382 | }, 383 | "function-bind": { 384 | "version": "1.1.1", 385 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 386 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 387 | "dev": true 388 | }, 389 | "get-port": { 390 | "version": "3.2.0", 391 | "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", 392 | "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=" 393 | }, 394 | "glob": { 395 | "version": "7.2.0", 396 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", 397 | "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", 398 | "dev": true, 399 | "requires": { 400 | "fs.realpath": "^1.0.0", 401 | "inflight": "^1.0.4", 402 | "inherits": "2", 403 | "minimatch": "^3.0.4", 404 | "once": "^1.3.0", 405 | "path-is-absolute": "^1.0.0" 406 | } 407 | }, 408 | "glob-parent": { 409 | "version": "5.1.2", 410 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 411 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 412 | "dev": true, 413 | "requires": { 414 | "is-glob": "^4.0.1" 415 | } 416 | }, 417 | "graceful-fs": { 418 | "version": "4.2.9", 419 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", 420 | "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", 421 | "dev": true 422 | }, 423 | "has": { 424 | "version": "1.0.3", 425 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 426 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 427 | "dev": true, 428 | "requires": { 429 | "function-bind": "^1.1.1" 430 | } 431 | }, 432 | "has-flag": { 433 | "version": "3.0.0", 434 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 435 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 436 | "dev": true 437 | }, 438 | "import-fresh": { 439 | "version": "3.3.0", 440 | "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", 441 | "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", 442 | "dev": true, 443 | "requires": { 444 | "parent-module": "^1.0.0", 445 | "resolve-from": "^4.0.0" 446 | } 447 | }, 448 | "inflight": { 449 | "version": "1.0.6", 450 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 451 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 452 | "dev": true, 453 | "requires": { 454 | "once": "^1.3.0", 455 | "wrappy": "1" 456 | } 457 | }, 458 | "inherits": { 459 | "version": "2.0.4", 460 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 461 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 462 | "dev": true 463 | }, 464 | "is-binary-path": { 465 | "version": "2.1.0", 466 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 467 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 468 | "dev": true, 469 | "requires": { 470 | "binary-extensions": "^2.0.0" 471 | } 472 | }, 473 | "is-core-module": { 474 | "version": "2.8.0", 475 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", 476 | "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", 477 | "dev": true, 478 | "requires": { 479 | "has": "^1.0.3" 480 | } 481 | }, 482 | "is-extglob": { 483 | "version": "2.1.1", 484 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 485 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", 486 | "dev": true 487 | }, 488 | "is-glob": { 489 | "version": "4.0.3", 490 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 491 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 492 | "dev": true, 493 | "requires": { 494 | "is-extglob": "^2.1.1" 495 | } 496 | }, 497 | "is-module": { 498 | "version": "1.0.0", 499 | "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", 500 | "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", 501 | "dev": true 502 | }, 503 | "is-number": { 504 | "version": "7.0.0", 505 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 506 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 507 | "dev": true 508 | }, 509 | "is-reference": { 510 | "version": "1.2.1", 511 | "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", 512 | "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", 513 | "dev": true, 514 | "requires": { 515 | "@types/estree": "*" 516 | } 517 | }, 518 | "jest-worker": { 519 | "version": "26.6.2", 520 | "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", 521 | "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", 522 | "dev": true, 523 | "requires": { 524 | "@types/node": "*", 525 | "merge-stream": "^2.0.0", 526 | "supports-color": "^7.0.0" 527 | }, 528 | "dependencies": { 529 | "has-flag": { 530 | "version": "4.0.0", 531 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 532 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 533 | "dev": true 534 | }, 535 | "supports-color": { 536 | "version": "7.2.0", 537 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 538 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 539 | "dev": true, 540 | "requires": { 541 | "has-flag": "^4.0.0" 542 | } 543 | } 544 | } 545 | }, 546 | "js-tokens": { 547 | "version": "4.0.0", 548 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 549 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 550 | "dev": true 551 | }, 552 | "kleur": { 553 | "version": "3.0.3", 554 | "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", 555 | "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" 556 | }, 557 | "livereload": { 558 | "version": "0.9.3", 559 | "resolved": "https://registry.npmjs.org/livereload/-/livereload-0.9.3.tgz", 560 | "integrity": "sha512-q7Z71n3i4X0R9xthAryBdNGVGAO2R5X+/xXpmKeuPMrteg+W2U8VusTKV3YiJbXZwKsOlFlHe+go6uSNjfxrZw==", 561 | "dev": true, 562 | "requires": { 563 | "chokidar": "^3.5.0", 564 | "livereload-js": "^3.3.1", 565 | "opts": ">= 1.2.0", 566 | "ws": "^7.4.3" 567 | } 568 | }, 569 | "livereload-js": { 570 | "version": "3.3.2", 571 | "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-3.3.2.tgz", 572 | "integrity": "sha512-w677WnINxFkuixAoUEXOStewzLYGI76XVag+0JWMMEyjJQKs0ibWZMxkTlB96Lm3EjZ7IeOxVziBEbtxVQqQZA==", 573 | "dev": true 574 | }, 575 | "local-access": { 576 | "version": "1.1.0", 577 | "resolved": "https://registry.npmjs.org/local-access/-/local-access-1.1.0.tgz", 578 | "integrity": "sha512-XfegD5pyTAfb+GY6chk283Ox5z8WexG56OvM06RWLpAc/UHozO8X6xAxEkIitZOtsSMM1Yr3DkHgW5W+onLhCw==" 579 | }, 580 | "lower-case": { 581 | "version": "2.0.2", 582 | "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", 583 | "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", 584 | "dev": true, 585 | "requires": { 586 | "tslib": "^2.0.3" 587 | } 588 | }, 589 | "magic-string": { 590 | "version": "0.25.7", 591 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", 592 | "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", 593 | "dev": true, 594 | "requires": { 595 | "sourcemap-codec": "^1.4.4" 596 | } 597 | }, 598 | "merge-stream": { 599 | "version": "2.0.0", 600 | "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", 601 | "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", 602 | "dev": true 603 | }, 604 | "merge2": { 605 | "version": "1.4.1", 606 | "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", 607 | "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", 608 | "dev": true 609 | }, 610 | "micromatch": { 611 | "version": "4.0.4", 612 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", 613 | "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", 614 | "dev": true, 615 | "requires": { 616 | "braces": "^3.0.1", 617 | "picomatch": "^2.2.3" 618 | } 619 | }, 620 | "min-indent": { 621 | "version": "1.0.1", 622 | "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", 623 | "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", 624 | "dev": true 625 | }, 626 | "minimatch": { 627 | "version": "3.0.4", 628 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 629 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 630 | "dev": true, 631 | "requires": { 632 | "brace-expansion": "^1.1.7" 633 | } 634 | }, 635 | "minimist": { 636 | "version": "1.2.5", 637 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 638 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", 639 | "dev": true 640 | }, 641 | "mkdirp": { 642 | "version": "0.5.5", 643 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", 644 | "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", 645 | "dev": true, 646 | "requires": { 647 | "minimist": "^1.2.5" 648 | } 649 | }, 650 | "mri": { 651 | "version": "1.2.0", 652 | "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", 653 | "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" 654 | }, 655 | "mrmime": { 656 | "version": "1.0.0", 657 | "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.0.tgz", 658 | "integrity": "sha512-a70zx7zFfVO7XpnQ2IX1Myh9yY4UYvfld/dikWRnsXxbyvMcfz+u6UfgNAtH+k2QqtJuzVpv6eLTx1G2+WKZbQ==" 659 | }, 660 | "no-case": { 661 | "version": "3.0.4", 662 | "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", 663 | "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", 664 | "dev": true, 665 | "requires": { 666 | "lower-case": "^2.0.2", 667 | "tslib": "^2.0.3" 668 | } 669 | }, 670 | "normalize-path": { 671 | "version": "3.0.0", 672 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 673 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 674 | "dev": true 675 | }, 676 | "once": { 677 | "version": "1.4.0", 678 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 679 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 680 | "dev": true, 681 | "requires": { 682 | "wrappy": "1" 683 | } 684 | }, 685 | "opts": { 686 | "version": "2.0.2", 687 | "resolved": "https://registry.npmjs.org/opts/-/opts-2.0.2.tgz", 688 | "integrity": "sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg==", 689 | "dev": true 690 | }, 691 | "parent-module": { 692 | "version": "1.0.1", 693 | "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 694 | "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 695 | "dev": true, 696 | "requires": { 697 | "callsites": "^3.0.0" 698 | } 699 | }, 700 | "pascal-case": { 701 | "version": "3.1.2", 702 | "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", 703 | "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", 704 | "dev": true, 705 | "requires": { 706 | "no-case": "^3.0.4", 707 | "tslib": "^2.0.3" 708 | } 709 | }, 710 | "path-is-absolute": { 711 | "version": "1.0.1", 712 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 713 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 714 | "dev": true 715 | }, 716 | "path-parse": { 717 | "version": "1.0.7", 718 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 719 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 720 | "dev": true 721 | }, 722 | "picomatch": { 723 | "version": "2.3.1", 724 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 725 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 726 | "dev": true 727 | }, 728 | "queue-microtask": { 729 | "version": "1.2.3", 730 | "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", 731 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", 732 | "dev": true 733 | }, 734 | "randombytes": { 735 | "version": "2.1.0", 736 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 737 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 738 | "dev": true, 739 | "requires": { 740 | "safe-buffer": "^5.1.0" 741 | } 742 | }, 743 | "readdirp": { 744 | "version": "3.6.0", 745 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 746 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 747 | "dev": true, 748 | "requires": { 749 | "picomatch": "^2.2.1" 750 | } 751 | }, 752 | "require-relative": { 753 | "version": "0.8.7", 754 | "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", 755 | "integrity": "sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=", 756 | "dev": true 757 | }, 758 | "resolve": { 759 | "version": "1.21.0", 760 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz", 761 | "integrity": "sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==", 762 | "dev": true, 763 | "requires": { 764 | "is-core-module": "^2.8.0", 765 | "path-parse": "^1.0.7", 766 | "supports-preserve-symlinks-flag": "^1.0.0" 767 | } 768 | }, 769 | "resolve-from": { 770 | "version": "4.0.0", 771 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 772 | "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 773 | "dev": true 774 | }, 775 | "reusify": { 776 | "version": "1.0.4", 777 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", 778 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", 779 | "dev": true 780 | }, 781 | "rimraf": { 782 | "version": "2.7.1", 783 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", 784 | "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", 785 | "dev": true, 786 | "requires": { 787 | "glob": "^7.1.3" 788 | } 789 | }, 790 | "rollup": { 791 | "version": "2.63.0", 792 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.63.0.tgz", 793 | "integrity": "sha512-nps0idjmD+NXl6OREfyYXMn/dar3WGcyKn+KBzPdaLecub3x/LrId0wUcthcr8oZUAcZAR8NKcfGGFlNgGL1kQ==", 794 | "dev": true, 795 | "requires": { 796 | "fsevents": "~2.3.2" 797 | } 798 | }, 799 | "rollup-plugin-css-only": { 800 | "version": "3.1.0", 801 | "resolved": "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-3.1.0.tgz", 802 | "integrity": "sha512-TYMOE5uoD76vpj+RTkQLzC9cQtbnJNktHPB507FzRWBVaofg7KhIqq1kGbcVOadARSozWF883Ho9KpSPKH8gqA==", 803 | "dev": true, 804 | "requires": { 805 | "@rollup/pluginutils": "4" 806 | }, 807 | "dependencies": { 808 | "@rollup/pluginutils": { 809 | "version": "4.1.2", 810 | "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.1.2.tgz", 811 | "integrity": "sha512-ROn4qvkxP9SyPeHaf7uQC/GPFY6L/OWy9+bd9AwcjOAWQwxRscoEyAUD8qCY5o5iL4jqQwoLk2kaTKJPb/HwzQ==", 812 | "dev": true, 813 | "requires": { 814 | "estree-walker": "^2.0.1", 815 | "picomatch": "^2.2.2" 816 | } 817 | } 818 | } 819 | }, 820 | "rollup-plugin-livereload": { 821 | "version": "2.0.5", 822 | "resolved": "https://registry.npmjs.org/rollup-plugin-livereload/-/rollup-plugin-livereload-2.0.5.tgz", 823 | "integrity": "sha512-vqQZ/UQowTW7VoiKEM5ouNW90wE5/GZLfdWuR0ELxyKOJUIaj+uismPZZaICU4DnWPVjnpCDDxEqwU7pcKY/PA==", 824 | "dev": true, 825 | "requires": { 826 | "livereload": "^0.9.1" 827 | } 828 | }, 829 | "rollup-plugin-svelte": { 830 | "version": "7.1.0", 831 | "resolved": "https://registry.npmjs.org/rollup-plugin-svelte/-/rollup-plugin-svelte-7.1.0.tgz", 832 | "integrity": "sha512-vopCUq3G+25sKjwF5VilIbiY6KCuMNHP1PFvx2Vr3REBNMDllKHFZN2B9jwwC+MqNc3UPKkjXnceLPEjTjXGXg==", 833 | "dev": true, 834 | "requires": { 835 | "require-relative": "^0.8.7", 836 | "rollup-pluginutils": "^2.8.2" 837 | } 838 | }, 839 | "rollup-plugin-terser": { 840 | "version": "7.0.2", 841 | "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", 842 | "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", 843 | "dev": true, 844 | "requires": { 845 | "@babel/code-frame": "^7.10.4", 846 | "jest-worker": "^26.2.1", 847 | "serialize-javascript": "^4.0.0", 848 | "terser": "^5.0.0" 849 | } 850 | }, 851 | "rollup-pluginutils": { 852 | "version": "2.8.2", 853 | "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", 854 | "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", 855 | "dev": true, 856 | "requires": { 857 | "estree-walker": "^0.6.1" 858 | }, 859 | "dependencies": { 860 | "estree-walker": { 861 | "version": "0.6.1", 862 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", 863 | "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", 864 | "dev": true 865 | } 866 | } 867 | }, 868 | "run-parallel": { 869 | "version": "1.2.0", 870 | "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", 871 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 872 | "dev": true, 873 | "requires": { 874 | "queue-microtask": "^1.2.2" 875 | } 876 | }, 877 | "sade": { 878 | "version": "1.8.0", 879 | "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.0.tgz", 880 | "integrity": "sha512-NRfCA8AVYuAA7Hu8bs18od6J4BdcXXwOv6OJuNgwbw8LcLK8JKwaM3WckLZ+MGyPJUS/ivVgK3twltrOIJJnug==", 881 | "requires": { 882 | "mri": "^1.1.0" 883 | } 884 | }, 885 | "safe-buffer": { 886 | "version": "5.2.1", 887 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 888 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 889 | "dev": true 890 | }, 891 | "sander": { 892 | "version": "0.5.1", 893 | "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", 894 | "integrity": "sha1-dB4kXiMfB8r7b98PEzrfohalAq0=", 895 | "dev": true, 896 | "requires": { 897 | "es6-promise": "^3.1.2", 898 | "graceful-fs": "^4.1.3", 899 | "mkdirp": "^0.5.1", 900 | "rimraf": "^2.5.2" 901 | } 902 | }, 903 | "semiver": { 904 | "version": "1.1.0", 905 | "resolved": "https://registry.npmjs.org/semiver/-/semiver-1.1.0.tgz", 906 | "integrity": "sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==" 907 | }, 908 | "serialize-javascript": { 909 | "version": "4.0.0", 910 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", 911 | "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", 912 | "dev": true, 913 | "requires": { 914 | "randombytes": "^2.1.0" 915 | } 916 | }, 917 | "sirv": { 918 | "version": "1.0.19", 919 | "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", 920 | "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", 921 | "requires": { 922 | "@polka/url": "^1.0.0-next.20", 923 | "mrmime": "^1.0.0", 924 | "totalist": "^1.0.0" 925 | } 926 | }, 927 | "sirv-cli": { 928 | "version": "1.0.14", 929 | "resolved": "https://registry.npmjs.org/sirv-cli/-/sirv-cli-1.0.14.tgz", 930 | "integrity": "sha512-yyUTNr984ANKDloqepkYbBSqvx3buwYg2sQKPWjSU+IBia5loaoka2If8N9CMwt8AfP179cdEl7kYJ//iWJHjQ==", 931 | "requires": { 932 | "console-clear": "^1.1.0", 933 | "get-port": "^3.2.0", 934 | "kleur": "^3.0.0", 935 | "local-access": "^1.0.1", 936 | "sade": "^1.6.0", 937 | "semiver": "^1.0.0", 938 | "sirv": "^1.0.13", 939 | "tinydate": "^1.0.0" 940 | } 941 | }, 942 | "sorcery": { 943 | "version": "0.10.0", 944 | "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.10.0.tgz", 945 | "integrity": "sha1-iukK19fLBfxZ8asMY3hF1cFaUrc=", 946 | "dev": true, 947 | "requires": { 948 | "buffer-crc32": "^0.2.5", 949 | "minimist": "^1.2.0", 950 | "sander": "^0.5.0", 951 | "sourcemap-codec": "^1.3.0" 952 | } 953 | }, 954 | "source-map": { 955 | "version": "0.7.3", 956 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", 957 | "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", 958 | "dev": true 959 | }, 960 | "source-map-support": { 961 | "version": "0.5.21", 962 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", 963 | "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", 964 | "dev": true, 965 | "requires": { 966 | "buffer-from": "^1.0.0", 967 | "source-map": "^0.6.0" 968 | }, 969 | "dependencies": { 970 | "source-map": { 971 | "version": "0.6.1", 972 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 973 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 974 | "dev": true 975 | } 976 | } 977 | }, 978 | "sourcemap-codec": { 979 | "version": "1.4.8", 980 | "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", 981 | "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", 982 | "dev": true 983 | }, 984 | "strip-indent": { 985 | "version": "3.0.0", 986 | "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", 987 | "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", 988 | "dev": true, 989 | "requires": { 990 | "min-indent": "^1.0.0" 991 | } 992 | }, 993 | "supports-color": { 994 | "version": "5.5.0", 995 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 996 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 997 | "dev": true, 998 | "requires": { 999 | "has-flag": "^3.0.0" 1000 | } 1001 | }, 1002 | "supports-preserve-symlinks-flag": { 1003 | "version": "1.0.0", 1004 | "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 1005 | "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 1006 | "dev": true 1007 | }, 1008 | "svelte": { 1009 | "version": "3.44.3", 1010 | "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.44.3.tgz", 1011 | "integrity": "sha512-aGgrNCip5PQFNfq9e9tmm7EYxWLVHoFsEsmKrtOeRD8dmoGDdyTQ+21xd7qgFd8MNdKGSYvg7F9dr+Tc0yDymg==", 1012 | "dev": true 1013 | }, 1014 | "svelte-check": { 1015 | "version": "2.2.11", 1016 | "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-2.2.11.tgz", 1017 | "integrity": "sha512-clotPGGZPj3LuS9qP1lk+Wwnsj+js42ehCPmHk+qtyaQh/dU95e0qkpPmtmOMYHN6My5Y75XqeN1QNLj5V5gwA==", 1018 | "dev": true, 1019 | "requires": { 1020 | "chalk": "^4.0.0", 1021 | "chokidar": "^3.4.1", 1022 | "fast-glob": "^3.2.7", 1023 | "import-fresh": "^3.2.1", 1024 | "minimist": "^1.2.5", 1025 | "sade": "^1.7.4", 1026 | "source-map": "^0.7.3", 1027 | "svelte-preprocess": "^4.0.0", 1028 | "typescript": "*" 1029 | }, 1030 | "dependencies": { 1031 | "ansi-styles": { 1032 | "version": "4.3.0", 1033 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 1034 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 1035 | "dev": true, 1036 | "requires": { 1037 | "color-convert": "^2.0.1" 1038 | } 1039 | }, 1040 | "chalk": { 1041 | "version": "4.1.2", 1042 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 1043 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 1044 | "dev": true, 1045 | "requires": { 1046 | "ansi-styles": "^4.1.0", 1047 | "supports-color": "^7.1.0" 1048 | } 1049 | }, 1050 | "color-convert": { 1051 | "version": "2.0.1", 1052 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 1053 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 1054 | "dev": true, 1055 | "requires": { 1056 | "color-name": "~1.1.4" 1057 | } 1058 | }, 1059 | "color-name": { 1060 | "version": "1.1.4", 1061 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 1062 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 1063 | "dev": true 1064 | }, 1065 | "has-flag": { 1066 | "version": "4.0.0", 1067 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 1068 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 1069 | "dev": true 1070 | }, 1071 | "supports-color": { 1072 | "version": "7.2.0", 1073 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 1074 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 1075 | "dev": true, 1076 | "requires": { 1077 | "has-flag": "^4.0.0" 1078 | } 1079 | } 1080 | } 1081 | }, 1082 | "svelte-preprocess": { 1083 | "version": "4.10.1", 1084 | "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.10.1.tgz", 1085 | "integrity": "sha512-NSNloaylf+o9UeyUR2KvpdxrAyMdHl3U7rMnoP06/sG0iwJvlUM4TpMno13RaNqovh4AAoGsx1jeYcIyuGUXMw==", 1086 | "dev": true, 1087 | "requires": { 1088 | "@types/pug": "^2.0.4", 1089 | "@types/sass": "^1.16.0", 1090 | "detect-indent": "^6.0.0", 1091 | "magic-string": "^0.25.7", 1092 | "sorcery": "^0.10.0", 1093 | "strip-indent": "^3.0.0" 1094 | } 1095 | }, 1096 | "svelte-routing": { 1097 | "version": "1.6.0", 1098 | "resolved": "https://registry.npmjs.org/svelte-routing/-/svelte-routing-1.6.0.tgz", 1099 | "integrity": "sha512-+DbrSGttLA6lan7oWFz1MjyGabdn3tPRqn8Osyc471ut2UgCrzM5x1qViNMc2gahOP6fKbKK1aNtZMJEQP2vHQ==", 1100 | "dev": true, 1101 | "requires": { 1102 | "svelte2tsx": "^0.1.157" 1103 | } 1104 | }, 1105 | "svelte2tsx": { 1106 | "version": "0.1.193", 1107 | "resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.1.193.tgz", 1108 | "integrity": "sha512-vzy4YQNYDnoqp2iZPnJy7kpPAY6y121L0HKrSBjU/IWW7DQ6T7RMJed2VVHFmVYm0zAGYMDl9urPc6R4DDUyhg==", 1109 | "dev": true, 1110 | "requires": { 1111 | "dedent-js": "^1.0.1", 1112 | "pascal-case": "^3.1.1" 1113 | } 1114 | }, 1115 | "terser": { 1116 | "version": "5.10.0", 1117 | "resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz", 1118 | "integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==", 1119 | "dev": true, 1120 | "requires": { 1121 | "commander": "^2.20.0", 1122 | "source-map": "~0.7.2", 1123 | "source-map-support": "~0.5.20" 1124 | } 1125 | }, 1126 | "tinydate": { 1127 | "version": "1.3.0", 1128 | "resolved": "https://registry.npmjs.org/tinydate/-/tinydate-1.3.0.tgz", 1129 | "integrity": "sha512-7cR8rLy2QhYHpsBDBVYnnWXm8uRTr38RoZakFSW7Bs7PzfMPNZthuMLkwqZv7MTu8lhQ91cOFYS5a7iFj2oR3w==" 1130 | }, 1131 | "to-regex-range": { 1132 | "version": "5.0.1", 1133 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1134 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1135 | "dev": true, 1136 | "requires": { 1137 | "is-number": "^7.0.0" 1138 | } 1139 | }, 1140 | "totalist": { 1141 | "version": "1.1.0", 1142 | "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", 1143 | "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==" 1144 | }, 1145 | "tslib": { 1146 | "version": "2.3.1", 1147 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", 1148 | "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", 1149 | "dev": true 1150 | }, 1151 | "typescript": { 1152 | "version": "4.5.4", 1153 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", 1154 | "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==", 1155 | "dev": true 1156 | }, 1157 | "wrappy": { 1158 | "version": "1.0.2", 1159 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1160 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 1161 | "dev": true 1162 | }, 1163 | "ws": { 1164 | "version": "7.5.6", 1165 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz", 1166 | "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==", 1167 | "dev": true 1168 | } 1169 | } 1170 | } 1171 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-app", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "rollup -c", 7 | "dev": "rollup -c -w", 8 | "start": "sirv public --no-clear", 9 | "check": "svelte-check --tsconfig ./tsconfig.json" 10 | }, 11 | "devDependencies": { 12 | "@rollup/plugin-commonjs": "^17.0.0", 13 | "@rollup/plugin-node-resolve": "^11.0.0", 14 | "@rollup/plugin-typescript": "^8.0.0", 15 | "@tsconfig/svelte": "^2.0.0", 16 | "rollup": "^2.3.4", 17 | "rollup-plugin-css-only": "^3.1.0", 18 | "rollup-plugin-livereload": "^2.0.0", 19 | "rollup-plugin-svelte": "^7.0.0", 20 | "rollup-plugin-terser": "^7.0.0", 21 | "svelte": "^3.0.0", 22 | "svelte-check": "^2.0.0", 23 | "svelte-preprocess": "^4.0.0", 24 | "svelte-routing": "^1.6.0", 25 | "tslib": "^2.0.0", 26 | "typescript": "^4.0.0" 27 | }, 28 | "dependencies": { 29 | "sirv-cli": "^1.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /web/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekgonecrazy/prismplus/511285ab1fa31f078d13a211fef760ebdf2869c5/web/public/favicon.png -------------------------------------------------------------------------------- /web/public/fonts/jura/Jura-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekgonecrazy/prismplus/511285ab1fa31f078d13a211fef760ebdf2869c5/web/public/fonts/jura/Jura-VariableFont_wght.ttf -------------------------------------------------------------------------------- /web/public/fonts/jura/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2019 The Jura Project Authors (https://github.com/ossobuffo/jura) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /web/public/fonts/jura/README.txt: -------------------------------------------------------------------------------- 1 | Jura Variable Font 2 | ================== 3 | 4 | This download contains Jura as both a variable font and static fonts. 5 | 6 | Jura is a variable font with this axis: 7 | wght 8 | 9 | This means all the styles are contained in a single file: 10 | Jura-VariableFont_wght.ttf 11 | 12 | If your app fully supports variable fonts, you can now pick intermediate styles 13 | that aren’t available as static fonts. Not all apps support variable fonts, and 14 | in those cases you can use the static font files for Jura: 15 | static/Jura-Light.ttf 16 | static/Jura-Regular.ttf 17 | static/Jura-Medium.ttf 18 | static/Jura-SemiBold.ttf 19 | static/Jura-Bold.ttf 20 | 21 | Get started 22 | ----------- 23 | 24 | 1. Install the font files you want to use 25 | 26 | 2. Use your app's font picker to view the font family and all the 27 | available styles 28 | 29 | Learn more about variable fonts 30 | ------------------------------- 31 | 32 | https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts 33 | https://variablefonts.typenetwork.com 34 | https://medium.com/variable-fonts 35 | 36 | In desktop apps 37 | 38 | https://theblog.adobe.com/can-variable-fonts-illustrator-cc 39 | https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts 40 | 41 | Online 42 | 43 | https://developers.google.com/fonts/docs/getting_started 44 | https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide 45 | https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts 46 | 47 | Installing fonts 48 | 49 | MacOS: https://support.apple.com/en-us/HT201749 50 | Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux 51 | Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows 52 | 53 | Android Apps 54 | 55 | https://developers.google.com/fonts/docs/android 56 | https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts 57 | 58 | License 59 | ------- 60 | Please read the full license text (OFL.txt) to understand the permissions, 61 | restrictions and requirements for usage, redistribution, and modification. 62 | 63 | You can use them freely in your products & projects - print or digital, 64 | commercial or otherwise. 65 | 66 | This isn't legal advice, please consider consulting a lawyer and see the full 67 | license for all details. 68 | -------------------------------------------------------------------------------- /web/public/fonts/jura/static/Jura-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekgonecrazy/prismplus/511285ab1fa31f078d13a211fef760ebdf2869c5/web/public/fonts/jura/static/Jura-Bold.ttf -------------------------------------------------------------------------------- /web/public/fonts/jura/static/Jura-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekgonecrazy/prismplus/511285ab1fa31f078d13a211fef760ebdf2869c5/web/public/fonts/jura/static/Jura-Light.ttf -------------------------------------------------------------------------------- /web/public/fonts/jura/static/Jura-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekgonecrazy/prismplus/511285ab1fa31f078d13a211fef760ebdf2869c5/web/public/fonts/jura/static/Jura-Medium.ttf -------------------------------------------------------------------------------- /web/public/fonts/jura/static/Jura-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekgonecrazy/prismplus/511285ab1fa31f078d13a211fef760ebdf2869c5/web/public/fonts/jura/static/Jura-Regular.ttf -------------------------------------------------------------------------------- /web/public/fonts/jura/static/Jura-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekgonecrazy/prismplus/511285ab1fa31f078d13a211fef760ebdf2869c5/web/public/fonts/jura/static/Jura-SemiBold.ttf -------------------------------------------------------------------------------- /web/public/global.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: Jura; 3 | src: url(/fonts/jura/Jura-VariableFont_wght.ttf); 4 | } 5 | 6 | :root { 7 | --bg-color:#323C46; 8 | --bg-color-rgb: 50, 60, 70; 9 | --fg-color: #B1FDF2; 10 | --fg-color-rgb: 177, 253, 242; 11 | --prim-color: #00EBFE; 12 | --prim-color-rgb: 0, 235, 254; 13 | --alt-color: #FEAF3E; 14 | --alt-color-rgb: 254, 175, 62; 15 | --acc-color: #2CFE00; 16 | --acc-color-rgb: 44, 254, 0; 17 | --neg-color: #ff7777; 18 | --neg-color-rgb: 255, 119, 119; 19 | --black: #111; 20 | --black-rgb: 17, 17, 17; 21 | --white: #eee; 22 | --white-rgb: 238, 238, 238; 23 | --grey-1: #666; 24 | --grey-2: #999; 25 | --grey-3: #ccc; 26 | --grey-4: #ddd; 27 | } 28 | 29 | html, body { 30 | position: relative; 31 | width: 100%; 32 | height: 100%; 33 | 34 | font-size: 16px; 35 | } 36 | 37 | body { 38 | box-sizing: border-box; 39 | margin: 0; 40 | padding: 0; 41 | 42 | background-color: var(--bg-color); 43 | color: var(--fg-color); 44 | 45 | font-family: Jura, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 46 | } 47 | 48 | h1, h2, h3, h4, h5, h6 { 49 | font-size: initial; 50 | margin: 0; 51 | padding: 0; 52 | } 53 | 54 | h1, h2, h3 { 55 | color: var(--prim-color); 56 | } 57 | h1 { 58 | text-transform: uppercase; 59 | } 60 | 61 | a { 62 | color: var(--acc-color); 63 | 64 | text-decoration: none; 65 | } 66 | 67 | a:hover { 68 | text-decoration: underline; 69 | } 70 | 71 | a:visited { 72 | color: var(--prim-color); 73 | 74 | text-decoration: line-through; 75 | } 76 | 77 | ul { 78 | list-style: none; 79 | } 80 | 81 | fieldset { 82 | display: flex; 83 | flex-direction: column; 84 | align-items: stretch; 85 | } 86 | fieldset button { 87 | align-self: center; 88 | } 89 | 90 | legend { 91 | font-weight: bold; 92 | color: var(--prim-color); 93 | } 94 | 95 | label { 96 | display: block; 97 | 98 | margin: 0.5em; 99 | } 100 | 101 | input, button, select, textarea { 102 | box-sizing: border-box; 103 | margin: 0 0 0.5em 0; 104 | padding: 0.4em; 105 | -webkit-padding: 0.4em 0; 106 | 107 | border: 1px solid var(--prim-color); 108 | border-radius: 2px; 109 | 110 | font-family: inherit; 111 | font-size: inherit; 112 | 113 | transition: background-color 0.25s border 0.25s; 114 | } 115 | 116 | input[type=checkbox] { 117 | width: unset; 118 | } 119 | 120 | input:disabled { 121 | color: var(--grey-3); 122 | } 123 | 124 | button { 125 | background-color: var(--prim-color); 126 | color: var(--black); 127 | border: 1px solid var(--black); 128 | outline: none; 129 | } 130 | button:hover { 131 | background-color: rgba(var(--prim-color-rgb), 0.5); 132 | } 133 | 134 | button:not(:disabled):active { 135 | background-color: var(--white); 136 | } 137 | 138 | button:disabled { 139 | color: var(--grey-2); 140 | } 141 | 142 | button:focus { 143 | border-color: var(--grey-4); 144 | } 145 | -------------------------------------------------------------------------------- /web/public/img/PrismPlusLogo_LinePrism.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekgonecrazy/prismplus/511285ab1fa31f078d13a211fef760ebdf2869c5/web/public/img/PrismPlusLogo_LinePrism.png -------------------------------------------------------------------------------- /web/public/img/PrismPlusLogo_LinePrismWhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekgonecrazy/prismplus/511285ab1fa31f078d13a211fef760ebdf2869c5/web/public/img/PrismPlusLogo_LinePrismWhite.png -------------------------------------------------------------------------------- /web/public/img/PrismPlus_PlatformColors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekgonecrazy/prismplus/511285ab1fa31f078d13a211fef760ebdf2869c5/web/public/img/PrismPlus_PlatformColors.png -------------------------------------------------------------------------------- /web/public/img/PrismPlus_PlatformColorsTrans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekgonecrazy/prismplus/511285ab1fa31f078d13a211fef760ebdf2869c5/web/public/img/PrismPlus_PlatformColorsTrans.png -------------------------------------------------------------------------------- /web/public/img/PrismPlus_PlatformColorsTrans_128x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekgonecrazy/prismplus/511285ab1fa31f078d13a211fef760ebdf2869c5/web/public/img/PrismPlus_PlatformColorsTrans_128x.png -------------------------------------------------------------------------------- /web/public/img/PrismPlus_PlatformColorsTrans_256x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekgonecrazy/prismplus/511285ab1fa31f078d13a211fef760ebdf2869c5/web/public/img/PrismPlus_PlatformColorsTrans_256x.png -------------------------------------------------------------------------------- /web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Prism+ 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /web/rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import livereload from 'rollup-plugin-livereload'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | import sveltePreprocess from 'svelte-preprocess'; 7 | import typescript from '@rollup/plugin-typescript'; 8 | import css from 'rollup-plugin-css-only'; 9 | 10 | const production = !process.env.ROLLUP_WATCH; 11 | 12 | function serve() { 13 | let server; 14 | 15 | function toExit() { 16 | if (server) server.kill(0); 17 | } 18 | 19 | return { 20 | writeBundle() { 21 | if (server) return; 22 | server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { 23 | stdio: ['ignore', 'inherit', 'inherit'], 24 | shell: true 25 | }); 26 | 27 | process.on('SIGTERM', toExit); 28 | process.on('exit', toExit); 29 | } 30 | }; 31 | } 32 | 33 | export default { 34 | input: 'src/main.ts', 35 | output: { 36 | sourcemap: true, 37 | format: 'iife', 38 | name: 'app', 39 | file: 'public/build/bundle.js' 40 | }, 41 | plugins: [ 42 | svelte({ 43 | preprocess: sveltePreprocess({ sourceMap: !production }), 44 | compilerOptions: { 45 | // enable run-time checks when not in production 46 | dev: !production 47 | } 48 | }), 49 | // we'll extract any component CSS out into 50 | // a separate file - better for performance 51 | css({ output: 'bundle.css' }), 52 | 53 | // If you have external dependencies installed from 54 | // npm, you'll most likely need these plugins. In 55 | // some cases you'll need additional configuration - 56 | // consult the documentation for details: 57 | // https://github.com/rollup/plugins/tree/master/packages/commonjs 58 | resolve({ 59 | browser: true, 60 | dedupe: ['svelte'] 61 | }), 62 | commonjs(), 63 | typescript({ 64 | sourceMap: !production, 65 | inlineSources: !production 66 | }), 67 | 68 | // In dev mode, call `npm run start` once 69 | // the bundle has been generated 70 | !production && serve(), 71 | 72 | // Watch the `public` directory and refresh the 73 | // browser on changes when not in production 74 | !production && livereload('public'), 75 | 76 | // If we're building for production (npm run build 77 | // instead of npm run dev), minify 78 | production && terser() 79 | ], 80 | watch: { 81 | clearScreen: false 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /web/src/App.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 |
16 |

17 | Prism plus logo --a 20 sided die with many colors streaming in and out of it. 21 |

22 |
23 | 24 |
25 | 26 | 27 | 28 | 29 |
30 | 31 | 45 |
46 | 47 | 139 | -------------------------------------------------------------------------------- /web/src/admin.svelte: -------------------------------------------------------------------------------- 1 | 192 | 193 |
194 |
195 | {#if !connected} 196 |
197 | Login to Admin 198 | 199 | 200 | 209 | 210 |
211 | 212 | 218 |
219 | 220 | 221 |
222 | {:else} 223 |
224 |

Admin Config

225 |
226 | 227 |
228 | Create new Streamer 229 | 230 | 231 | 239 | 240 | 241 | 249 | 250 |
251 | 252 | 258 |
259 | 260 | 263 |
264 | {/if} 265 |
266 | 267 | {#if streamers.length > 0} 268 |
269 |

Streamers

270 | 271 |
272 | {#if !fetch_error} 273 | {#each streamers as { id, name, streamKey }, i} 274 |
275 | Streamer ID: {id} 276 | 277 | 278 | 286 | 287 | 288 | 295 | 296 |
297 | 298 | showStreamerKey(e, i)} 303 | /> 304 |
305 | 306 | 311 |
312 | {/each} 313 | {/if} 314 |
315 |
316 | {/if} 317 | 318 | {#if sessions.length > 0} 319 |
320 |

Current Sessions

321 | 322 |
323 | {#if !fetch_error} 324 | {#each sessions as { key, destinations, nextDestinationId, active, end, streamHeaders }, i} 325 |
326 | Session {i} 327 | 328 | 329 | 336 | 337 | showSessionKey(e, i)} 342 | /> 343 | 344 |

Destinations

345 |
    346 | {#each Object.entries(destinations) as [_, { name, server, id }]} 347 |
  • 348 | 354 | {name} - {server} 355 |
  • 356 | {/each} 357 |
358 | 359 | 364 |
365 | {/each} 366 | {:else} 367 |

{err_message}

368 | {/if} 369 |
370 |
371 | {/if} 372 |
373 | 374 | 383 | -------------------------------------------------------------------------------- /web/src/global.d.ts: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /web/src/main.ts: -------------------------------------------------------------------------------- 1 | import App from './App.svelte'; 2 | 3 | const app = new App({ 4 | target: document.body, 5 | props: {} 6 | }); 7 | 8 | export default app; -------------------------------------------------------------------------------- /web/src/streamer.svelte: -------------------------------------------------------------------------------- 1 | 169 | 170 |
171 | {#if !connected} 172 |
173 |
174 | Login as Streamer 175 | 176 | 177 | 186 | 187 |
188 | 189 | 195 |
196 | 197 | 198 |
199 |
200 | {:else} 201 |
202 |
203 |

Stream Config

204 |

{streamer.name}

205 |
206 | 207 |
208 | {#if !fetch_error} 209 |
210 | Session 211 | 212 | 213 | 220 | 221 |
222 | 223 | 229 |
230 | 231 |

Destinations

232 |
    233 | {#each Object.entries(streamer.destinations) as [_, { name, server, key, id }]} 234 |
  • 235 | 241 | {name} - {server} 242 |
  • 243 | {/each} 244 |
245 | 246 | 249 | 256 | 257 | 260 | 267 | 268 | 271 | 279 | 280 |
281 | 284 | 290 |
291 | 292 | 295 |
296 | {:else} 297 |

298 | {err_message} 299 |

300 | {/if} 301 |
302 |
303 | {/if} 304 |
305 | 306 | 334 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | 4 | "include": ["src/**/*"], 5 | "exclude": ["node_modules/*", "__sapper__/*", "public/*"] 6 | } --------------------------------------------------------------------------------