├── .gitignore ├── README.md ├── go.mod ├── go.sum └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | peerConnections.gob 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webrtc-zero-downtime-restart 2 | 3 | webrtc-zero-downtime-restart is a simple Pion WebRTC broadcast server that can be restarted 4 | without disconnecting users. All the WebRTC state is suspended to disk. The 5 | next time the process is started that information is read into memory. All 6 | the remote PeerConnections will be unaware that they are even connected to a new 7 | process. This gives us the following benefits. 8 | 9 | ### Painless Deploys 10 | No more migrating users to a new process when you want to deploy code. At anytime you 11 | can replace the binary you are running and no users will be impacted. You don't need 12 | to implement extra signaling/error handling for your deploy process. 13 | 14 | ### Easier Scaling 15 | Move clients to an entirely different host without them knowing. You can do this with 16 | zero interruption in service or additional signaling. 17 | 18 | ### Greater Resiliency 19 | Since server state is constantly being written to disk you don't need to worry about 20 | crashes anymore. If you server goes down (and then restarts) it will automatically resume 21 | the last known good state. 22 | 23 | ## Running 24 | 25 | Execute `go run github.com/Sean-Der/webrtc-zero-downtime-restart@latest`. 26 | 27 | 28 | This will start the server. On startup the server will print. 29 | 30 | ``` 31 | Open http://localhost:8080 to access this demo 32 | ``` 33 | 34 | You can then access it at [http://localhost:8080](http://localhost:8080). The first user to connect will broadcast 35 | their webcam. Every user after can watch the broadcasted video. 36 | 37 | At anytime you can start+stop the process in your terminal. Users will not be disconnected and will 38 | be able to continue talking when the process is started again. 39 | 40 | ## What is next 41 | 42 | This demo uses reflection to access internal Pion WebRTC APIs. We will be working on designing the final 43 | APIs for the next major release of Pion WebRTC. We would love your feedback ideas either on the 44 | repo or [Slack](https://pion.ly/slack) 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module webrtc-zero-downtime-reload 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/pion/dtls/v2 v2.2.6 7 | github.com/pion/ice/v2 v2.3.1 8 | github.com/pion/rtcp v1.2.10 9 | github.com/pion/webrtc/v3 v3.1.59-0.20230326035336-9a0eb473514a 10 | ) 11 | 12 | require ( 13 | github.com/google/uuid v1.3.0 // indirect 14 | github.com/pion/datachannel v1.5.5 // indirect 15 | github.com/pion/interceptor v0.1.12 // indirect 16 | github.com/pion/logging v0.2.2 // indirect 17 | github.com/pion/mdns v0.0.7 // indirect 18 | github.com/pion/randutil v0.1.0 // indirect 19 | github.com/pion/rtp v1.7.13 // indirect 20 | github.com/pion/sctp v1.8.6 // indirect 21 | github.com/pion/sdp/v3 v3.0.6 // indirect 22 | github.com/pion/srtp/v2 v2.0.13-0.20230326035121-5f7175086aae // indirect 23 | github.com/pion/stun v0.4.0 // indirect 24 | github.com/pion/transport/v2 v2.0.2 // indirect 25 | github.com/pion/turn/v2 v2.1.0 // indirect 26 | github.com/pion/udp/v2 v2.0.1 // indirect 27 | golang.org/x/crypto v0.7.0 // indirect 28 | golang.org/x/net v0.8.0 // indirect 29 | golang.org/x/sys v0.6.0 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 5 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 6 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 7 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 8 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 9 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 10 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 11 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 12 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 13 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 14 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 15 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 16 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 17 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 18 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 19 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 20 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 21 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 22 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 23 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 24 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 25 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 26 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 27 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 28 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 29 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 30 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 31 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 32 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 33 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 34 | github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 35 | github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8= 36 | github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0= 37 | github.com/pion/dtls/v2 v2.2.6 h1:yXMxKr0Skd+Ub6A8UqXTRLSywskx93ooMRHsQUtd+Z4= 38 | github.com/pion/dtls/v2 v2.2.6/go.mod h1:t8fWJCIquY5rlQZwA2yWxUS1+OCrAdXrhVKXB5oD/wY= 39 | github.com/pion/ice/v2 v2.3.1 h1:FQCmUfZe2Jpe7LYStVBOP6z1DiSzbIateih3TztgTjc= 40 | github.com/pion/ice/v2 v2.3.1/go.mod h1:aq2kc6MtYNcn4XmMhobAv6hTNJiHzvD0yXRz80+bnP8= 41 | github.com/pion/interceptor v0.1.12 h1:CslaNriCFUItiXS5o+hh5lpL0t0ytQkFnUcbbCs2Zq8= 42 | github.com/pion/interceptor v0.1.12/go.mod h1:bDtgAD9dRkBZpWHGKaoKb42FhDHTG2rX8Ii9LRALLVA= 43 | github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= 44 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= 45 | github.com/pion/mdns v0.0.7 h1:P0UB4Sr6xDWEox0kTVxF0LmQihtCbSAdW0H2nEgkA3U= 46 | github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8= 47 | github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= 48 | github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= 49 | github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc= 50 | github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I= 51 | github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA= 52 | github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= 53 | github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= 54 | github.com/pion/sctp v1.8.6 h1:CUex11Vkt9YS++VhLf8b55O3VqKrWL6W3SDwX4jAqsI= 55 | github.com/pion/sctp v1.8.6/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= 56 | github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw= 57 | github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw= 58 | github.com/pion/srtp/v2 v2.0.12 h1:WrmiVCubGMOAObBU1vwWjG0H3VSyQHawKeer2PVA5rY= 59 | github.com/pion/srtp/v2 v2.0.12/go.mod h1:C3Ep44hlOo2qEYaq4ddsmK5dL63eLehXFbHaZ9F5V9Y= 60 | github.com/pion/srtp/v2 v2.0.13-0.20230326035121-5f7175086aae h1:GGa/BGQD+wVviFJC8umocBj276dYn4PhqTEtL6yyHq0= 61 | github.com/pion/srtp/v2 v2.0.13-0.20230326035121-5f7175086aae/go.mod h1:FA7u5fWpVITMYNL70TA3csQuMQJA5/+6ZMajGxveHgM= 62 | github.com/pion/stun v0.4.0 h1:vgRrbBE2htWHy7l3Zsxckk7rkjnjOsSM7PHZnBwo8rk= 63 | github.com/pion/stun v0.4.0/go.mod h1:QPsh1/SbXASntw3zkkrIk3ZJVKz4saBY2G7S10P3wCw= 64 | github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40= 65 | github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI= 66 | github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc= 67 | github.com/pion/transport/v2 v2.0.2 h1:St+8o+1PEzPT51O9bv+tH/KYYLMNR5Vwm5Z3Qkjsywg= 68 | github.com/pion/transport/v2 v2.0.2/go.mod h1:vrz6bUbFr/cjdwbnxq8OdDDzHf7JJfGsIRkxfpZoTA0= 69 | github.com/pion/turn/v2 v2.1.0 h1:5wGHSgGhJhP/RpabkUb/T9PdsAjkGLS6toYz5HNzoSI= 70 | github.com/pion/turn/v2 v2.1.0/go.mod h1:yrT5XbXSGX1VFSF31A3c1kCNB5bBZgk/uu5LET162qs= 71 | github.com/pion/udp/v2 v2.0.1 h1:xP0z6WNux1zWEjhC7onRA3EwwSliXqu1ElUZAQhUP54= 72 | github.com/pion/udp/v2 v2.0.1/go.mod h1:B7uvTMP00lzWdyMr/1PVZXtV3wpPIxBRd4Wl6AksXn8= 73 | github.com/pion/webrtc/v3 v3.1.59-0.20230317181827-3638bce3b6f0 h1:c0m8hGGG1KvIzwKm36mgWQRIysbTmb0Sul5eZ6BAFjc= 74 | github.com/pion/webrtc/v3 v3.1.59-0.20230317181827-3638bce3b6f0/go.mod h1:jJdqoqGBlZiE3y8Z1tg1fjSkyEDCZLL+foypUBn0Lhk= 75 | github.com/pion/webrtc/v3 v3.1.59-0.20230326035336-9a0eb473514a h1:rw8OZ//s6GeGcntBOuXvtfIBHFEFgXxmhRWcyBRJQis= 76 | github.com/pion/webrtc/v3 v3.1.59-0.20230326035336-9a0eb473514a/go.mod h1:4M9wYG6b6vJIoMGMmd4lVrOt/dc/JS5leIV0AaGLPbE= 77 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 78 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 79 | github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= 80 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 81 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 82 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 83 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 84 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 85 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 86 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 87 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 88 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 89 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 90 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 91 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 92 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 93 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 94 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 95 | golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= 96 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 97 | golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= 98 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 99 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 100 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 101 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 102 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 103 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 104 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 105 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 106 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 107 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 108 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 109 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 110 | golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 111 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 112 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 113 | golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= 114 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 115 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 116 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 117 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 118 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 119 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 120 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 121 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 122 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 123 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 124 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 125 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 126 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 127 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 128 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 129 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 130 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 131 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 132 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 133 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 134 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 135 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 136 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 137 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 138 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 139 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 140 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 141 | golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 142 | golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= 143 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 144 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 145 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 146 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 147 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 148 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 149 | golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 150 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 151 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 152 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 153 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 154 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 155 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 156 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 157 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 158 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 159 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 160 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 161 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 162 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 163 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 164 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 165 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 166 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 167 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 168 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 169 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 170 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 171 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 172 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 173 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 174 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 175 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 176 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 177 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 178 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | //go:build !js 2 | // +build !js 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "encoding/gob" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "io" 13 | "net/http" 14 | "os" 15 | "reflect" 16 | "strings" 17 | "sync" 18 | "sync/atomic" 19 | "time" 20 | "unsafe" 21 | 22 | "github.com/pion/dtls/v2" 23 | "github.com/pion/ice/v2" 24 | "github.com/pion/rtcp" 25 | "github.com/pion/webrtc/v3" 26 | ) 27 | 28 | const ( 29 | serializedPeerConnectionsFile = "peerConnections.gob" 30 | indexHtml = ` 31 | 32 | 33 | webrtc-zero-downtime-reload 34 | 35 | 36 | 37 |

38 | 39 | 40 | 41 | 88 | 89 | ` 90 | ) 91 | 92 | type GlobalState struct { 93 | PeerConnectionState []PeerConnectionState 94 | } 95 | 96 | type PeerConnectionState struct { 97 | RemoteDescription webrtc.SessionDescription 98 | 99 | ICEPort uint16 100 | ICEUsernameFragment string 101 | ICEPassword string 102 | 103 | DTLSConnectionState dtls.State 104 | 105 | SSRCAudio, SSRCVideo webrtc.SSRC 106 | SRTPState map[uint32]uint32 107 | } 108 | 109 | var ( 110 | audioTrack, videoTrack *webrtc.TrackLocalStaticRTP 111 | haveBroadcaster = atomic.Bool{} 112 | peerConnections = []*webrtc.PeerConnection{} 113 | peerConnectionsMutex sync.Mutex 114 | ) 115 | 116 | func main() { 117 | var err error 118 | if videoTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, "video", "pion"); err != nil { 119 | panic(err) 120 | } else if audioTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus}, "audio", "pion"); err != nil { 121 | panic(err) 122 | } 123 | 124 | state := GlobalState{} 125 | if buffer, err := os.ReadFile(serializedPeerConnectionsFile); err == nil { 126 | dec := gob.NewDecoder(bytes.NewBuffer(buffer)) 127 | if err := dec.Decode(&state); err != nil { 128 | panic(err) 129 | } 130 | } 131 | 132 | deserialize(state) 133 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 134 | fmt.Fprintf(w, indexHtml) 135 | }) 136 | http.HandleFunc("/doSignaling", doSignaling) 137 | http.HandleFunc("/haveBroadcaster", func(w http.ResponseWriter, r *http.Request) { 138 | out := struct { 139 | HaveBroadcaster bool 140 | }{haveBroadcaster.Load()} 141 | json.NewEncoder(w).Encode(&out) 142 | }) 143 | 144 | go func() { 145 | for range time.NewTicker(2 * time.Second).C { 146 | serialize() 147 | } 148 | }() 149 | 150 | fmt.Println("Open http://localhost:8080 to access this demo") 151 | panic(http.ListenAndServe(":8080", nil)) 152 | } 153 | 154 | func doSignaling(w http.ResponseWriter, r *http.Request) { 155 | s := webrtc.SettingEngine{} 156 | s.SetSRTPProtectionProfiles(dtls.SRTP_AEAD_AES_128_GCM) 157 | m := &webrtc.MediaEngine{} 158 | if err := m.RegisterDefaultCodecs(); err != nil { 159 | panic(err) 160 | } 161 | 162 | peerConnection, err := webrtc.NewAPI(webrtc.WithMediaEngine(m), webrtc.WithSettingEngine(s)).NewPeerConnection(webrtc.Configuration{}) 163 | if err != nil { 164 | panic(err) 165 | } 166 | 167 | peerConnection.OnConnectionStateChange(func(connectionState webrtc.PeerConnectionState) { 168 | onConnectionStateChangeHandler(peerConnection, connectionState) 169 | }) 170 | peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { 171 | onTrackHandler(peerConnection, track, receiver) 172 | }) 173 | 174 | var offer webrtc.SessionDescription 175 | if err = json.NewDecoder(r.Body).Decode(&offer); err != nil { 176 | panic(err) 177 | } 178 | 179 | if strings.Contains(offer.SDP, "recvonly") { 180 | if _, err = peerConnection.AddTrack(videoTrack); err != nil { 181 | panic(err) 182 | } else if _, err = peerConnection.AddTrack(audioTrack); err != nil { 183 | panic(err) 184 | } 185 | } 186 | 187 | if err = peerConnection.SetRemoteDescription(offer); err != nil { 188 | panic(err) 189 | } 190 | 191 | gatherComplete := webrtc.GatheringCompletePromise(peerConnection) 192 | answer, err := peerConnection.CreateAnswer(nil) 193 | if err != nil { 194 | panic(err) 195 | } else if err = peerConnection.SetLocalDescription(answer); err != nil { 196 | panic(err) 197 | } 198 | <-gatherComplete 199 | 200 | response, err := json.Marshal(*peerConnection.LocalDescription()) 201 | if err != nil { 202 | panic(err) 203 | } 204 | 205 | w.Header().Set("Content-Type", "application/json") 206 | if _, err := w.Write(response); err != nil { 207 | panic(err) 208 | } 209 | } 210 | 211 | func serialize() { 212 | state := GlobalState{ 213 | PeerConnectionState: []PeerConnectionState{}, 214 | } 215 | 216 | for i := range peerConnections { 217 | iceTransport := accessUnexported(peerConnections[i], "iceTransport").(*webrtc.ICETransport) 218 | dtlsTransport := accessUnexported(peerConnections[i], "dtlsTransport").(*webrtc.DTLSTransport) 219 | dtlsConn := accessUnexported(dtlsTransport, "conn").(*dtls.Conn) 220 | iceGatherer := accessUnexported(iceTransport, "gatherer").(*webrtc.ICEGatherer) 221 | iceAgent := accessUnexported(iceGatherer, "agent").(*ice.Agent) 222 | 223 | SSRCVideo, SSRCAudio := webrtc.SSRC(0), webrtc.SSRC(0) 224 | 225 | senders := peerConnections[i].GetSenders() 226 | for _, sender := range senders { 227 | encodes := sender.GetParameters().Encodings 228 | if len(encodes) == 0 { 229 | panic("Found no encodes") 230 | } 231 | 232 | if sender.Track().Kind() == webrtc.RTPCodecTypeVideo { 233 | SSRCVideo = encodes[0].SSRC 234 | } else { 235 | SSRCAudio = encodes[0].SSRC 236 | } 237 | 238 | } 239 | 240 | selectedCandidatePair, err := iceTransport.GetSelectedCandidatePair() 241 | if err != nil { 242 | panic(err) 243 | } 244 | 245 | localUfrag, localPwd, err := iceAgent.GetLocalUserCredentials() 246 | if err != nil { 247 | panic(err) 248 | } 249 | 250 | state.PeerConnectionState = append(state.PeerConnectionState, PeerConnectionState{ 251 | RemoteDescription: *peerConnections[i].RemoteDescription(), 252 | ICEPort: selectedCandidatePair.Local.Port, 253 | ICEUsernameFragment: localUfrag, 254 | ICEPassword: localPwd, 255 | DTLSConnectionState: dtlsConn.ConnectionState(), 256 | SSRCAudio: SSRCAudio, 257 | SSRCVideo: SSRCVideo, 258 | SRTPState: dtlsTransport.GetSRTPState(), 259 | }) 260 | } 261 | 262 | var toSave bytes.Buffer 263 | enc := gob.NewEncoder(&toSave) 264 | if err := enc.Encode(state); err != nil { 265 | panic(err) 266 | } 267 | if err := os.WriteFile(serializedPeerConnectionsFile, toSave.Bytes(), 0644); err != nil { 268 | panic(err) 269 | } 270 | } 271 | 272 | func deserialize(state GlobalState) { 273 | peerConnectionsMutex.Lock() 274 | defer peerConnectionsMutex.Unlock() 275 | 276 | fmt.Printf("Resuming %d sessions from '%s'\n", len(state.PeerConnectionState), serializedPeerConnectionsFile) 277 | 278 | for i := range state.PeerConnectionState { 279 | m := &webrtc.MediaEngine{} 280 | if err := m.RegisterDefaultCodecs(); err != nil { 281 | panic(err) 282 | } 283 | 284 | s := webrtc.SettingEngine{} 285 | s.SetSRTPProtectionProfiles(dtls.SRTP_AEAD_AES_128_GCM) 286 | s.SetICECredentials(state.PeerConnectionState[i].ICEUsernameFragment, state.PeerConnectionState[i].ICEPassword) 287 | if err := s.SetEphemeralUDPPortRange(state.PeerConnectionState[i].ICEPort, state.PeerConnectionState[i].ICEPort); err != nil { 288 | panic(err) 289 | } 290 | s.SetDTLSConnectionState(&state.PeerConnectionState[i].DTLSConnectionState) 291 | s.SetSRTPState(state.PeerConnectionState[i].SRTPState) 292 | 293 | peerConnection, err := webrtc.NewAPI(webrtc.WithSettingEngine(s), webrtc.WithMediaEngine(m)).NewPeerConnection(webrtc.Configuration{}) 294 | if err != nil { 295 | panic(err) 296 | } 297 | peerConnection.OnConnectionStateChange(func(connectionState webrtc.PeerConnectionState) { 298 | onConnectionStateChangeHandler(peerConnection, connectionState) 299 | }) 300 | peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { 301 | onTrackHandler(peerConnection, track, receiver) 302 | }) 303 | 304 | if strings.Contains(state.PeerConnectionState[i].RemoteDescription.SDP, "recvonly") { 305 | if _, err = peerConnection.AddTransceiverFromTrack(videoTrack, webrtc.RTPTransceiverInit{ 306 | Direction: webrtc.RTPTransceiverDirectionSendonly, 307 | SSRCOverride: state.PeerConnectionState[i].SSRCVideo, 308 | }); err != nil { 309 | panic(err) 310 | } else if _, err = peerConnection.AddTransceiverFromTrack(audioTrack, webrtc.RTPTransceiverInit{ 311 | Direction: webrtc.RTPTransceiverDirectionSendonly, 312 | SSRCOverride: state.PeerConnectionState[i].SSRCAudio, 313 | }); err != nil { 314 | panic(err) 315 | } 316 | } 317 | 318 | if err := peerConnection.SetRemoteDescription(state.PeerConnectionState[i].RemoteDescription); err != nil { 319 | panic(err) 320 | } 321 | 322 | answer, err := peerConnection.CreateAnswer(nil) 323 | if err != nil { 324 | panic(err) 325 | } else if err = peerConnection.SetLocalDescription(answer); err != nil { 326 | panic(err) 327 | } 328 | 329 | } 330 | } 331 | 332 | func onConnectionStateChangeHandler(peerConnection *webrtc.PeerConnection, connectionState webrtc.PeerConnectionState) { 333 | peerConnectionsMutex.Lock() 334 | defer peerConnectionsMutex.Unlock() 335 | 336 | fmt.Printf("PeerConnection is now: %s\n", connectionState) 337 | 338 | if connectionState == webrtc.PeerConnectionStateFailed { 339 | n := 0 340 | for _, savedPeerConnection := range peerConnections { 341 | if savedPeerConnection != peerConnection { 342 | peerConnections[n] = savedPeerConnection 343 | n++ 344 | } 345 | } 346 | peerConnections = peerConnections[:n] 347 | peerConnection.Close() 348 | } else if connectionState == webrtc.PeerConnectionStateConnected { 349 | peerConnections = append(peerConnections, peerConnection) 350 | } 351 | 352 | if connectionState == webrtc.PeerConnectionStateFailed || connectionState == webrtc.PeerConnectionStateConnected { 353 | serialize() 354 | } 355 | } 356 | 357 | func onTrackHandler(peerConnection *webrtc.PeerConnection, track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { 358 | haveBroadcaster.Store(true) 359 | go func() { 360 | ticker := time.NewTicker(time.Millisecond * 200) 361 | for range ticker.C { 362 | errSend := peerConnection.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: uint32(track.SSRC())}}) 363 | if errSend != nil { 364 | return 365 | } 366 | } 367 | }() 368 | 369 | outputTrack := videoTrack 370 | if strings.HasPrefix(track.Codec().MimeType, "audio") { 371 | outputTrack = audioTrack 372 | } 373 | 374 | for { 375 | // Read RTP packets being sent to Pion 376 | rtp, _, readErr := track.ReadRTP() 377 | if errors.Is(readErr, io.EOF) { 378 | return 379 | } else if readErr != nil { 380 | panic(readErr) 381 | } 382 | 383 | if writeErr := outputTrack.WriteRTP(rtp); writeErr != nil { 384 | panic(writeErr) 385 | } 386 | } 387 | } 388 | 389 | func accessUnexported(object any, field string) any { 390 | v := reflect.ValueOf(object).Elem().FieldByName(field) 391 | return reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr())).Elem().Interface() 392 | } 393 | --------------------------------------------------------------------------------