diff --git a/Common/Localizable.xcstrings b/Common/Localizable.xcstrings index bee6bd51f..1f480365e 100644 --- a/Common/Localizable.xcstrings +++ b/Common/Localizable.xcstrings @@ -142545,6 +142545,12 @@ } } } + }, + "OA6 Enhanced RTMP" : { + + }, + "OA6 Video Codec" : { + }, "OBS" : { "localizations" : { diff --git a/Moblin/Integrations/Dji/DjiDevice/DjiDevice.swift b/Moblin/Integrations/Dji/DjiDevice/DjiDevice.swift index 66c910d93..5857052f4 100644 --- a/Moblin/Integrations/Dji/DjiDevice/DjiDevice.swift +++ b/Moblin/Integrations/Dji/DjiDevice/DjiDevice.swift @@ -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, @@ -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 @@ -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) @@ -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, diff --git a/Moblin/Integrations/Dji/DjiDevice/DjiDeviceMessage.swift b/Moblin/Integrations/Dji/DjiDevice/DjiDeviceMessage.swift index b35a213a5..0ea663a8c 100644 --- a/Moblin/Integrations/Dji/DjiDevice/DjiDeviceMessage.swift +++ b/Moblin/Integrations/Dji/DjiDevice/DjiDeviceMessage.swift @@ -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]) @@ -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) diff --git a/Moblin/Various/Model/ModelDjiDevice.swift b/Moblin/Various/Model/ModelDjiDevice.swift index 55bb5e5b0..3d14135fe 100644 --- a/Moblin/Various/Model/ModelDjiDevice.swift +++ b/Moblin/Various/Model/ModelDjiDevice.swift @@ -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) diff --git a/Moblin/Various/Settings/SettingsDebug.swift b/Moblin/Various/Settings/SettingsDebug.swift index 50c7d1b7a..cad404e0c 100644 --- a/Moblin/Various/Settings/SettingsDebug.swift +++ b/Moblin/Various/Settings/SettingsDebug.swift @@ -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, @@ -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 @@ -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 { @@ -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() {} @@ -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) } } diff --git a/Moblin/View/Settings/Debug/DebugVideoSettingsView.swift b/Moblin/View/Settings/Debug/DebugVideoSettingsView.swift index 97564fdb9..bbc218df9 100644 --- a/Moblin/View/Settings/Debug/DebugVideoSettingsView.swift +++ b/Moblin/View/Settings/Debug/DebugVideoSettingsView.swift @@ -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") }