diff --git a/src/commands/relay.rs b/src/commands/relay.rs
index b02979b..7b712a3 100644
--- a/src/commands/relay.rs
+++ b/src/commands/relay.rs
@@ -1,15 +1,18 @@
 use std::net::ToSocketAddrs;
 use std::sync::{Arc, Mutex, Weak};
+use std::time::{SystemTime, UNIX_EPOCH, Duration};
 
 use bytes::{Buf, Bytes};
 use clap::Args;
 use futures::{prelude::*, stream::FuturesUnordered, Stream};
+use html_escape::encode_double_quoted_attribute;
 use hyper::{
     header::{CACHE_CONTROL, CONTENT_TYPE},
     Body, Response,
 };
 use stream::iter;
-use warp::{self, path, Filter};
+use warp::reply::{html, with_header};
+use warp::{self, path, Filter, Reply};
 use weak_table::WeakValueHashMap;
 use webmetro::{
     channel::{Channel, Handle, Listener, Transmitter},
@@ -59,6 +62,21 @@ fn media_response(body: Body) -> Response<Body> {
         .unwrap()
 }
 
+fn player_css() -> impl Reply {
+    let css = include_str!("../data/player.css");
+    with_header(css, CONTENT_TYPE, "text/css")
+}
+
+fn player_html(channel: impl AsRef<str>) -> impl Reply {
+    let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or(Duration::ZERO).as_nanos();
+    let player = format!(
+        include_str!("../data/player.html"),
+        channel = encode_double_quoted_attribute(channel.as_ref()),
+        cachebust = timestamp
+    );
+    html(player)
+}
+
 /// Hosts an HTTP-based relay server
 #[derive(Args, Debug)]
 pub struct RelayArgs {
@@ -107,7 +125,11 @@ pub async fn run(args: RelayArgs) -> Result<(), WebmetroError> {
             Response::new(Body::wrap_stream(post_stream(channel, stream)))
         });
 
-    let routes = head.or(get).or(post_put);
+    let live = head.or(get).or(post_put);
+    let watch = path!("watch" / String).map(player_html);
+    let css = path!("static" / "css").map(player_css);
+
+    let routes = live.or(watch).or(css);
 
     let mut server_futures: FuturesUnordered<_> = addrs
         .map(|addr| warp::serve(routes.clone()).try_bind(addr))
diff --git a/src/data/player.css b/src/data/player.css
new file mode 100644
index 0000000..d156bd4
--- /dev/null
+++ b/src/data/player.css
@@ -0,0 +1,20 @@
+body {
+    display: flex;
+    flex-flow: column;
+    align-items: center;
+    margin: 0;
+    padding: 0;
+
+    background: #111;
+    color: #777;
+    font-size: 16px;
+    line-height: 16px;
+    font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
+}
+section {
+    display: flex;
+    flex-flow: column;
+}
+video {
+    max-width: 100vw;
+}
diff --git a/src/data/player.html b/src/data/player.html
new file mode 100644
index 0000000..3ed1c13
--- /dev/null
+++ b/src/data/player.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Watchog Stream</title>
+    <meta name="viewport" content="initial-scale=1">
+    <link rel="stylesheet" href="../static/css" />
+</head>
+<body>
+    <section>
+        <video controls autoplay src="../live/{channel}?t={cachebust}"></video>
+        <p>The stream should begin automatically when ready;
+        if the video stutters, try pausing it for a second or two to allow a small buffer.</p>
+    </section>
+</body>
+</html>