Skip to main content

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

  1. A recording finishes and is finalized on the SD card.
  2. The device queues it and uploads it whenever it's on a real Wi-Fi network.
  3. 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:

PermissionWhy it's needed
Put objects into the bucketThe device uploads recordings and status reports
List objects under your prefixThe 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.

Aliyun OSS: there's a script for this

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 prefix in the policy and the prefix on the device must match

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:

ProviderEndpoint (example region)
AWS S3https://s3.us-east-1.amazonaws.com
Aliyun OSShttps://oss-cn-hangzhou.aliyuncs.com
Use the provider's real endpoint, not a CNAME

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 setup page can't do this one

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:

FieldNotes
ProviderFills in the endpoint shape for you
Endpoint URLRequired
RegionFilled in automatically for standard S3 and OSS endpoints
BucketRequired
Access key IDRequired
Secret access keyRequired
Prefix (optional)Recommended — see below. The greyed-out recordings/ is a hint, not a default
Leaving the prefix blank puts recordings at the bucket root

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.

The secret key is write-only by design

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.

Setting up over the device's hotspot

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.

Check that your uplink can keep up

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.

The image default is privacy-conscious — keep it that way deliberately

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:

SectionWhat it sets
Capture metadataTask, location, message, capturer
Cloud uploadEndpoint, region, bucket, access key ID, secret key, prefix
Auto-uploadOn or off
Video bitrate500–50000 kbps; takes effect after a reboot
Camera resolutionWidth and height; takes effect after a reboot
Device Wi-FiNetwork 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.

A settings QR is a written-down password

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.json sidecar 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.

Uploads don't overwrite each other

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:

BadgeMeaning
UploadingIn flight now
UploadedConfirmed in your bucket
Upload failedRetries exhausted — check the destination

The app can also list what's already in the bucket, under Uploaded to cloud.

Listing the cloud needs internet on the phone

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.

SymptomWhat it means and what to do
Test fails: signature rejectedThe 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 recognisedThe access key ID is wrong or the key was deleted. Re-enter the ID.
Test fails: access denied with no prefix setThe 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 setThe 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 redirectedUsually 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 foundThe bucket name is wrong, or it was never created.
Test is unavailableYou're on the device's setup hotspot — no internet. Save anyway, then test once the device is on Wi-Fi.
Nothing uploads, queue shows PendingThe 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 failedAlmost always credentials or permissions. Fix them and re-save — saving a corrected destination automatically resets failed items so they retry.
Queue grows and never drainsThe 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 upDeleting 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 RepairingIt 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 emptyPut 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.

But the recording timestamps are a separate problem

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