Documentation
¶
Overview ¶
Package android captures the Android screen from a pure-Go, CGO-free application, for an XR compositor that redraws every frame.
What Android permits, and what it does not ¶
Everything here is shaped by one measured fact: an ordinary, unprivileged APK may MIRROR the screen and may draw its own content on an attached display, and it may do nothing else. It cannot give itself extra desktops. On Android 15 (API 35), creating a virtual display and launching an activity on it fails with
SecurityException: Permission Denial: starting Intent { ... }
... with launchDisplayId=N
for ANY activity, including the caller's own; asking for VIRTUAL_DISPLAY_FLAG_TRUSTED — which is not in the public SDK at all — fails with "Requires ADD_TRUSTED_DISPLAY permission", a signature|role permission; and VirtualDeviceManager.createVirtualDevice fails with "Access denied, requires: android.permission.CREATE_VIRTUAL_DEVICE", which is internal|role. See the README for the whole transcript. So on Android an XR ribbon carries the phone's real screen, and the application's own content, and that is all.
The split, and why there is a Java host ¶
Android hands no drawable surface, and no MediaProjection, to a process that is not the app: both are behind JNI, and JNI needs cgo. So the app is two processes, exactly as in go-widgets/android — a small Java host owning the Android objects, and an ordinary CGO_ENABLED=0 GOOS=android executable owning everything above the pixels. This package is the application half.
The hot path ¶
Stream.Frame hands back a BORROWED view of the most recent frame: the bytes are the shared mapping the host wrote into, not a copy, and the borrow lasts until the next Stream.Frame, Stream.WaitFrame or Stream.Close. In steady state a Frame call performs no allocation at all. Copy out with Frame.CopyTight or Frame.NRGBA to keep it longer.
Stride ¶
A captured frame's rows may be PADDED: Stride is the number of BYTES per row and it is not necessarily Width*4. Always index with Stride, or use Frame.Row. The Android 15 emulator measured here reported stride 4320 for a 1080-wide capture — exactly Width*4, no padding — which is precisely why the API carries the number instead of trusting it.
Frames only arrive when something changes ¶
A MediaProjection capture is change-driven, like ScreenCaptureKit on macOS. Options.FPS is a CEILING, not a rate: with a still screen, this package's own probe took 39 frames in 4.0 s and then nothing at all; with the screen scrolling it took 574 frames in 11.9 s. Do not treat a missing frame as a failure; the second return value of Stream.Frame is the truth about whether anything changed.
Consent ¶
Every capture session needs the user to agree through a system dialog, and the agreement dies with the session. Authorized reports whether the host already holds a projection token; RequestAuthorization asks the host to put the dialog in front of the user and blocks until they answer.
Index ¶
- Constants
- Variables
- func ArtifactDir() (string, error)
- func Authorized() bool
- func Available() bool
- func ControlIn() uint8
- func ControlOut() uint8
- func DecodeBuffer(b []byte) (slots int, slotSize int64, err error)
- func DecodeConsent(b []byte) (bool, error)
- func DecodeOpenUSBDevice(b []byte) (string, error)
- func DeriveSocket(env string, envOK bool, home string) string
- func DeriveWallSocket(env string, envOK bool, home string) string
- func EncodeBuffer(slots int, slotSize int64) []byte
- func EncodeCameras(cs []Camera) []byte
- func EncodeConfig(c ConfigMessage) []byte
- func EncodeConsent(granted bool) []byte
- func EncodeDisplays(ds []Display) []byte
- func EncodeError(e ErrorMessage) []byte
- func EncodeFrame(f FrameMsg) []byte
- func EncodeOpenDisplay(m OpenDisplayMessage) []byte
- func EncodeOpenScreen(m OpenScreenMessage) []byte
- func EncodeOpenUSBDevice(name string) []byte
- func EncodePresent(m PresentMessage) []byte
- func EncodePresented(m PresentedMessage) []byte
- func EncodeStart(s StartMessage) []byte
- func EncodeStopped(s StoppedMessage) []byte
- func EncodeUSBDevices(ds []USBDevice) []byte
- func FrameMessage(typ uint8, body []byte) []byte
- func PickAlternate(ifaces []USBInterface, number int, payload uint32) (USBInterface, USBEndpoint, bool)
- func PickFormat(fs []UVCFormatDesc) (UVCFormatDesc, UVCFrameSize, error)
- func ReadMessage(r io.Reader) (typ uint8, body []byte, err error)
- func RequestAuthorization(ctx context.Context) (bool, error)
- func SaveFrame(name string, img *image.RGBA) (string, error)
- func SaveTranscript(name, text string) (string, error)
- func SentinelColorAt(w, h, x, y int) (uint32, bool)
- func WallAvailable() bool
- func WriteMessage(w io.Writer, typ uint8, body []byte) error
- type Camera
- type CameraFacing
- type CameraRoute
- type Canvas
- type ConfigMessage
- type Content
- type Display
- type DisplaySpec
- type ErrorMessage
- type Frame
- type FrameAssembler
- func (a *FrameAssembler) BadJPEG() int
- func (a *FrameAssembler) Boundaries() (toggles, eofs, shortBytes int)
- func (a *FrameAssembler) Decode(dst *image.RGBA, frame []byte) (*image.RGBA, error)
- func (a *FrameAssembler) Feed(p UVCPayload) ([]byte, bool)
- func (a *FrameAssembler) Format() UVCFormat
- func (a *FrameAssembler) Losses() (onToggle, onEOF int)
- func (a *FrameAssembler) MissingMarkers() (noStart, noEnd int)
- func (a *FrameAssembler) Size() (int, int)
- func (a *FrameAssembler) Stats() (frames, dropped, bad int)
- type FrameMsg
- type OpenDisplayMessage
- type OpenScreenMessage
- type Options
- type OwnedDisplay
- type PixelFormat
- type PresentMessage
- type PresentedMessage
- type Screen
- func (s *Screen) Close() error
- func (s *Screen) DisplayID() int
- func (s *Screen) Err() error
- func (s *Screen) Next(ctx context.Context) (Canvas, error)
- func (s *Screen) Options() ScreenOptions
- func (s *Screen) Present(ctx context.Context, c Canvas) error
- func (s *Screen) Size() (int, int)
- func (s *Screen) Stats() ScreenStats
- func (s *Screen) Stride() int
- func (s *Screen) String() string
- type ScreenOptions
- type ScreenStats
- type Sentinel
- type StartMessage
- type Stats
- type StoppedMessage
- type Stream
- func (st *Stream) Close() error
- func (st *Stream) Err() error
- func (st *Stream) Format() PixelFormat
- func (st *Stream) Frame() (Frame, bool)
- func (st *Stream) Options() Options
- func (st *Stream) Size() (int, int)
- func (st *Stream) Stats() Stats
- func (st *Stream) String() string
- func (st *Stream) WaitFrame(ctx context.Context) (Frame, error)
- type USBDescriptors
- type USBDevice
- type USBEndpoint
- type USBHandle
- func (h *USBHandle) ClaimInterface(n int) error
- func (h *USBHandle) Close() error
- func (h *USBHandle) Control(requestType, request uint8, value, index uint16, data []byte) (int, error)
- func (h *USBHandle) Descriptors() (USBDescriptors, error)
- func (h *USBHandle) Detached() []int
- func (h *USBHandle) Name() string
- func (h *USBHandle) Negotiate(iface int, f UVCFormatDesc, z UVCFrameSize) (UVCNegotiation, error)
- func (h *USBHandle) SetAltSetting(iface, alt int) error
- func (h *USBHandle) Stream(neg UVCNegotiation, ep USBEndpoint, w, h2 int) (*UVCStream, error)
- func (h *USBHandle) SubmitISO(endpoint USBEndpoint) error
- type USBInterface
- type USBTransferType
- type UVCFormat
- type UVCFormatDesc
- type UVCFrameSize
- type UVCNegotiation
- type UVCPayload
- type UVCStream
- type UVCStreamStats
- type Wall
- type Web
Constants ¶
const ( // USBClassPerInterface is a device whose class is declared by each // interface rather than by the device. Every composite device is one, and a // headset that is a camera and something else is composite. USBClassPerInterface = 0x00 // USBClassVideo is the Video class: UVC, which is what a webcam speaks and // what a headset's camera is. USBClassVideo = 0x0e )
USB class codes this package names, from the USB-IF class definitions.
const ( // USBSubclassVideoControl is the interface that describes the camera's // units and terminals. A UVC device has exactly one. USBSubclassVideoControl = 0x01 // USBSubclassVideoStreaming is the interface that carries the frames, and // the one whose endpoints decide whether Android can read them. USBSubclassVideoStreaming = 0x02 )
USB video subclasses, from the UVC specification.
const ( // MsgDisplays answers MsgListDisplays with everything the host can see. MsgDisplays uint8 = 0x01 // MsgConfig announces a started capture: the frame geometry, the pixel // format and the layout of the shared buffer the frames will appear in. MsgConfig uint8 = 0x02 // MsgFrame says a frame has been written into one slot of the shared // buffer. It carries no pixels: the pixels are already there. MsgFrame uint8 = 0x03 // MsgStopped says the capture ended, and why. The user revoking the // projection from the system UI arrives here, not as an error on a call. MsgStopped uint8 = 0x04 // MsgConsent answers MsgConsentRequest with the user's decision. MsgConsent uint8 = 0x05 // MsgError reports a failure the host could not answer in-band. MsgError uint8 = 0x06 // MsgBuffer hands the application the shared frame buffer, as an ancillary // descriptor, once a capture has been configured. It is the ONE message // carrying a descriptor, so it is always written in a single sendmsg. // // The buffer belongs to the HOST, not to the application, and that is // forced rather than chosen: an Android app cannot map a descriptor it // received read-write. SharedMemory.fromFileDescriptor rejects a Go memfd // outright ("FileDescriptor is not a valid ashmem fd"), and reopening it // through /proc/self/fd is refused with EACCES. Only a region the host // itself created with SharedMemory.create is writable by the host, so the // host creates it and lends it here. MsgBuffer uint8 = 0x07 // MsgPresented says one frame the application wrote has reached the view on // an existing display, so its slot is free again. // // It is the ONLY back-pressure [Screen] has. The pixels travel the opposite // way there — this process draws and the host blits — so nothing else stops // the application overwriting a slot the host has not finished reading, and // a tear in a headset is not a cosmetic defect. MsgPresented uint8 = 0x08 // MsgListDisplays asks the host what displays exist. MsgListDisplays uint8 = 0x81 // MsgConsentRequest asks the host to put the screen-capture consent dialog // in front of the user. Only the host can: consent is an Activity result, // and the application owns no Activity. MsgConsentRequest uint8 = 0x82 // MsgStart asks the host to begin capturing. MsgStart uint8 = 0x83 // MsgStop asks the host to end the capture but stay connected. MsgStop uint8 = 0x84 // MsgBye tells the host the application is going away. MsgBye uint8 = 0x85 // MsgOpenDisplay asks the wall host to create ONE virtual display, show a // Presentation carrying the named content on it, and stream the pixels // Android renders there. The host answers with MsgConfig — whose DisplayID // carries the id the platform assigned — and then MsgBuffer, exactly as // MsgStart does, so one Stream implementation serves both. MsgOpenDisplay uint8 = 0x86 // MsgOpenScreen asks the host to show a Presentation on a display that // ALREADY EXISTS — the glasses — carrying a view this process paints into. // // It is the mirror image of MsgOpenDisplay, and the difference is the // direction of the pixels, not the kind of window. MsgOpenDisplay exists to // get at what Android can render and a CGO-free Go process cannot; this // exists because the headset is an OUTPUT, and what belongs on it is the // ribbon this process composited. The host answers with MsgConfig and then // MsgBuffer, exactly as the other two do. MsgOpenScreen uint8 = 0x87 // MsgPresent says the application has finished drawing one slot and the // host should put it on the screen. It carries no pixels: the pixels are // already in the shared buffer. MsgPresent uint8 = 0x88 )
Message types. Host→app messages are below 0x80 and app→host at or above it, so a misrouted message is a decode error rather than a plausible other message.
const ( // StopUser is the user revoking the projection — from the cast chip in the // status bar, or from the notification the foreground service must show. // It is the normal way a capture ends and is NOT a malfunction. StopUser uint8 = 0 // StopSystem is the platform stopping us: a new projection started // elsewhere, the device locked, the service was killed. StopSystem uint8 = 1 // StopApp is this application having asked, with MsgStop or MsgBye. StopApp uint8 = 2 )
Reasons a capture stopped, carried by StoppedMessage.
const ( // MsgCameras answers MsgListCameras with every camera the camera2 API can // see, including external ones. MsgCameras uint8 = 0x09 // MsgUSBDevices answers MsgListUSBDevices with every device on the USB host // port, down to each interface's endpoints. MsgUSBDevices uint8 = 0x0a // MsgListCameras asks the host what cameras exist. MsgListCameras uint8 = 0x89 // MsgListUSBDevices asks the host what is attached to the USB host port. // // It reads the device list, which needs no permission: a permission is // needed to OPEN a device, not to see that it is there. So a census costs // the user no dialog, which is what makes it usable before deciding // anything. MsgListUSBDevices uint8 = 0x8a )
CensusMessage types. The census asks what hardware is there, which needs no permission of any kind; see census.go.
const ( // MsgUSBHandle answers MsgOpenUSBDevice with the device's file descriptor, // as an ancillary descriptor. It carries no body: the descriptor is the // message. // // It is the SECOND message in this protocol to ride on SCM_RIGHTS, and for // the opposite reason from MsgBuffer: there the host owns memory the // application reads, here the host owns a KERNEL HANDLE the application // drives with ioctl. Android will not let an ordinary process open // /dev/bus/usb at all, so the descriptor can only come from a component // that asked the user. MsgUSBHandle uint8 = 0x0b // MsgOpenUSBDevice asks the host to open one USB device by name, after // obtaining the user's permission for it, and to lend the descriptor. MsgOpenUSBDevice uint8 = 0x8b )
USB handover messages. Opening a USB device is the one thing in this package that ASKS THE USER — a system dialog naming the device and the application — so it is the one census question that costs a click.
const ( // MinScreenEdge is the smallest frame a screen will carry. Smaller than // this is a mistake rather than a frugal choice. MinScreenEdge = 16 // MaxScreenPixels bounds one frame, and with it the shared buffer. // // The WHOLE buffer has to fit an int32, which is what Android's // SharedMemory.create takes, and it is MaxQueueDepth slots of four bytes a // pixel: 32 megapixels is the largest frame that leaves the product below // two gigabytes at every accepted queue depth. It is eight 4K panels' worth, // so it constrains nothing anybody would ask for and still refuses a width // times a height that overflowed somewhere upstream. MaxScreenPixels = 32 << 20 )
Limits on a screen's frames.
const ( FlagSupportsProtectedBuffers uint32 = 1 << 0 FlagSecure uint32 = 1 << 1 FlagPrivate uint32 = 1 << 2 FlagPresentation uint32 = 1 << 3 FlagRound uint32 = 1 << 4 )
Display flags, mirroring android.view.Display's public FLAG_* constants. The two that matter to an XR application are FlagPresentation, which says the system considers a display suitable for an app's own full-screen content, and FlagPrivate, which says it belongs to one app.
const ( // DefaultFPS is the frame-rate ceiling used when Options.FPS is zero. DefaultFPS = 60.0 // DefaultQueueDepth is the slot count used when Options.QueueDepth is zero. DefaultQueueDepth = 3 // MinQueueDepth is the smallest slot count this package accepts. MinQueueDepth = 3 // MaxQueueDepth bounds the slot count. A slot is a whole framebuffer — // 10.4 MB for a 1080x2400 phone — so this is a real memory limit, not a // formality. MaxQueueDepth = 8 // MaxDimension is the largest frame edge accepted, a sanity bound well // above any real display. MaxDimension = 32768 )
Defaults applied to the zero value of the corresponding Options field.
const ( // MinDescriptorBytes is the smallest a blob can be and still hold a device // descriptor. Anything shorter is a short read rather than a device. MinDescriptorBytes = 18 // MaxDescriptorBytes bounds how much is read from a descriptor. // // ⛔ A READ LOOP WITH NO CEILING IS A READ LOOP. usbfs serves a few hundred // bytes, and 64 KiB is far past any real device -- but the thing on the // other end is a descriptor this process was handed, and a kernel that kept // answering would otherwise be read until memory ran out. MaxDescriptorBytes = 64 << 10 )
Bounds on a descriptor blob.
const ( // DefaultMaxDisplays is the limit [NewWall] applies when it is given 0. A // ribbon uses six to eight panels, so this is already generous. DefaultMaxDisplays = 8 // MaxDisplays is the largest limit [NewWall] accepts, however explicitly a // caller asks. It is nowhere near the count that killed system_server, and // the Java host refuses past it too. MaxDisplays = 32 // DefaultDensityDPI is the density given to an owned display whose spec // leaves DensityDPI at zero. It is Android's DENSITY_XHIGH, the density a // 1920x1080 panel of the size XR glasses present usually reports. DefaultDensityDPI = 320 // MinDisplayEdge is the smallest edge an owned display may have. A display // smaller than this cannot lay a view hierarchy out at all. MinDisplayEdge = 16 )
Limits on how many displays one application may create.
const ( SentinelBackground uint32 = 0xFF0000 // red SentinelTopLeft uint32 = 0x00FF00 // green SentinelBottomRight uint32 = 0x0000FF // blue )
The colours Sentinel paints, as packed 0xRRGGBB.
const ArtifactEnv = "XRKIT_ARTIFACT_DIR"
ArtifactEnv names the directory artefacts are written to, overriding the default.
const BytesPerPixelYUY2 = 2
BytesPerPixelYUY2 is what YUY2 costs: two bytes a pixel, the first of each pair being the luminance.
const ControlTimeout = 1000
ControlTimeout bounds one control transfer.
⚠ A CONTROL TRANSFER WITH NO TIMEOUT CAN HANG THE CALLER FOREVER. A camera that stops answering mid-negotiation would otherwise take the goroutine with it, and a probe that hangs is worse than one that fails: it reports nothing at all. One second is far longer than a camera takes to answer a nine-byte request.
const DefaultDisplayID = 0
DefaultDisplayID is android.view.Display.DEFAULT_DISPLAY.
const EnvSocket = "XR_ANDROID_SOCKET"
EnvSocket names the environment variable carrying the abstract socket the XR host listens on. A host that spawns this process sets it.
It is not the only route, and in the composition this package is really for it is not the route taken: the process is spawned by go-widgets/android's Activity, which owns the drawing surface, knows nothing about capture and sets no such variable. DeriveSocket covers that case from HOME.
const EnvWallSocket = "XR_ANDROID_WALL_SOCKET"
EnvWallSocket names the environment variable carrying the wall host's socket, the counterpart of EnvSocket.
const ISOPacketsPerURB = 8
ISOPacketsPerURB is how many isochronous packets one request carries.
A UVC camera is scheduled one packet per microframe, so 8 packets is a millisecond of video. It is small deliberately: this package submits ONE request to find out whether the kernel accepts it at all, and a large request that failed for being large would answer a different question.
const MaxFrameBytes = 64 << 20
MaxFrameBytes is the largest frame this package will assemble.
⛔⛔ IT EXISTS BECAUSE THE SIZE COMES FROM THE DEVICE. A frame's dimensions are read out of USB descriptors — two 16-bit fields — so a broken or hostile camera can ask for 65535x65535, and 65535*65535*2 is 8.6 GB that this process would try to allocate twice over (the assembly buffer and the image) before anything else happened. The device is on the other side of a cable and is not trusted with an allocation.
The ceiling is 64 MB — 33,554,432 YUY2 pixels, or 4096x8192 — which is some sixteen times the largest frame any camera here advertises; the Beast's own largest is 1920x1080, or 4 MB.
const MaxPayload = 1 << 16
MaxPayload bounds one decoded message body. The largest message a host legitimately sends is a display list, so a frame beyond this is a desynchronised stream — refused rather than allocated.
const MaxPendingFrames = 4
MaxPendingFrames is how many assembled frames may wait for the caller.
⛔ IT USED TO BE StreamURBs, WHICH WAS A COINCIDENCE RATHER THAN A REASON. This ceiling answers "how stale may a frame be before it is worthless", which is a question about the CONSUMER — head tracking wants the freshest picture — and has nothing to do with how many requests are in flight. Borrowing the ring's depth meant that raising the ring silently made the shedding branch unreachable, which the coverage gate caught the moment the ring went to sixteen.
Four, because a consumer four frames behind at 24 fps is already 170 ms late and a fifth frame would not help it.
const MinPayloadHeader = 2
MinPayloadHeader is the shortest UVC payload header: a length and the bits.
const MinProbeBytes = 26
MinProbeBytes is the shortest probe block any UVC version defines: 26 bytes for 1.0. Anything shorter is not a negotiation block.
const MinTrackingWidth = 320
MinTrackingWidth is the narrowest frame worth asking for.
headflow bins columns [headflow.BinWidth] wide — four — so 320 pixels across is already eighty bins to match a profile on, and its measured reach is bounded in bins rather than in pixels. Below this there is little left to match; above it, more width buys precision a head does not move with.
const ReapTries = 64
SubmitISO queues ONE isochronous request against an endpoint, and reports what the kernel made of it.
⛔ THIS IS THE QUESTION THE WHOLE FILE EXISTS FOR. Android's Java USB API cannot submit an isochronous transfer at all; whether SELinux lets an untrusted application do it through usbfs is undocumented, and every line of a UVC implementation depends on the answer.
It does not reap the result: a submission that the kernel ACCEPTS has already answered the question, and waiting for pixels would confuse "the transfer was allowed" with "the camera was streaming", which are different findings. ReapTries bounds how long USBHandle.SubmitISO waits to collect the request it cancelled.
⚠ IT IS A BOUND, NOT A TIMEOUT TO TUNE. A cancelled isochronous request completes within a frame — a millisecond at high speed — so a handful of tries is already generous. What the bound is really for is the case where it never comes back at all: waiting forever there would hang a probe on a question it has already answered.
const SocketSuffix = ".xr"
SocketSuffix is appended to the application's package name to form the abstract socket the host listens on when nothing names one explicitly. It matches XrHostService.SOCKET_SUFFIX.
const StreamPacketsPerURB = 8
StreamPacketsPerURB is how many isochronous packets one STREAMING request carries, which is a different question and needs a different answer.
⛔⛔ IT IS 8 BECAUSE 32 WAS MEASURED AND WAS WORSE, which is the opposite of what the arithmetic predicted. A 640x480 YUY2 frame at the committed 2400 bytes an interval takes 256 intervals — 32 milliseconds — so a deeper ring looked obviously right. On a Pixel 11 Pro Fold, 2026-10-11, with StreamURBs held at 4:
8 packets 7744 packets/s, 6.24 MB, 10 frames ← this 32 packets 480 packets/s, 5.4 KB, 0 frames collapsed
480 packets in fifteen seconds is one request a SECOND where the schedule gives one a millisecond, and the packets that did arrive carried 11 bytes each: the camera stopped sending. Raising StreamURBs to 16 at 8 packets changed nothing either (7968 packets, same 10 frames), so the ring depth is not what bounds this.
⚠ A CONTROL SAYS SO RATHER THAN A STORY: the previous run had left the interface detached, so two things differed at once. Putting 8 back with that same device state streamed again immediately — which is what makes the ring the cause and not the phone.
The frame-rate deficit that remains has a different cause, localised by the per-boundary census in FrameAssembler.Losses: every dropped frame ends on a FID toggle and almost none on an EOF, so the camera is sending same-identifier packets AFTER its end-of-frame bit. That is still open.
const StreamURBs = 16
StreamURBs is how many isochronous requests are kept in flight.
⛔ ONE IS NOT ENOUGH, AND THAT IS NOT A TUNING CHOICE. An isochronous endpoint is scheduled every microframe whether anybody is listening: between reaping one request and submitting the next, the camera's packets go nowhere.
⛔⛔ AND SIXTEEN IS MEASURED, AGAINST FOUR WHICH HALVED THE FRAME RATE. This was 4 for five releases on the strength of an experiment with a CHEAP consumer where deepening the ring changed nothing — an experiment that was right about its own conditions and wrong as a general conclusion. With a consumer costing more than the ring covers, the ring is dry while it works and the packets that arrive then are gone. Measured on a Pixel 11 Pro Fold with cmd/xrhead, which spends 4.8 ms a frame decoding and tracking:
requests fps dropped
4 10.5 79
8 21.5 14
16 23.8 0 <- this
32 0 the camera stops sending
23.8 fps against the 25 the camera advertises, and nothing dropped. Thirty-two fails the same way thirty-two PACKETS a request does; both axes have a ceiling of their own and neither is their product, since 4x32 and 16x8 are both 128 packets and only one of them works.
⛔ WHAT THIS CORRECTS IS A PUBLISHED CONCLUSION. This repository said "~10 fps is the camera's ceiling, flat across a fourfold change in pixel count", and the byte budget behind it said the camera filled 34.7 % of each interval. The budget closed; the causal reading was backwards. The camera under-filled because NOTHING WAS ASKING — at sixteen requests the same camera fills 77 %.
const VitureVendorID = 0x35ca
VitureVendorID is VITURE's USB vendor id, recorded in go-xrkit/xrkit/glasses: every VITURE headset reports it.
const WallSocketSuffix = ".xrwall"
WallSocketSuffix is appended to the package name to get the abstract socket the WALL host listens on, matching XrWallService.SOCKET_SUFFIX.
It is a second socket, and a second service, because the two have nothing in common but a wire format. Capture needs a MediaProjection, a consent dialog and — from API 34 — a mediaProjection foreground service, none of which an owned display needs: it asks for no permission at all. Keeping them apart means creating a ribbon panel cannot put a "recording your screen" chip in the status bar, and that a capture session ending cannot take a panel with it. The capture host also serves exactly one connection, whereas the wall serves one PER DISPLAY.
const ZeroBandwidthAlternate = 0
ZeroBandwidthAlternate is alternate setting 0 of a UVC streaming interface.
⛔ IT IS WHERE A STREAMING INTERFACE MUST SIT WHILE IT IS NEGOTIATED. Alternate 0 of a video streaming interface carries no isochronous endpoint at all — that is what "zero bandwidth" means — and the host parks there when not streaming so the bus's isochronous budget is not held. UVC then treats the move AWAY from it, after a commit, as the signal to start sending.
Variables ¶
var ( // ErrUnsupported is reported on every platform that is not Android, and on // Android when the process was not started by a host that speaks this // protocol. The module still builds and vets everywhere, so an application // keeps cross-building on a developer's workstation. ErrUnsupported = errors.New("android: screen capture is unavailable " + "(not running under a go-xrkit Android host)") // ErrPermissionDenied is reported when the user refused the screen-capture // consent dialog, or revoked a projection already running. Its message // names the remedy. ErrPermissionDenied = errors.New("android: screen-capture consent denied — " + "Android asks the user for permission once per capture session and the " + "answer cannot be remembered; call RequestAuthorization again and accept " + "the system dialog") // ErrNoDisplay is reported when a capture was asked for and the host // listed no display at all. ErrNoDisplay = errors.New("android: no capturable display") // ErrNotFound is reported when a display ID does not name anything the // host can see. ErrNotFound = errors.New("android: no such display") // ErrNotCapturable is reported for a display that exists but that an // unprivileged app may not capture. MediaProjection mirrors the DEFAULT // display and nothing else: capturing a second display needs // CAPTURE_VIDEO_OUTPUT, which is a signature permission. ErrNotCapturable = errors.New("android: only the default display can be captured " + "by an unprivileged app (capturing another display needs the signature " + "permission CAPTURE_VIDEO_OUTPUT)") // ErrClosed is reported by every [Stream] method after [Stream.Close]. ErrClosed = errors.New("android: stream is closed") // ErrNoFrame is reported by [Stream.WaitFrame] when no frame arrived // before its context expired. It is NOT a malfunction: a motionless screen // legitimately produces no frames. ErrNoFrame = errors.New("android: no frame available") // ErrInvalidOption is reported by [Options.Validate] and wraps a // description of the offending field. ErrInvalidOption = errors.New("android: invalid option") // ErrShortBuffer is reported by [Frame.CopyTight] when the destination is // too small to hold the frame. ErrShortBuffer = errors.New("android: destination buffer too short") )
Sentinel errors. All are stable and may be matched with errors.Is.
var ErrBadPayload = errors.New("android: malformed message payload")
ErrBadPayload reports a message whose body is structurally impossible — a string longer than the body that holds it, a negative count.
var ErrInRepository = errors.New("a capture must never be written where it can be committed")
ErrInRepository is reported when the chosen artefact directory is inside a git work tree.
var ErrNotPresentable = errors.New("android: display will not take a presentation")
ErrNotPresentable reports a display that cannot carry a Presentation.
The built-in panel is the usual one: it is where the launcher and every other application live, and the platform will not let an ordinary app put a Presentation over them. Display.Presentation says in advance, which is why this is answered here without asking the host.
var ErrScreenClosed = errors.New("android: screen is closed")
ErrScreenClosed is reported by Screen.Next and Screen.Present after Screen.Close.
var ErrShortPayload = errors.New("android: truncated message payload")
ErrShortPayload reports a message whose body is too short for its type.
var ErrTooManyDisplays = errors.New("android: too many virtual displays")
ErrTooManyDisplays is reported by NewWall when the requested limit exceeds MaxDisplays, and by Wall.Open when the wall is already at its limit. Nothing is created in either case.
var ErrUSBPermissionDenied = errors.New("android: permission to open the USB device was refused")
ErrUSBPermissionDenied reports the user declining the system's USB permission dialog, or an application asking for a device it was never granted.
It is a decision rather than a failure: opening a USB device shows a dialog naming the device and the application, and a person may say no.
A device that is no longer attached reports ErrNotFound instead, which this package already had: a headset on the end of a cable is unplugged more often than anything else here, and a second sentinel for it would be two names for one condition.
var ErrWallClosed = errors.New("android: wall is closed")
ErrWallClosed is reported by Wall.Open after Wall.Close.
Functions ¶
func ArtifactDir ¶ added in v0.4.0
ArtifactDir is where a command may write what it captured, created if it is not there. The rule above is what it enforces.
func Authorized ¶
func Authorized() bool
Authorized reports whether the host already holds a projection token, so a capture can start without a dialog. It never prompts.
Unlike a macOS TCC grant this is NOT a lasting permission: Android issues a token per session and forgets it, so this answers "right now", not "ever".
func Available ¶
func Available() bool
Available reports whether this process is running under a host that speaks this protocol. It consults the environment only: it opens nothing and blocks on nothing, so it is safe to call before deciding whether Android capture is this program's platform at all.
func ControlIn ¶ added in v0.9.0
func ControlIn() uint8
ControlIn builds the bmRequestType of a class request that brings data back from one interface, which is every UVC GET.
func ControlOut ¶ added in v0.9.0
func ControlOut() uint8
ControlOut builds the bmRequestType of a class request that carries data to one interface, which is every UVC SET.
func DecodeBuffer ¶
DecodeBuffer decodes the layout of the shared buffer.
func DecodeConsent ¶
DecodeConsent decodes the user's decision.
func DecodeOpenUSBDevice ¶ added in v0.7.0
DecodeOpenUSBDevice decodes it.
func DeriveSocket ¶
DeriveSocket works out which abstract socket to dial.
An explicit name wins: a host that spawned this process sets EnvSocket and that is the end of it. But in the composition this package is really for, the process is spawned by a DIFFERENT host — go-widgets/android's Activity, which owns the drawing surface and knows nothing about capture — and never sets it. Android gives such a process no way to ask its own package name either.
What it does give it is HOME, which that host sets to the app's private files directory: "/data/user/0/org.example.app/files". The package name is in there, and the host's socket name follows from it. That is why the two hosts can live in one APK without either one having to know about the other.
It returns "" when neither route yields a name.
func DeriveWallSocket ¶ added in v0.2.0
DeriveWallSocket works out which abstract socket the wall host listens on, exactly as DeriveSocket does for the capture host.
func EncodeBuffer ¶
EncodeBuffer encodes the layout of the shared buffer accompanying MsgBuffer.
func EncodeCameras ¶ added in v0.6.0
EncodeCameras encodes a camera list.
func EncodeConsent ¶
EncodeConsent encodes the user's decision.
func EncodeDisplays ¶
EncodeDisplays encodes a display list.
func EncodeOpenDisplay ¶ added in v0.2.0
func EncodeOpenDisplay(m OpenDisplayMessage) []byte
EncodeOpenDisplay encodes an OpenDisplayMessage.
func EncodeOpenScreen ¶ added in v0.5.0
func EncodeOpenScreen(m OpenScreenMessage) []byte
EncodeOpenScreen encodes an OpenScreenMessage.
func EncodeOpenUSBDevice ¶ added in v0.7.0
EncodeOpenUSBDevice encodes the name of the device to open — the node name from USBDevice.Name, which is what a USB permission is granted against.
func EncodePresent ¶ added in v0.5.0
func EncodePresent(m PresentMessage) []byte
EncodePresent encodes a PresentMessage.
func EncodePresented ¶ added in v0.5.0
func EncodePresented(m PresentedMessage) []byte
EncodePresented encodes a PresentedMessage.
func EncodeStopped ¶
func EncodeStopped(s StoppedMessage) []byte
EncodeStopped encodes a StoppedMessage.
func EncodeUSBDevices ¶ added in v0.6.0
EncodeUSBDevices encodes a USB device list, down to each endpoint.
func FrameMessage ¶
FrameMessage returns one framed message: a 4-byte big-endian length covering the type byte and the body, then the type byte, then the body.
It exists as bytes rather than as writes because a message that carries an ancillary descriptor has to reach the host in ONE sendmsg: split across two writes, the host could attribute the descriptor to the wrong message.
func PickAlternate ¶ added in v0.11.0
func PickAlternate(ifaces []USBInterface, number int, payload uint32) (USBInterface, USBEndpoint, bool)
PickAlternate chooses the alternate setting of one interface that can carry a committed payload, and that alternate's isochronous IN endpoint.
⛔⛔ THE SMALLEST THAT FITS, AND AFTER THE COMMIT. Not the largest: an alternate's endpoint reserves bus bandwidth for as long as it is selected, so taking 3072 bytes an interval to carry 2400 holds part of every high-speed microframe away from everything else on the bus for nothing. And not before the commit, because the payload is not known until the camera has answered.
func PickFormat ¶ added in v0.9.0
func PickFormat(fs []UVCFormatDesc) (UVCFormatDesc, UVCFrameSize, error)
PickFormat chooses what to ask a camera for: the fastest frame in a format this package can actually read.
⭐ YUY2 FIRST, AND THE REASON IS WHAT IS IMPLEMENTED. Head tracking reads luminance only, and in YUY2 every other byte IS the luminance — no decoder, no colour conversion, nothing allocated per frame. MJPEG on the camera measured here is FASTER, 30 frames a second at every size against YUY2's 25, and it is the better choice the day there is a JPEG decode in the frame path. There is not, so preferring YUY2 reflects the code rather than the hardware, and saying so is the point.
⚠ A format this package cannot name is skipped rather than attempted. A camera offering only those is a different finding from one offering none, and the error says how many there were.
func ReadMessage ¶
ReadMessage reads one framed message. It returns io.EOF when the stream ends cleanly between messages, so a caller can tell a closed host from a truncated one.
func RequestAuthorization ¶
RequestAuthorization asks the host to put Android's screen-capture consent dialog in front of the user, and blocks until they answer or ctx expires.
It is a separate call rather than something CaptureDisplay does implicitly because the dialog takes over the screen: an application wants to choose when that happens, and to say why first.
func SaveFrame ¶ added in v0.14.0
SaveFrame writes one captured frame beside the transcript, as a PNG, and returns the path it used.
⛔⛔ A FRAME IS A PICTURE OF WHATEVER THE CAMERA WAS POINTED AT, which on a headset is a room and the people in it. It goes exactly where a transcript goes and under exactly the rule above — never inside a work tree, including one somebody points ArtifactEnv at. That is also why there is no option to put it anywhere else.
⭐ PNG, AND NOT A FORMAT OF OUR OWN. These frames exist to be fed to go-xrkit/xrkit/headflow, which lives in another module with a dependency tree this one has no business acquiring — HCL and eight indirect modules, for a diagnostic binary. A FILE is the interface between them, `image/png` is in the standard library on both sides, so the two halves share no code at all; and a person can also just look at the picture.
⚠ GREY, BECAUSE GREY IS WHAT IS READ. headflow.Profile sums R+G+B down each column, and every decoder here already writes the luminance into all three channels, so a grey PNG loses nothing a tracker uses and is a quarter of the size.
func SaveTranscript ¶ added in v0.5.0
SaveTranscript writes a command's transcript where it can still be read after the cable has been somewhere else, and returns the path it used.
⛔ THE APP'S OWN files DIRECTORY, NOT ITS EXTERNAL ONE. A file in /storage/emulated/0/Android/data/<pkg>/ is hidden from the shell user on a modern Android, and `adb shell run-as` cannot traverse the FUSE layer that mediates it: a whole run was measured, written there, and unreadable. The private directory -- which the host hands over as HOME -- is what run-as can read, and this is where that lands.
Off a device there is no HOME of that kind, so it falls back to ArtifactDir. A transcript is text this process composed rather than a picture of anybody's screen, so the rule about work trees does not govern it -- but the fallback obeys it anyway, because the cheapest way to keep a rule is to have one path.
func SentinelColorAt ¶ added in v0.2.0
SentinelColorAt is the colour a correct Sentinel frame holds at (x, y) on a display of the given size, as 0xRRGGBB. It reports false for a point outside the display, or one close enough to a quadrant edge or to the label that anti-aliasing makes the exact value unsafe to assert on.
Sample with it rather than comparing whole images: the label is rendered by Android's own text stack and will not be byte-identical between releases, while the quadrants are flat colour and are.
func WallAvailable ¶ added in v0.2.0
func WallAvailable() bool
WallAvailable reports whether this process is running under a host that serves owned displays. It dials nothing.
Types ¶
type Camera ¶ added in v0.6.0
type Camera struct {
// ID is the camera2 id, which is what opens it. It is a small number as a
// string for built-in cameras and is not necessarily one for an external.
ID string
// Facing is which way it points; see [FacingExternal].
Facing CameraFacing
// Width and Height are the largest still size it advertises, in pixels.
// Zero when the host could not read its characteristics.
Width, Height int
}
Camera is one camera the camera2 API can see.
func Cameras ¶ added in v0.6.0
Cameras returns every camera the camera2 API can see, including external USB ones.
⛔ IT ASKS THE WALL HOST, like Screens, and for the same reason: listing cameras needs NO permission at all. android.permission.CAMERA is required to OPEN one, not to be told it is there, so a census costs the user no dialog — which is exactly what makes it usable before deciding anything.
An empty list is an answer: it says this phone's HAL offers no external camera, which is the thing that decides whether a headset's camera can be reached through the platform at all. See ChooseRoute.
func DecodeCameras ¶ added in v0.6.0
DecodeCameras decodes a camera list.
type CameraFacing ¶ added in v0.6.0
type CameraFacing int
CameraFacing says which way a camera points, as android.hardware.camera2's LENS_FACING.
const ( // FacingFront is the selfie camera. FacingFront CameraFacing = 0 // FacingBack is the main camera. FacingBack CameraFacing = 1 // FacingExternal is a camera that is not fixed to the device and may come // and go — a USB camera, which is what a headset's is. // // ⭐ THIS IS THE ONE THAT MATTERS HERE, and the one a phone may simply not // offer: supporting external cameras is left to the vendor's HAL. FacingExternal CameraFacing = 2 )
The facings camera2 defines.
func (CameraFacing) String ¶ added in v0.6.0
func (f CameraFacing) String() string
String renders the facing.
type CameraRoute ¶ added in v0.6.0
type CameraRoute int
CameraRoute says how an application could reach a headset's camera on this phone, which is a measurement rather than a choice; see the file comment.
const ( // RouteNone is no route at all: the phone offers no external camera and no // USB device that could be read. RouteNone CameraRoute = iota // RouteCamera2 is the camera2 API with an external camera, which is the // route to want: the platform does the UVC negotiation, the format // conversion and the buffering. RouteCamera2 // RouteUSBBulk is claiming the UVC interface over the USB host API. It // needs the streaming endpoints to be bulk, and it means writing the UVC // negotiation by hand. RouteUSBBulk // RouteUSBIsochronous is a UVC device whose frames are isochronous. It is // listed because it is what a census will usually find, and NOT because it // is usable through Android's Java USB API — reaching it means usbfs ioctls // on the descriptor that API hands out. RouteUSBIsochronous )
The routes, in the order they are preferred.
func ChooseRoute ¶ added in v0.6.0
func ChooseRoute(cams []Camera, devs []USBDevice) CameraRoute
ChooseRoute says how a headset's camera could be reached, given what the census found.
It prefers camera2 whenever an external camera is there, because the platform then does the UVC negotiation, the format conversion and the buffering that the USB route would mean writing by hand.
⚠ IT ANSWERS FROM THE CENSUS, NOT FROM THE HEADSET. A phone with some other USB camera attached would be reported as reachable, and it would be — just not the headset's. USBDevice.Viture is how a caller narrows it, and cmd/xrcam prints the device it is talking about alongside the verdict.
func (CameraRoute) String ¶ added in v0.6.0
func (r CameraRoute) String() string
String renders the route.
func (CameraRoute) Usable ¶ added in v0.6.0
func (r CameraRoute) Usable() bool
Usable reports whether this route can be taken with Android's own APIs and nothing else.
type Canvas ¶ added in v0.5.0
type Canvas struct {
// Pix is the frame's pixels, Stride bytes a row, Height rows.
Pix []byte
// Stride is the distance in bytes between two rows. It is at least
// Width*4 and is usually more: the host announces the alignment its
// surface wants, and writing Width*4 instead would shear the image.
Stride int
// Width and Height are the frame's size in pixels.
Width, Height int
// contains filtered or unexported fields
}
A Canvas is one frame slot of a screen's shared buffer, lent to the application to draw into.
The bytes are BORROWED and belong to the host: they are valid from the Screen.Next that returned them until the Screen.Present that hands them back, and touching them afterwards races the blit. A Canvas is not safe for concurrent use.
Pix is RGBA, eight bits a channel — the same memory layout as image.RGBA, which is what Canvas.RGBA hands back so the whole of image/draw can paint a frame.
func (Canvas) RGBA ¶ added in v0.5.0
RGBA returns the canvas as an image.RGBA sharing the same memory, so a frame can be painted with image/draw, a font renderer, or anything else that takes a draw.Image.
It allocates only the header: the pixels are the host's, and drawing through this writes straight into the shared buffer.
func (Canvas) Row ¶ added in v0.5.0
Row returns the bytes of one row, for a compositor that writes spans rather than going through image/draw. It reports nil for a row outside the frame.
The slice stops at Width*4 and cannot be appended past the row: the padding a stride leaves is the host's, and a span that ran into it would corrupt the next row rather than fail.
type ConfigMessage ¶
type ConfigMessage struct {
// Width and Height are the frame size in pixels.
Width, Height int
// Stride is the number of BYTES per row in the shared buffer. It is the
// host's ImageReader row stride and is NOT necessarily Width*4.
Stride int
// Format is the pixel layout, always [FormatRGBA] today.
Format PixelFormat
// Slots is how many frame slots the shared buffer holds.
Slots int
// SlotSize is the byte size of one slot, Stride*Height rounded up to the
// host's alignment.
SlotSize int64
// DisplayID names the display being captured.
DisplayID int
}
ConfigMessage announces a started capture.
func DecodeConfig ¶
func DecodeConfig(b []byte) (ConfigMessage, error)
DecodeConfig decodes a ConfigMessage.
type Content ¶ added in v0.2.0
type Content interface {
// Describe renders the content for logs.
Describe() string
// Validate reports whether the content is usable, wrapping
// [ErrInvalidOption].
Validate() error
// contains filtered or unexported methods
}
Content is what Android renders on an owned display.
The seam, and why it is a DECLARATION rather than pixels ¶
The obvious seam would hand the host a framebuffer for it to blit. That is the wrong way round here, and would be a round trip with no product: pixels this process drew, sent to Android, read back unchanged. If the application is drawing, it should composite in Go and skip the display entirely.
The reason to route a ribbon panel through a real Android display is the opposite one — to get at what Android renders and this process CANNOT: a WebView, a MediaCodec surface, a PdfRenderer, a maps view. None of those can be reached from a CGO-free Go process, all of them are ordinary Views, and a Presentation is how a View gets onto a display. So Content NAMES what should render, the Java host builds it, and the pixels come back through the same borrowed-frame path as a screen capture.
That also keeps the rule the rest of this package is built on: the host owns the Android objects and decides nothing. A URL is a decision the application made; a WebView is the host obeying it.
The set is deliberately closed — a Content implemented outside this package would name a Java class the host does not have. Implementations are Web and Sentinel.
type Display ¶
type Display struct {
// ID is the android.view.Display display id. 0 is the built-in screen.
ID int
// Name is what the system calls the display. For the built-in panel this
// is a device string ("Built-in Screen" on the Android 15 emulator); for a
// display attached over USB-C DP Alt Mode it comes from the sink's EDID,
// which is how XR glasses announce their model.
Name string
// Width and Height are the display's real size in pixels.
Width, Height int
// DensityDPI is the display's density in dots per inch.
DensityDPI int
// RefreshRate is the active mode's rate in hertz.
RefreshRate float64
// Flags are android.view.Display's FLAG_* bits; see [FlagPresentation].
Flags uint32
}
Display is a display the host can see.
Width and Height are in PIXELS — Android reports no separate point space, so unlike the macOS sibling there is only one pair of numbers, and DensityDPI carries what a scale factor would carry elsewhere.
func DecodeDisplays ¶
DecodeDisplays decodes a display list.
func DefaultDisplay ¶
DefaultDisplay returns the built-in screen, the only display an unprivileged app may capture.
func Displays ¶
Displays returns every display the host can see.
Only the default one can be captured (see ErrNotCapturable), but the others matter all the same: a headset attached over USB-C DP Alt Mode appears here, and its Display.Name is how a catalogue identifies the model.
func Screens ¶ added in v0.5.0
Screens returns the displays an application may put a Screen on: the ones the platform will take a Presentation on, minus the built-in panel.
⛔ IT ASKS THE WALL HOST, NOT THE CAPTURE HOST, and that is the difference between needing a MediaProjection and needing nothing. Displays goes to the capture host because that is where a capture's display list belongs; an application that only wants to paint on the glasses would have had to start the whole mediaProjection apparatus — a foreground service, a consent dialog, a "recording your screen" chip — to find out that a pair of glasses is plugged in. This asks the service that already owns Presentations.
The connection is dialled and dropped: there is no session to keep, and a display list is a question with an answer rather than a stream.
func (Display) Default ¶
Default reports whether this is the built-in screen, the only display an unprivileged app may capture.
func (Display) Presentation ¶
Presentation reports whether the system considers this display suitable for an application's own full-screen content — which is what an XR headset attached over DP Alt Mode is for.
type DisplaySpec ¶ added in v0.2.0
type DisplaySpec struct {
// Width and Height are the display's size in PIXELS. Both are required.
//
// Making them small does NOT buy more displays — the count that kills
// system_server was the same at 640x480 and at 1920x1080 — so choose the
// size the content wants to be read at.
Width, Height int
// DensityDPI is the display's density. Zero means [DefaultDensityDPI].
// Android lays the view hierarchy out against this, so it decides how
// large text and controls come out in the ribbon.
DensityDPI int
// Content is what Android renders on the display. It is required; see
// [Content].
Content Content
// QueueDepth is how many frame slots the shared buffer holds, exactly as
// in [Options.QueueDepth]. Zero means [DefaultQueueDepth].
QueueDepth int
}
DisplaySpec says what an owned display should be.
The zero DisplaySpec is not usable: a display has no natural size to fall back on the way a capture does, so Width, Height and Content must be given.
func (DisplaySpec) String ¶ added in v0.2.0
func (s DisplaySpec) String() string
String renders the spec for logs.
func (DisplaySpec) Validate ¶ added in v0.2.0
func (s DisplaySpec) Validate() error
Validate reports whether the spec describes a display Android could make, wrapping ErrInvalidOption. It does not consult the system, and it does not apply defaults: a zero DensityDPI or QueueDepth is refused here and filled in by Wall.Open before this is reached.
type ErrorMessage ¶
type ErrorMessage struct {
// Code is the host's own numeric code, 0 when it has none.
Code int
// Op names what failed, e.g. "getMediaProjection".
Op string
// Detail is the platform's message, e.g. a SecurityException's text.
Detail string
}
ErrorMessage is a failure the host could not answer in-band.
func DecodeError ¶
func DecodeError(b []byte) (ErrorMessage, error)
DecodeError decodes an ErrorMessage.
func (ErrorMessage) Error ¶
func (e ErrorMessage) Error() string
Error renders the operation, the code and the platform's message.
type Frame ¶
type Frame struct {
// Pix is the frame's bytes in [FormatRGBA], Stride bytes per row, Height
// rows. len(Pix) == Stride*Height.
Pix []byte
// Width and Height are the frame's size in pixels.
Width, Height int
// Stride is the number of BYTES per row. It may be padded and is NOT
// necessarily Width*4.
Stride int
// Seq counts frames since the stream started; it is 0 before the first
// frame and strictly increases afterwards.
Seq uint64
// At is when the host's ImageReader delivered the frame.
At time.Time
}
Frame is a BORROWED view of one captured frame.
Pix aliases the shared mapping the host wrote into. It stays valid only until the next Stream.Frame, Stream.WaitFrame or Stream.Close on the stream that produced it. Do not retain it; copy with Frame.CopyTight or Frame.NRGBA if you need it to outlive the borrow.
func (Frame) CopyTight ¶
CopyTight copies the frame into dst with the row padding removed, so dst holds Width*4*Height bytes of contiguous RGBA. It reports how many bytes it wrote, or ErrShortBuffer if dst is too small. It allocates nothing.
func (Frame) NRGBA ¶
NRGBA copies the frame into a freshly allocated image.NRGBA. It is the convenience path for saving a frame to disk; it allocates, so it does not belong in a per-frame loop.
Android's RGBA_8888 is already image.NRGBA's byte order, so unlike the macOS sibling this is a copy with no channel swap — but it is still a copy, because the source is padded and borrowed.
func (Frame) Row ¶
Row returns row y of the frame, Width*4 bytes with the padding trimmed off. It does not allocate. It returns nil for an out-of-range y or an invalid frame.
type FrameAssembler ¶ added in v0.10.0
type FrameAssembler struct {
// contains filtered or unexported fields
}
A FrameAssembler turns a stream of payloads into whole frames.
⭐ IT ENDS A FRAME ON THE FID TOGGLE, not on the end-of-frame bit. EOF is optional in practice — cameras that never set it exist — and a boundary that depended on it would run every frame into the next. EOF is still honoured when it comes, because it ends the frame one payload earlier and saves carrying a frame until the next one starts.
⛔ AND A SHORT FRAME IS DROPPED RATHER THAN PADDED. A stream that lost a packet leaves a frame missing a band; padding it with zeros produces a black stripe that a tracker reads as structure, which is worse than no frame at all. [FrameAssembler.Dropped] counts them.
func NewFrameAssembler ¶ added in v0.10.0
func NewFrameAssembler(w, h int) (*FrameAssembler, error)
NewFrameAssembler returns an assembler for uncompressed YUY2 frames of the given size. See NewFrameAssemblerFor for the compressed ones.
func NewFrameAssemblerFor ¶ added in v0.12.0
func NewFrameAssemblerFor(kind UVCFormat, w, h int) (*FrameAssembler, error)
NewFrameAssemblerFor returns an assembler for frames of the given format and size.
⛔⛔ THE FORMAT CHANGES WHAT "A WHOLE FRAME" MEANS, which is the whole reason this is not one code path with a flag. A YUY2 frame is whole when it is w*h*2 bytes: the LENGTH is the test, and a short one is a frame that lost packets. An MJPEG frame is a JPEG, so its length is whatever the compressor made it and the length says nothing at all — a frame that lost packets is the same length as a frame of a plainer scene. What tells them apart is the MARKERS, and nothing else does.
Reaching for MJPEG is measured rather than preferred: the Beast's camera advertises 30 fps at every size for MJPEG and 25 fps only at 640x480 and below for YUY2, and the byte budget of a measured run says why — it fills 832 of the 2400 bytes an interval it committed to, which is 10.5 uncompressed frames a second whatever is asked of it.
func (*FrameAssembler) BadJPEG ¶ added in v0.12.0
func (a *FrameAssembler) BadJPEG() int
BadJPEG is how many MJPEG frames arrived whole — both markers present — and still would not decode, or decoded to a size other than the one committed.
⛔ IT IS NOT PART OF Stats BECAUSE IT IS A DIFFERENT FAULT. A dropped frame never arrived; one of these did, and is still not a picture, which points at the camera's compressor or at a corrupted middle rather than at a lost tail.
func (*FrameAssembler) Boundaries ¶ added in v0.11.0
func (a *FrameAssembler) Boundaries() (toggles, eofs, shortBytes int)
Boundaries reports how each frame ended and how much the dropped ones were missing: identifier toggles, end-of-frame bits, and the total shortfall in bytes.
⭐ IT IS A CENSUS PER CHANNEL. "frames" and "dropped" cannot say whether a camera split its frames or a bus lost packets — the first shows up as more boundaries than frames, the second as a shortfall spread thinly. Averaging the shortfall over the dropped count says which.
func (*FrameAssembler) Decode ¶ added in v0.10.0
Decode turns one assembled YUY2 frame into an RGBA image.
⭐ IT WRITES LUMINANCE INTO ALL THREE CHANNELS, which is not a shortcut. The consumer is go-xrkit/xrkit/headflow, whose Profile sums R+G+B down each column: a grey image carries exactly what it reads, and a full YUV-to-RGB conversion would compute chrominance for it to add up and discard. Every other byte of YUY2 IS the luminance, so this is one pass and no arithmetic.
dst is reused when it is already the right size, so a steady stream allocates nothing.
func (*FrameAssembler) Feed ¶ added in v0.10.0
func (a *FrameAssembler) Feed(p UVCPayload) ([]byte, bool)
Feed takes one payload and returns a whole frame's bytes when one completed.
⛔⛔ THE BYTES ARE THE CALLER'S, AND THIS COMMENT USED TO SAY THE OPPOSITE. It claimed they were borrowed until the next Feed; [FrameAssembler.complete] hands the buffer over and allocates a fresh one, so a completed frame stays valid for as long as the caller keeps it. The code was the safer of the two and the promise was the wrong one — which matters, because that ownership is exactly what makes UVCStream's queue of pending frames legal. A reader who trusted the old sentence would have read that queue as a use-after-free.
⚠ It costs one allocation a frame, measured at 611 KB and under 0.15 ms at 640x480 — two orders of magnitude below the frame interval, which is why it is spent rather than pooled. A free list would buy nothing here and would put an ownership bug where there is now none.
func (*FrameAssembler) Format ¶ added in v0.12.0
func (a *FrameAssembler) Format() UVCFormat
Format is the format this assembler was built for.
func (*FrameAssembler) Losses ¶ added in v0.11.0
func (a *FrameAssembler) Losses() (onToggle, onEOF int)
Losses reports which boundary ended the frames that were dropped.
⛔ THE SPLIT IS THE DIAGNOSIS. Equal numbers of toggles and EOFs with half the frames nearly empty fits two opposite stories — a camera setting EOF early, or one toggling early — and they call for opposite fixes.
func (*FrameAssembler) MissingMarkers ¶ added in v0.13.0
func (a *FrameAssembler) MissingMarkers() (noStart, noEnd int)
MissingMarkers splits the MJPEG frames that were dropped by WHICH marker they lacked: one that began mid-picture, and one that lost its tail.
⛔ THEY ARE OPPOSITE FAULTS. No start-of-image means a boundary fired where the camera had not finished a picture; a start with no end means the tail went missing. "Dropped" cannot tell them apart, and they call for different answers — the same reason FrameAssembler.Losses splits by boundary.
func (*FrameAssembler) Size ¶ added in v0.10.0
func (a *FrameAssembler) Size() (int, int)
Size is the frame size this assembler was built for.
func (*FrameAssembler) Stats ¶ added in v0.10.0
func (a *FrameAssembler) Stats() (frames, dropped, bad int)
Stats reports how many whole frames came out, how many were dropped for being short, and how many payloads the camera itself marked bad.
type FrameMsg ¶
type FrameMsg struct {
// Seq counts frames since the capture started, from 1.
Seq uint64
// Slot is which slot of the shared buffer holds it.
Slot int
// Width, Height and Stride describe THIS frame. They normally repeat the
// config, but the host may resize a capture in flight — a phone rotating
// under a capture does exactly that — so every frame carries its own
// geometry rather than inheriting one that may be stale.
Width, Height, Stride int
// AtUnixNano is when the host's ImageReader delivered it.
AtUnixNano int64
}
FrameMsg says a frame has been written into a slot of the shared buffer.
type OpenDisplayMessage ¶ added in v0.2.0
type OpenDisplayMessage struct {
// Width and Height are the display's size in pixels.
Width, Height int
// DensityDPI is the density Android lays the view hierarchy out against.
DensityDPI int
// Slots is how many frame slots the shared buffer holds.
Slots int
// ContentKind names the View the host should build; see [Content].
ContentKind uint32
// ContentPayload is that content's one string argument — a URL for a
// WebView, a label for the sentinel.
ContentPayload string
}
OpenDisplayMessage asks the wall host for one owned display.
func DecodeOpenDisplay ¶ added in v0.2.0
func DecodeOpenDisplay(b []byte) (OpenDisplayMessage, error)
DecodeOpenDisplay decodes an OpenDisplayMessage.
type OpenScreenMessage ¶ added in v0.5.0
type OpenScreenMessage struct {
// DisplayID is the android.view.Display id to present on. It must be a
// display the host can see and whose FLAG_PRESENTATION is set; the host
// refuses anything else rather than guessing.
DisplayID int
// Width and Height are the size of the frames this process will write, in
// pixels. They need not match the display: the host scales the bitmap to
// fill it, which is how a ribbon rendered at one resolution reaches panels
// of another.
Width, Height int
// Slots is how many frame slots the shared buffer holds.
Slots int
}
OpenScreenMessage asks the host for a Presentation on a display that already exists, and for the shared buffer this process will paint into.
func DecodeOpenScreen ¶ added in v0.5.0
func DecodeOpenScreen(b []byte) (OpenScreenMessage, error)
DecodeOpenScreen decodes an OpenScreenMessage.
type Options ¶
type Options struct {
// Width and Height are the requested frame size in PIXELS. Zero means the
// display's native size.
Width, Height int
// FPS is the CEILING on the frame rate, not a guarantee: a MediaProjection
// only produces a frame when the content changed. Zero means [DefaultFPS].
FPS float64
// QueueDepth is how many frame slots the shared buffer holds. Zero means
// [DefaultQueueDepth]. It must leave room for the frame lent out and the
// one being written, so values below [MinQueueDepth] are rejected — with
// two slots a host that produced two frames while the consumer held one
// would overwrite the borrow.
QueueDepth int
}
Options configures a capture stream.
The zero Options is usable: it captures the default display at its native pixel size, at up to 60 frames per second.
func (Options) Validate ¶
Validate reports whether the options are self-consistent, wrapping ErrInvalidOption. It does not consult the system.
type OwnedDisplay ¶ added in v0.2.0
type OwnedDisplay struct {
*Stream
// contains filtered or unexported fields
}
OwnedDisplay is one display this application created, and the feed of pixels Android rendered on it.
It embeds Stream, so a ribbon consumes it exactly as it consumes a screen capture — Stream.Frame lends borrowed pixels with the stride carried and says whether they are fresh, Stream.WaitFrame blocks for the next one. That is the point: the ribbon does not care whether a panel is the mirrored phone or a display we made.
func (*OwnedDisplay) Close ¶ added in v0.2.0
func (d *OwnedDisplay) Close() error
Close releases the display and its feed, and gives the slot back to the wall. It is idempotent.
func (*OwnedDisplay) ID ¶ added in v0.2.0
func (d *OwnedDisplay) ID() int
ID is the android.view.Display id the platform gave this display. It is only meaningful for logs and for matching against `adb shell dumpsys display`; no other application may do anything with it.
func (*OwnedDisplay) Spec ¶ added in v0.2.0
func (d *OwnedDisplay) Spec() DisplaySpec
Spec is the specification this display was opened with, with its zero fields filled in.
func (*OwnedDisplay) String ¶ added in v0.2.0
func (d *OwnedDisplay) String() string
String renders the display for logs.
type PixelFormat ¶
type PixelFormat uint32
PixelFormat names the layout of a captured frame, using Android's own android.graphics.PixelFormat constants so the value on the wire is the one the host read off the ImageReader.
const FormatRGBA PixelFormat = 1 // android.graphics.PixelFormat.RGBA_8888
FormatRGBA is 32-bit RGBA, android.graphics.PixelFormat.RGBA_8888. It is the only format this package streams, and it is what ImageReader produces for a MediaProjection mirror. It is also byte-for-byte what a go-widgets painter writes, so a captured frame composites with no conversion.
This differs from the sibling packages on other platforms: macOS ScreenCaptureKit hands back BGRA. Frame.NRGBA hides the difference, and anything compositing raw bytes must ask Stream.Format.
func (PixelFormat) BytesPerPixel ¶
func (f PixelFormat) BytesPerPixel() int
BytesPerPixel is the size of one pixel in this format, 0 for a format this package does not stream.
func (PixelFormat) String ¶
func (f PixelFormat) String() string
String renders the format for logs.
type PresentMessage ¶ added in v0.5.0
type PresentMessage struct {
// Seq numbers the frame, from 1. It comes back in [PresentedMessage].
Seq int64
// Slot is which slot of the shared buffer holds the pixels.
Slot int
// Width, Height and Stride describe the pixels in that slot.
Width, Height, Stride int
}
PresentMessage hands one slot of the shared buffer to the host to put on the screen. The geometry travels with it rather than being implied by the config, for the same reason FrameMsg carries it: the one place a wrong number would become an out-of-range slice over shared memory is the place to state it explicitly and check it.
func DecodePresent ¶ added in v0.5.0
func DecodePresent(b []byte) (PresentMessage, error)
DecodePresent decodes a PresentMessage.
type PresentedMessage ¶ added in v0.5.0
type PresentedMessage struct {
// Seq is the [PresentMessage.Seq] of the frame that was presented.
Seq int64
// Slot is the slot it occupied, which the application may now draw into.
Slot int
}
PresentedMessage says one frame reached the screen and its slot is free.
func DecodePresented ¶ added in v0.5.0
func DecodePresented(b []byte) (PresentedMessage, error)
DecodePresented decodes a PresentedMessage.
type Screen ¶ added in v0.5.0
type Screen struct {
// contains filtered or unexported fields
}
A Screen is a Presentation on a display that ALREADY EXISTS — the glasses — carrying pixels this process painted.
Why this is a different thing from a Wall ¶
A Wall creates displays and reads what Android renders on them. A Screen does neither: the display is the headset, the platform made it when the cable was plugged in, and the pixels travel the other way. The two share a host, a socket and a wire, and nothing else — a Wall is an INPUT, a Screen is the OUTPUT, and an XR application wants both at once.
Why an application may have one at all ¶
An ordinary APK may not start an activity on a display it does not own: that is refused with a SecurityException, and the flag that would lift it needs a signature permission. A Presentation is not an activity start, so it is not what that check refuses, and it is the only route there is. Whether a given display will take one is the platform's own answer, carried in Display.Presentation — measured on a Pixel 11 Pro Fold with VITURE Beast glasses on the USB-C port:
APPEARED id 12 "VITURE Beast" 1920x1080 @110dpi 60.0Hz flags 0x8088 presentation true
No permission, no consent dialog and no MediaProjection are involved, exactly as for a Wall.
The back-pressure is the point ¶
The host lends a shared buffer of ScreenOptions.QueueDepth slots. Screen.Next hands out a free one, the application draws into it, Screen.Present gives it to the host, and the slot comes back only once the host says the frame reached the view. Nothing else stops the application overwriting a slot mid-blit: the pixels go the other way here, so there is no stream of host frames to pace against, and a tear in a headset is not a cosmetic defect.
A Screen is safe for concurrent use. A Canvas is not: it is a window onto shared memory, and two goroutines drawing into one race.
func ShowOn ¶ added in v0.5.0
ShowOn puts a Presentation on an existing display and returns the screen the application draws into.
It refuses a display the platform will not take a Presentation on with ErrNotPresentable, BEFORE dialling anything: the answer is already in the Display the host handed over, and a round trip would come back saying less. It wraps ErrInvalidOption for options that cannot describe a frame, and reports ErrUnsupported where there is no host.
Close the screen when done; it holds a socket, a mapping and a window.
func (*Screen) Close ¶ added in v0.5.0
Close takes the Presentation down, unmaps the buffer and releases the socket. It is idempotent.
Any Canvas still out is invalid afterwards: the memory under it is gone.
func (*Screen) DisplayID ¶ added in v0.5.0
DisplayID is the android.view.Display id this screen presents on.
func (*Screen) Err ¶ added in v0.5.0
Err reports the failure that ended the screen, or nil while it is live.
func (*Screen) Next ¶ added in v0.5.0
Next returns a canvas to draw the next frame into, waiting for a slot if every one is still in flight.
The pixels it hands back are BORROWED from the shared buffer and are whatever the frame that last used that slot left there, not blank: a compositor that paints the whole frame wants that, and one that does not must clear it.
It reports the context's error if the wait is abandoned, ErrScreenClosed after Screen.Close, and the host's failure if the host went away.
func (*Screen) Options ¶ added in v0.5.0
func (s *Screen) Options() ScreenOptions
Options are the options this screen was opened with, with the zero fields filled in.
func (*Screen) Present ¶ added in v0.5.0
Present hands a drawn canvas to the host to put on the screen. The canvas is invalid afterwards whether this succeeded or not.
It refuses a canvas this screen did not lend, and one presented twice, with ErrInvalidOption: the slot number reaches the host as an offset into shared memory, and freeing a slot that is in use would let the next frame be drawn over one being blitted.
func (*Screen) Size ¶ added in v0.5.0
Size is the frame size in pixels, which is what Screen.Next hands back. The display may be a different size; the host scales to fill it.
func (*Screen) Stats ¶ added in v0.5.0
func (s *Screen) Stats() ScreenStats
Stats reports what the screen has done.
type ScreenOptions ¶ added in v0.5.0
type ScreenOptions struct {
// Width and Height are the size of the frames the application will draw,
// in pixels. Zero means the display's own size.
//
// They need not match the display. The host scales the frame to fill it,
// so a ribbon composited once at one resolution reaches panels of another —
// and a screen rendered deliberately small is how a weak device keeps its
// frame rate.
Width, Height int
// QueueDepth is how many frame slots the shared buffer holds. Zero means
// [DefaultQueueDepth], and it is bounded by [MinQueueDepth] and
// [MaxQueueDepth] exactly as a capture's is: the floor leaves room for the
// frame being drawn, the one in flight and the one being blitted.
QueueDepth int
}
ScreenOptions says what frames an application will put on a screen.
The zero value is usable and means: frames the size of the display, with DefaultQueueDepth slots.
func (ScreenOptions) String ¶ added in v0.5.0
func (o ScreenOptions) String() string
String renders the options for logs.
func (ScreenOptions) Validate ¶ added in v0.5.0
func (o ScreenOptions) Validate() error
Validate reports whether the options describe frames a host could carry, wrapping ErrInvalidOption. It does not apply defaults: a zero Width or QueueDepth is refused here and filled in by ShowOn before this is reached.
type ScreenStats ¶ added in v0.5.0
type ScreenStats struct {
// Presented is how many frames the application handed over.
Presented int64
// Acknowledged is how many of those the host reported on the screen. The
// difference is what is in flight.
Acknowledged int64
// Waited is how many times [Screen.Next] had to block because every slot
// was in flight. A large number against a small QueueDepth means the
// application is drawing faster than the headset can take it.
Waited int64
}
ScreenStats counts what a screen has done.
func (ScreenStats) String ¶ added in v0.5.0
func (s ScreenStats) String() string
String renders the counters for logs.
type Sentinel ¶ added in v0.2.0
type Sentinel struct {
// Label is drawn across the middle in white. An empty label draws none.
Label string
}
Sentinel renders quadrants of exactly known colour with a label across them: green top-left, blue bottom-right, red elsewhere, white text.
It is this package's SELF-TEST content, and it is exported because a consumer needs it too. A feed that "works" and delivers a black buffer is the classic silent failure of this whole mechanism, and the only way to catch it is to draw something whose pixels are known in advance and then SAMPLE them rather than look at them. SentinelColorAt says what a correct frame must hold at a given point, and that is what this package's live proof asserts on.
type StartMessage ¶
type StartMessage struct {
// DisplayID is the display to capture. Only the default display can be
// captured by an unprivileged app; see [ErrNotCapturable].
DisplayID int
// Width and Height are the requested frame size in pixels; zero means the
// display's native size.
Width, Height int
// MilliFPS is the frame-rate ceiling in thousandths of a frame per second,
// so 60 fps is 60000 and a rate below one frame a second is still exact.
MilliFPS uint32
// Slots is how many frame slots the shared buffer holds.
Slots int
}
StartMessage asks the host to begin capturing.
func DecodeStart ¶
func DecodeStart(b []byte) (StartMessage, error)
DecodeStart decodes a StartMessage.
type Stats ¶
type Stats struct {
// Frames is the number of frames the host delivered.
Frames uint64
// Superseded is the number of delivered frames replaced by a newer one
// before the consumer ever asked for them. A large value next to Frames
// means the consumer is slower than the capture.
Superseded uint64
// Last is when the most recent frame arrived.
Last time.Time
// Interval is the gap between the two most recent frames.
Interval time.Duration
}
Stats reports what a stream has seen since it started.
func (Stats) FPS ¶
FPS is the instantaneous rate implied by Stats.Interval, 0 when fewer than two frames have arrived.
type StoppedMessage ¶
type StoppedMessage struct {
// Reason is one of [StopUser], [StopSystem] or [StopApp].
Reason uint8
// Detail is the host's own wording, which may be empty.
Detail string
}
StoppedMessage says a capture ended and why.
func DecodeStopped ¶
func DecodeStopped(b []byte) (StoppedMessage, error)
DecodeStopped decodes a StoppedMessage.
func (StoppedMessage) String ¶
func (s StoppedMessage) String() string
String renders the reason and the detail for a log or an error.
type Stream ¶
type Stream struct {
// contains filtered or unexported fields
}
Stream is a running capture.
func CaptureDisplay ¶
CaptureDisplay starts streaming a display.
The user must already have consented — see RequestAuthorization — and only the default display can be captured. The returned stream must be closed.
func (*Stream) Close ¶
Close ends the capture and releases the shared mapping. It is idempotent, and every borrowed frame is invalid afterwards.
func (*Stream) Err ¶
Err reports why the SYSTEM stopped the capture, nil while it is running. The user revoking the projection arrives here wrapping ErrPermissionDenied.
func (*Stream) Format ¶
func (st *Stream) Format() PixelFormat
Format returns the pixel layout of every frame this stream produces.
func (*Stream) Frame ¶
Frame returns the most recent frame and whether it is newer than the one the previous call returned.
The bytes are BORROWED from the shared mapping and stay valid until the next Frame, WaitFrame or Close. In steady state this allocates nothing.
func (*Stream) Options ¶
Options returns the options actually in force, with every zero field resolved.
type USBDescriptors ¶ added in v0.7.0
type USBDescriptors []byte
USBDescriptors is a device's raw descriptor bytes, as usbfs serves them: the device descriptor followed by every configuration descriptor and everything nested inside them.
They are kept RAW because the class-specific parts are what a camera is described by — UVC's format and frame descriptors live inside the interface descriptors and no general USB parser reports them — and because a byte slice read from a kernel is a thing a test can hold.
func (USBDescriptors) Device ¶ added in v0.7.0
func (d USBDescriptors) Device() (vendor, product, class, subclass, protocol int, err error)
Device reports the device descriptor's own fields: the vendor and product ids, and the device's class triple.
It reads the first 18 bytes and nothing else, so it is valid for any blob long enough to be a device at all.
func (USBDescriptors) Interfaces ¶ added in v0.7.0
func (d USBDescriptors) Interfaces() ([]USBInterface, error)
Interfaces returns every interface descriptor's identity, in the order the device reports them: number, alternate setting and class triple.
It is how a caller finds the UVC streaming interface and its alternate settings without this package growing a full USB parser.
func (USBDescriptors) UVCFormats ¶ added in v0.9.0
func (d USBDescriptors) UVCFormats(iface int) ([]UVCFormatDesc, error)
UVCFormats reads what one streaming interface offers, from the device's raw descriptors.
⛔ IT WALKS THE CLASS-SPECIFIC DESCRIPTORS, WHICH NO GENERAL PARSER REPORTS. A camera's formats and frame sizes live in type 0x24 descriptors nested after its streaming interface, and USBDescriptors.Interfaces steps straight over them. Everything needed to commit a format is in there and nowhere else.
It associates each frame descriptor with the format that preceded it, which is how UVC orders them, and stops at the next interface so a device with two streaming interfaces does not have its formats merged.
func (USBDescriptors) Walk ¶ added in v0.7.0
func (d USBDescriptors) Walk(fn func(typ int, b []byte) bool) error
Walk calls fn for every descriptor in the blob, with its type and its whole bytes including the two-byte header.
⛔ A DESCRIPTOR OF LENGTH ZERO WOULD LOOP FOREVER, and a length running past the end would read somebody else's memory. Both are refused: a kernel will not produce either, and this parses bytes that crossed a process boundary.
type USBDevice ¶ added in v0.6.0
type USBDevice struct {
// Name is the device node the system calls it, which is what a USB
// permission is granted against.
Name string
// VendorID and ProductID identify the device. VITURE is 0x35ca.
VendorID, ProductID int
// Manufacturer and Product are the device's own strings, when it has them.
Manufacturer, Product string
// Class, Subclass and Protocol are the DEVICE's class triple, which is
// [USBClassPerInterface] for anything composite.
Class, Subclass, Protocol int
// Interfaces are every interface of every alternate setting.
Interfaces []USBInterface
}
USBDevice is one device attached to the phone's USB host port.
func DecodeUSBDevices ¶ added in v0.6.0
DecodeUSBDevices decodes a USB device list.
func USBDevices ¶ added in v0.6.0
USBDevices returns every device on the phone's USB host port, down to each interface's endpoints.
The endpoints are the point: whether a headset's camera can be read through Android's Java USB API comes down to whether its UVC streaming endpoints are bulk, and nothing but a census says. See USBDevice.BulkVideo.
It needs no permission either. A USB permission is granted against a device to OPEN it; the device list is public.
func (USBDevice) BulkVideo ¶ added in v0.6.0
BulkVideo reports whether any of this device's UVC streaming interfaces delivers frames over a BULK endpoint.
⛔ IT IS THE WHOLE QUESTION FOR THE USB ROUTE. Android's Java USB API submits control, bulk and interrupt transfers and NOTHING ELSE: there is no isochronous request in UsbDeviceConnection or UsbRequest, which is why every UVC library on Android carries a native libusb. A camera whose streaming endpoints are all isochronous cannot be read through that API at all, however much permission it is given.
func (USBDevice) Readable ¶ added in v0.6.0
func (d USBDevice) Readable() []USBInterface
Readable returns the interfaces of this device whose IN endpoints Android's Java USB API can actually carry — bulk and interrupt, never isochronous.
⭐ IT IS WHAT IS LEFT WHEN THE CAMERA IS CLOSED. A headset whose camera is isochronous is not a headset with nothing to read: the VITURE Beast also presents a CDC-ACM serial interface with bulk endpoints and a HID interface with interrupt ones, and both of those UsbDeviceConnection can read today. What travels on them is a separate question this package does not answer — see go-xrkit/xrkit/headflow on the Beast publishing no orientation — but "unreachable" and "reachable and silent" are different findings and a census that could not tell them apart would be worth little.
func (USBDevice) VideoStreaming ¶ added in v0.6.0
func (d USBDevice) VideoStreaming() []USBInterface
VideoStreaming returns the UVC streaming interfaces this device has, across every alternate setting.
type USBEndpoint ¶ added in v0.6.0
type USBEndpoint struct {
// Address is bEndpointAddress: the endpoint number, with bit 7 set for an
// IN endpoint.
Address int
// Attributes is bmAttributes, whose low two bits are the transfer type.
Attributes int
// MaxPacketSize is wMaxPacketSize for this endpoint's current setting.
MaxPacketSize int
// Interval is bInterval, the polling period.
Interval int
}
USBEndpoint is one endpoint of a USB interface.
func (USBEndpoint) ISOPacketSize ¶ added in v0.8.0
func (e USBEndpoint) ISOPacketSize() int
ISOPacketSize is how many bytes one isochronous service interval may carry.
⛔⛔ wMaxPacketSize IS NOT A SIZE ON A HIGH-SPEED ISOCHRONOUS ENDPOINT. Bits 10:0 are the packet size; bits 12:11 are the number of ADDITIONAL transactions per microframe, so the bytes per interval are the product. The Beast's camera advertises 5120 on its largest alternate, which is 0x1400:
size = 0x1400 & 0x7ff = 1024 mult = ((0x1400>>11) & 3)+1 = 3 bytes per interval = 3072
Passing the raw 5120 as a packet length had the kernel answer EMSGSIZE — "message too long" — and that was read here as the platform REFUSING the transfer. It was refusing nothing: the request was malformed. This is the trap libusb's get_max_iso_packet_size exists to hide.
func (USBEndpoint) In ¶ added in v0.6.0
func (e USBEndpoint) In() bool
In reports whether this endpoint carries data towards the host. A camera's streaming endpoint is one.
func (USBEndpoint) String ¶ added in v0.6.0
func (e USBEndpoint) String() string
String renders the endpoint for logs.
func (USBEndpoint) Type ¶ added in v0.6.0
func (e USBEndpoint) Type() USBTransferType
Type is the endpoint's transfer type.
type USBHandle ¶ added in v0.7.0
type USBHandle struct {
// contains filtered or unexported fields
}
A USBHandle is one USB device this application was given permission to open, held as the kernel's own descriptor.
⛔ IT IS NOT A STREAM AND NOT AN API TO BUILD ON. It exists to answer whether Android lets an untrusted application drive usbfs at all — see usbfs.go. Every method here is a step of that question, and the package stops when the question is answered.
A USBHandle is safe for concurrent use; the device it names is not, and two callers claiming the same interface will have the kernel refuse the second.
func OpenUSB ¶ added in v0.7.0
OpenUSB asks the host to open a USB device and lends its descriptor.
⚠ IT PUTS A DIALOG IN FRONT OF THE USER, naming the device and this application, and they may say no — which is reported as ErrUSBPermissionDenied and is a decision rather than a failure. A device that is no longer attached is ErrNotFound.
Close the handle when done: it holds a socket, a descriptor, and the host's claim on the device.
func (*USBHandle) ClaimInterface ¶ added in v0.7.0
ClaimInterface takes an interface from whatever kernel driver holds it, which is required before any transfer on its endpoints.
On a UVC camera the kernel's own uvcvideo driver usually holds it, and whether Android lets an application take it away is part of the question.
func (*USBHandle) Close ¶ added in v0.7.0
Close releases every interface this handle claimed, closes the descriptor and drops the host connection, which lets the host release the device. It is idempotent.
func (*USBHandle) Control ¶ added in v0.9.0
func (h *USBHandle) Control(requestType, request uint8, value, index uint16, data []byte) (int, error)
Control performs one control transfer and returns how many bytes moved.
⛔⛔ data IS WRITTEN INTO BY THE KERNEL ON AN IN TRANSFER, and its length is what the device is told to send. A slice shorter than the device's answer gets a short read; one longer invites it to overrun nothing, because the length in the request is the ceiling. Either way the number returned is what actually moved, and callers must use it rather than len(data).
It is the whole of UVC's negotiation: asking a camera what it can do and telling it what to do are both control transfers on its streaming interface.
func (*USBHandle) Descriptors ¶ added in v0.7.0
func (h *USBHandle) Descriptors() (USBDescriptors, error)
Descriptors reads the device's raw descriptor blob.
⭐ IT IS A PLAIN read(2), NOT AN IOCTL. usbfs serves the descriptors as the file's contents, so this is the one thing here that works without the kernel granting anything beyond the open — which makes it the right FIRST step of the probe: a failure here is the descriptor itself being useless, and a success narrows every later failure to the ioctl that produced it.
func (*USBHandle) Detached ¶ added in v0.8.0
Detached returns the interfaces whose kernel driver this handle took away.
⛔⛔ IT IS NOT BOOKKEEPING, IT IS A DEBT. Taking a UVC streaming interface from uvcvideo is what makes the route work at all, and USBDEVFS_CONNECT does NOT reliably undo it: on a Pixel 11 Pro Fold the reconnect ioctl SUCCEEDS and the driver still does not rebind —
before: 1-1.1:1.0 driver=uvcvideo 1-1.1:1.1 driver=uvcvideo after: 1-1.1:1.0 driver=uvcvideo 1-1.1:1.1 driver=
/dev/video0 survives, because the control interface still carries it, but the streaming interface is left with no driver until the device is UNPLUGGED AND PLUGGED BACK. A caller that shares the camera with anything else on the phone needs to know that, which is why this is reported rather than hidden.
func (*USBHandle) Negotiate ¶ added in v0.9.0
func (h *USBHandle) Negotiate(iface int, f UVCFormatDesc, z UVCFrameSize) (UVCNegotiation, error)
Negotiate performs UVC's probe/commit on a streaming interface and returns what the camera agreed to.
The sequence is the specification's, and every step of it is a control transfer:
- GET_LEN on the probe control, so the block's size is LEARNED rather than guessed from a version number. It is 26 bytes in UVC 1.0, 34 in 1.1 and 48 in 1.5, and a block of the wrong length is refused outright;
- GET_CUR, to take the camera's own block and leave every field this package does not understand exactly as it was;
- SET_CUR with the format, frame and interval written in;
- GET_CUR again — THE ANSWER. This is where a camera says what it will actually do, which may not be what it was asked;
- SET_CUR on the commit control, which is the same block accepted.
⛔ THE INTERFACE MUST ALREADY BE CLAIMED. These transfers address the streaming interface by number, and a kernel driver still holding it will answer them itself or refuse them.
func (*USBHandle) SetAltSetting ¶ added in v0.7.0
SetAltSetting selects an alternate setting of an interface. For a UVC streaming interface this is how bandwidth is chosen, and setting 0 is the zero-bandwidth one every UVC device has.
func (*USBHandle) Stream ¶ added in v0.10.0
func (h *USBHandle) Stream(neg UVCNegotiation, ep USBEndpoint, w, h2 int) (*UVCStream, error)
Stream starts reading frames from a camera that has already been negotiated.
⛔ THE ORDER MATTERS AND NOTHING ENFORCES IT BUT THIS SENTENCE: the interface must be claimed, its alternate selected and the format committed before this is called. A stream started before the commit reaps packets of nothing, which looks exactly like a camera with the lens covered.
func (*USBHandle) SubmitISO ¶ added in v0.7.0
func (h *USBHandle) SubmitISO(endpoint USBEndpoint) error
type USBInterface ¶ added in v0.6.0
type USBInterface struct {
// Number is bInterfaceNumber, and Alternate is bAlternateSetting.
Number, Alternate int
// Class, Subclass and Protocol are the interface's own class triple.
Class, Subclass, Protocol int
// Endpoints are the endpoints of this alternate setting.
Endpoints []USBEndpoint
}
USBInterface is one interface of a USB device, in one alternate setting.
func (USBInterface) String ¶ added in v0.6.0
func (i USBInterface) String() string
String renders the interface for logs.
func (USBInterface) Video ¶ added in v0.6.0
func (i USBInterface) Video() bool
Video reports whether this interface is a UVC one.
func (USBInterface) VideoStreaming ¶ added in v0.6.0
func (i USBInterface) VideoStreaming() bool
VideoStreaming reports whether this is the UVC interface that carries frames.
type USBTransferType ¶ added in v0.6.0
type USBTransferType int
USBTransferType is an endpoint's transfer type, the low two bits of its bmAttributes.
const ( USBTransferControl USBTransferType = 0 USBTransferIsochronous USBTransferType = 1 USBTransferBulk USBTransferType = 2 USBTransferInterrupt USBTransferType = 3 )
The transfer types USB defines.
func (USBTransferType) String ¶ added in v0.6.0
func (t USBTransferType) String() string
String renders the transfer type.
type UVCFormat ¶ added in v0.9.0
type UVCFormat int
UVC format identities this package names.
const ( // FormatUnknown is a format descriptor this package did not recognise. It // is kept rather than dropped: a camera that offers only formats we cannot // name is a different finding from a camera that offers none. FormatUnknown UVCFormat = iota // FormatYUY2 is uncompressed 4:2:2, two bytes a pixel, luminance first. // ⭐ It is the one to want: see the note at the top of this file. FormatYUY2 // FormatMJPEG is a JPEG per frame, which Go's image/jpeg decodes. FormatMJPEG )
The formats a camera here is expected to offer.
type UVCFormatDesc ¶ added in v0.9.0
type UVCFormatDesc struct {
// Index is the camera's own bFormatIndex.
Index int
// Format is what this package made of its GUID or subtype.
Format UVCFormat
// Sizes are the frame sizes it offers, in the order the camera lists them.
Sizes []UVCFrameSize
}
A UVCFormatDesc is one format a streaming interface offers, with its sizes.
func (UVCFormatDesc) Fastest ¶ added in v0.9.0
func (d UVCFormatDesc) Fastest() (UVCFrameSize, bool)
Fastest returns the size with the HIGHEST FRAME RATE this format offers, at no less than MinTrackingWidth across, breaking ties toward the wider frame.
⛔⛔ RATE, NOT SIZE, AND THAT WAS MEASURED THE WRONG WAY ROUND FIRST. This picked the largest frame, reasoning that more pixels across means more to match. The camera's own table says what that costs:
YUY2 1920x1080 @5fps 1280x720 @10fps 800x600 @20fps 640x480 @25fps MJPEG 1920x1080 @30fps … 30fps at every size
So "largest" chose 1920x1080 at FIVE frames a second — 200 ms a frame, worse than the 46–107 ms the same algorithm already achieves on macOS, for a precision nothing here needs. Latency is what head tracking is bounded by; headflow's own documentation says 1:1 locking wants under 20 ms and that choosing which screen somebody faces is comfortable at 107 ms.
func (UVCFormatDesc) String ¶ added in v0.9.0
func (d UVCFormatDesc) String() string
String renders the format and its sizes.
type UVCFrameSize ¶ added in v0.9.0
type UVCFrameSize struct {
// Index is the camera's own bFrameIndex, which is what a commit names.
Index int
// Width and Height are in pixels.
Width, Height int
// Interval is the default frame interval in 100-nanosecond units, which is
// how UVC states frame rates: 333333 is 30 per second.
Interval uint32
}
A UVCFrameSize is one size a format offers, with the shortest interval between frames the camera will sustain at it.
func (UVCFrameSize) FPS ¶ added in v0.9.0
func (s UVCFrameSize) FPS() float64
FPS is the frame rate the interval states, or 0 when the camera gave none.
func (UVCFrameSize) String ¶ added in v0.9.0
func (s UVCFrameSize) String() string
String renders the size for logs.
type UVCNegotiation ¶ added in v0.9.0
type UVCNegotiation struct {
// Format and Frame are the indices the camera settled on.
Format, Frame int
// Interval is the frame interval it settled on, in 100ns units.
Interval uint32
// MaxPayload is the largest transfer it will send, which bounds how much
// one isochronous request can usefully carry.
MaxPayload uint32
// Asked is what it was given, kept so a caller can SEE a change rather
// than infer one.
Asked UVCFrameSize
// Kind is the format's KIND, as against Format which is the camera's own
// index for it.
//
// ⛔ A NEGOTIATION RESULT SHOULD SAY WHAT WAS AGREED, and an index is a name
// the camera chose rather than a fact about the bytes that follow. Without
// it a stream cannot tell whether to assemble by LENGTH or by MARKERS — and
// that distinction is what makes a compressed frame possible at all. The
// alternative was a second argument on [USBHandle.Stream], which would have
// put the same fact in two places and let them disagree.
Kind UVCFormat
}
A UVCNegotiation is what a camera agreed to.
⛔ AGREED TO, NOT ASKED FOR. A UVC camera may answer a probe with something other than what it was given — a different interval, a smaller payload — and the host is expected to accept that answer. Reporting the REQUEST as though it were the outcome is how a stream comes out at a frame rate nobody chose.
⚠ IT IS DECLARED HERE, NOT BESIDE Negotiate. The type is data and travels everywhere; only the negotiation needs a device. Declaring it twice, once per platform, left String on one of them — which go vet caught as a %s against a type with no String, on the lane that could not see the other copy.
func (UVCNegotiation) Changed ¶ added in v0.9.0
func (n UVCNegotiation) Changed() bool
Changed reports whether the camera settled on something other than what it was asked for.
func (UVCNegotiation) FPS ¶ added in v0.9.0
func (n UVCNegotiation) FPS() float64
FPS is the frame rate the camera settled on.
func (UVCNegotiation) String ¶ added in v0.9.0
func (n UVCNegotiation) String() string
String renders the outcome for logs.
type UVCPayload ¶ added in v0.10.0
type UVCPayload struct {
// Header is the payload's own bmHeaderInfo.
Header byte
// Data is the pixels, which is whatever followed the header.
Data []byte
}
A UVCPayload is one isochronous packet's worth of a frame.
func ParsePayload ¶ added in v0.10.0
func ParsePayload(b []byte) (UVCPayload, error)
ParsePayload splits one isochronous packet into its header and its pixels.
⛔ bHeaderLength IS THE OFFSET AND IT IS NOT CONSTANT. Cameras use 2, 12 and more; taking a fixed two bytes would prepend ten bytes of timestamp to every frame's pixels, which does not fail — it shears the image by five YUY2 pixels and looks like a camera mounted crooked.
A packet shorter than its own header, or one whose header is longer than the packet, is refused rather than clamped: both describe a packet that is not the one the camera meant to send.
func (UVCPayload) Bad ¶ added in v0.10.0
func (p UVCPayload) Bad() bool
Bad reports whether the camera marked the payload itself as in error.
func (UVCPayload) EOF ¶ added in v0.10.0
func (p UVCPayload) EOF() bool
EOF reports whether the camera marked this the last payload of its frame.
func (UVCPayload) FID ¶ added in v0.10.0
func (p UVCPayload) FID() bool
FID is the frame this payload belongs to, as a single bit that toggles.
type UVCStream ¶ added in v0.10.0
type UVCStream struct {
// contains filtered or unexported fields
}
A UVCStream is a running camera.
⛔ IT IS NOT A Stream. The capture and wall paths deliver frames the HOST wrote into shared memory and hand them over borrowed; this one is built from isochronous packets this process reaped itself, assembled, and decoded. They share no machinery and the names are deliberately different.
A UVCStream is NOT safe for concurrent use: the URB ring, the assembler and the decode buffer are all single-reader state, and there is no reason for two goroutines to read one camera.
func (*UVCStream) Assembler ¶ added in v0.11.0
func (s *UVCStream) Assembler() *FrameAssembler
Assembler is the frame assembler this stream feeds, for the counters that describe HOW frames ended rather than how many did.
func (*UVCStream) Close ¶ added in v0.10.0
Close cancels every outstanding request. It is idempotent.
⛔ IT DISCARDS WHAT IS STILL IN FLIGHT. A request left outstanding has the kernel writing into this process's memory on the camera's schedule, and the Go heap is entitled to move on without it.
func (*UVCStream) Frame ¶ added in v0.10.0
Frame reads until one whole frame has been assembled, and returns it decoded.
The image is REUSED between calls, so a steady stream allocates nothing after the first frame. A caller that keeps one copies it.
It reports the context's error if the wait is abandoned, and ErrClosed after UVCStream.Close.
func (*UVCStream) Negotiation ¶ added in v0.10.0
func (s *UVCStream) Negotiation() UVCNegotiation
Negotiation is what the camera agreed to for this stream.
func (*UVCStream) Stats ¶ added in v0.10.0
func (s *UVCStream) Stats() UVCStreamStats
Stats reports what the stream has seen.
type UVCStreamStats ¶ added in v0.10.0
type UVCStreamStats struct {
// Packets is every isochronous packet reaped, empty ones included.
Packets int
// Empty is how many carried nothing, which between frames is most of them.
Empty int
// Bytes is the payload actually delivered, headers and all.
Bytes int
// Frames, Dropped and Bad come from the assembler: whole frames, frames
// abandoned for being short, and payloads the camera marked in error.
Frames, Dropped, Bad int
// Late is whole frames thrown away because the CALLER had not come back for
// the ones before them.
//
// ⭐ IT IS SEPARATE FROM Dropped ON PURPOSE. A dropped frame is the
// camera's or the bus's fault and says the link is lossy; a late one is
// OURS and says the consumer is slower than the camera. They are different
// problems with different fixes, and a single counter would make a slow
// tracker look like a failing cable.
Late int
// Foreign is completions collected from this descriptor that belonged to
// some other request.
//
// ⛔ IT IS COUNTED BECAUSE A COMPLETION QUEUE BELONGS TO THE DESCRIPTOR,
// not to whoever submitted. Anything else reading the same device — or, as
// happened here, this package's own [USBHandle.SubmitISO] cancelling a test
// request without collecting it — puts a completion in the way. Skipping it
// is right; skipping it silently would make a stream that is eating other
// people's requests look merely slow.
Foreign int
// Decoding is how long the decodes have taken in all.
//
// ⛔⛔ IT IS MEASURED ON THE DEVICE BECAUSE A BENCHMARK ELSEWHERE IS AN
// OPTIMISTIC BOUND AND NOT A MEASUREMENT. Choosing between YUY2 and MJPEG
// turns on whether the decode fits inside the frame interval, and the
// processor that has to fit it is the phone's. On a workstation MJPEG costs
// 3 % of a 30 fps interval at 320x240, 15-18 % at 640x480 and 69-84 % at
// 1920x1080 — figures that say the shape of the answer and not the answer.
Decoding time.Duration
// BadJPEG is frames that arrived whole, both markers present, and still
// would not decode. See [FrameAssembler.BadJPEG] for why it is apart from
// Dropped.
BadJPEG int
}
UVCStreamStats counts what a stream has seen.
It is declared here rather than beside the stream because it is data and travels everywhere: declaring a type once per platform is how String ends up on only one of them, which go vet caught once already in this package.
func (UVCStreamStats) String ¶ added in v0.10.0
func (s UVCStreamStats) String() string
String renders the counters for logs.
type Wall ¶ added in v0.2.0
type Wall struct {
// contains filtered or unexported fields
}
A Wall is a set of displays this application created and owns, and the limit on how many of them may exist at once.
Why an application may have these at all ¶
An ordinary APK may not manufacture desktops for OTHER people's applications: launching an activity onto a display the app created fails with a SecurityException even for the app's own activity, and the flag that would lift it, VIRTUAL_DISPLAY_FLAG_TRUSTED, needs a signature permission. That half of the refusal is real and is not going anywhere.
It may, however, create a virtual display and show a [Presentation] on it — a Dialog attached to a Display, which is not an activity start and so does not meet the check that refuses the other thing. Measured on Android 15 (API 35, arm64):
Q1 ANSWER: Presentation.show() SUCCEEDED on the app's OWN virtual display 3 Q2 frame 640x480 ... black=0/307200 topLeft=#FF00FF00 bottomRight=#FF0000FF
No permission, no consent dialog and no MediaProjection are involved, which is why a Wall needs none of RequestAuthorization's machinery. So an XR ribbon on Android carries the mirrored phone — see CaptureDisplay — AND as many screens of Android-rendered content as the application asks for.
THE LIMIT IS THE SAFETY, AND IT IS NOT A FORMALITY ¶
On the emulator this package was measured against, creating the 304th virtual display did not fail: it killed system_server with android.view.Surface$OutOfResourcesException: NO_MEMORY out of SurfaceControl.nativeCreate, and the device soft-rebooted. An unprivileged APK with no permissions at all did that to the machine it was running on.
READ THIS BEFORE RAISING THE LIMIT: the ceiling was the SAME COUNT at 640x480 and at 1920x1080 — nine times the pixels, same number, with the app's Java heap at 14 MiB of 192. It is SurfaceControl handles, not graphics memory. So MAKING THE SCREENS SMALLER DOES NOT HELP, which is exactly the mitigation somebody reaches for. 304 is one emulator's measurement and is deliberately not a constant here; a real device's number is unknown and could be lower.
A Wall therefore refuses past its limit with ErrTooManyDisplays instead of asking the platform and hoping. DefaultMaxDisplays is 8, which is more than a ribbon uses; MaxDisplays is 32, which is the most this package will let a caller ask for however explicit they are about wanting it. The Java host enforces the same 32 independently, so a second process cannot get past it either.
A Wall is safe for concurrent use.
func NewWall ¶ added in v0.2.0
NewWall returns a wall that will hold at most max displays at once. A max of 0 means DefaultMaxDisplays; a negative max, or one above MaxDisplays, is refused with ErrTooManyDisplays and no wall is returned.
NewWall talks to nothing and can fail for no other reason, so a caller may build one before knowing whether a host is there.
func (*Wall) Close ¶ added in v0.2.0
Close releases every display the wall still holds and refuses further Wall.Open calls. It is idempotent, and reports the first failure a display reported while still closing the rest.
func (*Wall) Open ¶ added in v0.2.0
func (w *Wall) Open(ctx context.Context, spec DisplaySpec) (*OwnedDisplay, error)
Open creates one display, shows a Presentation carrying spec.Content on it, and returns a feed of the pixels Android rendered there.
It refuses with ErrTooManyDisplays when the wall is already full, and it refuses BEFORE asking the host, so a full wall costs no round trip and creates nothing. It refuses with ErrWallClosed after Wall.Close, and wraps ErrInvalidOption for a spec that cannot describe a display.
type Web ¶ added in v0.2.0
type Web struct {
// URL is what the WebView loads. http, https and data URLs are accepted.
URL string
}
Web renders a WebView on the display.
It is the content that justifies the whole mechanism: a CGO-free Go process cannot host a web engine, and a ribbon of web panels is a real product that falls straight out of a Presentation on an owned display.
The host's WebView has JavaScript enabled and no file access. Reaching a http(s) URL needs android.permission.INTERNET in the APK, which is protectionLevel normal; a data: URL needs nothing and is what this package's own tests use.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
xrcam
command
Command xrcam says whether this phone can reach the glasses' camera, and by which route.
|
Command xrcam says whether this phone can reach the glasses' camera, and by which route. |
|
xrcapture
command
Command xrcapture is the on-device proof for github.com/go-xrkit/android: an ordinary CGO_ENABLED=0 GOOS=android binary that asks the host for consent, captures the screen, and reports what it actually got.
|
Command xrcapture is the on-device proof for github.com/go-xrkit/android: an ordinary CGO_ENABLED=0 GOOS=android binary that asks the host for consent, captures the screen, and reports what it actually got. |
|
xrglasses
command
Command xrglasses waits for XR glasses to be attached to the phone and says exactly what Android makes of them.
|
Command xrglasses waits for XR glasses to be attached to the phone and says exactly what Android makes of them. |
|
xrhead
command
Command xrhead tracks a head ON THE PHONE, live, from the headset's camera.
|
Command xrhead tracks a head ON THE PHONE, live, from the headset's camera. |
|
xrscreen
command
Command xrscreen paints frames from Go onto the glasses and says how many of them Android took.
|
Command xrscreen paints frames from Go onto the glasses and says how many of them Android took. |
|
xrusb
command
Command xrusb answers ONE question: will Android let an ordinary application submit an isochronous USB transfer?
|
Command xrusb answers ONE question: will Android let an ordinary application submit an isochronous USB transfer? |
|
xrwall
command
Command xrwall proves, on a real device, that this application can create displays of its own, render Android content on them and read the pixels back — and that the limit that keeps it from rebooting the phone actually refuses.
|
Command xrwall proves, on a real device, that this application can create displays of its own, render Android content on them and read the pixels back — and that the limit that keeps it from rebooting the phone actually refuses. |
|
xrwide
command
Command xrwide measures how WIDE a display this application owns can be, on the device it is running on, and whether the pixels actually arrive at that width.
|
Command xrwide measures how WIDE a display this application owns can be, on the device it is running on, and whether the pixels actually arrive at that width. |
|
tools
|
|
|
xrrun
command
Command xrrun runs one of this repository's probes on a device and brings its transcript back.
|
Command xrrun runs one of this repository's probes on a device and brings its transcript back. |
