Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Common/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -142545,6 +142545,12 @@
}
}
}
},
"OA6 Enhanced RTMP" : {

},
"OA6 Video Codec" : {

},
"OBS" : {
"localizations" : {
Expand Down
20 changes: 14 additions & 6 deletions Moblin/Integrations/Dji/DjiDevice/DjiDevice.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ class DjiDevice: NSObject {
private let stopStreamingTimer = SimpleTimer(queue: .main)
private var model: SettingsDjiDeviceModel = .unknown
private var batteryPercentage: Int?
private var oa6Codec = "AVC"
private var oa6EnhancedRtmp = false

func startLiveStream(
wifiSsid: String,
Expand All @@ -77,7 +79,9 @@ class DjiDevice: NSObject {
bitrate: UInt32,
imageStabilization: SettingsDjiDeviceImageStabilization,
deviceId: UUID,
model: SettingsDjiDeviceModel
model: SettingsDjiDeviceModel,
oa6Codec: String,
oa6EnhancedRtmp: Bool
) {
logger.debug("dji-device: Start live stream for \(model)")
self.wifiSsid = wifiSsid
Expand All @@ -89,6 +93,8 @@ class DjiDevice: NSObject {
self.imageStabilization = imageStabilization
self.deviceId = deviceId
self.model = model
self.oa6Codec = oa6Codec
self.oa6EnhancedRtmp = oa6EnhancedRtmp
reset()
startStartStreamingTimer()
setState(state: .discovering)
Expand Down Expand Up @@ -383,15 +389,17 @@ extension DjiDevice: CBPeripheralDelegate {
payload: payload.encode()))
case .osmoAction6:
// The Osmo Action 6 uses the same JSON-wrapped start-streaming
// payload style as the Pocket 4 (not the legacy binary format), but
// pins the codec to AVC. Reverse-engineered from a BTSnoop capture of
// the official DJI app. This is what lets heavier image-stabilization
// modes stream without lag.
// payload style as the Pocket 4 (not the legacy binary format).
// Reverse-engineered from a BTSnoop capture of the official DJI app.
// The JSON codec and EnhancedRTMP fields are configurable via debug
// settings.
let payload = DjiStartStreamingMessagePayloadOsmoAction6(
rtmpUrl: rtmpUrl,
resolution: resolution,
fps: fps,
bitrateKbps: bitrateKbps
bitrateKbps: bitrateKbps,
codec: oa6Codec,
enhancedRtmp: oa6EnhancedRtmp
)
writeMessage(message: DjiMessage(target: startStreamingTarget,
id: startStreamingTransactionId,
Expand Down
38 changes: 21 additions & 17 deletions Moblin/Integrations/Dji/DjiDevice/DjiDeviceMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,19 +132,11 @@ struct DjiStartStreamingMessagePayloadPocket4 {
}
}

private struct StartStreamingPayloadOsmoAction6: Codable {
let rtmpAddress: String
let watermark: Int
let codec: String
let EnhancedRTMP: Bool
let supportStopLive: Bool
}

struct DjiStartStreamingMessagePayloadOsmoAction6 {
// Same JSON-wrapped framing as the Pocket 4, but with the Osmo Action 6
// specific header/middle bytes and a JSON body that pins the codec to AVC
// (H.264) instead of HEVC. Reverse-engineered from a BTSnoop capture of the
// official DJI app streaming from an Osmo Action 6.
// specific header/middle bytes. Reverse-engineered from a BTSnoop capture of
// the official DJI app streaming from an Osmo Action 6. The JSON codec and
// EnhancedRTMP fields are configurable via debug settings.
private static let header = Data([0x01, 0x9C, 0x00])
private static let middle = Data([0xFE, 0x00])
private static let padding = Data([0x00, 0x00, 0x00])
Expand All @@ -153,20 +145,32 @@ struct DjiStartStreamingMessagePayloadOsmoAction6 {
var resolution: SettingsDjiDeviceResolution
var bitrateKbps: UInt16
var fps: Int
var codec: String
var enhancedRtmp: Bool

init(rtmpUrl: String, resolution: SettingsDjiDeviceResolution, fps: Int, bitrateKbps: UInt16) {
init(
rtmpUrl: String,
resolution: SettingsDjiDeviceResolution,
fps: Int,
bitrateKbps: UInt16,
codec: String,
enhancedRtmp: Bool
) {
self.rtmpUrl = rtmpUrl
self.resolution = resolution
self.fps = fps
self.bitrateKbps = bitrateKbps
self.codec = codec
self.enhancedRtmp = enhancedRtmp
}

func encode() -> Data {
let payload = StartStreamingPayloadOsmoAction6(rtmpAddress: rtmpUrl,
watermark: 0,
codec: "AVC",
EnhancedRTMP: false,
supportStopLive: false)
let payload = StartStreamingPayload(codec: codec,
EnhancedRTMP: enhancedRtmp,
supportStopLive: false,
watermark: 0,
rtmpAddress: rtmpUrl,
orientation: "landscape")
let data = (try? JSONEncoder().encode(payload)) ?? Data()
let writer = ByteWriter()
writer.writeBytes(Self.header)
Expand Down
4 changes: 3 additions & 1 deletion Moblin/Various/Model/ModelDjiDevice.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,9 @@ extension Model {
bitrate: device.bitrate,
imageStabilization: device.imageStabilization,
deviceId: deviceId,
model: device.model
model: device.model,
oa6Codec: database.debug.djiOa6Codec.rawValue,
oa6EnhancedRtmp: database.debug.djiOa6EnhancedRtmp
)
}
startDjiDeviceTimer(djiDeviceWrapper: djiDeviceWrapper, device: device)
Expand Down
13 changes: 13 additions & 0 deletions Moblin/Various/Settings/SettingsDebug.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ enum SettingsLogLevel: String, Codable, CaseIterable {
case debug = "Debug"
}

enum SettingsDjiOa6Codec: String, Codable, CaseIterable {
case hevc = "HEVC"
case avc = "AVC"
}

let pixelFormats = ["32BGRA", "420YpCbCr8BiPlanarFullRange", "420YpCbCr8BiPlanarVideoRange"]
let pixelFormatTypes = [
kCVPixelFormatType_32BGRA,
Expand Down Expand Up @@ -51,6 +56,8 @@ class SettingsDebug: Codable, ObservableObject {
@Published var videoBitrateChange: Bool = false
@Published var highQualityDownsampling: Bool = false
@Published var httpProxy: Bool = false
@Published var djiOa6Codec: SettingsDjiOa6Codec = .avc
@Published var djiOa6EnhancedRtmp: Bool = false

enum CodingKeys: CodingKey {
case logLevel
Expand Down Expand Up @@ -94,6 +101,8 @@ class SettingsDebug: Codable, ObservableObject {
case videoBitrateChangeEnabled
case highQualityDownsampling
case httpProxy
case djiOa6Codec
case djiOa6EnhancedRtmp
}

func encode(to encoder: any Encoder) throws {
Expand Down Expand Up @@ -132,6 +141,8 @@ class SettingsDebug: Codable, ObservableObject {
try container.encode(.videoBitrateChangeEnabled, videoBitrateChange)
try container.encode(.highQualityDownsampling, highQualityDownsampling)
try container.encode(.httpProxy, httpProxy)
try container.encode(.djiOa6Codec, djiOa6Codec)
try container.encode(.djiOa6EnhancedRtmp, djiOa6EnhancedRtmp)
}

init() {}
Expand Down Expand Up @@ -184,5 +195,7 @@ class SettingsDebug: Codable, ObservableObject {
videoBitrateChange = container.decode(.videoBitrateChangeEnabled, Bool.self, false)
highQualityDownsampling = container.decode(.highQualityDownsampling, Bool.self, false)
httpProxy = container.decode(.httpProxy, Bool.self, false)
djiOa6Codec = container.decode(.djiOa6Codec, SettingsDjiOa6Codec.self, .avc)
djiOa6EnhancedRtmp = container.decode(.djiOa6EnhancedRtmp, Bool.self, false)
}
}
8 changes: 8 additions & 0 deletions Moblin/View/Settings/Debug/DebugVideoSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ struct DebugVideoSettingsView: View {
Section {
Toggle("Periodic video bitrate change", isOn: $debug.videoBitrateChange)
}
Section {
Picker("OA6 Video Codec", selection: $debug.djiOa6Codec) {
ForEach(SettingsDjiOa6Codec.allCases, id: \.self) {
Text($0.rawValue)
}
}
Toggle("OA6 Enhanced RTMP", isOn: $debug.djiOa6EnhancedRtmp)
}
}
.navigationTitle("Video")
}
Expand Down
Loading