Kristian Buriasco

Creations · macOS app

GogglesView

The live picture from DJI Goggles 3, on a Mac, over a single USB-C cable. No dongle, no phone, no capture card.

  • 146 mscamera to screen, mean of five stopwatch readings, each good to one frame (±17 ms)
  • 60 fpsfull frame rate, 1920×1080, when the goggles send it
  • 24releases in 20 days, 0.1 on 3 Oct to 0.12.0 on 22 Oct 2026
  • 342commits, all mine
GogglesView 0.3.0 window. The goggles' feed fills it: a whale seen from above, the goggles' own menu at the top and a red OTG Wired Connection banner. A small stats box reads 1920x1080, 61 fps, 9.2 Mbps, 0 dropped.
An early build, 0.3.0. Everything inside the picture, including the menu at the top and the red “OTG Wired Connection” banner, is drawn by the goggles themselves. GogglesView adds the window and the stats box: 1920×1080, 61 fps, 9.2 Mbps, 0 dropped frames. The interface has changed since.

Why it exists

The Goggles 3 show what the drone sees, but DJI gives no API for getting that picture onto a computer, to record it, put it into OBS or stream it.

They do have a USB-C port and an “OTG Wired Connection to Computer” switch, and I wanted to know what came out of it.

The hard part

It started as a Python prototype, which stayed the reference for every protocol detail in the Swift app. What it established on the real goggles:

  • USB device 2CA3:0020 with 8 interfacesTwo of them are a network adapter (RNDIS). macOS sees it but never brings the link up, so the OS can't be used.
  • A network driver written in userspace, over libusbThat brings the link up: the goggles answer at 192.168.60.2.
  • A 48-byte UDP packet to port 9003Starts the video: H.264, 1920×1080, High Profile.
  • A random session id per connectionA fixed one breaks video on reconnect. That was a real bug.
  • One keyframe, everThe goggles send a single full picture when Share Liveview starts. Recordings and streams need regular ones, so the app re-encodes with the Mac's hardware encoder.

Claiming the USB interfaces needs root. So the app is split in two: a privileged helper daemon, registered with SMAppService, owns the USB device and runs the protocol, and the app talks to it over XPC, decodes with VideoToolbox and draws the picture.

A goggles frame: the camera picture in the middle, flight data around the edges, and a dashed box marking a centred crop of about 1.4x
The goggles burn their own overlay into the video. With a drone linked, the camera picture sat in the middle at about 70% of the frame, so the app can crop it out.

Dead ends and bugs

From the project's own notes. Not everything worked, and some of it still doesn't.

macOS's own network driver
macOS attaches the goggles' network adapter as en3/en4, reports “media inactive” and never gets an address. Its RNDIS stack is unusable, which is why the app carries its own driver.
A fixed session id
Reusing the same session id broke keyframe delivery on reconnect. It is now random per connection, 1 to 0xFFFE.
Two parameter sets in one blob
The SPS and PPS the decoder needs arrive together in one ~40-byte NAL, not as two. That broke an early attempt at gating the decoder.
Asking for a keyframe
DUML command 02:B3, “request I-frame”, retried every 1.5 s, does not reliably produce one. The research notes suspect it is sent in the wrong kind of packet; that is untested. The only reliable way is switching Share Liveview off and on in the goggles' menu.
The vendor control interfaces
Commands to the goggles over interfaces 3 to 7 are rejected with status 0xE0, the same commands to the air unit get no reply, and four of those interfaces never wake. Only the Wi-Fi credential queries on interface 4 work.
Frame numbers wrap at 256
In the Python prototype a lost fragment left a half frame behind, and a later frame with the same number merged into it: a corrupt picture. The Swift app drops entries older than about 5 frame numbers or 250 ms and counts the drop.
A two-second gap
Fixed in 0.9.1: a brief radio dropout re-armed the keyframe gate, which threw away every frame until a keyframe came, and none ever did. A pause loses no frames, so the stream now simply continues.
Telemetry
The goggles send small packets about 10 times a second. In the captures they held flow control and a once-a-second heartbeat carrying the goggles' serial. Registering as DJI's app brought nothing new. The goggles' battery percentage is the only live reading; flight data isn't exposed over this connection.
The 8-byte header on every UDP 9003 packet, from design.md
OffsetSizeField
0u16 LEtotal length, top bit set
2u16 LEsession id
4u16 LEsequence number
6u8type: 00 handshake, 01 telemetry, 02 video, 04/06 ack
7u8XOR of bytes 0 to 6

The handshake body, 40 constant bytes. With the header, that is the 48-byte packet that starts the video:

d0 e9 64 00 64 00 c0 05 14 00 00 0a 00 64 00 64
00 c0 05 14 00 00 64 00 14 00 64 00 c0 05 14 00
00 64 00 01 01 04 0a 02

Measuring 146 ms

Point the goggles at a stopwatch running on the Mac's own screen. The picture in the app shows an older time than the stopwatch does, and the gap is the whole delay: camera, air link, goggles, USB, decode, display.

Each reading is good to about one frame at 60 Hz, 17 ms. The app's own share of the total is a few milliseconds.

Five readings, stopwatch in seconds
RunStopwatchIn the appDelay
149.5549.42130 ms
252.4452.27170 ms
353.4553.29160 ms
448.1948.05140 ms
546.8546.72130 ms
Mean146 ms

What it does with the picture

Records the stream exactly as the goggles send it, to .mov or .mp4, with no re-encode. Instant replay, loop recording, markers that become chapters, and a clip gallery that trims without re-encoding.

Sends it out five ways: UDP MPEG-TS, RTMP for Twitch or YouTube, SRT, a local web viewer (HLS) you open on a phone by scanning a QR code, and NDI, still experimental.

Gets out of the way for OBS: a chrome-less capture window, and obs-websocket control that starts recording and switches scenes when the goggles go live.

Stream keys and tokens live in the macOS Keychain. The app is in English, Italian, French and Dutch.

GogglesView settings, Streaming tab: keyframe re-encode at 20 Mbps, network streaming to 127.0.0.1 port 5000, RTMP push with stream key
Settings, Streaming. Each output has its own card; the keyframe re-encode at the top is what makes late-joining streams work.

At an event

The live stream of the 24 uur loop, the 24-hour run in Leuven on 20–21 October 2026, ran on GogglesView.

For events with several goggles, one Mac runs one window per pair, an operator overview with a green, amber or red dot per feed, battery warnings at 25% and 10%, and an alarm for a black or frozen picture. Up to four HDMI program outputs, each a single feed or a grid with NO SIGNAL tiles, and a read-only status page for a phone.

The limit is the Mac's hardware encoder. Measured on an M1 Pro with software goggles at 1080p60:

Frame rate each window keeps, by number of goggles
GogglesRe-encode onAt 30 fpsOff
1606060
2596060
3476060
4376060
6275260

About 150 encoded frames per second in total on that Mac: two goggles at 60 fps, or five at 30. Software numbers; the docs say to check them with real goggles.

Honest limits

Apple Silicon and macOS 14 or later only. Releases are signed but not notarized, so the first launch is right-click, Open. Live view, recording and the 60 fps path are tested on real goggles; the newer outputs (RTMP, SRT, NDI, multi-window) are unit-tested but have had little time on real hardware. A command-line receiver for Linux and Windows exists but hasn't met real goggles yet.

Not affiliated with DJI. The protocol was reverse-engineered for interoperability.

brew install --cask Kristian-Buriasco/gogglesview/gogglesview
GogglesView settings, Recording tab
Settings, Recording: split by length or size, loop mode, auto-delete of old files.