Summary#
Two devices, one shape. Camera and Mic each have a device pair and a recording pair:
| acquire / release | record | |
|---|---|---|
| camera | Camera.Start(facing: Back) · Camera.Stop() | Camera.Record() · Camera.StopRecording() |
| microphone | Mic.Start() · Mic.Stop() | Mic.Record() · Mic.StopRecording() |
The camera also takes still photographs with Camera.Capture(), and shows what it sees through the Camera()
atom — the viewfinder.
Everything that produces a file — Capture, and both StopRecordings — answers an UploadedFile, which is the
same value the upload control hands an onUploaded action. So a photograph and a chosen file are stored the
same way, by the same code.
Signature#
Camera.Start(facing: CameraFacing.Back) // ask for the camera; CameraFacing is Any (default), Front or Back
Camera.Start(withSound: true) // …and the microphone, if this app will record video WITH sound
Camera.Stop() // release it
UploadedFile photo = Camera.Capture() // photograph the viewfinder
Camera.Record() // start recording
UploadedFile clip = Camera.StopRecording() // stop, and answer the whole clip
Mic.Start() // ask for the microphone
Mic.Record()
UploadedFile note = Mic.StopRecording()
Mic.Stop()Description#
A whole photo booth#
using Osysharp.Storage;
[Page("/booth")] [AllowAnonymous]
component Booth() {
bool live = false;
string problem = "";
string shot = "";
action Begin() {
try {
Camera.Start(facing: CameraFacing.Front);
live = true;
problem = "";
} catch (DeviceUnavailableException ex) {
problem = ex.Message;
}
}
action Snap() {
try {
UploadedFile photo = Camera.Capture();
shot = photo.Path;
} catch (DeviceUnavailableException ex) {
problem = ex.Message;
}
}
action Done() { Camera.Stop(); live = false; }
render {
Stack(gap: 3) {
if (!live) { Pressable("Turn the camera on", onClick: Begin); }
if (live) {
Camera();
Row(gap: 2) {
Pressable("Take a photo", onClick: Snap);
Pressable("Done", onClick: Done);
}
}
if (shot != "") { Image(shot); }
if (problem != "") { Text(problem); }
}
}
}Being refused is normal, and it is one exception#
A person saying no to a permission prompt is the system working, not a failure. So every capture verb refuses the
same way — DeviceUnavailableException — and an app that uses both devices writes one catch.
ex.Message is a sentence you can show, and it says which of four things happened:
| what happened | what the person can do |
|---|---|
| they refused the prompt | change it from the padlock in the address bar |
| the machine has no camera or microphone | nothing — hide the feature |
| another application is holding the device | close the other application |
| the page is not on https or localhost | nothing they can do — see below |
⛔ The last one is a deployment problem, not an answer. A browser hands out a camera or microphone only on
https:// or localhost, and there is no fallback of any kind — unlike Clipboard, which has a second path
for exactly this case. An app served over plain http:// on a LAN address cannot have a camera. Nothing at
compile time can see that, which is why it arrives as this exception rather than as a refusal to build.
The device and the recording are separate#
Start/Stop hold the device. Record/StopRecording are one clip. They are separate because holding the
microphone across several recordings is the normal case, and re-acquiring it per clip would prompt every time.
Mic.Start(); // one prompt
Mic.Record(); UploadedFile a = Mic.StopRecording();
Mic.Record(); UploadedFile b = Mic.StopRecording();
Mic.Stop(); // the indicator goes out hereStop() always means the device and StopRecording() always means the clip, on both ambients — even though
Mic.Stop() could have meant "stop recording", since a microphone has nothing to show. One vocabulary is worth more
than a locally shorter name.
Sound is asked for when you start, not when you record#
Camera.Start() acquires video only. To record video with sound, say so up front:
Camera.Start(facing: Back, withSound: true);Acquiring the microphone later, at Camera.Record(), would show a second permission prompt at the moment somebody
presses record — the worst possible time. And an app that only takes photographs should not be asking for a
microphone at all. One prompt, for exactly what you declared.
The viewfinder#
Camera() is a live <video>. It takes no props of its own — which camera, and when, is Camera.Start's business,
because that is the call that can be refused. Style it like any other element.
A component's verbs act on its own Camera(), the same rule Canvas follows: a viewfinder inside a nested
component belongs to that component, and the one that starts the camera must be the one that declares it.
Playing it back#
Video and Audio play a file the app has stored — a recording you just made, or anything else in its file store.
Both always show controls.
Video(clip.Path);
Video(clip.Path, poster: still.Path, loop: true);
Audio(note.Path);What comes out#
A photograph is a JPEG at the camera's own resolution — not the size of the element on screen, so a photo does not change with the page layout.
A recording is whatever the browser records: WebM on Chrome and Firefox, MP4 on Safari. There is no single format every browser produces, so the platform asks for the best each one supports rather than insisting on one and failing on the others. Whatever arrives, the server derives the stored file's real type from its bytes.
Trying it before it exists#
Camera.Capture() refuses if the camera has not produced its first frame yet, rather than storing a blank image. If
you photograph immediately after Camera.Start(), expect that refusal — its message says the camera is still
starting up.