shogo4405 / HaishinKit.swift

Camera and Microphone streaming library via RTMP and SRT for iOS, macOS, tvOS and visionOS.
https://docs.haishinkit.com/swift/latest
BSD 3-Clause "New" or "Revised" License
2.78k stars 618 forks source link

RTMP再生するとクラッシュしてしまう #618

Closed omochi closed 4 years ago

omochi commented 4 years ago

Describe the bug

iOSアプリにおいて、 下記コードにてRTMP再生をしたところクラッシュしてしまいました。

        let conn = RTMPConnection()
        conn.connect("rtmp://<host>/live")
        let stream = RTMPStream(connection: conn)
        stream.play("<name>")
        hkView.attachStream(stream)

<host><name>の部分は実際には有効な値が入っています。 また、このRTMPStreamはVLCプレイヤーでは再生できる事を確認しています。

To Reproduce

すみません、機密のためこのURLを共有できません。

Expected behavior

クラッシュしないで再生できる

Screenshots

なし

Smartphone (please complete the following information):

Additional context

クラッシュは、AudioConverter.swiftconvertメソッドの内部で呼ばれた、 inDestinationFormatプロパティのgetアクセサの内部で生じます。

        get {
            if _inDestinationFormat == nil {
                _inDestinationFormat = destination.audioStreamBasicDescription(inSourceFormat, sampleRate: sampleRate, channels: channels)
                CMAudioFormatDescriptionCreate(
                    allocator: kCFAllocatorDefault,
                    asbd: &_inDestinationFormat!,
                    layoutSize: 0,
                    layout: nil,
                    magicCookieSize: 0,
                    magicCookie: nil,
                    extensions: nil,
                    formatDescriptionOut: &formatDescription
                )
            }

上記のコードにおいて、destination.audioStreamBasicDescriptionからnilが返り、CMAudioFormatDescriptionCreate_inDestinationFormatを渡すところのforce unwrapに失敗するようでした。

挙動を調べたところ、 audioStreamBasicDescriptionnilを返す理由は、inSourceFormatnilを返すためでした。

また、関係あるかどうかわかりませんが、RTMPMessage.swiftRTMPAudioMessageクラスのexecuteメソッドを監視したところ、下記のコード

        switch FLVAACPacketType(rawValue: payload[1]) {
        case .seq?:
            let config = AudioSpecificConfig(bytes: [UInt8](payload[codec.headerSize..<payload.count]))
            stream.mixer.audioIO.encoder.destination = .PCM
            stream.mixer.audioIO.encoder.inSourceFormat = config?.audioStreamBasicDescription()
        case .raw?:
            payload.withUnsafeMutableBytes { (buffer: UnsafeMutableRawBufferPointer) -> Void in
                stream.mixer.audioIO.encoder.encodeBytes(buffer.baseAddress?.advanced(by: codec.headerSize), count: payload.count - codec.headerSize, presentationTimeStamp: CMTime(seconds: stream.audioTimestamp / 1000, preferredTimescale: 1000))
            }
        case .none:
            break
        }

において、今回のRTMPストリームはcase .raw?:のフローにしか処理が遷移していませんでした。

実装が期待している挙動は、.seqFLVAACPacketが、.rawのパケットよりも先に受信されることで、inSourceFormatがセットされる挙動なのかなと考えましたが、 これ以上はRTMPプロトコルについての知識がないため、手が出ませんでした。

対応する上で情報が足りていないかもしれませんが、 もし可能なら修正していただけると嬉しいです。

shogo4405 commented 4 years ago

差し支えなければ以下の2点が知りたいです。

  1. RTMPのサーバー名
  2. case .raw? の内部の payload の先頭 15バイト 程度が数行 print(payload[0..<15])
    • もしかするとフォーマット形式が 生AACではなく、ADTS + AAC 形式と仮説を立てています。
omochi commented 4 years ago

ありがとうございます、明日金曜日に回答します。

omochi commented 4 years ago

RTMPのサーバ名

問い合わせていますが、もしかしたら回答できないかもしれません。

補足になるかもしれないので、VLCプレイヤーのメディア情報ウィンドウの表示を添付します。

スクリーンショット 2019-11-29 11 46 42

先頭バイト

下記のようにコードを書き換えて、先頭64バイトをダンプしました。 出力されたログを添付します。 (この状態にするとメモリリークが生じるため、10秒ほど経過するとクラッシュします)

    override func execute(_ connection: RTMPConnection, type: RTMPChunkType) {
        guard let stream: RTMPStream = connection.streams[streamId] else {
            return
        }
        OSAtomicAdd64(Int64(payload.count), &stream.info.byteCount)
        guard codec.isSupported else {
            return
        }
        switch type {
        case .zero:
            stream.audioTimestamp = Double(timestamp)
        default:
            stream.audioTimestamp += Double(timestamp)
        }

        let packetType = FLVAACPacketType(rawValue: payload[1])

        let dataStr = dumpData(payload.subdata(in: 0..<min(64, payload.count)))
        let now = Date()
        let fmt = DateFormatter()
        fmt.dateFormat = "HH:mm:ss.SSS"
        fmt.locale = Locale(identifier: "en_US_POSIX")
        fmt.timeZone = TimeZone(abbreviation: "JST")!
        let nowStr = fmt.string(from: now)

        print("[\(nowStr)] \(payload.count) bytes")
        print(dataStr)

        switch FLVAACPacketType(rawValue: payload[1]) {
        case .seq?:
            let config = AudioSpecificConfig(bytes: [UInt8](payload[codec.headerSize..<payload.count]))
            stream.mixer.audioIO.encoder.destination = .PCM
            stream.mixer.audioIO.encoder.inSourceFormat = config?.audioStreamBasicDescription()
        case .raw?:
            break
//            payload.withUnsafeMutableBytes { (buffer: UnsafeMutableRawBufferPointer) -> Void in
//                stream.mixer.audioIO.encoder.encodeBytes(buffer.baseAddress?.advanced(by: codec.headerSize), count: payload.count - codec.headerSize, presentationTimeStamp: CMTime(seconds: stream.audioTimestamp / 1000, preferredTimescale: 1000))
//            }
        case .none:
            break
        }
    }

dump.txt

shogo4405 commented 4 years ago

@omochi dump結果ありがとうございました。以下のbranchで改善するとは思います。 https://github.com/shogo4405/HaishinKit.swift/compare/fix-issues-618

omochi commented 4 years ago

@shogo4405 早急な対応ありがとうございます! 上記ブランチを使用したところ、クラッシュしなくなり、音声が再生されるようになりました。

配信側のサーバーソフトウェアですが下記のものとのことです。

nginxにnginx-rtmp-moduleを組み込んだもの

さて、音声は再生されるようになったのですが、画像が表示されない問題があり、困っております。

iOS 13.0 と iOS 13.2.3 の実デバイスにて、GLHKViewを使用しています。 音声は再生されているけれど、画像は表示されず真っ黒のままです。

試しにsetNeedsDisplay()を繰り返し呼んでみたところ、 1度だけ画像が表示されるようになりました。

setNeedsDisplayを呼び出すたびに、 public func glkView(_ view: GLKView, drawIn rect: CGRect) が呼びだされています。

一方、func draw(image: CIImage)が、1度しか呼び出されません。 また、これの呼び出しの瞬間に画像が表示されるようです。

shogo4405 commented 4 years ago

nginxにnginx-rtmp-moduleを組み込んだもの

この情報で僕の方でデバッグ可能になりました。ありがとうございます。 ただ改善するには時間がかかりそうで年末年始の空き時間を利用してとなりそうです。

フィードバックありがとうございましたmm

omochi commented 4 years ago

ありがとうございます。

shogo4405 commented 4 years ago

現状のmasterブランチで、nginx + rtmp module で視聴できるようになりました。 音声がない mp4。音声有りの mp4。および、HaishinKit経由での配信の視聴で確認しております。