129 lines
4.5 KiB
Swift
129 lines
4.5 KiB
Swift
import Foundation
|
|
|
|
/// 火山引擎大模型流式 ASR 二进制帧编解码。
|
|
///
|
|
/// 帧结构通常为:4 字节 header + 可选 sequence + 4 字节大端 payload size + payload。
|
|
/// 为了避免运行时依赖 gzip 实现,这里显式使用 no compression;官方协议允许客户端选择
|
|
/// no compression,服务端会沿用客户端声明的压缩方式。
|
|
enum VolcengineFrame {
|
|
|
|
enum MessageType: UInt8 {
|
|
case fullClientRequest = 0b0001
|
|
case audioOnlyRequest = 0b0010
|
|
case fullServerResponse = 0b1001
|
|
case errorMessage = 0b1111
|
|
}
|
|
|
|
enum Flags: UInt8 {
|
|
case none = 0b0000
|
|
case positiveSequence = 0b0001
|
|
case lastPacket = 0b0010
|
|
case negativeSequence = 0b0011
|
|
}
|
|
|
|
enum Serialization: UInt8 {
|
|
case none = 0b0000
|
|
case json = 0b0001
|
|
}
|
|
|
|
enum CompressionMethod: UInt8 {
|
|
case none = 0b0000
|
|
}
|
|
|
|
static func build(
|
|
messageType: MessageType,
|
|
flags: Flags,
|
|
serialization: Serialization,
|
|
payload: Data,
|
|
sequence: Int32? = nil
|
|
) -> Data {
|
|
var frame = Data()
|
|
frame.append(0x11)
|
|
frame.append((messageType.rawValue << 4) | flags.rawValue)
|
|
frame.append((serialization.rawValue << 4) | CompressionMethod.none.rawValue)
|
|
frame.append(0x00)
|
|
|
|
// positiveSequence / negativeSequence 必须带 4 字节大端 seq;其它 flag 不带。
|
|
if let sequence, flags == .positiveSequence || flags == .negativeSequence {
|
|
var seq = UInt32(bitPattern: sequence).bigEndian
|
|
withUnsafeBytes(of: &seq) { frame.append(contentsOf: $0) }
|
|
}
|
|
|
|
var size = UInt32(payload.count).bigEndian
|
|
withUnsafeBytes(of: &size) { frame.append(contentsOf: $0) }
|
|
frame.append(payload)
|
|
return frame
|
|
}
|
|
|
|
static func parse(_ data: Data) -> ParsedFrame? {
|
|
guard data.count >= 8 else { return nil }
|
|
|
|
let headerSize = Int(data[0] & 0x0F) * 4
|
|
guard headerSize >= 4, data.count >= headerSize + 4 else { return nil }
|
|
|
|
let messageTypeRaw = (data[1] >> 4) & 0x0F
|
|
let messageType = MessageType(rawValue: messageTypeRaw)
|
|
let flagsRaw = data[1] & 0x0F
|
|
let compression = data[2] & 0x0F
|
|
guard compression == CompressionMethod.none.rawValue else { return nil }
|
|
|
|
var offset = headerSize
|
|
var sequence: Int32?
|
|
|
|
if hasSequence(flagsRaw) {
|
|
guard let value = readInt32(data, at: offset) else { return nil }
|
|
sequence = value
|
|
offset += 4
|
|
}
|
|
|
|
if messageType == .errorMessage {
|
|
guard let code = readUInt32(data, at: offset),
|
|
let messageSize = readUInt32(data, at: offset + 4) else {
|
|
return nil
|
|
}
|
|
offset += 8
|
|
guard data.count >= offset + Int(messageSize) else { return nil }
|
|
let payload = data.subdata(in: offset..<(offset + Int(messageSize)))
|
|
return ParsedFrame(messageType: messageType, flags: flagsRaw, sequence: sequence, errorCode: code, payload: payload)
|
|
}
|
|
|
|
guard let payloadSize = readUInt32(data, at: offset) else { return nil }
|
|
offset += 4
|
|
guard data.count >= offset + Int(payloadSize) else { return nil }
|
|
let payload = data.subdata(in: offset..<(offset + Int(payloadSize)))
|
|
return ParsedFrame(messageType: messageType, flags: flagsRaw, sequence: sequence, errorCode: nil, payload: payload)
|
|
}
|
|
|
|
struct ParsedFrame {
|
|
let messageType: MessageType?
|
|
let flags: UInt8
|
|
let sequence: Int32?
|
|
let errorCode: UInt32?
|
|
let payload: Data
|
|
|
|
var isFinal: Bool {
|
|
flags == Flags.lastPacket.rawValue
|
|
|| flags == Flags.negativeSequence.rawValue
|
|
|| (sequence ?? 0) < 0
|
|
}
|
|
}
|
|
|
|
private static func hasSequence(_ flags: UInt8) -> Bool {
|
|
flags == Flags.positiveSequence.rawValue || flags == Flags.negativeSequence.rawValue
|
|
}
|
|
|
|
private static func readUInt32(_ data: Data, at offset: Int) -> UInt32? {
|
|
guard offset >= 0, data.count >= offset + 4 else { return nil }
|
|
var value: UInt32 = 0
|
|
for byte in data[offset..<(offset + 4)] {
|
|
value = (value << 8) | UInt32(byte)
|
|
}
|
|
return value
|
|
}
|
|
|
|
private static func readInt32(_ data: Data, at offset: Int) -> Int32? {
|
|
guard let unsigned = readUInt32(data, at: offset) else { return nil }
|
|
return Int32(bitPattern: unsigned)
|
|
}
|
|
}
|