Skip to content

Commit

Permalink
IOStreamObserver and confirmed IOStreamRecorder.
Browse files Browse the repository at this point in the history
  • Loading branch information
shogo4405 committed Apr 7, 2024
1 parent 9ee78ef commit bb7e5b5
Show file tree
Hide file tree
Showing 8 changed files with 73 additions and 100 deletions.
4 changes: 4 additions & 0 deletions HaishinKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@
BCD63ADD26FDF34C0084842D /* HaishinKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 2945CBBD1B4BE66000104112 /* HaishinKit.framework */; platformFilter = ios; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
BCD63AE126FDF3500084842D /* Logboard.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = BC34DFD125EBB12C005F975A /* Logboard.xcframework */; };
BCD63AE226FDF3500084842D /* Logboard.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BC34DFD125EBB12C005F975A /* Logboard.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
BCD8702B2BC266CD009E495B /* IOStreamObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD8702A2BC266CD009E495B /* IOStreamObserver.swift */; };
BCD91C0D2A700FF50033F9E1 /* IOAudioRingBufferTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD91C0C2A700FF50033F9E1 /* IOAudioRingBufferTests.swift */; };
BCE0E33D2AD369550082C16F /* NetStreamSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE0E33B2AD369410082C16F /* NetStreamSwitcher.swift */; };
BCFB355524FA27EA00DC5108 /* PlaybackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCFB355324FA275600DC5108 /* PlaybackViewController.swift */; };
Expand Down Expand Up @@ -676,6 +677,7 @@
BCD63AB626FDF1250084842D /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
BCD63AB826FDF12A0084842D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
BCD63ABB26FDF12A0084842D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
BCD8702A2BC266CD009E495B /* IOStreamObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOStreamObserver.swift; sourceTree = "<group>"; };
BCD91C0C2A700FF50033F9E1 /* IOAudioRingBufferTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOAudioRingBufferTests.swift; sourceTree = "<group>"; };
BCE0E33B2AD369410082C16F /* NetStreamSwitcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetStreamSwitcher.swift; sourceTree = "<group>"; };
BCFB355324FA275600DC5108 /* PlaybackViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1092,6 +1094,7 @@
BC4078C32AD5CC7E00BBB4FA /* IOMuxer.swift */,
29AF3FCE1D7C744C00E41212 /* IOStream.swift */,
BC6692F22AC2F717009EC058 /* IOStreamBitRateStrategyConvertible.swift */,
BCD8702A2BC266CD009E495B /* IOStreamObserver.swift */,
2976A47D1D48C5C700B53EF2 /* IOStreamRecorder.swift */,
BC9CFA9223BDE8B700917EEF /* IOStreamView.swift */,
BCC4F4142AD6FC1100954EF5 /* IOTellyUnit.swift */,
Expand Down Expand Up @@ -1835,6 +1838,7 @@
BC1DC4A429F4F74F00E928ED /* AVCaptureSession+Extension.swift in Sources */,
29EA87D81E79A0090043A5F8 /* URL+Extension.swift in Sources */,
BC9F9C7826F8C16600B01ED0 /* Choreographer.swift in Sources */,
BCD8702B2BC266CD009E495B /* IOStreamObserver.swift in Sources */,
BC93792F2ADD76BE001097DB /* AVAudioCompressedBuffer+Extension.swift in Sources */,
29B876BC1CD70B3900FC07DA /* ByteArray.swift in Sources */,
29B876831CD70AE800FC07DA /* AudioSpecificConfig.swift in Sources */,
Expand Down
18 changes: 0 additions & 18 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,6 @@
"revision" : "6a7cbf54553936103084ed72cfb6d6f836758229",
"version" : "2.4.1"
}
},
{
"identity" : "swift-docc-plugin",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-docc-plugin",
"state" : {
"revision" : "26ac5758409154cc448d7ab82389c520fa8a8247",
"version" : "1.3.0"
}
},
{
"identity" : "swift-docc-symbolkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-docc-symbolkit",
"state" : {
"revision" : "b45d1f2ed151d057b54504d653e0da5552844e34",
"version" : "1.0.0"
}
}
],
"version" : 2
Expand Down
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ stream.attachCamera(front, channel: 0) { videoUnit, error in

### 🔊 [AudioCodecSettings](https://shogo4405.github.io/HaishinKit.swift/Structs/AudioCodecSettings.html)
When you specify the sampling rate, it will perform resampling. Additionally, in the case of multiple channels, downsampling can be applied.
```
```swift
stream.audioSettings = AudioCodecSettings(
bitRate: Int = 64 * 1000,
sampleRate: Float64 = 0,
Expand All @@ -293,7 +293,7 @@ stream.audioSettings = AudioCodecSettings(
```

### 🎥 [VideoCodecSettings](https://shogo4405.github.io/HaishinKit.swift/Structs/VideoCodecSettings.html)
```
```swift
stream.videoSettings = VideoCodecSettings(
videoSize: .init(width: 854, height: 480),
profileLevel: kVTProfileLevel_H264_Baseline_3_1 as String,
Expand All @@ -307,9 +307,12 @@ stream.videoSettings = VideoCodecSettings(
```

### ⏺️ Recording
```
```swift
// Specifies the recording settings. 0" means the same of input.
stream.startRecording(self, settings: [
var recorder = IOStreamRecorder()
stream.addObserver(recorder)

recorder.outputSettings = [
AVMediaType.audio: [
AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
AVSampleRateKey: 0,
Expand All @@ -328,7 +331,10 @@ stream.startRecording(self, settings: [
]
*/
]
])
]

recorder.startRunning()

```

## 📜 License
Expand Down
3 changes: 0 additions & 3 deletions Sources/IO/IOMixer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ final class IOMixer {

private(set) var isRunning: Atomic<Bool> = .init(false)

private(set) lazy var recorder = IOStreamRecorder()

private(set) lazy var audioIO = {
var audioIO = IOAudioUnit()
audioIO.mixer = self
Expand Down Expand Up @@ -187,7 +185,6 @@ extension IOMixer: IOAudioUnitDelegate {

func audioUnit(_ audioUnit: IOAudioUnit, didOutput audioBuffer: AVAudioPCMBuffer, when: AVAudioTime) {
delegate?.mixer(self, didOutput: audioBuffer, when: when)
recorder.append(audioBuffer, when: when)
}
}

Expand Down
54 changes: 27 additions & 27 deletions Sources/IO/IOStream.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ import UIKit

/// The interface an IOStream uses to inform its delegate.
public protocol IOStreamDelegate: AnyObject {
/// Tells the receiver to an audio packet incoming.
func stream(_ stream: IOStream, didOutput audio: AVAudioBuffer, when: AVAudioTime)
/// Tells the receiver to a video incoming.
func stream(_ stream: IOStream, didOutput video: CMSampleBuffer)
/// Tells the receiver to video error occured.
func stream(_ stream: IOStream, videoErrorOccurred error: IOVideoUnitError)
/// Tells the receiver to audio error occured.
Expand Down Expand Up @@ -45,19 +41,19 @@ open class IOStream: NSObject {
return lhs.rawValue == rhs.rawValue
}

/// NetStream has been created.
/// IOStream has been created.
case initialized
/// NetStream waiting for new method.
/// IOStream waiting for new method.
case open
/// NetStream play() has been called.
/// IOStream play() has been called.
case play
/// NetStream play and server was accepted as playing
/// IOStream play and server was accepted as playing
case playing
/// NetStream publish() has been called
/// IOStream publish() has been called
case publish
/// NetStream publish and server accpted as publising.
/// IOStream publish and server accpted as publising.
case publishing(muxer: any IOMuxer)
/// NetStream close() has been called.
/// IOStream close() has been called.
case closed

var rawValue: UInt8 {
Expand Down Expand Up @@ -139,7 +135,7 @@ open class IOStream: NSObject {
#if os(iOS) || os(tvOS)
/// Specifies the AVCaptureMultiCamSession enabled.
/// Warning: If there is a possibility of using multiple cameras, please set it to true initially.
@available(tvOS 17.0, iOS 13.0, *)
@available(tvOS 17.0, *)
public var isMultiCamSessionEnabled: Bool {
get {
return mixer.session.isMultiCamSessionEnabled
Expand Down Expand Up @@ -236,11 +232,6 @@ open class IOStream: NSObject {
return mixer.audioIO.inputFormat
}

/// The isRecording value that indicates whether the recorder is recording.
public var isRecording: Bool {
return mixer.recorder.isRunning.value
}

/// Specifies the controls sound.
public var soundTransform: SoundTransform {
get {
Expand Down Expand Up @@ -305,6 +296,8 @@ open class IOStream: NSObject {
return telly
}()

private var observers: [any IOStreamObserver] = []

/// Creates a NetStream object.
override public init() {
super.init()
Expand All @@ -314,6 +307,10 @@ open class IOStream: NSObject {
#endif
}

deinit {
observers.removeAll()
}

/// Attaches the camera object.
@available(tvOS 17.0, *)
public func attachCamera(_ device: AVCaptureDevice?, channel: UInt8 = 0, configuration: IOVideoCaptureConfigurationBlock? = nil) {
Expand Down Expand Up @@ -388,16 +385,19 @@ open class IOStream: NSObject {
}
}

/// Starts recording.
public func startRecording(_ delegate: any IOStreamRecorderDelegate, settings: [AVMediaType: [String: Any]] = IOStreamRecorder.defaultOutputSettings) {
mixer.recorder.delegate = delegate
mixer.recorder.outputSettings = settings
mixer.recorder.startRunning()
/// Adds an observer.
public func addObserver(_ observer: any IOStreamObserver) {
guard observers.firstIndex(where: { $0 === observer }) == nil else {

Check warning on line 390 in Sources/IO/IOStream.swift

View workflow job for this annotation

GitHub Actions / build

Contains over First not Nil Violation: Prefer `contains` over `firstIndex(where:) != nil` (contains_over_first_not_nil)
return
}
observers.append(observer)
}

/// Stop recording.
public func stopRecording() {
mixer.recorder.stopRunning()
/// Removes an observer.
public func removeObserver(_ observer: any IOStreamObserver) {
if let index = observers.firstIndex(where: { $0 === observer }) {
observers.remove(at: index)
}
}

/// A handler that receives stream readyState will update.
Expand Down Expand Up @@ -450,11 +450,11 @@ open class IOStream: NSObject {
extension IOStream: IOMixerDelegate {
// MARK: IOMixerDelegate
func mixer(_ mixer: IOMixer, didOutput video: CMSampleBuffer) {
delegate?.stream(self, didOutput: video)
observers.forEach { $0.stream(self, didOutput: video) }
}

func mixer(_ mixer: IOMixer, didOutput audio: AVAudioPCMBuffer, when: AVAudioTime) {
delegate?.stream(self, didOutput: audio, when: when)
observers.forEach { $0.stream(self, didOutput: audio, when: when) }
}

func mixer(_ mixer: IOMixer, audioErrorOccurred error: IOAudioUnitError) {
Expand Down
10 changes: 10 additions & 0 deletions Sources/IO/IOStreamObserver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import AVFoundation
import CoreMedia
import Foundation

public protocol IOStreamObserver: AnyObject {
/// Tells the receiver to an audio packet incoming.
func stream(_ stream: IOStream, didOutput audio: AVAudioBuffer, when: AVAudioTime)
/// Tells the receiver to a video incoming.
func stream(_ stream: IOStream, didOutput video: CMSampleBuffer)
}
64 changes: 21 additions & 43 deletions Sources/IO/IOStreamRecorder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ public protocol IOStreamRecorderDelegate: AnyObject {
}

// MARK: -
/// The IOStreamRecorderDelegate class represents video and audio recorder.
/// The IOStreamRecorder class represents video and audio recorder.
public final class IOStreamRecorder {
/// The IOStreamRecorderDelegate error domain codes.
/// The IOStreamRecorder error domain codes.
public enum Error: Swift.Error {
/// Failed to create the AVAssetWriter.
case failedToCreateAssetWriter(error: any Swift.Error)
Expand All @@ -26,8 +26,8 @@ public final class IOStreamRecorder {
case failedToFinishWriting(error: (any Swift.Error)?)
}

/// The default output settings for an IORecorder.
public static let defaultOutputSettings: [AVMediaType: [String: Any]] = [
/// The default output settings for an IOStreamRecorder.
public static let defaultSettings: [AVMediaType: [String: Any]] = [
.audio: [
AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
AVSampleRateKey: 0,
Expand All @@ -43,7 +43,7 @@ public final class IOStreamRecorder {
/// Specifies the delegate.
public weak var delegate: (any IOStreamRecorderDelegate)?
/// Specifies the recorder settings.
public var outputSettings: [AVMediaType: [String: Any]] = IOStreamRecorder.defaultOutputSettings
public var settings: [AVMediaType: [String: Any]] = IOStreamRecorder.defaultSettings
/// The running indicies whether recording or not.
public private(set) var isRunning: Atomic<Bool> = .init(false)

Expand All @@ -52,7 +52,7 @@ public final class IOStreamRecorder {
guard let writer = writer else {
return false
}
return outputSettings.count == writer.inputs.count
return settings.count == writer.inputs.count
}
private var writer: AVAssetWriter?
private var writerInputs: [AVMediaType: AVAssetWriterInput] = [:]
Expand All @@ -72,7 +72,7 @@ public final class IOStreamRecorder {
#endif

/// Append a sample buffer for recording.
public func append(_ sampleBuffer: CMSampleBuffer) {
func append(_ sampleBuffer: CMSampleBuffer) {
guard isRunning.value else {
return
}
Expand Down Expand Up @@ -114,41 +114,6 @@ public final class IOStreamRecorder {
}
}

/// Append a pixel buffer for recording.
public func append(_ pixelBuffer: CVPixelBuffer, withPresentationTime: CMTime) {
guard isRunning.value else {
return
}
lockQueue.async {
if self.dimensions.width != pixelBuffer.width || self.dimensions.height != pixelBuffer.height {
self.dimensions = .init(width: Int32(pixelBuffer.width), height: Int32(pixelBuffer.height))
}
guard
let writer = self.writer,
let input = self.makeWriterInput(.video, sourceFormatHint: nil),
let adaptor = self.makePixelBufferAdaptor(input),
self.isReadyForStartWriting && self.videoPresentationTime.seconds < withPresentationTime.seconds else {
return
}

switch writer.status {
case .unknown:
writer.startWriting()
writer.startSession(atSourceTime: withPresentationTime)
default:
break
}

if input.isReadyForMoreMediaData {
if adaptor.append(pixelBuffer, withPresentationTime: withPresentationTime) {
self.videoPresentationTime = withPresentationTime
} else {
self.delegate?.recorder(self, errorOccured: .failedToAppend(error: writer.error))
}
}
}
}

func append(_ audioPCMBuffer: AVAudioPCMBuffer, when: AVAudioTime) {
guard isRunning.value else {
return
Expand Down Expand Up @@ -184,7 +149,7 @@ public final class IOStreamRecorder {
}

var outputSettings: [String: Any] = [:]
if let defaultOutputSettings: [String: Any] = self.outputSettings[mediaType] {
if let defaultOutputSettings: [String: Any] = self.settings[mediaType] {
switch mediaType {
case .audio:
guard
Expand Down Expand Up @@ -244,6 +209,19 @@ public final class IOStreamRecorder {
}
}

extension IOStreamRecorder: IOStreamObserver {
// MARK: IOStreamObserver
public func stream(_ stream: IOStream, didOutput video: CMSampleBuffer) {
append(video)
}

public func stream(_ stream: IOStream, didOutput audio: AVAudioBuffer, when: AVAudioTime) {
if let audio = audio as? AVAudioPCMBuffer {
append(audio, when: when)
}
}
}

extension IOStreamRecorder: Running {
// MARK: Running
public func startRunning() {
Expand Down
4 changes: 0 additions & 4 deletions Sources/IO/IOVideoUnit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -278,9 +278,5 @@ extension IOVideoUnit: IOVideoMixerDelegate {
presentationTimeStamp: presentationTimeStamp,
duration: .invalid
)
mixer?.recorder.append(
imageBuffer,
withPresentationTime: presentationTimeStamp
)
}
}

0 comments on commit bb7e5b5

Please sign in to comment.