Beholder is a Scala 3 HTTP microservice that extracts individual frames from videos as JPEG or PNG images using ffmpeg. It exposes POST /capture endpoints that accept a video URL and an elapsed time in milliseconds, return the image, and cache results on disk to avoid redundant ffmpeg invocations.
Capture a frame from a video at the given elapsed time. Returns the image directly (JPEG by default), or a cached copy if one exists. Use the optional imageType field to request a PNG instead.
Headers
| Header | Required | Description |
|---|---|---|
X-Api-Key |
Yes | API key for access (default: foo) |
Content-Type |
Yes | Must be application/json |
Query Parameters
| Parameter | Default | Description |
|---|---|---|
accurate |
true |
When true, seek to the exact elapsed time. When false, seek to the nearest keyframe before it (faster) |
nokey |
false |
When true, skip non-key frames after the seek point. Useful for fast processing of some codecs |
Request Body
| Field | Required | Description |
|---|---|---|
videoUrl |
Yes | URL of the video file |
elapsedTimeMillis |
Yes | Elapsed time in milliseconds |
imageType |
No | "jpg" or "png" (default: "jpg") |
{
"videoUrl": "http://m3.shore.mbari.org/videos/M3/proxy/DocRicketts/2022/03/1436/D1436_20220322T132758Z_h264.mp4",
"elapsedTimeMillis": 1234,
"imageType": "png"
}Important
PNGs are considerably larger than JPEGs and will increase latency as well as reduce the number of images that can be stored in cache.
Example — JPEG (default)
curl -X POST http://localhost:8080/capture \
-H "X-Api-Key: foo" \
-H "Content-Type: application/json" \
-d '{"videoUrl":"http://example.com/video.mp4","elapsedTimeMillis":1234}' \
--output frame.jpgExample — PNG
curl -X POST http://localhost:8080/capture \
-H "X-Api-Key: foo" \
-H "Content-Type: application/json" \
-d '{"videoUrl":"http://example.com/video.mp4","elapsedTimeMillis":1234,"imageType":"png"}' \
--output frame.pngExample with query params
curl -X POST "http://localhost:8080/capture?accurate=false&nokey=true" \
-H "X-Api-Key: foo" \
-H "Content-Type: application/json" \
-d '{"videoUrl":"http://example.com/video.mp4","elapsedTimeMillis":5000}' \
--output frame.jpgConvenience endpoint that always returns a JPEG, ignoring any imageType in the request body. Accepts the same headers, query parameters, and body fields as POST /capture.
curl -X POST http://localhost:8080/capture/jpg \
-H "X-Api-Key: foo" \
-H "Content-Type: application/json" \
-d '{"videoUrl":"http://example.com/video.mp4","elapsedTimeMillis":1234}' \
--output frame.jpgConvenience endpoint that always returns a PNG, ignoring any imageType in the request body. Accepts the same headers, query parameters, and body fields as POST /capture.
curl -X POST http://localhost:8080/capture/png \
-H "X-Api-Key: foo" \
-H "Content-Type: application/json" \
-d '{"videoUrl":"http://example.com/video.mp4","elapsedTimeMillis":1234}' \
--output frame.pngReturns HTTP 200 if the service is running.
Interactive API documentation is available at http://localhost:8080/docs when the server is running.
Settings can be provided via environment variables, a config file, or CLI flags. CLI flags take the highest precedence, followed by environment variables, then the defaults in reference.conf.
| Config key | Environment variable | CLI flag | Default |
|---|---|---|---|
beholder.api.key |
BEHOLDER_API_KEY |
-k / --apikey |
foo |
beholder.http.port |
BEHOLDER_HTTP_PORT |
-p / --port |
8080 |
beholder.cache.size |
BEHOLDER_CACHE_SIZE |
-c / --cachesize |
500 (MB) |
beholder.cache.freepct |
BEHOLDER_CACHE_FREEPCT |
-f / --freepct |
0.20 |
The cache root directory is a required positional CLI argument (or BEHOLDER_CACHE_ROOT env var when running via Docker).
# Compile
sbt compile
# Run all tests (requires ffmpeg)
sbt test
# Run a single test suite
sbt "testOnly org.mbari.beholder.JpegCacheSuite"
# Format code (run before committing)
sbt scalafmtAll
# Generate scaladoc
sbt doc
# Check for dependency updates
sbt dependencyUpdates# Start the server with a cache directory
sbt "run /tmp/beholder-cache"
# With custom port and API key
sbt "run --port 9090 --apikey secret /tmp/beholder-cache"Frames are stored under the cache root as:
<root>/<video-url-host>/<video-url-path>/<hh_mm_ss.sss>.<ext>
where <ext> is jpg or png depending on the requested image type. For example:
/tmp/beholder-cache/m3.shore.mbari.org/videos/M3/proxy/DocRicketts/2022/03/1436/D1436_20220322T132758Z_h264.mp4/00_00_01.234.jpg
/tmp/beholder-cache/m3.shore.mbari.org/videos/M3/proxy/DocRicketts/2022/03/1436/D1436_20220322T132758Z_h264.mp4/00_00_01.234.png
JPEG and PNG versions of the same frame are cached independently. The in-memory index is rebuilt from disk on startup, so cached frames survive service restarts.
Pre-built multi-architecture images (linux/amd64, linux/arm64) are published to Docker Hub as mbari/beholder.
docker run -d \
-p 8080:8080 \
-e BEHOLDER_API_KEY=your-secret-key \
-v /data/beholder-cache:/opt/beholder/cache \
mbari/beholder:latestThe container expects a writable volume mounted at /opt/beholder/cache. The port defaults to 8080.
Environment variables for Docker
docker run -d \
-p 9090:9090 \
-e BEHOLDER_API_KEY=your-secret-key \
-e BEHOLDER_HTTP_PORT=9090 \
-e BEHOLDER_CACHE_SIZE=1000 \
-e BEHOLDER_CACHE_FREEPCT=0.25 \
-v /data/beholder-cache:/opt/beholder/cache \
mbari/beholder:latestStandard build (from any platform targeting linux/amd64)
sbt 'Docker / publish'Multi-architecture build (required on Apple Silicon)
./build.shThis uses docker buildx to produce linux/amd64 and linux/arm64 images and pushes them to Docker Hub. Run docker login first.
Releases are triggered automatically by pushing a git tag that matches the version pattern [0-9]+.* (no leading v):
git tag 1.2.3
git push origin 1.2.3The release workflow builds and pushes a multi-arch Docker image tagged with both the version number and latest.
Full scaladoc is published at https://mbari-org.github.io/beholder/
Additional documentation can be added as Markdown files in src/docs/ and will be included automatically when running sbt doc.
Beholder image Copyright © 2022 Aine Schlining

