mirror of
https://github.com/HaishinKit/HaishinKit.swift.git
synced 2026-05-07 20:12:28 +00:00
183 lines
6.3 KiB
Swift
183 lines
6.3 KiB
Swift
import AVFoundation
|
|
import HaishinKit
|
|
import RTCHaishinKit
|
|
import SwiftUI
|
|
|
|
@MainActor
|
|
final class PublishViewModel: ObservableObject {
|
|
@Published var currentFPS: FPS = .fps30
|
|
@Published var visualEffectItem: VideoEffectItem = .none
|
|
@Published private(set) var error: Error?
|
|
@Published var isShowError = false
|
|
@Published private(set) var isTorchEnabled = false
|
|
@Published private(set) var readyState: StreamSessionReadyState = .closed
|
|
private(set) var mixer = MediaMixer(captureSessionMode: .multi)
|
|
private var tasks: [Task<Void, Swift.Error>] = []
|
|
private var session: (any StreamSession)?
|
|
private var currentPosition: AVCaptureDevice.Position = .back
|
|
@ScreenActor private var currentVideoEffect: VideoEffect?
|
|
|
|
func startPublishing(_ preference: PreferenceViewModel) {
|
|
Task {
|
|
guard let session else {
|
|
return
|
|
}
|
|
do {
|
|
try await session.connect {
|
|
Task { @MainActor in
|
|
self.isShowError = true
|
|
}
|
|
}
|
|
} catch {
|
|
self.error = error
|
|
self.isShowError = true
|
|
logger.error(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
func stopPublishing() {
|
|
Task {
|
|
do {
|
|
try await session?.close()
|
|
} catch {
|
|
logger.error(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
func makeSession(_ preference: PreferenceViewModel) async {
|
|
// Make session.
|
|
do {
|
|
session = try await StreamSessionBuilderFactory.shared.make(preference.makeURL())
|
|
.setMode(.publish)
|
|
.build()
|
|
guard let session else {
|
|
return
|
|
}
|
|
await mixer.addOutput(session.stream)
|
|
tasks.append(Task {
|
|
for await readyState in await session.readyState {
|
|
self.readyState = readyState
|
|
}
|
|
})
|
|
} catch {
|
|
self.error = error
|
|
isShowError = true
|
|
}
|
|
do {
|
|
if let session {
|
|
try await session.stream.setAudioSettings(preference.makeAudioCodecSettings(session.stream.audioSettings))
|
|
}
|
|
} catch {
|
|
self.error = error
|
|
isShowError = true
|
|
}
|
|
do {
|
|
if let session {
|
|
try await session.stream.setVideoSettings(preference.makeVideoCodecSettings(session.stream.videoSettings))
|
|
}
|
|
} catch {
|
|
self.error = error
|
|
isShowError = true
|
|
}
|
|
}
|
|
|
|
func startRunning(_ preference: PreferenceViewModel) {
|
|
Task {
|
|
// SetUp a mixer.
|
|
var videoMixerSettings = await mixer.videoMixerSettings
|
|
videoMixerSettings.mode = .offscreen
|
|
await mixer.setVideoMixerSettings(videoMixerSettings)
|
|
// Attach devices
|
|
let back = AVCaptureDevice.default(for: .video)
|
|
try? await mixer.attachVideo(back, track: 0)
|
|
let audio = AVCaptureDevice.default(for: .audio)
|
|
try? await mixer.attachAudio(audio, track: 0)
|
|
await mixer.startRunning()
|
|
await makeSession(preference)
|
|
}
|
|
Task { @ScreenActor in
|
|
await mixer.screen.size = .init(width: 1280, height: 720)
|
|
await mixer.screen.backgroundColor = NSColor.black.cgColor
|
|
|
|
let assetScreenObject = AssetScreenObject()
|
|
assetScreenObject.size = .init(width: 180, height: 180)
|
|
assetScreenObject.layoutMargin = .init(top: 16, left: 16, bottom: 0, right: 0)
|
|
try? assetScreenObject.startReading(AVAsset(url: URL(fileURLWithPath: Bundle.main.path(forResource: "SampleVideo_360x240_5mb", ofType: "mp4") ?? "")))
|
|
try? await mixer.screen.addChild(assetScreenObject)
|
|
|
|
let image = ImageScreenObject()
|
|
image.size = .init(width: 120, height: 120)
|
|
image.horizontalAlignment = .right
|
|
image.verticalAlignment = .bottom
|
|
image.layoutMargin = .init(top: 0, left: 0, bottom: 16, right: 16)
|
|
let appIconFile = URL(fileURLWithPath: Bundle.main.path(forResource: "AppIcon", ofType: "png") ?? "")
|
|
if let nsImage = NSImage(contentsOf: appIconFile), let cgImage = nsImage.cgImage(forProposedRect: nil, context: nil, hints: nil) {
|
|
let ciImage = CIImage(cgImage: cgImage)
|
|
image.ciImage = ciImage
|
|
}
|
|
try? await mixer.screen.addChild(image)
|
|
}
|
|
}
|
|
|
|
func stopRunning() {
|
|
Task {
|
|
await mixer.stopRunning()
|
|
try? await mixer.attachAudio(nil)
|
|
try? await mixer.attachVideo(nil)
|
|
if let session {
|
|
await mixer.removeOutput(session.stream)
|
|
}
|
|
tasks.forEach { $0.cancel() }
|
|
tasks.removeAll()
|
|
}
|
|
}
|
|
|
|
func setVisualEffet(_ videoEffect: VideoEffectItem) {
|
|
Task { @ScreenActor in
|
|
if let currentVideoEffect {
|
|
_ = await mixer.screen.unregisterVideoEffect(currentVideoEffect)
|
|
}
|
|
if let videoEffect = videoEffect.makeVideoEffect() {
|
|
currentVideoEffect = videoEffect
|
|
_ = await mixer.screen.registerVideoEffect(videoEffect)
|
|
}
|
|
}
|
|
}
|
|
|
|
func setFrameRate(_ fps: Float64) {
|
|
Task {
|
|
do {
|
|
// Sets to input frameRate.
|
|
try? await mixer.configuration(video: 0) { video in
|
|
do {
|
|
try video.setFrameRate(fps)
|
|
} catch {
|
|
logger.error(error)
|
|
}
|
|
}
|
|
try? await mixer.configuration(video: 1) { video in
|
|
do {
|
|
try video.setFrameRate(fps)
|
|
} catch {
|
|
logger.error(error)
|
|
}
|
|
}
|
|
// Sets to output frameRate.
|
|
try await mixer.setFrameRate(fps)
|
|
} catch {
|
|
logger.error(error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension PublishViewModel: MTHKViewRepresentable.PreviewSource {
|
|
nonisolated func connect(to view: HaishinKit.MTHKView) {
|
|
Task {
|
|
await mixer.addOutput(view)
|
|
}
|
|
}
|
|
}
|