mirror of
https://github.com/HaishinKit/HaishinKit.swift.git
synced 2026-05-07 20:12:28 +00:00
Advanced protocol MediaMixerOutput.
This commit is contained in:
@@ -7,6 +7,8 @@ import libsrt
|
||||
/// An actor that provides the interface to control a one-way channel over a SRTConnection.
|
||||
public actor SRTStream {
|
||||
@Published public private(set) var readyState: HKStreamReadyState = .idle
|
||||
public private(set) var videoTrackId: UInt8? = UInt8.max
|
||||
public private(set) var audioTrackId: UInt8? = UInt8.max
|
||||
private var name: String?
|
||||
private var action: (() async -> Void)?
|
||||
private var outputs: [any HKStreamOutput] = []
|
||||
@@ -191,6 +193,17 @@ extension SRTStream: HKStream {
|
||||
}
|
||||
}
|
||||
|
||||
public func selectTrack(_ id: UInt8?, mediaType: CMFormatDescription.MediaType) {
|
||||
switch mediaType {
|
||||
case .audio:
|
||||
audioTrackId = id
|
||||
case .video:
|
||||
videoTrackId = id
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func dispatch(_ event: NetworkMonitorEvent) async {
|
||||
await bitrateStorategy?.adjustBitrate(event, stream: self)
|
||||
}
|
||||
@@ -198,17 +211,11 @@ extension SRTStream: HKStream {
|
||||
|
||||
extension SRTStream: MediaMixerOutput {
|
||||
// MARK: MediaMixerOutput
|
||||
nonisolated public func mixer(_ mixer: MediaMixer, track: UInt8, didOutput sampleBuffer: CMSampleBuffer) {
|
||||
guard track == UInt8.max else {
|
||||
return
|
||||
}
|
||||
nonisolated public func mixer(_ mixer: MediaMixer, didOutput sampleBuffer: CMSampleBuffer) {
|
||||
Task { await append(sampleBuffer) }
|
||||
}
|
||||
|
||||
nonisolated public func mixer(_ mixer: MediaMixer, track: UInt8, didOutput buffer: AVAudioPCMBuffer, when: AVAudioTime) {
|
||||
guard track == UInt8.max else {
|
||||
return
|
||||
}
|
||||
nonisolated public func mixer(_ mixer: MediaMixer, didOutput buffer: AVAudioPCMBuffer, when: AVAudioTime) {
|
||||
Task { await append(buffer, when: when) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ public protocol HKStream: Actor, MediaMixerOutput {
|
||||
/// Removes an output observer.
|
||||
func removeOutput(_ observer: some HKStreamOutput)
|
||||
|
||||
/// Selects track id for streaming.
|
||||
func selectTrack(_ id: UInt8?, mediaType: CMFormatDescription.MediaType)
|
||||
|
||||
/// Dispatch a network monitor event.
|
||||
func dispatch(_ event: NetworkMonitorEvent) async
|
||||
}
|
||||
|
||||
@@ -67,6 +67,8 @@ public actor HKStreamRecorder {
|
||||
public private(set) var isRecording = false
|
||||
/// The the movie fragment interval in sec.
|
||||
public private(set) var movieFragmentInterval: Double?
|
||||
public private(set) var videoTrackId: UInt8? = UInt8.max
|
||||
public private(set) var audioTrackId: UInt8? = UInt8.max
|
||||
private var isReadyForStartWriting: Bool {
|
||||
guard let writer = writer else {
|
||||
return false
|
||||
@@ -74,7 +76,6 @@ public actor HKStreamRecorder {
|
||||
return settings.count == writer.inputs.count
|
||||
}
|
||||
private var writer: AVAssetWriter?
|
||||
private var trackId: UInt8 = 0
|
||||
private var continuation: AsyncStream<Error>.Continuation?
|
||||
private var writerInputs: [AVMediaType: AVAssetWriterInput] = [:]
|
||||
private var audioPresentationTime: CMTime = .zero
|
||||
@@ -95,14 +96,6 @@ public actor HKStreamRecorder {
|
||||
public init() {
|
||||
}
|
||||
|
||||
/// Sets the video track id for recording.
|
||||
public func setTrackId(_ trackId: UInt8) throws {
|
||||
guard isRecording else {
|
||||
throw Error.invalidState
|
||||
}
|
||||
self.trackId = trackId
|
||||
}
|
||||
|
||||
/// Sets the movie fragment interval in sec.
|
||||
///
|
||||
/// This value allows the file to be written continuously, so the file will remain even if the app crashes or is forcefully terminated. A value of 10 seconds or more is recommended.
|
||||
@@ -162,6 +155,17 @@ public actor HKStreamRecorder {
|
||||
return writer.outputURL
|
||||
}
|
||||
|
||||
public func selectTrack(_ id: UInt8?, mediaType: CMFormatDescription.MediaType) {
|
||||
switch mediaType {
|
||||
case .audio:
|
||||
audioTrackId = id
|
||||
case .video:
|
||||
videoTrackId = id
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func append(_ sampleBuffer: CMSampleBuffer) {
|
||||
guard isRecording else {
|
||||
return
|
||||
@@ -273,16 +277,13 @@ extension HKStreamRecorder: HKStreamOutput {
|
||||
|
||||
extension HKStreamRecorder: MediaMixerOutput {
|
||||
// MARK: MediaMixerOutput
|
||||
nonisolated public func mixer(_ mixer: MediaMixer, track: UInt8, didOutput sampleBuffer: CMSampleBuffer) {
|
||||
nonisolated public func mixer(_ mixer: MediaMixer, didOutput sampleBuffer: CMSampleBuffer) {
|
||||
Task {
|
||||
guard await trackId == track else {
|
||||
return
|
||||
}
|
||||
await append(sampleBuffer)
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated public func mixer(_ mixer: MediaMixer, track: UInt8, didOutput buffer: AVAudioPCMBuffer, when: AVAudioTime) {
|
||||
nonisolated public func mixer(_ mixer: MediaMixer, didOutput buffer: AVAudioPCMBuffer, when: AVAudioTime) {
|
||||
guard let sampleBuffer = buffer.makeSampleBuffer(when) else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -314,19 +314,22 @@ extension MediaMixer: AsyncRunner {
|
||||
Task { @ScreenActor in
|
||||
screen.append(inputs.0, buffer: inputs.1)
|
||||
}
|
||||
for output in outputs where await output.videoTrackId == inputs.0 {
|
||||
output.mixer(self, didOutput: inputs.1)
|
||||
}
|
||||
}
|
||||
}
|
||||
Task {
|
||||
for await video in videoIO.output where isRunning {
|
||||
for output in outputs {
|
||||
output.mixer(self, track: UInt8.max, didOutput: video)
|
||||
for output in outputs where await output.videoTrackId == UInt8.max {
|
||||
output.mixer(self, didOutput: video)
|
||||
}
|
||||
}
|
||||
}
|
||||
Task {
|
||||
for await audio in audioIO.output where isRunning {
|
||||
for output in outputs {
|
||||
output.mixer(self, track: UInt8.max, didOutput: audio.0, when: audio.1)
|
||||
for output in outputs where await output.audioTrackId == UInt8.max {
|
||||
output.mixer(self, didOutput: audio.0, when: audio.1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -337,7 +340,9 @@ extension MediaMixer: AsyncRunner {
|
||||
guard let buffer = screen.makeSampleBuffer() else {
|
||||
continue
|
||||
}
|
||||
await outputs.forEach { $0.mixer(self, track: UInt8.max, didOutput: buffer) }
|
||||
for output in await self.outputs where await output.videoTrackId == UInt8.max {
|
||||
output.mixer(self, didOutput: buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
#if os(iOS) || os(tvOS) || os(visionOS)
|
||||
|
||||
@@ -2,8 +2,12 @@ import AVFoundation
|
||||
|
||||
/// A delegate protocol implements to receive stream output events.
|
||||
public protocol MediaMixerOutput: AnyObject, Sendable {
|
||||
/// Tells the receiver to a video track id.
|
||||
var videoTrackId: UInt8? { get async }
|
||||
/// Tells the receiver to an audio track id.
|
||||
var audioTrackId: UInt8? { get async }
|
||||
/// Tells the receiver to a video buffer incoming.
|
||||
func mixer(_ mixer: MediaMixer, track: UInt8, didOutput sampleBuffer: CMSampleBuffer)
|
||||
func mixer(_ mixer: MediaMixer, didOutput sampleBuffer: CMSampleBuffer)
|
||||
/// Tells the receiver to an audio buffer incoming.
|
||||
func mixer(_ mixer: MediaMixer, track: UInt8, didOutput buffer: AVAudioPCMBuffer, when: AVAudioTime)
|
||||
func mixer(_ mixer: MediaMixer, didOutput buffer: AVAudioPCMBuffer, when: AVAudioTime)
|
||||
}
|
||||
|
||||
@@ -191,6 +191,9 @@ public actor RTMPStream {
|
||||
/// The stream's name used for FMLE-compatible sequences.
|
||||
public private(set) var fcPublishName: String?
|
||||
|
||||
public private(set) var videoTrackId: UInt8? = UInt8.max
|
||||
public private(set) var audioTrackId: UInt8? = UInt8.max
|
||||
|
||||
private var isPaused = false
|
||||
private var startedAt = Date() {
|
||||
didSet {
|
||||
@@ -802,6 +805,17 @@ extension RTMPStream: HKStream {
|
||||
self.bitrateStorategy = bitrateStorategy
|
||||
}
|
||||
|
||||
public func selectTrack(_ id: UInt8?, mediaType: CMFormatDescription.MediaType) {
|
||||
switch mediaType {
|
||||
case .audio:
|
||||
audioTrackId = id
|
||||
case .video:
|
||||
videoTrackId = id
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func dispatch(_ event: NetworkMonitorEvent) async {
|
||||
await bitrateStorategy?.adjustBitrate(event, stream: self)
|
||||
currentFPS = frameCount
|
||||
@@ -812,17 +826,11 @@ extension RTMPStream: HKStream {
|
||||
|
||||
extension RTMPStream: MediaMixerOutput {
|
||||
// MARK: MediaMixerOutput
|
||||
nonisolated public func mixer(_ mixer: MediaMixer, track: UInt8, didOutput sampleBuffer: CMSampleBuffer) {
|
||||
guard track == UInt8.max else {
|
||||
return
|
||||
}
|
||||
nonisolated public func mixer(_ mixer: MediaMixer, didOutput sampleBuffer: CMSampleBuffer) {
|
||||
Task { await append(sampleBuffer) }
|
||||
}
|
||||
|
||||
nonisolated public func mixer(_ mixer: MediaMixer, track: UInt8, didOutput buffer: AVAudioPCMBuffer, when: AVAudioTime) {
|
||||
guard track == UInt8.max else {
|
||||
return
|
||||
}
|
||||
nonisolated public func mixer(_ mixer: MediaMixer, didOutput buffer: AVAudioPCMBuffer, when: AVAudioTime) {
|
||||
Task { await append(buffer, when: when) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ public class MTHKView: MTKView {
|
||||
public var videoGravity: AVLayerVideoGravity = .resizeAspect
|
||||
|
||||
/// Specifies how the video is displayed with in track.
|
||||
public var track = UInt8.max
|
||||
public var videoTrackId: UInt8? = UInt8.max
|
||||
public var audioTrackId: UInt8?
|
||||
|
||||
private var displayImage: CIImage?
|
||||
private let colorSpace: CGColorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
@@ -120,14 +121,11 @@ public class MTHKView: MTKView {
|
||||
|
||||
extension MTHKView: MediaMixerOutput {
|
||||
// MARK: MediaMixerOutput
|
||||
nonisolated public func mixer(_ mixer: MediaMixer, track: UInt8, didOutput buffer: AVAudioPCMBuffer, when: AVAudioTime) {
|
||||
nonisolated public func mixer(_ mixer: MediaMixer, didOutput buffer: AVAudioPCMBuffer, when: AVAudioTime) {
|
||||
}
|
||||
|
||||
nonisolated public func mixer(_ mixer: MediaMixer, track: UInt8, didOutput sampleBuffer: CMSampleBuffer) {
|
||||
nonisolated public func mixer(_ mixer: MediaMixer, didOutput sampleBuffer: CMSampleBuffer) {
|
||||
Task { @MainActor in
|
||||
guard self.track == track else {
|
||||
return
|
||||
}
|
||||
displayImage = try? sampleBuffer.imageBuffer?.makeCIImage()
|
||||
#if os(macOS)
|
||||
self.needsDisplay = true
|
||||
|
||||
@@ -13,14 +13,14 @@ public class PiPHKView: UIView {
|
||||
AVSampleBufferDisplayLayer.self
|
||||
}
|
||||
|
||||
/// Specifies how the video is displayed with in track.
|
||||
public var track = UInt8.max
|
||||
|
||||
/// The view’s Core Animation layer used for rendering.
|
||||
override public var layer: AVSampleBufferDisplayLayer {
|
||||
super.layer as! AVSampleBufferDisplayLayer
|
||||
}
|
||||
|
||||
public var videoTrackId: UInt8? = UInt8.max
|
||||
public var audioTrackId: UInt8?
|
||||
|
||||
/// A value that specifies how the video is displayed within a player layer’s bounds.
|
||||
public var videoGravity: AVLayerVideoGravity = .resizeAspect {
|
||||
didSet {
|
||||
@@ -67,7 +67,8 @@ public class PiPHKView: NSView {
|
||||
}
|
||||
|
||||
/// Specifies how the video is displayed with in track.
|
||||
public var track = UInt8.max
|
||||
public var videoTrackId: UInt8? = UInt8.max
|
||||
public var audioTrackId: UInt8?
|
||||
|
||||
/// Initializes and returns a newly allocated view object with the specified frame rectangle.
|
||||
override public init(frame: CGRect) {
|
||||
@@ -96,14 +97,11 @@ public class PiPHKView: NSView {
|
||||
|
||||
extension PiPHKView: MediaMixerOutput {
|
||||
// MARK: MediaMixerOutput
|
||||
nonisolated public func mixer(_ mixer: MediaMixer, track: UInt8, didOutput buffer: AVAudioPCMBuffer, when: AVAudioTime) {
|
||||
nonisolated public func mixer(_ mixer: MediaMixer, didOutput buffer: AVAudioPCMBuffer, when: AVAudioTime) {
|
||||
}
|
||||
|
||||
nonisolated public func mixer(_ mixer: MediaMixer, track: UInt8, didOutput sampleBuffer: CMSampleBuffer) {
|
||||
nonisolated public func mixer(_ mixer: MediaMixer, didOutput sampleBuffer: CMSampleBuffer) {
|
||||
Task { @MainActor in
|
||||
guard self.track == track else {
|
||||
return
|
||||
}
|
||||
#if os(macOS)
|
||||
(layer as? AVSampleBufferDisplayLayer)?.enqueue(sampleBuffer)
|
||||
self.needsDisplay = true
|
||||
|
||||
Reference in New Issue
Block a user