Files
2026-02-11 17:51:48 +09:00

101 lines
4.1 KiB
Swift

import HaishinKit
@preconcurrency import Logboard
import MediaPlayer
import ReplayKit
import RTCHaishinKit
import RTMPHaishinKit
import SRTHaishinKit
import VideoToolbox
nonisolated let logger = LBLogger.with("com.haishinkit.Screencast")
final class SampleHandler: RPBroadcastSampleHandler, @unchecked Sendable {
private var slider: UISlider?
private var session: StreamSession?
private var mixer = MediaMixer(captureSessionMode: .manual, multiTrackAudioMixingEnabled: true)
private var needVideoConfiguration = true
override init() {
Task {
await StreamSessionBuilderFactory.shared.register(RTMPSessionFactory())
await StreamSessionBuilderFactory.shared.register(SRTSessionFactory())
await StreamSessionBuilderFactory.shared.register(HTTPSessionFactory())
await SRTLogger.shared.setLevel(.debug)
await RTCLogger.shared.setLevel(.info)
}
}
override func broadcastStarted(withSetupInfo setupInfo: [String: NSObject]?) {
LBLogger.with(kHaishinKitIdentifier).level = .trace
LBLogger.with(kRTMPHaishinKitIdentifier).level = .trace
LBLogger.with(kSRTHaishinKitIdentifier).level = .trace
LBLogger.with(kRTCHaishinKitIdentifier).level = .trace
// mixer.audioMixerSettings.tracks[1] = .default
Task {
do {
session = try await StreamSessionBuilderFactory.shared.make(Preference.default.makeURL()).build()
// ReplayKit is sensitive to memory, so we limit the queue to a maximum of five items.
var videoSetting = await mixer.videoMixerSettings
videoSetting.mode = .passthrough
await session?.stream.setVideoInputBufferCounts(5)
await mixer.setVideoMixerSettings(videoSetting)
await mixer.startRunning()
if let session {
await mixer.addOutput(session.stream)
try? await session.connect {
}
}
} catch {
logger.error(error)
}
}
// The volume of the audioApp can be obtained even when muted. A hack to synchronize with the volume.
DispatchQueue.main.async {
let volumeView = MPVolumeView(frame: CGRect.zero)
if let slider = volumeView.subviews.compactMap({ $0 as? UISlider }).first {
self.slider = slider
}
}
}
override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
switch sampleBufferType {
case .video:
Task {
if needVideoConfiguration, let dimensions = sampleBuffer.formatDescription?.dimensions {
var videoSettings = await session?.stream.videoSettings
videoSettings?.videoSize = .init(
width: CGFloat(dimensions.width),
height: CGFloat(dimensions.height)
)
videoSettings?.profileLevel = kVTProfileLevel_H264_Baseline_AutoLevel as String
if let videoSettings {
try? await session?.stream.setVideoSettings(videoSettings)
}
needVideoConfiguration = false
}
}
Task { await mixer.append(sampleBuffer) }
case .audioMic:
if sampleBuffer.dataReadiness == .ready {
Task { await mixer.append(sampleBuffer, track: 0) }
}
case .audioApp:
Task { @MainActor in
if let volume = slider?.value {
var audioMixerSettings = await mixer.audioMixerSettings
audioMixerSettings.tracks[1] = .default
audioMixerSettings.tracks[1]?.volume = volume * 0.5
await mixer.setAudioMixerSettings(audioMixerSettings)
}
}
if sampleBuffer.dataReadiness == .ready {
Task { await mixer.append(sampleBuffer, track: 1) }
}
@unknown default:
break
}
}
}