Cloud upload
The device can push finished recordings straight into your own object storage — an AWS S3 or Aliyun OSS bucket you control. Once it's set up, an operator never touches a file: put the headset on, record, and the recordings appear in your bucket whenever the device is on Wi-Fi.
This is one of the device's three export paths, and the one built for unattended fleet collection. The other two — pulling the MicroSD card, and reading the card over USB — are covered in Retrieve & view data. Prefer a video of the app flow, including the S3 screen? See the walkthrough on Connect, preview & capture.
Recordings still land on the MicroSD card first. Upload is a background job on top of that, so a network outage delays delivery rather than losing data.
How it works
- A recording finishes and is finalized on the SD card.
- The device queues it and uploads it whenever it's on a real Wi-Fi network.
- On success, the local copy is removed to free the card for the next session.
Five properties are worth knowing before you plan around it:
- Upload needs your Wi-Fi, not the setup hotspot. The device's own hotspot has no internet. While the device is on it, uploads pause and resume later — nothing is lost.
- Only finished recordings upload. A recording part is uploadable only once it has been closed and carries the MCAP end-of-file marker, so the part currently being written is skipped until it's complete. An in-progress capture is never half-shipped.
- Long recordings become several objects. Capture is written in 256 MB parts, and each part uploads independently as soon as it closes. A long session starts arriving in your bucket while it is still being recorded.
- The queue survives reboots and firmware upgrades. It is stored outside the part of the filesystem an update replaces, so an interrupted upload resumes where it left off.
- Each part is uploaded in a single request. There is no multipart upload, which is the reason for the 256 MB part size.
Failures retry with a widening backoff — from about a second up to five minutes between attempts — and after 12 attempts a part stops retrying and shows as Failed. Saving corrected credentials resets failed items so they retry immediately.
Step 1 — Prepare the bucket and keys
You need a private bucket and one access key. GI Labs deliberately asks for the narrowest permissions that work:
| Permission | Why it's needed |
|---|---|
| Put objects into the bucket | The device uploads recordings and status reports |
| List objects under your prefix | The app's cloud recordings list, and the Test button on the device |
No delete permission is required or requested. Nothing in the system ever deletes from your bucket.
scripts/oss_setup.py in the visio-embedded repo provisions the whole thing in one run — private bucket,
both RAM users with least-privilege policies, the CORS rule a browser dashboard needs, and a lifecycle rule
that expires status reports after 30 days. It is standard-library-only Python: copy the one file and run
it, no pip install and no Aliyun CLI.
It finishes by replaying the exact requests the device and the app make with the freshly minted keys, so a green run means the keys genuinely work rather than that the API calls returned 200.
# interactive — asks for region and bucket, then does everything
oss_setup.py
# non-interactive, writing the resulting settings to a 0600 file
oss_setup.py --region cn-hangzhou --bucket my-recording-bucket --json out.json
# show what it would do; signs nothing and needs no credentials
oss_setup.py --region cn-hangzhou --bucket my-bucket --dry-run
Your admin credentials (needed to create buckets and RAM users) come from ALIYUN_ACCESS_KEY_ID /
ALIYUN_ACCESS_KEY_SECRET, from an ossutil config, or from an interactive prompt. They are never written to
disk.
This script is Aliyun OSS only — it creates RAM users and OSS policies. For AWS S3, use the IAM policy below.
The least-privilege policy
For AWS S3, this is the device key's policy. Substitute your bucket name and prefix:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DeviceUploads",
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-recording-bucket/*"
},
{
"Sid": "ListRecordingsPrefix",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-recording-bucket",
"Condition": {
"StringLike": { "s3:prefix": ["recordings/*", "recordings/"] }
}
}
]
}
The equivalent on Aliyun OSS — which is what oss_setup.py attaches — is oss:PutObject on
acs:oss:*:*:my-recording-bucket/*, plus oss:ListObjects on the bucket conditioned to the same prefix.
Confining the list permission to your recordings prefix is what stops the device's key from enumerating your status-report subtree.
The list condition is the one place these two settings have to agree. If the policy allows listing
recordings/ but the device's Prefix field is blank, the device lists the bucket root — which a
prefix-scoped key denies — and the Test button fails with AccessDenied even though the credentials are
perfect and uploads would work fine. Set the prefix the key was issued for.
A separate read-only key for dashboards
If you want to run a fleet-status dashboard, mint a second, read-only key scoped to the status prefix:
GetObject on <bucket>/status/* and ListBucket conditioned to status/. oss_setup.py creates this one
too. Keeping the two separate means the device's key can't read the status subtree and the dashboard's key
can't write anything.
The endpoint URL
Note the endpoint URL for your bucket's region — this is what you'll type into the app. Substitute your own region:
| Provider | Endpoint (example region) |
|---|---|
| AWS S3 | https://s3.us-east-1.amazonaws.com |
| Aliyun OSS | https://oss-cn-hangzhou.aliyuncs.com |
The device picks its request-signing scheme from the endpoint's hostname — there is no provider toggle to
get wrong. That also means a custom CNAME domain in front of an OSS bucket is signed as though it were
AWS S3, and the bucket rejects it. Enter the standard oss-<region>.aliyuncs.com or
s3.<region>.amazonaws.com host.
Step 2 — Enter the destination in the app
Credentials are entered from the Visio Companion app. Connect to the device (see Connect, preview & capture), open it, and tap Cloud upload.
The device's built-in setup page at http://192.168.4.1 handles Wi-Fi, capture metadata, recording control,
and preview — but not storage credentials. Cloud upload is configured from the app, or in bulk from a
settings QR code.
Under Destination, fill in:
| Field | Notes |
|---|---|
| Provider | Fills in the endpoint shape for you |
| Endpoint URL | Required |
| Region | Filled in automatically for standard S3 and OSS endpoints |
| Bucket | Required |
| Access key ID | Required |
| Secret access key | Required |
| Prefix (optional) | Recommended — see below. The greyed-out recordings/ is a hint, not a default |
The Prefix (optional) field really is optional, and if you leave it empty the device uploads to the
root of the bucket — it does not silently use recordings/. Set a prefix explicitly. It keeps your bucket
tidy, and it is what the least-privilege list permission is scoped to.
Then tap Test. The device — not your phone — probes the bucket by listing a single key under your prefix, so a pass confirms the endpoint, the region, the bucket name, both halves of the key, and your list permission in one shot. Success reports Connection OK — "Bucket is reachable with these credentials." Tap Save.
Once saved, the secret is never displayed again — the field shows Saved — re-enter to change, and the app explains that "for security, the secret key is never shown." To rotate credentials, type the new secret in full.
If you're provisioning over the device's setup hotspot, there's no internet, so Test is unavailable. The app says so directly. You can still Save — the credentials take effect as soon as the device is on a real network. Test later, or provision over your Wi-Fi to test immediately.
Step 3 — Turn on auto-upload
In the Recording upload section, enable Auto-upload over Wi-Fi.
With it off, recordings stay on the SD card until you turn it back on — which is the right setting when you want to review before anything leaves the device, or when the site's network shouldn't carry capture traffic. Turning it back on resumes the queue from where it stopped; nothing is dropped and the credentials are kept.
At the default 8 Mbps per camera, a recording device produces roughly 16 Mbps of video. If you record more or less continuously with auto-upload on, the site's upload bandwidth has to beat that sustained, or the queue grows for as long as you keep recording and the card fills behind it. Either give the fleet uplink headroom, lower the bitrate (see Connect, preview & capture), or plan on the queue draining after hours.
Step 4 (optional) — Status reports
Also on the Cloud upload screen: Send status reports periodically uploads device health plus a low-resolution camera image, so you can see at a glance whether a fleet is healthy without touching each unit.
Reports go every 5 minutes to their own Status prefix (default status/) — the same credentials, a
separate subtree from your recordings, as the app notes on the field itself. The image is 640 × 360.
Reports are inert until a destination is saved: on a unit that was never provisioned the reporter starts, finds no valid credentials, and stays quiet. What arms it is the destination, not the toggle.
By default the status image is captured only while the device is recording. Idle reports carry health numbers and no picture. This is the right default for a head-worn camera: it means the device cannot send images of whatever the wearer happens to be looking at while not on task. Capturing regardless of recording state is possible, but it should be a deliberate, documented fleet decision — not a convenience toggle.
Provisioning a fleet with a QR code
Typing an endpoint, bucket, and two long keys into a phone once is fine. Doing it forty times is not.
For fleets, GI Labs generates a settings QR code that carries a whole configuration at once. In the app, use Scan settings: it reads the code, shows every field it is about to apply for confirmation — "nothing is sent until you apply" — and then applies them.
A code can carry any subset of these sections:
| Section | What it sets |
|---|---|
| Capture metadata | Task, location, message, capturer |
| Cloud upload | Endpoint, region, bucket, access key ID, secret key, prefix |
| Auto-upload | On or off |
| Video bitrate | 500–50000 kbps; takes effect after a reboot |
| Camera resolution | Width and height; takes effect after a reboot |
| Device Wi-Fi | Network name and password |
Two behaviors to know: the secret key may be omitted, in which case the device keeps the secret it already has — useful for re-provisioning a fleet's Wi-Fi without reprinting credentials. And Wi-Fi is applied last, because the device switches networks at that point and your phone may lose contact; the app follows the device onto the new network where it can.
Generate the codes with scripts/gen_settings_qr.py in the visio-embedded repo — interactively, or from a
JSON file so a site's configuration is version-controlled and reproducible. --check-only validates a config
without rendering anything, and --dry-run prints the exact payload the app will parse.
The code embeds the storage secret key and the Wi-Fi passphrase in plaintext. Anyone who can photograph the code has both, and no scanning app is needed to read them. Treat a printed code exactly as you'd treat a password on paper: don't put it in shared photo libraries, don't leave it taped to a wall visible to visitors, shred it when the site is done, and reprint after rotating credentials.
The same applies to oss_setup.py's output, which prints two long-lived secrets to your terminal. Prefer its
--json mode, which writes them to a 0600 file, over copying them through chat.
Where the files land
Objects are laid out by device and session under your prefix:
<prefix>/<device-serial>/<session>-<start-time>/<part>.mcap
For example:
recordings/26887a068c4e93f0/session_00042-1784017895/ego_0000.mcap
recordings/26887a068c4e93f0/session_00042-1784017895/ego_0001.mcap
<device-serial>is the 16-character device serial, also shown in the app under Device info.<session>-<start-time>is the session counter plus its start time as a Unix timestamp. The timestamp is what makes the key unique: session counters restart when a card is swapped or formatted, so without it two different recordings could collide on the same key.- No sidecar file is uploaded. The task, location, capturer, and message you set before recording are
written inside the MCAP itself, so a single object carries both the data and its labels. (A
session.jsonsidecar does exist next to the parts on the SD card, but it stays there.)
Status reports land in a separate, time-partitioned tree — status/<date>/<hour>/<time>-<serial>.json — which
is why they want their own prefix and their own lifecycle rule.
Because keys are unique by construction, a re-tried upload can only ever collide with itself. On Aliyun OSS the device also sends a forbid-overwrite header on every PUT, so a replace is refused rather than silently performed; AWS S3 ignores that header, and there the uniqueness of the key is the guarantee. Either way, an upload cannot quietly replace an earlier recording.
Monitoring uploads
On the device screen, the Upload queue shows three counters: Pending, In flight, and Failed.
In the recordings list, each session carries a badge. A session with several parts shows the worst state among them, so a single stuck part is visible rather than averaged away:
| Badge | Meaning |
|---|---|
| Uploading | In flight now |
| Uploaded | Confirmed in your bucket |
| Upload failed | Retries exhausted — check the destination |
The app can also list what's already in the bucket, under Uploaded to cloud.
The Uploaded to cloud list is your phone talking to your bucket, not to the device. If your phone is on the device's hotspot it has no internet and the list can't load — the app tells you to join a normal Wi-Fi network, which is the fix.
Troubleshooting
The device turns the service's own error code into a specific message rather than a generic failure, so read
what the Test button says — AccessDenied, SignatureDoesNotMatch and InvalidAccessKeyId are all
HTTP 403 and mean three completely different things.
| Symptom | What it means and what to do |
|---|---|
| Test fails: signature rejected | The secret key or the endpoint is wrong — not the bucket policy. Re-enter the secret in full; a key pasted with a trailing space is the most common cause. Check you used the provider's real endpoint host, not a CNAME. |
| Test fails: access key not recognised | The access key ID is wrong or the key was deleted. Re-enter the ID. |
| Test fails: access denied with no prefix set | The device listed the bucket root, which a prefix-scoped key denies. Set the Prefix the key was issued for. |
| Test fails: access denied with a prefix set | The policy doesn't allow listing that prefix. Confirm the prefix in the policy condition matches the prefix on the device exactly, trailing slash included. |
| Test fails: endpoint redirected | Usually the bucket lives in a different region. A signed request can't follow a redirect, so point the endpoint URL at the region the error names. |
| Test fails: bucket not found | The bucket name is wrong, or it was never created. |
| Test is unavailable | You're on the device's setup hotspot — no internet. Save anyway, then test once the device is on Wi-Fi. |
| Nothing uploads, queue shows Pending | The device isn't on a network with internet access. Check Device info → Wi-Fi; if the state reads AP fallback, it's on its own hotspot and uploads are paused by design. |
| Everything shows Upload failed | Almost always credentials or permissions. Fix them and re-save — saving a corrected destination automatically resets failed items so they retry. |
| Queue grows and never drains | The uplink can't keep up with what you're recording. Compare your sustained upload bandwidth against ~16 Mbps at the default bitrate. |
| Uploads work but the card still fills up | Deleting after upload is what frees the card. A part that uploaded but couldn't be deleted is reported in the queue's last error. |
| A session is stuck Repairing | It was torn by a power cut mid-write. After a minute of no new bytes the device treats the part as dead, repairs it in place, and then uploads it. Stop recordings before removing power to avoid this. |
| Recordings upload, but the bucket list is empty | Put is working and list is not. The key is missing list permission on your recordings prefix — the two permissions fail independently. |
Device clock skew
The device has no NTP client. Its clock comes from its RTC at boot, and is corrected when a phone connects. Since both S3 and OSS reject any request signed more than 15 minutes from server time, a unit with a wrong clock could in principle upload nothing at all.
It handles this itself. Every response the bucket sends carries the server's time, so the device learns the true time from the first rejection and signs correctly from then on. In practice the first request after a cold boot with a dead RTC fails, and the retry succeeds. You do not need to do anything.
The self-correction applies to request signing only — it is deliberately not written back to the system clock. A device that has never been contacted by a phone can therefore upload perfectly while writing recordings stamped 1970. If the timestamps inside your MCAP files matter, connect the app to each unit once after it is deployed.
Networks with captive portals
The device joins Wi-Fi with a network name and a passphrase, and nothing else. It cannot get through a captive portal — the guest-Wi-Fi splash page that wants a click-through, a room number, or a browser login. The device will associate and get an IP, so it looks connected in the app, but every upload fails because the portal intercepts the requests.
Put the device on a network that needs no portal: a WPA2/WPA3 network with a pre-shared key, a dedicated IoT SSID, or a phone hotspot. If the site only offers portal Wi-Fi, use the MicroSD card or USB instead.
Where to go next
- Connect, preview & capture — get the device onto your Wi-Fi first.
- Retrieve & view data — the manual paths, via the SD card or USB.
- SD cards — capacity and speed, which set how much can queue up offline.