Stream games via Moonlight and fpv.wtf to your DJI FPV Goggles!
The DJI Moonlight project is made up of three parts:
Latency is good, in the 7-14ms range at 120Hz (w/ 5900X + 3080Ti via GeForce Experience).
Connect your goggles to your PC via USB.
Select Moonlight
from the menu.
The shim will start and wait for a connection.
Use dji-moonlight-gui to stream video from your PC.
Press the BACK button on the goggles to exit the shim at any time.
Technically, this will accept any Annex B H.264 stream since all this does really is pipe the stream right into the decoder.
First, it expects a connect header:
struct connect_header_s {
uint32_t magic; // 0x42069
uint32_t width;
uint32_t height;
uint32_t fps;
}
After which, it expects a stream of frames. Each frame should be sent prefixed
with a uint32_t
length header, followed by the frame data itself.
The maximum frame size is 1MB (1000000 bytes)
, which is slightly under the
maximum packet size for the decoder. Any larger and this would need to handle
packet splitting. But also, this is absolutely enormous for a single frame.
For RNDIS, the shim hosts a TCP server on port 42069
that accepts a single
connection at a time. Normal client/server stuff applies here.
For BULK, the shim reads from the FunctionFS bulk endpoint already setup by DJI
at /dev/usb-ffs/bulk/ep1
. FunctionFS does not support non-blocking IO nor does
it support polling so reading is done from a thread into a pipe.
Currently, there's no way to tell if the BULK side has connected or disconnected, so on startup the shim just waits for the magic number to appear in this file, at which point it assumes it's about to get the rest of the connect header, followed by the rest of the stream. After that, we rely on a watchdog timer to detect if the connection has been lost (i.e., no data received for some seconds).
The dji-moonlight-shim
that gets popped into /opt/bin
is actually a wrapper
around the actual binary in /opt/moonlight/
. The glasses service and the shim
can't co-exist so this wrapper handles stopping (and restarting) it.
Everything around decoding lives in dmi and is probably the best way to understand how this works. Start from dmi_pb.c.
It's driven through a handful of devices via ioctl/iomap:
/dev/dmi_media_control
: general control, starting/stoping the decoder, etc./dev/dmi_video_playback
: the place where frames go./dev/mem
: general shared mem, mainly just for frame timing info here though.The setup is roughly:
Then for each frame you want to decode:
dmi_video_playback
device, which gives you a chunk of
shared memory to write the packet to.Everything else: see LICENSE.