Got the movie working in the gallery
This commit is contained in:
1 parent
8861858f0d
commit
782820261e
15 files changed
+1291
-111
No files matched your search
+105
-9
@@ -2,13 +2,18 @@
|
||||
"""Serves gallery/ (from scripts/make-gallery.sh) as a web page on every interface, for
|
||||
looking at the example renders from another machine on the network.
|
||||
|
||||
python3 scripts/serve-gallery.py [--port 8000]
|
||||
python3 scripts/serve-gallery.py [--port 8000] [--live-port 8010]
|
||||
|
||||
Each example scene gets a card with its L0 and L1 renders side by side, captioned
|
||||
from the comment at the top of its file in examples/scenes/. Click an image to see it
|
||||
full size; arrow keys step through them and Escape closes. The page is built on each
|
||||
request, so rerunning make-gallery.sh and reloading shows the new images. Only the
|
||||
PNG files in gallery/ are served.
|
||||
Stills: each example scene gets a card with its L0 and L1 renders side by side,
|
||||
captioned from the comment at the top of its file in examples/scenes/. Click an image
|
||||
to see it full size; arrow keys step through them and Escape closes.
|
||||
|
||||
Movie: gallery/movie.mp4 (from make-gallery.sh or resolved-movie --out) to play or
|
||||
download, and the live stream from `resolved-movie --serve 0.0.0.0:8010` on this
|
||||
machine, which the page connects to only while the tab is open.
|
||||
|
||||
The page is built on each request, so rerunning make-gallery.sh and reloading shows
|
||||
the new images. Only the PNG files and movie.mp4 in gallery/ are served.
|
||||
|
||||
Needs only the Python standard library.
|
||||
"""
|
||||
@@ -75,9 +80,37 @@ figcaption b { color: var(--text); font-weight: 600; }
|
||||
#view.open { display: flex; }
|
||||
#view img { max-width: 100%; max-height: calc(100vh - 4rem); }
|
||||
#view div { color: var(--muted); font-size: 0.9rem; }
|
||||
[hidden] { display: none !important; }
|
||||
nav { max-width: 1400px; margin: 0 auto; padding: 0 1.5rem; display: flex; gap: 0.5rem; }
|
||||
nav a {
|
||||
color: var(--muted); text-decoration: none; padding: 0.45rem 1rem; border-radius: 999px;
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
nav a.on { color: var(--bg); background: var(--accent); border-color: var(--accent); }
|
||||
.movie video, .movie .live {
|
||||
width: 100%; aspect-ratio: 16 / 9; display: block; background: #000;
|
||||
border-radius: 8px; border: 1px solid var(--line);
|
||||
}
|
||||
.movie .live { object-fit: contain; }
|
||||
.movie a.download { color: var(--accent); }
|
||||
.movie code { color: var(--text); }
|
||||
"""
|
||||
|
||||
SCRIPT = """
|
||||
const live = document.getElementById('live');
|
||||
function tab() {
|
||||
const movie = location.hash === '#movie';
|
||||
// The panels' ids differ from the links' anchors, so a link switches the tab
|
||||
// instead of scrolling the page to it.
|
||||
document.getElementById('stills-tab').hidden = movie;
|
||||
document.getElementById('movie-tab').hidden = !movie;
|
||||
document.querySelectorAll('nav a').forEach(
|
||||
(a) => a.classList.toggle('on', (a.hash === '#movie') === movie));
|
||||
// The live stream renders only while someone watches: connect only on this tab.
|
||||
if (live) live.src = movie ? live.dataset.src : '';
|
||||
}
|
||||
window.addEventListener('hashchange', tab);
|
||||
tab();
|
||||
const images = [...document.querySelectorAll('figure img')];
|
||||
const view = document.getElementById('view');
|
||||
const big = view.querySelector('img');
|
||||
@@ -120,7 +153,40 @@ def caption(scene):
|
||||
return paragraphs
|
||||
|
||||
|
||||
def page():
|
||||
def movie_section(host, live_port):
|
||||
movie = GALLERY / "movie.mp4"
|
||||
if movie.is_file():
|
||||
stamp = int(movie.stat().st_mtime)
|
||||
size = movie.stat().st_size / 1e6
|
||||
recorded = (
|
||||
f'<video src="/movie.mp4?{stamp}" controls loop playsinline></video>'
|
||||
f'<p class="caption"><a class="download" href="/movie.mp4?{stamp}" '
|
||||
f'download="resolved-movie.mp4">Download the MP4</a> ({size:.1f} MB)</p>'
|
||||
)
|
||||
else:
|
||||
recorded = (
|
||||
'<p class="caption">No movie yet: run scripts/make-gallery.sh, or '
|
||||
"<code>cargo run --release -p resolved-showcase -- --out gallery/movie.mp4</code>"
|
||||
".</p>"
|
||||
)
|
||||
stream = f"http://{host}:{live_port}/stream"
|
||||
return f"""<section class="movie"><h2>The movie</h2>
|
||||
<p class="caption">An inspector holds station 120 m behind and above a Landsat 8 model
|
||||
705 km up; the target is propagated by astrokit with J2 and everything is rendered at
|
||||
lighting L1. The camera drifts on a small natural relative orbit around its station and
|
||||
sets its exposure frame by frame. Time runs fast over the daylit Earth, slows as the
|
||||
Moon rises over the limb ahead, and slows almost to real time as the spacecraft passes
|
||||
into Earth's shadow.</p>{recorded}</section>
|
||||
<section class="movie"><h2>Live</h2>
|
||||
<p class="caption">Rendered in real time on this machine's GPU and streamed as it is
|
||||
made. Start it with
|
||||
<code>cargo run --release -p resolved-showcase -- --serve 0.0.0.0:{live_port}</code>;
|
||||
it renders only while this tab is open.</p>
|
||||
<img id="live" class="live" data-src="{html.escape(stream)}" alt="live stream (not running)">
|
||||
</section>"""
|
||||
|
||||
|
||||
def page(host, live_port):
|
||||
names = sorted({p.stem.rsplit("-", 1)[0] for p in GALLERY.glob("*.png")})
|
||||
cards = []
|
||||
count = 0
|
||||
@@ -168,7 +234,9 @@ def page():
|
||||
<body><header><h1><span>resolved</span> gallery</h1>
|
||||
<p>{count} renders of {len(names)} example scenes, last rendered {when}.
|
||||
Click an image to see it full size.</p></header>
|
||||
<main>{"".join(cards)}</main>
|
||||
<nav><a href="#stills">Stills</a><a href="#movie">Movie</a></nav>
|
||||
<main id="stills-tab">{"".join(cards)}</main>
|
||||
<main id="movie-tab" hidden>{movie_section(host, live_port)}</main>
|
||||
<div id="view"><img alt=""><div></div></div>
|
||||
<script>{SCRIPT}</script></body></html>"""
|
||||
|
||||
@@ -177,7 +245,11 @@ class Handler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
path = unquote(self.path.split("?", 1)[0])
|
||||
if path in ("/", "/index.html"):
|
||||
self.send(200, "text/html; charset=utf-8", page().encode())
|
||||
host = (self.headers.get("Host") or "localhost").rsplit(":", 1)[0]
|
||||
body = page(host, self.server.live_port).encode()
|
||||
self.send(200, "text/html; charset=utf-8", body)
|
||||
elif path == "/movie.mp4" and (GALLERY / "movie.mp4").is_file():
|
||||
self.send_movie(GALLERY / "movie.mp4")
|
||||
elif path.startswith("/img/"):
|
||||
name = path[len("/img/"):]
|
||||
# Only a PNG directly in gallery/, by its exact name.
|
||||
@@ -189,6 +261,27 @@ class Handler(BaseHTTPRequestHandler):
|
||||
else:
|
||||
self.send(404, "text/plain", b"not found")
|
||||
|
||||
def send_movie(self, movie):
|
||||
"""The movie, or the byte range asked for: browsers seek in a video by range."""
|
||||
data = movie.read_bytes()
|
||||
wanted = self.headers.get("Range", "")
|
||||
if wanted.startswith("bytes="):
|
||||
first, _, last = wanted[len("bytes="):].partition("-")
|
||||
start = int(first) if first else max(len(data) - int(last), 0)
|
||||
end = int(last) if first and last else len(data) - 1
|
||||
end = min(end, len(data) - 1)
|
||||
part = data[start:end + 1]
|
||||
self.send_response(206)
|
||||
self.send_header("Content-Range", f"bytes {start}-{end}/{len(data)}")
|
||||
else:
|
||||
part = data
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "video/mp4")
|
||||
self.send_header("Accept-Ranges", "bytes")
|
||||
self.send_header("Content-Length", str(len(part)))
|
||||
self.end_headers()
|
||||
self.wfile.write(part)
|
||||
|
||||
def send(self, status, kind, body):
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", kind)
|
||||
@@ -215,8 +308,11 @@ def lan_address():
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
||||
parser.add_argument("--port", type=int, default=8000)
|
||||
parser.add_argument("--live-port", type=int, default=8010,
|
||||
help="the port resolved-movie --serve streams on")
|
||||
args = parser.parse_args()
|
||||
server = ThreadingHTTPServer(("0.0.0.0", args.port), Handler)
|
||||
server.live_port = args.live_port
|
||||
print(f"serving {GALLERY} at http://{lan_address()}:{args.port}/ (Ctrl+C to stop)", flush=True)
|
||||
try:
|
||||
server.serve_forever()
|
||||
|
||||
Reference in new issue
Block a user