Init
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
import Foundation
|
||||
import VideoToolbox
|
||||
import CoreImage
|
||||
import IOSurface
|
||||
import os.log
|
||||
|
||||
final class NativePortalStream: NSObject, URLSessionDataDelegate {
|
||||
private var session: URLSession!
|
||||
private var task: URLSessionDataTask?
|
||||
private var pending = Data()
|
||||
private let decoder = H264Decoder()
|
||||
private var dataCallbacks = 0
|
||||
var onFrame: ((CVPixelBuffer) -> Void)?
|
||||
var onStatus: ((String) -> Void)?
|
||||
|
||||
func start(host: String, token: String? = PortalAuth.token) {
|
||||
stop()
|
||||
dataCallbacks = 0
|
||||
decoder.onFrame = { [weak self] b in self?.onFrame?(b) }
|
||||
let cfg = URLSessionConfiguration.default
|
||||
cfg.timeoutIntervalForRequest = .greatestFiniteMagnitude
|
||||
cfg.timeoutIntervalForResource = 7 * 24 * 60 * 60
|
||||
session = URLSession(configuration: cfg, delegate: self, delegateQueue: OperationQueue())
|
||||
guard let u = URL(string: "https://\(host)/video.h264") else {
|
||||
onStatus?("Invalid Portal address")
|
||||
return
|
||||
}
|
||||
onStatus?("Connecting native H.264 (HTTPS)…")
|
||||
var req = URLRequest(url: u)
|
||||
if let token {
|
||||
req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
task = session.dataTask(with: req)
|
||||
task?.resume()
|
||||
}
|
||||
|
||||
func stop() {
|
||||
task?.cancel()
|
||||
task = nil
|
||||
session?.invalidateAndCancel()
|
||||
session = nil
|
||||
pending.removeAll()
|
||||
decoder.stop()
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
PortalTlsPinning.evaluate(challenge: challenge, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
|
||||
os_log(.default, "PortalCam stream: response %{public}@", String(describing: response))
|
||||
completionHandler(.allow)
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
|
||||
dataCallbacks += 1
|
||||
if dataCallbacks <= 3 {
|
||||
os_log(.default, "PortalCam stream: received %{public}d bytes", data.count)
|
||||
}
|
||||
pending.append(data)
|
||||
consume()
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||
if let error {
|
||||
onStatus?("Stream stopped: \(error.localizedDescription)")
|
||||
} else {
|
||||
onStatus?("Stream completed")
|
||||
}
|
||||
}
|
||||
|
||||
private func consume() {
|
||||
while let first = startCode(in: pending, from: 0) {
|
||||
guard let next = startCode(in: pending, from: first.0 + first.1) else { break }
|
||||
let sc = next.0
|
||||
let nal = pending[(first.0 + first.1)..<sc]
|
||||
decoder.decode(nal: Data(nal))
|
||||
pending.removeSubrange(0..<sc)
|
||||
}
|
||||
if pending.count > 2 * 1024 * 1024 {
|
||||
pending = pending.suffix(256 * 1024)
|
||||
}
|
||||
}
|
||||
|
||||
private func startCode(in d: Data, from: Int) -> (Int, Int)? {
|
||||
if d.count < 4 || from >= d.count { return nil }
|
||||
for i in from..<(d.count - 2) {
|
||||
if d[i] == 0 && d[i+1] == 0 && d[i+2] == 1 { return (i, 3) }
|
||||
if i + 3 < d.count && d[i] == 0 && d[i+1] == 0 && d[i+2] == 0 && d[i+3] == 1 { return (i, 4) }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private final class H264Decoder {
|
||||
private var sps = Data(), pps = Data(), session: VTDecompressionSession?, format: CMVideoFormatDescription?
|
||||
var onFrame: ((CVPixelBuffer) -> Void)?
|
||||
private var decodedFrames = 0
|
||||
|
||||
func stop() {
|
||||
if let session { VTDecompressionSessionInvalidate(session) }
|
||||
session = nil
|
||||
format = nil
|
||||
}
|
||||
|
||||
func decode(nal: Data) {
|
||||
guard let type = nal.first.map({ $0 & 0x1f }) else { return }
|
||||
if type == 7 {
|
||||
os_log(.default, "PortalCam decoder: SPS %{public}d bytes", nal.count)
|
||||
sps = nal
|
||||
rebuild()
|
||||
return
|
||||
}
|
||||
if type == 8 {
|
||||
os_log(.default, "PortalCam decoder: PPS %{public}d bytes", nal.count)
|
||||
pps = nal
|
||||
rebuild()
|
||||
return
|
||||
}
|
||||
guard (type == 1 || type == 5), let format else { return }
|
||||
var size = UInt32(nal.count).bigEndian
|
||||
var avcc = Data(bytes: &size, count: 4)
|
||||
avcc.append(nal)
|
||||
var block: CMBlockBuffer?
|
||||
avcc.withUnsafeBytes { raw in
|
||||
_ = CMBlockBufferCreateWithMemoryBlock(
|
||||
allocator: kCFAllocatorDefault,
|
||||
memoryBlock: UnsafeMutableRawPointer(mutating: raw.baseAddress!),
|
||||
blockLength: raw.count,
|
||||
blockAllocator: kCFAllocatorNull,
|
||||
customBlockSource: nil,
|
||||
offsetToData: 0,
|
||||
dataLength: raw.count,
|
||||
flags: 0,
|
||||
blockBufferOut: &block
|
||||
)
|
||||
}
|
||||
guard let block else { return }
|
||||
var sample: CMSampleBuffer?
|
||||
var timing = CMSampleTimingInfo(
|
||||
duration: CMTime(value: 1, timescale: 30),
|
||||
presentationTimeStamp: CMClockGetTime(CMClockGetHostTimeClock()),
|
||||
decodeTimeStamp: .invalid
|
||||
)
|
||||
var sizes = [avcc.count]
|
||||
_ = CMSampleBufferCreateReady(
|
||||
allocator: kCFAllocatorDefault,
|
||||
dataBuffer: block,
|
||||
formatDescription: format,
|
||||
sampleCount: 1,
|
||||
sampleTimingEntryCount: 1,
|
||||
sampleTimingArray: &timing,
|
||||
sampleSizeEntryCount: 1,
|
||||
sampleSizeArray: &sizes,
|
||||
sampleBufferOut: &sample
|
||||
)
|
||||
if let sample {
|
||||
let rc = VTDecompressionSessionDecodeFrame(
|
||||
session!,
|
||||
sampleBuffer: sample,
|
||||
flags: [],
|
||||
frameRefcon: Unmanaged.passUnretained(self).toOpaque(),
|
||||
infoFlagsOut: nil
|
||||
)
|
||||
if rc != noErr {
|
||||
os_log(.error, "PortalCam decoder: decode returned %{public}d", rc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func rebuild() {
|
||||
guard !sps.isEmpty, !pps.isEmpty else { return }
|
||||
stop()
|
||||
var f: CMVideoFormatDescription?
|
||||
let rc: OSStatus = sps.withUnsafeBytes { sr in
|
||||
pps.withUnsafeBytes { pr in
|
||||
var ps = [sr.bindMemory(to: UInt8.self).baseAddress!, pr.bindMemory(to: UInt8.self).baseAddress!]
|
||||
var lens = [sps.count, pps.count]
|
||||
return CMVideoFormatDescriptionCreateFromH264ParameterSets(
|
||||
allocator: kCFAllocatorDefault,
|
||||
parameterSetCount: 2,
|
||||
parameterSetPointers: &ps,
|
||||
parameterSetSizes: &lens,
|
||||
nalUnitHeaderLength: 4,
|
||||
formatDescriptionOut: &f
|
||||
)
|
||||
}
|
||||
}
|
||||
guard rc == noErr, let f else {
|
||||
os_log(.error, "PortalCam decoder: format creation failed %{public}d", rc)
|
||||
return
|
||||
}
|
||||
os_log(.default, "PortalCam decoder: format ready")
|
||||
format = f
|
||||
let callback: VTDecompressionOutputCallback = { refCon, _, status, _, image, _, _ in
|
||||
if status == noErr, let image, let refCon {
|
||||
let decoder = Unmanaged<H264Decoder>.fromOpaque(refCon).takeUnretainedValue()
|
||||
decoder.decodedFrames += 1
|
||||
if decoder.decodedFrames <= 3 {
|
||||
os_log(.default, "PortalCam decoder: frame decoded (#%{public}d)", decoder.decodedFrames)
|
||||
}
|
||||
decoder.onFrame?(image)
|
||||
}
|
||||
}
|
||||
var cb = VTDecompressionOutputCallbackRecord(
|
||||
decompressionOutputCallback: callback,
|
||||
decompressionOutputRefCon: Unmanaged.passUnretained(self).toOpaque()
|
||||
)
|
||||
var attrs: CFDictionary = [
|
||||
kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_32BGRA,
|
||||
kCVPixelBufferIOSurfacePropertiesKey: [:]
|
||||
] as CFDictionary
|
||||
var ds: VTDecompressionSession?
|
||||
let rc2 = VTDecompressionSessionCreate(
|
||||
allocator: kCFAllocatorDefault,
|
||||
formatDescription: f,
|
||||
decoderSpecification: nil,
|
||||
imageBufferAttributes: attrs,
|
||||
outputCallback: &cb,
|
||||
decompressionSessionOut: &ds
|
||||
)
|
||||
os_log(.default, "PortalCam decoder: session create %{public}d", rc2)
|
||||
session = ds
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user