Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Strict Concurrency and Swift 6.0 beta 1 #1525

Merged
merged 5 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions Examples/iOS/AudioCapture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ protocol AudioCaptureDelegate: AnyObject {
}

final class AudioCapture {
var isRunning: Atomic<Bool> = .init(false)
var isRunning = false
weak var delegate: (any AudioCaptureDelegate)?
private let audioEngine = AVAudioEngine()
}

extension AudioCapture: Running {
extension AudioCapture: Runner {
func startRunning() {
guard !isRunning.value else {
guard !isRunning else {
return
}
let input = audioEngine.inputNode
Expand All @@ -25,17 +25,17 @@ extension AudioCapture: Running {
}
do {
try audioEngine.start()
isRunning.mutate { $0 = true }
isRunning = true
} catch {
logger.error(error)
}
}

func stopRunning() {
guard isRunning.value else {
guard isRunning else {
return
}
audioEngine.stop()
isRunning.mutate { $0 = false }
isRunning = false
}
}
268 changes: 134 additions & 134 deletions Examples/iOS/IngestViewController.swift

Large diffs are not rendered by default.

189 changes: 56 additions & 133 deletions Examples/iOS/NetStreamSwitcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,179 +3,102 @@ import Foundation
import HaishinKit
import SRTHaishinKit

final class NetStreamSwitcher {
private static let maxRetryCount: Int = 5
actor NetStreamSwitcher {
static let maxRetryCount: Int = 5

enum Mode {
case rtmp
case srt

func makeStream(_ swithcer: NetStreamSwitcher) -> IOStream {
switch self {
case .rtmp:
let connection = RTMPConnection()
swithcer.connection = connection
return RTMPStream(connection: connection)
case .srt:
let connection = SRTConnection()
swithcer.connection = connection
return SRTStream(connection: connection)
}
}
}

enum Method {
case ingest
case playback
}

var uri = "" {
didSet {
if uri.contains("srt://") {
mode = .srt
return
}
mode = .rtmp
}
}
private(set) var mode: Mode = .rtmp {
didSet {
stream = mode.makeStream(self)
}
}
private var retryCount = 0
private var preference: Preference?
private(set) var mode: Mode = .rtmp
private var connection: Any?
private var method: Method = .ingest
private(set) var stream: IOStream = .init() {
didSet {
stream.delegate = self
private(set) var stream: (any IOStream)?

func setPreference(_ preference: Preference) async {
self.preference = preference
if preference.uri?.contains("srt://") == true {
let connection = SRTConnection()
self.connection = connection
stream = await SRTStream(connection: connection)
mode = .srt
} else {
let connection = RTMPConnection()
self.connection = connection
stream = RTMPStream(connection: connection)
mode = .rtmp
}
}

func open(_ method: Method) {
func open(_ method: Method) async {
guard let preference else {
return
}
self.method = method
switch mode {
case .rtmp:
guard let connection = connection as? RTMPConnection else {
guard
let connection = connection as? RTMPConnection,
let stream = stream as? RTMPStream else {
return
}
switch method {
case .ingest:
// Performing operations for FMLE compatibility purposes.
(stream as? RTMPStream)?.fcPublishName = Preference.default.streamName
case .playback:
break
do {
let response = try await connection.connect(preference.uri ?? "")
logger.info(response)
switch method {
case .ingest:
let response = try await stream.publish(Preference.default.streamName)
logger.info(response)
case .playback:
let response = try await stream.play(Preference.default.streamName)
logger.info(response)
}
} catch RTMPConnection.Error.requestFailed(let response) {
logger.warn(response)
} catch RTMPStream.Error.requestFailed(let response) {
logger.warn(response)
} catch {
logger.warn(error)
}
connection.addEventListener(.rtmpStatus, selector: #selector(rtmpStatusHandler), observer: self)
connection.addEventListener(.ioError, selector: #selector(rtmpErrorHandler), observer: self)
connection.connect(uri)
case .srt:
guard let connection = connection as? SRTConnection, let stream = stream as? SRTStream else {
return
}
Task {
do {
try await connection.open(URL(string: uri))
switch method {
case .playback:
stream.play()
case .ingest:
stream.publish()
}
} catch {
logger.warn(error)
do {
try await connection.open(URL(string: preference.uri ?? ""))
switch method {
case .playback:
await stream.play()
case .ingest:
await stream.publish()
}
} catch {
logger.warn(error)
}
}
}

func close() {
func close() async {
switch mode {
case .rtmp:
guard let connection = connection as? RTMPConnection else {
return
}
connection.close()
connection.removeEventListener(.rtmpStatus, selector: #selector(rtmpStatusHandler), observer: self)
connection.removeEventListener(.ioError, selector: #selector(rtmpErrorHandler), observer: self)
try? await connection.close()
logger.info("conneciton.close")
case .srt:
guard let connection = connection as? SRTConnection else {
return
}
Task {
await connection.close()
}
await connection.close()
logger.info("conneciton.close")
}
}

@objc
private func rtmpStatusHandler(_ notification: Notification) {
let e = Event.from(notification)
guard let data: ASObject = e.data as? ASObject, let code: String = data["code"] as? String else {
return
}
logger.info(code)
switch code {
case RTMPConnection.Code.connectSuccess.rawValue:
retryCount = 0
switch method {
case .playback:
(stream as? RTMPStream)?.play(Preference.default.streamName!)
case .ingest:
(stream as? RTMPStream)?.publish(Preference.default.streamName!)
}
case RTMPConnection.Code.connectFailed.rawValue, RTMPConnection.Code.connectClosed.rawValue:
guard retryCount <= NetStreamSwitcher.maxRetryCount else {
return
}
Thread.sleep(forTimeInterval: pow(2.0, Double(retryCount)))
(connection as? RTMPConnection)?.connect(uri)
retryCount += 1
default:
break
}
}

@objc
private func rtmpErrorHandler(_ notification: Notification) {
logger.error(notification)
(connection as? RTMPConnection)?.connect(Preference.default.uri!)
}
}

extension NetStreamSwitcher: IOStreamDelegate {
// MARK: NetStreamDelegate
func stream(_ stream: IOStream, track: UInt8, didInput buffer: AVAudioBuffer, when: AVAudioTime) {
}

func stream(_ stream: IOStream, track: UInt8, didInput buffer: CMSampleBuffer) {
}

/// Tells the receiver to video codec error occured.
func stream(_ stream: IOStream, videoErrorOccurred error: IOVideoUnitError) {
}

/// Tells the receiver to audio codec error occured.
func stream(_ stream: IOStream, audioErrorOccurred error: IOAudioUnitError) {
}

/// Tells the receiver that the ready state will change.
func stream(_ stream: IOStream, willChangeReadyState state: IOStream.ReadyState) {
}

/// Tells the receiver that the ready state did change.
func stream(_ stream: IOStream, didChangeReadyState state: IOStream.ReadyState) {
}

#if os(iOS) || os(tvOS)
/// Tells the receiver to session was interrupted.
@available(tvOS 17.0, *)
func stream(_ stream: IOStream, sessionWasInterrupted session: AVCaptureSession, reason: AVCaptureSession.InterruptionReason?) {
}

/// Tells the receiver to session interrupted ended.
@available(tvOS 17.0, *)
func stream(_ stream: IOStream, sessionInterruptionEnded session: AVCaptureSession) {
}
#endif
}
55 changes: 34 additions & 21 deletions Examples/iOS/PlaybackViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,22 @@ import UIKit
final class PlaybackViewController: UIViewController {
@IBOutlet private weak var playbackButton: UIButton!
private let netStreamSwitcher: NetStreamSwitcher = .init()
private var stream: IOStream {
return netStreamSwitcher.stream
}
private var pictureInPictureController: AVPictureInPictureController?

override func viewWillAppear(_ animated: Bool) {
logger.info("viewWillAppear")
super.viewWillAppear(animated)
netStreamSwitcher.uri = Preference.default.uri ?? ""
(view as? (any IOStreamView))?.attachStream(stream)
if #available(iOS 15.0, *), let layer = view.layer as? AVSampleBufferDisplayLayer, pictureInPictureController == nil {
pictureInPictureController = AVPictureInPictureController(contentSource: .init(sampleBufferDisplayLayer: layer, playbackDelegate: self))
}
Task {
await netStreamSwitcher.setPreference(Preference.default)
if let stream = await netStreamSwitcher.stream {
if let view = view as? (any IOStreamObserver) {
await stream.addObserver(view)
}
}
}
}

override func viewWillDisappear(_ animated: Bool) {
Expand All @@ -32,52 +35,62 @@ final class PlaybackViewController: UIViewController {
}

@IBAction func didPlaybackButtonTap(_ button: UIButton) {
if button.isSelected {
UIApplication.shared.isIdleTimerDisabled = false
netStreamSwitcher.close()
button.setTitle("●", for: [])
} else {
UIApplication.shared.isIdleTimerDisabled = true
netStreamSwitcher.open(.playback)
button.setTitle("■", for: [])
Task {
if button.isSelected {
UIApplication.shared.isIdleTimerDisabled = false
await netStreamSwitcher.close()
button.setTitle("●", for: [])
} else {
UIApplication.shared.isIdleTimerDisabled = true
await netStreamSwitcher.open(.playback)
button.setTitle("■", for: [])
}
button.isSelected.toggle()
}
button.isSelected.toggle()
}

@objc
private func didBecomeActive(_ notification: Notification) {
logger.info(notification)
if pictureInPictureController?.isPictureInPictureActive == false {
(stream as? RTMPStream)?.receiveVideo = true
Task {
if let stream = await netStreamSwitcher.stream as? RTMPStream {
_ = try? await stream.receiveVideo(true)
}
}
}
}

@objc
private func didEnterBackground(_ notification: Notification) {
logger.info(notification)
if pictureInPictureController?.isPictureInPictureActive == false {
(stream as? RTMPStream)?.receiveVideo = false
Task {
if let stream = await netStreamSwitcher.stream as? RTMPStream {
_ = try? await stream.receiveVideo(false)
}
}
}
}
}

extension PlaybackViewController: AVPictureInPictureSampleBufferPlaybackDelegate {
// MARK: AVPictureInPictureControllerDelegate
func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, setPlaying playing: Bool) {
nonisolated func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, setPlaying playing: Bool) {
}

func pictureInPictureControllerTimeRangeForPlayback(_ pictureInPictureController: AVPictureInPictureController) -> CMTimeRange {
nonisolated func pictureInPictureControllerTimeRangeForPlayback(_ pictureInPictureController: AVPictureInPictureController) -> CMTimeRange {
return CMTimeRange(start: .zero, duration: .positiveInfinity)
}

func pictureInPictureControllerIsPlaybackPaused(_ pictureInPictureController: AVPictureInPictureController) -> Bool {
nonisolated func pictureInPictureControllerIsPlaybackPaused(_ pictureInPictureController: AVPictureInPictureController) -> Bool {
return false
}

func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, didTransitionToRenderSize newRenderSize: CMVideoDimensions) {
nonisolated func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, didTransitionToRenderSize newRenderSize: CMVideoDimensions) {
}

func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, skipByInterval skipInterval: CMTime, completion completionHandler: @escaping () -> Void) {
nonisolated func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, skipByInterval skipInterval: CMTime, completion completionHandler: @escaping () -> Void) {
completionHandler()
}
}
Loading