VirtualCamera
publishes the sketch to the system-wide Ollin Camera, so Photo Booth, QuickTime, Zoom, and any browser read it as a live webcam. The scene is a broadcast-style "on air" picture, so the feed is obviously live. It needs the camera extension installed once, and it explains what to do when the extension is missing (publishVirtualCamera)
- Run
swift run --package-path Examples Example-Integration-VirtualCamera- Imports
OllinCamera- Source
- Examples/Integration/VirtualCamera/Sketch.swift
import Ollin
import OllinCamera
/// Publishes the sketch to the **Ollin Camera virtual camera**, so every app
/// that takes a webcam — Photo Booth, QuickTime, Zoom, Meet, OBS, and browser
/// tools like Hydra through `getUserMedia` — reads it as a live camera. One
/// call in `setup()` does it; the canvas doubles as a broadcast-style "on air"
/// scene so the feed is obviously live on the other side.
///
/// Needs the Ollin Camera extension installed once (see `Apps/OllinCameraApp`);
/// without it the sketch still runs and explains what to do. While no sketch is
/// publishing, the camera shows its built-in "no signal" test card — run this,
/// and the picture switches to the sketch; quit, and the test card returns.
///
/// To see it: run this sketch, open Photo Booth (or QuickTime ▸ New Movie
/// Recording), and pick "Ollin Camera".
@main
final class VirtualCamera: Sketch {
// Match the camera frame (1280×720), so the feed fills it with no bars.
override var canvasSize: CanvasSize { .size(1280, 720) }
var camera: VirtualCameraServer!
let palette = CosinePalette.neon
override func setup() {
camera = publishVirtualCamera()
noStroke()
}
override func draw() {
background(Color(white: 0.08))
// A wavy field of colored ribbons — enough motion to read as video.
let bands = 7
for band in 0..<bands {
let phase = Double(band) / Double(bands)
var ribbon = palette.color(at: phase)
ribbon.alpha = 0.85
fill(ribbon)
let baseY = height * (0.25 + 0.5 * phase)
drawShape { path in
path.move(to: Vector2(0, height))
for x in stride(from: 0.0, through: width, by: 16) {
let y = baseY + sin(x * 0.006 + time * (0.8 + phase)) * 60
+ cos(x * 0.013 - time * 1.4) * 24
path.line(to: Vector2(x, y))
}
path.line(to: Vector2(width, height))
path.close()
}
}
// An orbiting satellite dot, for parallax against the ribbons.
let orbit = Vector2(width * 0.5, height * 0.42)
let dot = orbit + Vector2(angle: time * 0.9) * 230
fill(.white)
drawCircle(center: dot, radius: 16 + 5 * sin(time * 3))
drawBadge()
}
/// The broadcast badge: ON AIR while frames reach the camera, and what to
/// do about it when they don't.
private func drawBadge() {
let publishing = camera.isPublishing && camera.isAvailable
let badge = Rectangle(x: 40, y: 40, width: 230, height: 64)
fill(publishing ? Color(red: 0.8, green: 0.1, blue: 0.1) : Color(white: 0.25))
drawRect(badge, cornerRadius: 14)
// The classic blinking tally light.
fill(publishing && sin(time * 4) > 0 ? .white : Color(white: 0.6, alpha: 0.5))
drawCircle(badge.x + 36, badge.y + 32, 11)
fill(.white)
textSize(30)
drawText(publishing ? "ON AIR" : "OFF AIR", badge.x + 64, badge.y + 42)
if let reason = camera.unavailableReason {
fill(Color(white: 0.75))
textSize(20)
drawText(reason, 40, height - 36)
} else if publishing {
fill(Color(white: 0.75))
textSize(20)
drawText("Publishing to \"\(camera.deviceName)\" — open Photo Booth and pick it.", 40, height - 36)
}
}
}