Remove the need to use Chrome Canary.
Barely works but getting there.
This commit is contained in:
parent
476f0fbce1
commit
ff73adc537
|
@ -1,2 +1,3 @@
|
|||
*.mp4
|
||||
logs/
|
||||
.DS_Store
|
||||
|
|
16
README.md
16
README.md
|
@ -50,7 +50,7 @@ wget http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBun
|
|||
|
||||
Use ffmpeg to create a LL-DASH playlist. This creates a segment every 2s and MP4 fragment every 10ms.
|
||||
```
|
||||
ffmpeg -i media/source.mp4 -f dash -use_timeline 0 -r:v 24 -g:v 48 -keyint_min:v 48 -sc_threshold:v 0 -tune zerolatency -streaming 1 -ldash 1 -seg_duration 2 -frag_duration 0.01 -frag_type duration media/playlist.mpd
|
||||
./media/generate
|
||||
```
|
||||
|
||||
You can increase the `frag_duration` (microseconds) to slightly reduce the file size in exchange for higher latency.
|
||||
|
@ -62,10 +62,10 @@ If you have a valid certificate you can use it instead of self-signing. The go b
|
|||
|
||||
Otherwise, use [mkcert](https://github.com/FiloSottile/mkcert) to install a self-signed CA:
|
||||
```
|
||||
mkcert -install
|
||||
./generate/cert
|
||||
```
|
||||
|
||||
With no arguments, the server will generate self-signed cert using this root CA.
|
||||
With no arguments, the server will generate self-signed cert using this root CA. This certificate is only valid for *2 weeks* due to how WebTransport performs certificate fingerprinting.
|
||||
|
||||
## Server
|
||||
The Warp server supports WebTransport, pushing media over streams once a connection has been established. A more refined implementation would load content based on the WebTransport URL or some other messaging scheme.
|
||||
|
@ -89,13 +89,3 @@ yarn serve
|
|||
These can be accessed on `https://localhost:4444` by default.
|
||||
|
||||
If you use a custom domain for the Warp server, make sure to override the server URL with the `url` query string parameter, e.g. `https://localhost:4444/?url=https://warp.demo`.
|
||||
|
||||
## Chrome
|
||||
Now we need to make Chrome accept these certificates, which normally would involve trusting a root CA but this was not working with WebTransport when I last tried.
|
||||
|
||||
Instead, we need to run a *fresh instance* of Chrome, instructing it to allow our self-signed certificate. This command will not work if Chrome is already running, so it's easier to use Chrome Canary instead.
|
||||
|
||||
Launch a new instance of Chrome Canary:
|
||||
```
|
||||
/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary --allow-insecure-localhost --origin-to-force-quic-on=localhost:4443 https://localhost:4444
|
||||
```
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
HOST="localhost"
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
# Outputs the certificate fingerprint in the format Chrome expects
|
||||
openssl x509 -pubkey -noout -in "${HOST}.crt" |
|
||||
openssl rsa -pubin -outform der 2>/dev/null |
|
||||
openssl dgst -sha256 -binary |
|
||||
base64
|
|
@ -3,4 +3,20 @@ set -euxo pipefail
|
|||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
mkcert -cert-file localhost.crt -key-file localhost.key localhost 127.0.0.1 ::1
|
||||
# Generate a new RSA key/cert for local development
|
||||
HOST="localhost"
|
||||
CRT="$HOST.crt"
|
||||
KEY="$HOST.key"
|
||||
|
||||
# Install the system certificate if it's not already
|
||||
mkcert -install
|
||||
|
||||
# Generate a new certificate for localhost
|
||||
mkcert -ecdsa -cert-file "$CRT" -key-file "$KEY" localhost 127.0.0.1 ::1
|
||||
|
||||
# Reduce the expiration time of the certificate to 14 days; the WebTransport maximum.
|
||||
# TODO https://github.com/FiloSottile/mkcert/pull/513
|
||||
openssl x509 -days 14 -in "$CRT" -signkey "$KEY" -out "$CRT"
|
||||
|
||||
# Compute the sha256 fingerprint of the certificate for WebTransport
|
||||
# openssl x509 -in "$CRT" -outform der | openssl dgst -sha256
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"source": "src/index.html",
|
||||
"scripts": {
|
||||
"serve": "parcel serve --https --host localhost --port 4444 --cert ../cert/localhost.crt --key ../cert/localhost.key",
|
||||
"serve": "parcel serve --https --cert ../cert/localhost.crt --key ../cert/localhost.key --host localhost --port 4444 --open",
|
||||
"build": "parcel build",
|
||||
"check": "tsc --noEmit"
|
||||
},
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<meta charset = "UTF-8">
|
||||
<title>WARP</title>
|
||||
|
||||
<link rel="stylesheet" href="player.css">
|
||||
<link rel="stylesheet" href="index.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -29,54 +29,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { Player } from "./player.ts"
|
||||
|
||||
// This is so ghetto but I'm too lazy to improve it right now
|
||||
const vidRef = document.getElementById("vid")
|
||||
const liveRef = document.getElementById("live")
|
||||
const throttleRef = document.getElementById("throttle")
|
||||
const statsRef = document.getElementById("stats")
|
||||
const playRef = document.getElementById("play")
|
||||
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
|
||||
const player = new Player({
|
||||
url: params.get("url") || "https://localhost:4443",
|
||||
|
||||
vid: vidRef,
|
||||
stats: statsRef,
|
||||
throttle: throttleRef,
|
||||
})
|
||||
|
||||
liveRef.addEventListener("click", (e) => {
|
||||
e.preventDefault()
|
||||
player.goLive()
|
||||
})
|
||||
|
||||
throttleRef.addEventListener("click", (e) => {
|
||||
e.preventDefault()
|
||||
player.throttle()
|
||||
})
|
||||
|
||||
playRef.addEventListener('click', (e) => {
|
||||
vidRef.play()
|
||||
e.preventDefault()
|
||||
})
|
||||
|
||||
function playFunc(e) {
|
||||
playRef.style.display = "none"
|
||||
//player.goLive()
|
||||
|
||||
// Only fire once to restore pause/play functionality
|
||||
vidRef.removeEventListener('play', playFunc)
|
||||
}
|
||||
|
||||
vidRef.addEventListener('play', playFunc)
|
||||
vidRef.volume = 0.5
|
||||
|
||||
// Try to autoplay but ignore errors on mobile; they need to click
|
||||
//vidRef.play().catch((e) => console.warn(e))
|
||||
</script>
|
||||
<script src="index.ts" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
import { Player } from "./player"
|
||||
|
||||
// This is so ghetto but I'm too lazy to improve it right now
|
||||
const videoRef = document.querySelector<HTMLVideoElement>("video#vid")!;
|
||||
const liveRef = document.querySelector<HTMLElement>("#live")!;
|
||||
const throttleRef = document.querySelector<HTMLElement>("#throttle")!;
|
||||
const statsRef = document.querySelector<HTMLElement>("#stats")!;
|
||||
const playRef = document.querySelector<HTMLElement>("#play")!;
|
||||
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
|
||||
const url = params.get("url") || "https://localhost:4443/watch"
|
||||
|
||||
const player = new Player({
|
||||
url: url,
|
||||
videoRef: videoRef,
|
||||
statsRef: statsRef,
|
||||
throttleRef: throttleRef,
|
||||
})
|
||||
|
||||
liveRef.addEventListener("click", (e) => {
|
||||
e.preventDefault()
|
||||
player.goLive()
|
||||
})
|
||||
|
||||
throttleRef.addEventListener("click", (e) => {
|
||||
e.preventDefault()
|
||||
player.throttle()
|
||||
})
|
||||
|
||||
playRef.addEventListener('click', (e) => {
|
||||
videoRef.play()
|
||||
e.preventDefault()
|
||||
})
|
||||
|
||||
function playFunc(e: Event) {
|
||||
playRef.style.display = "none"
|
||||
//player.goLive()
|
||||
|
||||
// Only fire once to restore pause/play functionality
|
||||
videoRef.removeEventListener('play', playFunc)
|
||||
}
|
||||
|
||||
videoRef.addEventListener('play', playFunc)
|
||||
videoRef.volume = 0.5
|
||||
|
||||
// Try to autoplay but ignore errors on mobile; they need to click
|
||||
//vidRef.play().catch((e) => console.warn(e))
|
|
@ -7,6 +7,18 @@ import { Message, MessageInit, MessageSegment } from "./message"
|
|||
|
||||
///<reference path="./types/webtransport.d.ts"/>
|
||||
|
||||
export interface PlayerInit {
|
||||
url: string;
|
||||
|
||||
videoRef: HTMLVideoElement;
|
||||
statsRef: HTMLElement;
|
||||
throttleRef: HTMLElement;
|
||||
}
|
||||
|
||||
/*
|
||||
*/
|
||||
|
||||
|
||||
export class Player {
|
||||
mediaSource: MediaSource;
|
||||
|
||||
|
@ -20,17 +32,17 @@ export class Player {
|
|||
// References to elements in the DOM
|
||||
vidRef: HTMLVideoElement; // The video element itself
|
||||
statsRef: HTMLElement; // The stats div
|
||||
throttleRef: HTMLButtonElement; // The throttle button
|
||||
throttleRef: HTMLElement; // The throttle button
|
||||
throttleCount: number; // number of times we've clicked the button in a row
|
||||
|
||||
interval: number;
|
||||
|
||||
timeRef?: DOMHighResTimeStamp;
|
||||
|
||||
constructor(props: any) {
|
||||
this.vidRef = props.vid
|
||||
this.statsRef = props.stats
|
||||
this.throttleRef = props.throttle
|
||||
constructor(props: PlayerInit) {
|
||||
this.vidRef = props.videoRef
|
||||
this.statsRef = props.statsRef
|
||||
this.throttleRef = props.throttleRef
|
||||
this.throttleCount = 0
|
||||
|
||||
this.mediaSource = new MediaSource()
|
||||
|
@ -43,8 +55,7 @@ export class Player {
|
|||
this.interval = setInterval(this.tick.bind(this), 100)
|
||||
this.vidRef.addEventListener("waiting", this.tick.bind(this))
|
||||
|
||||
const quic = new WebTransport(props.url)
|
||||
this.quic = quic.ready.then(() => { return quic });
|
||||
this.quic = this.connect(props.url)
|
||||
|
||||
// Create a unidirectional stream for all of our messages
|
||||
this.api = this.quic.then((q) => {
|
||||
|
@ -63,6 +74,38 @@ export class Player {
|
|||
(await this.quic).close()
|
||||
}
|
||||
|
||||
async connect(url: string): Promise<WebTransport> {
|
||||
// TODO remove this when WebTransport supports the system CA pool
|
||||
const fingerprintURL = new URL(url);
|
||||
fingerprintURL.pathname = "/fingerprint"
|
||||
|
||||
const response = await fetch(fingerprintURL)
|
||||
if (!response.ok) {
|
||||
throw new Error('failed to get server fingerprint');
|
||||
}
|
||||
|
||||
const hex = await response.text()
|
||||
|
||||
// Convert the hex to binary.
|
||||
let fingerprint = [];
|
||||
for (let c = 0; c < hex.length; c += 2) {
|
||||
fingerprint.push(parseInt(hex.substring(c, c+2), 16));
|
||||
}
|
||||
|
||||
//const fingerprint = Uint8Array.from(atob(hex), c => c.charCodeAt(0))
|
||||
|
||||
const quic = new WebTransport(url, {
|
||||
"serverCertificateHashes": [{
|
||||
"algorithm": "sha-256",
|
||||
"value": new Uint8Array(fingerprint),
|
||||
}]
|
||||
})
|
||||
|
||||
await quic.ready
|
||||
|
||||
return quic
|
||||
}
|
||||
|
||||
async sendMessage(msg: any) {
|
||||
const payload = JSON.stringify(msg)
|
||||
const size = payload.length + 8
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
{
|
||||
"include": ["src/**/*"],
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"target": "es2021",
|
||||
"strict": true,
|
||||
"typeRoots": [
|
||||
"typeRoots": [
|
||||
"src/types"
|
||||
],
|
||||
"allowJs": true
|
||||
],
|
||||
"allowJs": true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
module github.com/kixelated/warp-demo/server
|
||||
module github.com/kixelated/warp/server
|
||||
|
||||
go 1.18
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@ github.com/kisielk/errcheck v1.4.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
|
|||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kixelated/invoker v1.0.0 h1:0wYlvK39yQPbkwIFy+YN41AhF89WOtGyWqV2pZB39xw=
|
||||
github.com/kixelated/invoker v1.0.0/go.mod h1:RjG3iqm/sKwZjOpcW4SGq+l+4DJCDR/yUtc70VjCRB8=
|
||||
github.com/kixelated/quic-go v1.31.0 h1:O3JomeXPnLNSCNpZF415NWOyfpzbFfuvP6dlIDg8VEA=
|
||||
github.com/kixelated/quic-go v1.31.0 h1:p2vq3Otvtmz+0EP23vjumnO/HU4Q/DFxNF6xNryVfmA=
|
||||
github.com/kixelated/quic-go v1.31.0/go.mod h1:AO7pURnb8HXHmdalp5e09UxQfsuwseEhl0NLmwiSOFY=
|
||||
github.com/kixelated/webtransport-go v1.4.1 h1:ZtY3P7hVe1wK5fAt71b+HHnNISFDcQ913v+bvaNATxA=
|
||||
github.com/kixelated/webtransport-go v1.4.1/go.mod h1:6RV5pTXF7oP53T83bosSDsLdSdw31j5cfpMDqsO4D5k=
|
||||
|
|
|
@ -20,20 +20,23 @@ import (
|
|||
)
|
||||
|
||||
type Server struct {
|
||||
inner *webtransport.Server
|
||||
media *Media
|
||||
|
||||
inner *webtransport.Server
|
||||
media *Media
|
||||
sessions invoker.Tasks
|
||||
cert *tls.Certificate
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
type Config struct {
|
||||
Addr string
|
||||
Cert *tls.Certificate
|
||||
LogDir string
|
||||
Media *Media
|
||||
}
|
||||
|
||||
func NewServer(config ServerConfig, media *Media) (s *Server, err error) {
|
||||
func New(config Config) (s *Server, err error) {
|
||||
s = new(Server)
|
||||
s.cert = config.Cert
|
||||
s.media = config.Media
|
||||
|
||||
quicConfig := &quic.Config{}
|
||||
|
||||
|
@ -52,10 +55,12 @@ func NewServer(config ServerConfig, media *Media) (s *Server, err error) {
|
|||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{*config.Cert},
|
||||
Certificates: []tls.Certificate{*s.cert},
|
||||
}
|
||||
|
||||
// Host a HTTP/3 server to serve the WebTransport endpoint
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/watch", s.handleWatch)
|
||||
|
||||
s.inner = &webtransport.Server{
|
||||
H3: http3.Server{
|
||||
|
@ -67,28 +72,6 @@ func NewServer(config ServerConfig, media *Media) (s *Server, err error) {
|
|||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
s.media = media
|
||||
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
hijacker, ok := w.(http3.Hijacker)
|
||||
if !ok {
|
||||
panic("unable to hijack connection: must use kixelated/quic-go")
|
||||
}
|
||||
|
||||
conn := hijacker.Connection()
|
||||
|
||||
sess, err := s.inner.Upgrade(w, r)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to upgrade session", 500)
|
||||
return
|
||||
}
|
||||
|
||||
err = s.serve(r.Context(), conn, sess)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
})
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
|
@ -98,7 +81,7 @@ func (s *Server) runServe(ctx context.Context) (err error) {
|
|||
|
||||
func (s *Server) runShutdown(ctx context.Context) (err error) {
|
||||
<-ctx.Done()
|
||||
s.inner.Close()
|
||||
s.inner.Close() // close on context shutdown
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
|
@ -106,7 +89,27 @@ func (s *Server) Run(ctx context.Context) (err error) {
|
|||
return invoker.Run(ctx, s.runServe, s.runShutdown, s.sessions.Repeat)
|
||||
}
|
||||
|
||||
func (s *Server) serve(ctx context.Context, conn quic.Connection, sess *webtransport.Session) (err error) {
|
||||
func (s *Server) handleWatch(w http.ResponseWriter, r *http.Request) {
|
||||
hijacker, ok := w.(http3.Hijacker)
|
||||
if !ok {
|
||||
panic("unable to hijack connection: must use kixelated/quic-go")
|
||||
}
|
||||
|
||||
conn := hijacker.Connection()
|
||||
|
||||
sess, err := s.inner.Upgrade(w, r)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to upgrade session", 500)
|
||||
return
|
||||
}
|
||||
|
||||
err = s.serveSession(r.Context(), conn, sess)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) serveSession(ctx context.Context, conn quic.Connection, sess *webtransport.Session) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
sess.CloseWithError(1, err.Error())
|
|
@ -0,0 +1,64 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/kixelated/invoker"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
inner http.Server
|
||||
config Config
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Addr string
|
||||
CertFile string
|
||||
KeyFile string
|
||||
Fingerprint string // the TLS certificate fingerprint
|
||||
}
|
||||
|
||||
func New(config Config) (s *Server) {
|
||||
s = new(Server)
|
||||
s.config = config
|
||||
|
||||
s.inner = http.Server{
|
||||
Addr: config.Addr,
|
||||
}
|
||||
|
||||
http.HandleFunc("/fingerprint", s.handleFingerprint)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Server) Run(ctx context.Context) (err error) {
|
||||
return invoker.Run(ctx, s.runServe, s.runShutdown)
|
||||
}
|
||||
|
||||
func (s *Server) runServe(context.Context) (err error) {
|
||||
// NOTE: Doesn't support context, which is why we need runShutdown
|
||||
err = s.inner.ListenAndServeTLS(s.config.CertFile, s.config.KeyFile)
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Gracefully shut down the server when the context is cancelled
|
||||
func (s *Server) runShutdown(ctx context.Context) (err error) {
|
||||
<-ctx.Done()
|
||||
|
||||
timeout, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
_ = s.inner.Shutdown(timeout)
|
||||
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// Return the sha256 of the certificate as a temporary work-around for local development.
|
||||
// TODO remove this when WebTransport uses the system CA
|
||||
func (s *Server) handleFingerprint(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
_, _ = w.Write([]byte(s.config.Fingerprint))
|
||||
}
|
|
@ -2,13 +2,16 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/kixelated/invoker"
|
||||
"github.com/kixelated/warp-demo/server/internal/warp"
|
||||
"github.com/kixelated/warp/server/internal/warp"
|
||||
"github.com/kixelated/warp/server/internal/web"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -38,18 +41,31 @@ func run(ctx context.Context) (err error) {
|
|||
return fmt.Errorf("failed to load TLS certificate: %w", err)
|
||||
}
|
||||
|
||||
config := warp.ServerConfig{
|
||||
warpConfig := warp.Config{
|
||||
Addr: *addr,
|
||||
Cert: &tlsCert,
|
||||
LogDir: *logDir,
|
||||
Media: media,
|
||||
}
|
||||
|
||||
ws, err := warp.NewServer(config, media)
|
||||
warpServer, err := warp.New(warpConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create warp server: %w", err)
|
||||
}
|
||||
|
||||
hash := sha256.Sum256(tlsCert.Certificate[0])
|
||||
fingerprint := hex.EncodeToString(hash[:])
|
||||
|
||||
webConfig := web.Config{
|
||||
Addr: *addr,
|
||||
CertFile: *cert,
|
||||
KeyFile: *key,
|
||||
Fingerprint: fingerprint,
|
||||
}
|
||||
|
||||
webServer := web.New(webConfig)
|
||||
|
||||
log.Printf("listening on %s", *addr)
|
||||
|
||||
return invoker.Run(ctx, invoker.Interrupt, ws.Run)
|
||||
return invoker.Run(ctx, invoker.Interrupt, warpServer.Run, webServer.Run)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue