HaishinKit.swift/Sources/RTMP/RTMPStream.swift

629 lines
22 KiB
Swift

import AVFoundation
/**
flash.net.NetStream for Swift
*/
open class RTMPStream: NetStream {
/**
NetStatusEvent#info.code for NetStream
*/
public enum Code: String {
case bufferEmpty = "NetStream.Buffer.Empty"
case bufferFlush = "NetStream.Buffer.Flush"
case bufferFull = "NetStream.Buffer.Full"
case connectClosed = "NetStream.Connect.Closed"
case connectFailed = "NetStream.Connect.Failed"
case connectRejected = "NetStream.Connect.Rejected"
case connectSuccess = "NetStream.Connect.Success"
case drmUpdateNeeded = "NetStream.DRM.UpdateNeeded"
case failed = "NetStream.Failed"
case multicastStreamReset = "NetStream.MulticastStream.Reset"
case pauseNotify = "NetStream.Pause.Notify"
case playFailed = "NetStream.Play.Failed"
case playFileStructureInvalid = "NetStream.Play.FileStructureInvalid"
case playInsufficientBW = "NetStream.Play.InsufficientBW"
case playNoSupportedTrackFound = "NetStream.Play.NoSupportedTrackFound"
case playReset = "NetStream.Play.Reset"
case playStart = "NetStream.Play.Start"
case playStop = "NetStream.Play.Stop"
case playStreamNotFound = "NetStream.Play.StreamNotFound"
case playTransition = "NetStream.Play.Transition"
case playUnpublishNotify = "NetStream.Play.UnpublishNotify"
case publishBadName = "NetStream.Publish.BadName"
case publishIdle = "NetStream.Publish.Idle"
case publishStart = "NetStream.Publish.Start"
case recordAlreadyExists = "NetStream.Record.AlreadyExists"
case recordFailed = "NetStream.Record.Failed"
case recordNoAccess = "NetStream.Record.NoAccess"
case recordStart = "NetStream.Record.Start"
case recordStop = "NetStream.Record.Stop"
case recordDiskQuotaExceeded = "NetStream.Record.DiskQuotaExceeded"
case secondScreenStart = "NetStream.SecondScreen.Start"
case secondScreenStop = "NetStream.SecondScreen.Stop"
case seekFailed = "NetStream.Seek.Failed"
case seekInvalidTime = "NetStream.Seek.InvalidTime"
case seekNotify = "NetStream.Seek.Notify"
case stepNotify = "NetStream.Step.Notify"
case unpauseNotify = "NetStream.Unpause.Notify"
case unpublishSuccess = "NetStream.Unpublish.Success"
case videoDimensionChange = "NetStream.Video.DimensionChange"
public var level: String {
switch self {
case .bufferEmpty:
return "status"
case .bufferFlush:
return "status"
case .bufferFull:
return "status"
case .connectClosed:
return "status"
case .connectFailed:
return "error"
case .connectRejected:
return "error"
case .connectSuccess:
return "status"
case .drmUpdateNeeded:
return "status"
case .failed:
return "error"
case .multicastStreamReset:
return "status"
case .pauseNotify:
return "status"
case .playFailed:
return "error"
case .playFileStructureInvalid:
return "error"
case .playInsufficientBW:
return "warning"
case .playNoSupportedTrackFound:
return "status"
case .playReset:
return "status"
case .playStart:
return "status"
case .playStop:
return "status"
case .playStreamNotFound:
return "status"
case .playTransition:
return "status"
case .playUnpublishNotify:
return "status"
case .publishBadName:
return "error"
case .publishIdle:
return "status"
case .publishStart:
return "status"
case .recordAlreadyExists:
return "status"
case .recordFailed:
return "error"
case .recordNoAccess:
return "error"
case .recordStart:
return "status"
case .recordStop:
return "status"
case .recordDiskQuotaExceeded:
return "error"
case .secondScreenStart:
return "status"
case .secondScreenStop:
return "status"
case .seekFailed:
return "error"
case .seekInvalidTime:
return "error"
case .seekNotify:
return "status"
case .stepNotify:
return "status"
case .unpauseNotify:
return "status"
case .unpublishSuccess:
return "status"
case .videoDimensionChange:
return "status"
}
}
func data(_ description: String) -> ASObject {
[
"code": rawValue,
"level": level,
"description": description
]
}
}
/**
flash.net.NetStreamPlayTransitions for Swift
*/
public enum PlayTransition: String {
case append
case appendAndWait
case reset
case resume
case stop
case swap
case `switch`
}
public struct PlayOption: CustomDebugStringConvertible {
public var len: Double = 0
public var offset: Double = 0
public var oldStreamName: String = ""
public var start: Double = 0
public var streamName: String = ""
public var transition: PlayTransition = .switch
public var debugDescription: String {
Mirror(reflecting: self).debugDescription
}
}
public enum HowToPublish: String {
case record
case append
case appendWithGap
case live
case localRecord
}
enum ReadyState: UInt8 {
case initialized
case open
case play
case playing
case publish
case publishing
}
static let defaultID: UInt32 = 0
public static let defaultAudioBitrate: UInt32 = AudioCodec.defaultBitrate
public static let defaultVideoBitrate: UInt32 = H264Encoder.defaultBitrate
open weak var delegate: RTMPStreamDelegate?
open internal(set) var info = RTMPStreamInfo()
open private(set) var objectEncoding: RTMPObjectEncoding = RTMPConnection.defaultObjectEncoding
/// The number of frames per second being displayed.
@objc open private(set) dynamic var currentFPS: UInt16 = 0
open var soundTransform: SoundTransform {
get { mixer.audioIO.soundTransform }
set { mixer.audioIO.soundTransform = newValue }
}
/// Incoming audio plays on the stream or not.
open var receiveAudio = true {
didSet {
lockQueue.async {
guard self.readyState == .playing else {
return
}
self.rtmpConnection.socket.doOutput(chunk: RTMPChunk(message: RTMPCommandMessage(
streamId: self.id,
transactionId: 0,
objectEncoding: self.objectEncoding,
commandName: "receiveAudio",
commandObject: nil,
arguments: [self.receiveAudio]
)), locked: nil)
}
}
}
/// Incoming video plays on the stream or not.
open var receiveVideo = true {
didSet {
lockQueue.async {
guard self.readyState == .playing else {
return
}
self.rtmpConnection.socket.doOutput(chunk: RTMPChunk(message: RTMPCommandMessage(
streamId: self.id,
transactionId: 0,
objectEncoding: self.objectEncoding,
commandName: "receiveVideo",
commandObject: nil,
arguments: [self.receiveVideo]
)), locked: nil)
}
}
}
/// Pauses playback or publish of a video stream or not.
open var paused = false {
didSet {
lockQueue.async {
switch self.readyState {
case .publish, .publishing:
self.mixer.audioIO.encoder.muted = self.paused
self.mixer.videoIO.encoder.muted = self.paused
default:
break
}
}
}
}
var id: UInt32 = RTMPStream.defaultID
var readyState: ReadyState = .initialized {
didSet {
guard oldValue != readyState else {
return
}
didChangeReadyState(readyState, oldValue: oldValue)
}
}
var audioTimestamp: Double = 0.0
var videoTimestamp: Double = 0.0
private let muxer = RTMPMuxer()
private var messages: [RTMPCommandMessage] = []
private var frameCount: UInt16 = 0
private var dispatcher: IEventDispatcher!
private var audioWasSent = false
private var videoWasSent = false
private var howToPublish: RTMPStream.HowToPublish = .live
private var rtmpConnection: RTMPConnection
public init(connection: RTMPConnection) {
self.rtmpConnection = connection
super.init()
dispatcher = EventDispatcher(target: self)
addEventListener(.rtmpStatus, selector: #selector(on(status:)), observer: self)
rtmpConnection.addEventListener(.rtmpStatus, selector: #selector(on(status:)), observer: self)
if rtmpConnection.connected {
rtmpConnection.createStream(self)
}
}
deinit {
mixer.stopRunning()
removeEventListener(.rtmpStatus, selector: #selector(on(status:)), observer: self)
rtmpConnection.removeEventListener(.rtmpStatus, selector: #selector(on(status:)), observer: self)
}
/// Plays a live stream from RTMPServer.
open func play(_ arguments: Any?...) {
lockQueue.async {
guard let name: String = arguments.first as? String else {
switch self.readyState {
case .play, .playing:
self.info.resourceName = nil
self.close(withLockQueue: false)
default:
break
}
return
}
self.info.resourceName = name
let message = RTMPCommandMessage(
streamId: self.id,
transactionId: 0,
objectEncoding: self.objectEncoding,
commandName: "play",
commandObject: nil,
arguments: arguments
)
switch self.readyState {
case .initialized:
self.messages.append(message)
default:
self.readyState = .play
self.rtmpConnection.socket.doOutput(chunk: RTMPChunk(message: message), locked: nil)
}
}
}
/// Seeks the keyframe.
open func seek(_ offset: Double) {
lockQueue.async {
guard self.readyState == .playing else {
return
}
self.rtmpConnection.socket.doOutput(chunk: RTMPChunk(message: RTMPCommandMessage(
streamId: self.id,
transactionId: 0,
objectEncoding: self.objectEncoding,
commandName: "seek",
commandObject: nil,
arguments: [offset]
)), locked: nil)
}
}
/// Sends streaming audio, vidoe and data message from client.
open func publish(_ name: String?, type: RTMPStream.HowToPublish = .live) {
lockQueue.async {
guard let name: String = name else {
switch self.readyState {
case .publish, .publishing:
self.close(withLockQueue: false)
default:
break
}
return
}
if self.info.resourceName == name && self.readyState == .publishing {
switch type {
case .localRecord:
self.mixer.recorder.fileName = FilenameUtil.fileName(resourceName: self.info.resourceName)
self.mixer.recorder.startRunning()
default:
self.mixer.recorder.stopRunning()
}
self.howToPublish = type
return
}
self.info.resourceName = name
self.howToPublish = type
let message = RTMPCommandMessage(
streamId: self.id,
transactionId: 0,
objectEncoding: self.objectEncoding,
commandName: "publish",
commandObject: nil,
arguments: [name, type == .localRecord ? RTMPStream.HowToPublish.live.rawValue : type.rawValue]
)
switch self.readyState {
case .initialized:
self.messages.append(message)
default:
self.readyState = .publish
self.rtmpConnection.socket.doOutput(chunk: RTMPChunk(message: message), locked: nil)
}
}
}
/// Stops playing or publishing and makes available other uses.
open func close() {
close(withLockQueue: true)
}
/// Sends a message on a published stream to all subscribing clients.
open func send(handlerName: String, arguments: Any?...) {
lockQueue.async {
guard self.readyState == .publishing else {
return
}
let length: Int = self.rtmpConnection.socket.doOutput(chunk: RTMPChunk(message: RTMPDataMessage(
streamId: self.id,
objectEncoding: self.objectEncoding,
handlerName: handlerName,
arguments: arguments
)), locked: nil)
self.info.byteCount.mutate { $0 += Int64(length) }
}
}
open func createMetaData() -> ASObject {
metadata.removeAll()
#if os(iOS) || os(macOS)
if let _: AVCaptureInput = mixer.videoIO.input {
metadata["width"] = mixer.videoIO.encoder.width
metadata["height"] = mixer.videoIO.encoder.height
metadata["framerate"] = mixer.videoIO.fps
metadata["videocodecid"] = FLVVideoCodec.avc.rawValue
metadata["videodatarate"] = mixer.videoIO.encoder.bitrate / 1000
}
if let _: AVCaptureInput = mixer.audioIO.input {
metadata["audiocodecid"] = FLVAudioCodec.aac.rawValue
metadata["audiodatarate"] = mixer.audioIO.encoder.bitrate / 1000
}
#endif
return metadata
}
func close(withLockQueue: Bool) {
if withLockQueue {
lockQueue.sync {
self.close(withLockQueue: false)
}
return
}
guard ReadyState.open.rawValue < readyState.rawValue else {
return
}
readyState = .open
rtmpConnection.socket?.doOutput(chunk: RTMPChunk(
type: .zero,
streamId: RTMPChunk.StreamID.command.rawValue,
message: RTMPCommandMessage(
streamId: 0,
transactionId: 0,
objectEncoding: self.objectEncoding,
commandName: "closeStream",
commandObject: nil,
arguments: [self.id]
)), locked: nil)
}
func on(timer: Timer) {
currentFPS = frameCount
frameCount = 0
info.on(timer: timer)
}
private func didChangeReadyState(_ readyState: ReadyState, oldValue: ReadyState) {
switch oldValue {
case .playing:
mixer.stopDecoding()
case .publishing:
FCUnpublish()
#if os(iOS)
mixer.videoIO.screen?.stopRunning()
#endif
mixer.audioIO.encoder.delegate = nil
mixer.videoIO.encoder.delegate = nil
mixer.audioIO.encoder.stopRunning()
mixer.videoIO.encoder.stopRunning()
mixer.recorder.stopRunning()
default:
break
}
switch readyState {
case .open:
currentFPS = 0
frameCount = 0
info.clear()
delegate?.rtmpStreamDidClear(self)
for message in messages {
rtmpConnection.currentTransactionId += 1
message.streamId = id
message.transactionId = rtmpConnection.currentTransactionId
switch message.commandName {
case "play":
self.readyState = .play
case "publish":
self.readyState = .publish
default:
break
}
rtmpConnection.socket.doOutput(chunk: RTMPChunk(message: message), locked: nil)
}
messages.removeAll()
case .playing:
mixer.delegate = self
mixer.startDecoding(rtmpConnection.audioEngine)
case .publish:
muxer.dispose()
muxer.delegate = self
#if os(iOS)
mixer.videoIO.screen?.startRunning()
#endif
mixer.audioIO.encoder.delegate = muxer
mixer.videoIO.encoder.delegate = muxer
// sampler?.delegate = muxer
mixer.startRunning()
videoWasSent = false
audioWasSent = false
FCPublish()
case .publishing:
send(handlerName: "@setDataFrame", arguments: "onMetaData", createMetaData())
mixer.audioIO.encoder.startRunning()
mixer.videoIO.encoder.startRunning()
if howToPublish == .localRecord {
mixer.recorder.fileName = FilenameUtil.fileName(resourceName: info.resourceName)
mixer.recorder.startRunning()
}
default:
break
}
}
@objc
private func on(status: Notification) {
let e = Event.from(status)
guard let data: ASObject = e.data as? ASObject, let code: String = data["code"] as? String else {
return
}
switch code {
case RTMPConnection.Code.connectSuccess.rawValue:
readyState = .initialized
rtmpConnection.createStream(self)
case RTMPStream.Code.playStart.rawValue:
readyState = .playing
case RTMPStream.Code.publishStart.rawValue:
readyState = .publishing
default:
break
}
}
}
extension RTMPStream {
func FCPublish() {
guard let name: String = info.resourceName, rtmpConnection.flashVer.contains("FMLE/") else {
return
}
rtmpConnection.call("FCPublish", responder: nil, arguments: name)
}
func FCUnpublish() {
guard let name: String = info.resourceName, rtmpConnection.flashVer.contains("FMLE/") else {
return
}
rtmpConnection.call("FCUnpublish", responder: nil, arguments: name)
}
}
extension RTMPStream: IEventDispatcher {
// MARK: IEventDispatcher
public func addEventListener(_ type: Event.Name, selector: Selector, observer: AnyObject? = nil, useCapture: Bool = false) {
dispatcher.addEventListener(type, selector: selector, observer: observer, useCapture: useCapture)
}
public func removeEventListener(_ type: Event.Name, selector: Selector, observer: AnyObject? = nil, useCapture: Bool = false) {
dispatcher.removeEventListener(type, selector: selector, observer: observer, useCapture: useCapture)
}
public func dispatch(event: Event) {
dispatcher.dispatch(event: event)
}
public func dispatch(_ type: Event.Name, bubbles: Bool, data: Any?) {
dispatcher.dispatch(type, bubbles: bubbles, data: data)
}
}
extension RTMPStream: RTMPMuxerDelegate {
// MARK: RTMPMuxerDelegate
func metadata(_ metadata: ASObject) {
send(handlerName: "@setDataFrame", arguments: "onMetaData", metadata)
}
func sampleOutput(audio buffer: Data, withTimestamp: Double, muxer: RTMPMuxer) {
guard readyState == .publishing else {
return
}
let type: FLVTagType = .audio
let length: Int = rtmpConnection.socket.doOutput(chunk: RTMPChunk(
type: audioWasSent ? .one : .zero,
streamId: type.streamId,
message: RTMPAudioMessage(streamId: id, timestamp: UInt32(audioTimestamp), payload: buffer)
), locked: nil)
audioWasSent = true
info.byteCount.mutate { $0 += Int64(length) }
audioTimestamp = withTimestamp + (audioTimestamp - floor(audioTimestamp))
}
func sampleOutput(video buffer: Data, withTimestamp: Double, muxer: RTMPMuxer) {
guard readyState == .publishing else {
return
}
let type: FLVTagType = .video
OSAtomicOr32Barrier(1, &mixer.videoIO.encoder.locked)
let length: Int = rtmpConnection.socket.doOutput(chunk: RTMPChunk(
type: videoWasSent ? .one : .zero,
streamId: type.streamId,
message: RTMPVideoMessage(streamId: id, timestamp: UInt32(videoTimestamp), payload: buffer)
), locked: &mixer.videoIO.encoder.locked)
if !videoWasSent {
logger.debug("first video frame was sent")
}
videoWasSent = true
info.byteCount.mutate { $0 += Int64(length) }
videoTimestamp = withTimestamp + (videoTimestamp - floor(videoTimestamp))
frameCount += 1
}
}
extension RTMPStream: AVMixerDelegate {
// MARK: AVMixerDelegate
func didOutputVideo(_ buffer: CMSampleBuffer) {
frameCount += 1
delegate?.rtmpStream(self, didOutput: buffer)
}
func didOutputAudio(_ buffer: AVAudioPCMBuffer, presentationTimeStamp: CMTime) {
delegate?.rtmpStream(self, didOutput: buffer, presentationTimeStamp: presentationTimeStamp)
}
}