Skip to content

Commit 946bdf3

Browse files
authored
Support for OA6.
Implement initial OA6 protocol support based on Bluetooth capture analysis Reverse-engineered the OA6 protocol from a Bluetooth capture of the Mimo app (simon_offiziell) using Claude 4.7 and implemented a minimal change set. Testing with an Osmo Action 6 shows that lag issues in certain stabilization modes are resolved. Streaming can now be started and behaves similarly to the existing OA4 flow. Further testing is needed to validate stability and uncover any remaining issues.
1 parent 19b5aeb commit 946bdf3

2 files changed

Lines changed: 70 additions & 2 deletions

File tree

Moblin/Integrations/Dji/DjiDevice/DjiDevice.swift

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,10 @@ extension DjiDevice: CBPeripheralDelegate {
321321
switch model {
322322
case .osmoAction2, .osmoAction3:
323323
sendStartStreaming()
324-
case .osmoAction4:
324+
case .osmoAction4, .osmoAction6:
325+
// The Osmo Action 6 uses the same configure byte (0x08) as the OA4,
326+
// not the 0x1A value used by the OA5 Pro / Osmo 360. Confirmed
327+
// against a BTSnoop capture of the official DJI app.
325328
guard let imageStabilization else {
326329
return
327330
}
@@ -331,7 +334,7 @@ extension DjiDevice: CBPeripheralDelegate {
331334
type: configureType,
332335
payload: payload.encode()))
333336
setState(state: .configuring)
334-
case .osmoAction5Pro, .osmoAction6, .osmo360:
337+
case .osmoAction5Pro, .osmo360:
335338
guard let imageStabilization else {
336339
return
337340
}
@@ -378,6 +381,22 @@ extension DjiDevice: CBPeripheralDelegate {
378381
id: startStreamingTransactionId,
379382
type: startStreamingType,
380383
payload: payload.encode()))
384+
case .osmoAction6:
385+
// The Osmo Action 6 uses the same JSON-wrapped start-streaming
386+
// payload style as the Pocket 4 (not the legacy binary format), but
387+
// pins the codec to AVC. Reverse-engineered from a BTSnoop capture of
388+
// the official DJI app. This is what lets heavier image-stabilization
389+
// modes stream without lag.
390+
let payload = DjiStartStreamingMessagePayloadOsmoAction6(
391+
rtmpUrl: rtmpUrl,
392+
resolution: resolution,
393+
fps: fps,
394+
bitrateKbps: bitrateKbps
395+
)
396+
writeMessage(message: DjiMessage(target: startStreamingTarget,
397+
id: startStreamingTransactionId,
398+
type: startStreamingType,
399+
payload: payload.encode()))
381400
default:
382401
let payload = DjiStartStreamingMessagePayload(
383402
rtmpUrl: rtmpUrl,

Moblin/Integrations/Dji/DjiDevice/DjiDeviceMessage.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,55 @@ struct DjiStartStreamingMessagePayloadPocket4 {
132132
}
133133
}
134134

135+
private struct StartStreamingPayloadOsmoAction6: Codable {
136+
let rtmpAddress: String
137+
let watermark: Int
138+
let codec: String
139+
let EnhancedRTMP: Bool
140+
let supportStopLive: Bool
141+
}
142+
143+
struct DjiStartStreamingMessagePayloadOsmoAction6 {
144+
// Same JSON-wrapped framing as the Pocket 4, but with the Osmo Action 6
145+
// specific header/middle bytes and a JSON body that pins the codec to AVC
146+
// (H.264) instead of HEVC. Reverse-engineered from a BTSnoop capture of the
147+
// official DJI app streaming from an Osmo Action 6.
148+
private static let header = Data([0x01, 0x9C, 0x00])
149+
private static let middle = Data([0xFE, 0x00])
150+
private static let padding = Data([0x00, 0x00, 0x00])
151+
152+
var rtmpUrl: String
153+
var resolution: SettingsDjiDeviceResolution
154+
var bitrateKbps: UInt16
155+
var fps: Int
156+
157+
init(rtmpUrl: String, resolution: SettingsDjiDeviceResolution, fps: Int, bitrateKbps: UInt16) {
158+
self.rtmpUrl = rtmpUrl
159+
self.resolution = resolution
160+
self.fps = fps
161+
self.bitrateKbps = bitrateKbps
162+
}
163+
164+
func encode() -> Data {
165+
let payload = StartStreamingPayloadOsmoAction6(rtmpAddress: rtmpUrl,
166+
watermark: 0,
167+
codec: "AVC",
168+
EnhancedRTMP: false,
169+
supportStopLive: false)
170+
let data = (try? JSONEncoder().encode(payload)) ?? Data()
171+
let writer = ByteWriter()
172+
writer.writeBytes(Self.header)
173+
writer.writeUInt8(toDjiResolution(resolution: resolution))
174+
writer.writeUInt16Le(bitrateKbps)
175+
writer.writeBytes(Self.middle)
176+
writer.writeUInt8(toDjiFps(fps: fps))
177+
writer.writeBytes(Self.padding)
178+
writer.writeUInt16Le(UInt16(truncatingIfNeeded: data.count))
179+
writer.writeBytes(data)
180+
return writer.data
181+
}
182+
}
183+
135184
struct DjiStopStreamingMessagePayload {
136185
static let payload = Data([0x01, 0x01, 0x1A, 0x00, 0x01, 0x02])
137186

0 commit comments

Comments
 (0)