私のGitHubへようこそ
以下は、Xinchen のすべてのオリジナル作品の分類と要約です (サポートするソース コードを含む): https://github.com/zq2599/blog_demos
シリーズ「Golang Streaming Media Combat」へのリンク
準備
- この記事で学ぶべきことは、rtmp ストリーミングを処理する lalserver の機能コードであるため、rtmp プロトコル、少なくともハンドシェイク、チャンク、メッセージ、メッセージ タイプ、および amf0 コマンドの基本的な概念を理解する必要があります。はすでにインターネット上で非常に優れています. ここでは拡張しません. 参考としてウィキを提供します:
- lalserver ソース ウェアハウス: https://github.com/q191201771/lal
- ソースコードに多くのロジックと分岐がある場合、lal ログを組み合わせて実際の実行シーケンスを決定し、よりリッチな内容のログを出力することができるため、ストリームは最初にここにプッシュされ、lal のログが取得されます。プッシュ操作は記事「Project lal」で「オープンソースを体験する」に記載されていますが、コマンドは以下の通りです。
./ffmpeg \
-re \
-stream_loop -1 \
-i ../videos/sample.mp4 \
-c copy \
-f flv \
'rtmp://127.0.0.1:1935/live/test110'
この記事の概要
- ストリーミング メディア技術の基本機能であるプッシュ プル ストリーム、この記事では lal のソース コードを読むことでプッシュ ストリーム機能の具体的な実装を理解します。
- 今回はrtmpストリーミングサーバーのソースコードを学んでいます.大まかなストリーミング処理の流れは以下の通りです.
- TCP接続を受信
- 握手
- チャンク パッケージを受け取り、メッセージを作成する
- messageType に従ってメッセージを個別に処理する
- amf0 タイプのメッセージについても、異なる amf0 コマンドに従って個別に処理されます。
- ストリーミング終了時の関連操作
- 次に、lal のストリーミング ソース コードを一緒に学習し、これを使用してストリーミング機能の理解を深めましょう。
rtmp ストリーミング リクエストを処理するためのエントリ
- lalserver 側では、開始点は rtmp サーバーがリモート TCP 接続 (デフォルトではポート 1935) を受信することです。lal がそれをどのように処理するか、つまり rtmp サーバーの処理ロジックを見てみましょう。コードは lal/pkg にあります。 /rtmp/server.go _
func (server *Server) RunLoop() error {
for {
conn, err := server.ln.Accept()
if err != nil {
return err
}
go server.handleTcpConnect(conn)
}
}
- 上記のコードからわかるように、TCP 接続が受信されるたびに、handleTcpConnect メソッドを使用して、コルーチンで接続を処理します. handleTcpConnect メソッドには詳細は含まれず、内容は非常に単純です: TCP 接続し、特定の処理を ServerSession オブジェクトに引き渡す実装
func (server *Server) handleTcpConnect(conn net.Conn) {
Log.Infof("accept a rtmp connection. remoteAddr=%s", conn.RemoteAddr().String())
session := NewServerSession(server, conn)
_ = session.RunLoop()
if session.DisposeByObserverFlag {
return
}
switch session.sessionStat.BaseType() {
case base.SessionBaseTypePubStr:
server.observer.OnDelRtmpPubSession(session)
case base.SessionBaseTypeSubStr:
server.observer.OnDelRtmpSubSession(session)
}
}
- 上記のコードからわかるように、メインのビジネス ロジックは ServerSession オブジェクトの RunLoop メソッドにあり、展開後は次のように示され、最初に握手 (handshake) し、次に特定のロジックを実行 (runReadLoop) します。
func (s *ServerSession) RunLoop() (err error) {
if err = s.handshake(); err != nil {
_ = s.dispose(err)
return err
}
err = s.runReadLoop()
_ = s.dispose(err)
return err
}
握手
- ハンドシェイク コードを見る前に、rtmp ハンドシェイク プロセスを確認してください。
- ハンドシェイクのソースコードを見ると一目瞭然ですが、コードは写真とは少し異なります: lal のコードでは、S0S1S2 がバーストで送信されます。
func (s *ServerSession) handshake() error {
if err := s.hs.ReadC0C1(s.conn); err != nil {
return err
}
Log.Infof("[%s] < R Handshake C0+C1.", s.UniqueKey())
Log.Infof("[%s] > W Handshake S0+S1+S2.", s.UniqueKey())
if err := s.hs.WriteS0S1S2(s.conn); err != nil {
return err
}
if err := s.hs.ReadC2(s.conn); err != nil {
return err
}
Log.Infof("[%s] < R Handshake C2.", s.UniqueKey())
return nil
}
- C0、C1、および C2 のデータを読み取る特定のロジックを見たいですか? 下の図に示すように、最も一般的な io read は
チャンクの読み取りと処理
- ハンドシェイクが成功した後、lal/pkg/rtmp/chunk_composer.go#RunLoop を直接呼び出す lal/pkg/rtmp/server_session.go#runReadLoop メソッドは次のとおりです。チャンク 完全なメッセージの後のコールバック メソッド (つまり、lal/pkg/rtmp/server_session.go#doMsg) も重要なポイントです。
func (s *ServerSession) runReadLoop() error {
return s.chunkComposer.RunLoop(s.conn, s.doMsg)
}
- 本当のコア コードが到着しました. ストリーミング クライアントと lalserver が正常に握手した後、ストリーミング操作のすべてのロジックは次のとおりです: lal/pkg/rtmp/chunk_composer.go#RunLoop、ここのコードはいくつかの部分に分割され、コードは少し長いので、もう投稿しません。いくつかの重要なロジックを見てみましょう
- 最初に表示されるのは無限ループです.1 つのチャンク パッケージを処理した後、次のチャンク パッケージを処理し続けます。
- 1 つ以上のチャンク パッケージで構成されるチャンク ストリーム ID (csid) に従って、現在のパッケージに対応するメッセージを決定します。
- 次のパラグラフはより重要です. 各メッセージには独自の csid があり, これはストリーム オブジェクトに対応します. メッセージに対応するすべてのパッケージはストリームのメンバ変数に格納され, 保存された長さが記録されます. 保存されたコンテンツがlength がメッセージ長に達した場合、メッセージに対応するすべてのデータが取得されたことを意味し、メッセージを処理するためのコードを実行できます。
- 上図の赤い矢印 2 で示される Flush メソッドは、実際にはメモリやハードディスクの読み書き操作を行わず、位置変数を変更するだけであり、バッファ内のものが正式なメッセージの内容であることを示しています。学ぶ
- チャンクから完全なメッセージを取得した後、次のステップはメッセージを処理するロジックを実行することです。下の図に示すように、2 つのコールバック コードがある理由は、メッセージ タイプが集約メッセージ (Aggregate Message) である場合です。 、22)、つまり、チャンクで取得されたメッセージは実際には複数のメッセージであり、処理のために分割して1つずつコールバックする必要があります
- 上記はチャンクの処理ロジックです. チャンクから完全なメッセージを取得したので、メッセージの処理ロジックを見ていきます.
メッセージを処理する
- 前の図からわかるように、メッセージを処理するためのコードは赤い矢印で示されたcb(stream)であり、実際の対応するコードはserver_session.go#doMsgです。
- doMsg のコードは単純明快であり、異なるメッセージ タイプに応じて異なる操作が実行されます。
func (s *ServerSession) doMsg(stream *Stream) error {
if err := s.writeAcknowledgementIfNeeded(stream); err != nil {
return err
}
//log.Debugf("%d %d %v", stream.header.msgTypeId, stream.msgLen, stream.header)
switch stream.header.MsgTypeId {
case base.RtmpTypeIdWinAckSize:
return s.doWinAckSize(stream)
case base.RtmpTypeIdSetChunkSize:
// noop
// 因为底层的 chunk composer 已经处理过了,这里就不用处理
case base.RtmpTypeIdCommandMessageAmf0:
return s.doCommandMessage(stream)
case base.RtmpTypeIdCommandMessageAmf3:
return s.doCommandAmf3Message(stream)
case base.RtmpTypeIdMetadata:
return s.doDataMessageAmf0(stream)
case base.RtmpTypeIdAck:
return s.doAck(stream)
case base.RtmpTypeIdUserControl:
s.doUserControl(stream)
case base.RtmpTypeIdAudio:
fallthrough
case base.RtmpTypeIdVideo:
if s.sessionStat.BaseType() != base.SessionBaseTypePubStr {
return nazaerrors.Wrap(base.ErrRtmpUnexpectedMsg)
}
s.avObserver.OnReadRtmpAvMsg(stream.toAvMsg())
default:
Log.Warnf("[%s] read unknown message. typeid=%d, %s", s.UniqueKey(), stream.header.MsgTypeId, stream.toDebugString())
}
return nil
}
- 問題は、ストリーミングの知識を習得するには、各メッセージの処理ロジック コードを読み取る必要があるかということです。少し多すぎるように思えますし、この順序で読むとメッセージ間の順序や依存関係がわからないので、この時点で怠惰になる必要があります...
- ストリーミング時のプロトコル相互作用の特定の状況を理解するために、doMsg メソッドを少し変更し、下図の黄色い矢印のコード行を追加してから、コンパイルして実行します。
- FFmpeg を使用してプッシュ ストリームを再度実行し、新しいログを取得します。
- ストリーミング後のログは次のとおりです (grep コマンドを使用して、ログの上記の行のみを確認してください)。
msg header {
Csid:3 MsgLen:139 MsgTypeId:20 MsgStreamId:0 TimestampAbs:0} - server_session.go:215
msg header {
Csid:2 MsgLen:4 MsgTypeId:1 MsgStreamId:0 TimestampAbs:0} - server_session.go:215
msg header {
Csid:3 MsgLen:36 MsgTypeId:20 MsgStreamId:0 TimestampAbs:0} - server_session.go:215
msg header {
Csid:3 MsgLen:32 MsgTypeId:20 MsgStreamId:0 TimestampAbs:0} - server_session.go:215
msg header {
Csid:3 MsgLen:25 MsgTypeId:20 MsgStreamId:0 TimestampAbs:0} - server_session.go:215
msg header {
Csid:8 MsgLen:37 MsgTypeId:20 MsgStreamId:1 TimestampAbs:0} - server_session.go:215
msg header {
Csid:4 MsgLen:388 MsgTypeId:18 MsgStreamId:1 TimestampAbs:0} - server_session.go:215
msg header {
Csid:6 MsgLen:43 MsgTypeId:9 MsgStreamId:1 TimestampAbs:0} - server_session.go:215
msg header {
Csid:4 MsgLen:4 MsgTypeId:8 MsgStreamId:1 TimestampAbs:0} - server_session.go:215
msg header {
Csid:6 MsgLen:105227 MsgTypeId:9 MsgStreamId:1 TimestampAbs:0} - server_session.go:215
msg header {
Csid:4 MsgLen:969 MsgTypeId:8 MsgStreamId:1 TimestampAbs:0} - server_session.go:215
msg header {
Csid:4 MsgLen:1013 MsgTypeId:8 MsgStreamId:1 TimestampAbs:21} - server_session.go:215
msg header {
Csid:6 MsgLen:1559 MsgTypeId:9 MsgStreamId:1 TimestampAbs:40} - server_session.go:215
msg header {
Csid:4 MsgLen:1028 MsgTypeId:8 MsgStreamId:1 TimestampAbs:43} - server_session.go:215
msg header {
Csid:4 MsgLen:1032 MsgTypeId:8 MsgStreamId:1 TimestampAbs:64} - server_session.go:215
msg header {
Csid:6 MsgLen:2158 MsgTypeId:9 MsgStreamId:1 TimestampAbs:80} - server_session.go:215
msg header {
Csid:4 MsgLen:992 MsgTypeId:8 MsgStreamId:1 TimestampAbs:85} - server_session.go:215
msg header {
Csid:4 MsgLen:960 MsgTypeId:8 MsgStreamId:1 TimestampAbs:107} - server_session.go:215
msg header {
Csid:6 MsgLen:2213 MsgTypeId:9 MsgStreamId:1 TimestampAbs:120} - server_session.go:215
msg header {
Csid:4 MsgLen:975 MsgTypeId:8 MsgStreamId:1 TimestampAbs:128} - server_session.go:215
msg header {
Csid:4 MsgLen:991 MsgTypeId:8 MsgStreamId:1 TimestampAbs:149} - server_session.go:215
msg header {
Csid:6 MsgLen:2528 MsgTypeId:9 MsgStreamId:1 TimestampAbs:160} - server_session.go:215
msg header {
Csid:4 MsgLen:1011 MsgTypeId:8 MsgStreamId:1 TimestampAbs:171} - server_session.go:215
msg header {
Csid:4 MsgLen:1002 MsgTypeId:8 MsgStreamId:1 TimestampAbs:192} - server_session.go:215
- wikiのメッセージタイプの説明を見てみましょう
- プロトコルとログを組み合わせると、ストリーミングが開始された後、2 番目の設定チャンクを除いて、その他は主に AMF0 によってエンコードされた RTMP コマンド メッセージと、オーディオおよびビデオ データ パケットであることがわかります。
- ログには MsgTypeId が 20 に等しいメッセージが多数あり、対応する 16 進数は 0x14 であり、これが AMF0 コマンドです。したがって、このようなメッセージの処理ロジックに注目する必要があります。そのようなメッセージを処理するメソッドがdoCommandMessageである doMsg コード
func (s *ServerSession) doCommandMessage(stream *Stream) error {
cmd, err := stream.msg.readStringWithType()
if err != nil {
return err
}
tid, err := stream.msg.readNumberWithType()
if err != nil {
return err
}
switch cmd {
case "connect":
return s.doConnect(tid, stream)
case "createStream":
return s.doCreateStream(tid, stream)
case "publish":
return s.doPublish(tid, stream)
case "play":
return s.doPlay(tid, stream)
case "releaseStream":
fallthrough
case "FCPublish":
fallthrough
case "FCUnpublish":
fallthrough
case "getStreamLength":
fallthrough
case "deleteStream":
Log.Debugf("[%s] read command message, ignore it. cmd=%s, %s", s.UniqueKey(), cmd, stream.toDebugString())
default:
Log.Errorf("[%s] read unknown command message. cmd=%s, %s", s.UniqueKey(), cmd, stream.toDebugString())
}
return nil
}
- doConnect、doCreateStream、doPublish、doPlayの各コマンドの処理方法には、内部にそれぞれの特徴を表すログが含まれているため、ログの内容から、ストリーミング時に受信したコマンドの順序を簡単に整理すると、次のようになります。
connect
->
releaseStream
->
FCPublish
->
createStream
->
publish
->
MetaDzta
->
然后就是音频视频的chunk包
- 次のように、doConnect メソッドを開いて、FFmpeg から接続コマンドを受信した後に lalserver が何をしたかを確認します。 connect コマンドのパラメーター :1935/live)、コマンドの形式でメッセージを FFmpeg に継続的に返信します。
func (s *ServerSession) doConnect(tid int, stream *Stream) error {
val, err := stream.msg.readObjectWithType()
if err != nil {
return err
}
s.appName, err = val.FindString("app")
if err != nil {
return err
}
s.tcUrl, err = val.FindString("tcUrl")
if err != nil {
Log.Warnf("[%s] tcUrl not exist.", s.UniqueKey())
}
Log.Infof("[%s] < R connect('%s'). tcUrl=%s", s.UniqueKey(), s.appName, s.tcUrl)
s.observer.OnRtmpConnect(s, val)
Log.Infof("[%s] > W Window Acknowledgement Size %d.", s.UniqueKey(), windowAcknowledgementSize)
if err := s.packer.writeWinAckSize(s.conn, windowAcknowledgementSize); err != nil {
return err
}
Log.Infof("[%s] > W Set Peer Bandwidth.", s.UniqueKey())
if err := s.packer.writePeerBandwidth(s.conn, peerBandwidth, peerBandwidthLimitTypeDynamic); err != nil {
return err
}
Log.Infof("[%s] > W SetChunkSize %d.", s.UniqueKey(), LocalChunkSize)
if err := s.packer.writeChunkSize(s.conn, LocalChunkSize); err != nil {
return err
}
Log.Infof("[%s] > W _result('NetConnection.Connect.Success').", s.UniqueKey())
oe, err := val.FindNumber("objectEncoding")
if oe != 0 && oe != 3 {
oe = 0
}
if err := s.packer.writeConnectResult(s.conn, tid, oe); err != nil {
return err
}
return nil
}
- 接続が完了したら createStream コマンドです. 対応する doCreateStream メソッドは次のとおりです. すぐに返信するという非常に単純な方法であり, メッセージタイプは引き続き createStream. それについても考えてみてください. 関連する情報ストリームは lal 側で準備ができており、ストリームを作成するコマンドも受信されます。アクションは不要です。
func (s *ServerSession) doCreateStream(tid int, stream *Stream) error {
Log.Infof("[%s] < R createStream().", s.UniqueKey())
Log.Infof("[%s] > W _result().", s.UniqueKey())
if err := s.packer.writeCreateStreamResult(s.conn, tid); err != nil {
return err
}
return nil
}
- 次のステップは publish コマンドです。コードは投稿されません。主にストリーム名の取得、onStatus の応答、接続のタイムアウト時間の設定などです。さらに、オブザーバーが存在する場合は、publish- も送信します。関連するイベントをそれに関連付けて、publish イベントを消費するコードも一見の価値があります lal/pkg/logic/server_manager__.go#OnNewRtmpPubSession では、次のように、主に認証、フローに関連するグループ処理が含まれていることがわかります。 , 外部監視の通知. さらに, 設定で記録機能が有効になっている場合, group.AddRtmpPubSession メソッドで, 関連する初期化操作が行われます.
func (sm *ServerManager) OnNewRtmpPubSession(session *rtmp.ServerSession) error {
sm.mutex.Lock()
defer sm.mutex.Unlock()
info := base.Session2PubStartInfo(session)
// 先做simple auth鉴权
if err := sm.option.Authentication.OnPubStart(info); err != nil {
return err
}
group := sm.getOrCreateGroup(session.AppName(), session.StreamName())
if err := group.AddRtmpPubSession(session); err != nil {
return err
}
info.HasInSession = group.HasInSession()
info.HasOutSession = group.HasOutSession()
sm.option.NotifyHandler.OnPubStart(info)
return nil
}
- 次はMetadata型のメッセージ処理です. メッセージにはプッシュするストリームの幅や高さ, ビットレート, フレームレートなどの詳細なパラメータが含まれています. 対応するメソッドは doDataMessageAmf0. このメソッドで最も重要なのはNotified を実行するには、対応するメソッドが lal/pkg/logic/group__core_streaming.go#OnReadRtmpAvMsg にあります。
- OnReadRtmpAvMsgメソッドは、重要なポイントであるbroadcastByRtmpMsgを呼び出します.グループに保存されたサブスクライバーに従ってブロードキャストの送信を開始し、すべてのサブスクライバーがメディアストリームの詳細なパラメーターを取得できるようにします(ここのコードは非常に複雑で、さまざまなプロトコルが含まれます)録画、リツイート、ストリーミングなど)、ストリーミング シーンでは、このコードに焦点を当てることができます。
- 上記のすべてのコマンドが応答されるまで待ちます。つまり、準備作業が完了し、メディア ストリーム データの到着を待つことができます。
音声およびビデオ メッセージの処理
- 次の図に示すように、doMsg メソッドに戻ってオーディオ メッセージとビデオ メッセージの処理を確認します。まず基本的なチェックを行い、処理のために lal/pkg/logic/group__core_streaming.go#OnReadRtmpAvMsg に渡します。
- 再び OnReadRtmpAvMsg メソッドです. 以前はメタデータに応答したメソッドでした. 今でもオーディオ メッセージとビデオ メッセージを処理するメソッドです. コードを読むのは簡単ですが、処理のためにブロードキャストByRtmpMsg メソッドに引き渡されます.
- broadcastByRtmpMsg で、seqheader とキー フレームのコンテンツをキャッシュする lal/pkg/remux/gop_cache.go#Feed に注目します。
- broadcastByRtmpMsg メソッドには、この記事で最も重要なコードが 2 つあります (そう思う)。
- 最初に、下の図に示すように、新しいストリーミング リクエストの場合、メディア ストリームの属性がストリーミング エンドに書き込まれ、キー フレームとその seqheader 情報がキャッシュされるだけで、ストリーミング エンドが接続を確立したばかりです。ストリーマーによってプッシュされた最新のキー フレームを待たずに、キー フレームを取得できます (最初のフレームの効果をすばやく取得します)。
- 2 番目のキー コードは次の図に示すように、write2RtmpSubSessions を呼び出して、今回受信したオーディオ メッセージとビデオ メッセージを転送します。
- write2RtmpSubSessions を展開して、ふと気がついたのですが、個人的には、これがプッシュプル ストリームのコア コードだと思います。受信した各オーディオ データとビデオ データを、プル ストリーム エンドの TCP 接続から直接書き込みます (session.Write メソッドを展開して表示します)。
func (group *Group) write2RtmpSubSessions(b []byte) {
for session := range group.rtmpSubSessionSet {
if session.IsFresh || session.ShouldWaitVideoKeyFrame {
continue
}
_ = session.Write(b)
}
}
ストリーミング終了の扱い
- Ctrl+C を使用して FFmpeg ストリーミングを終了すると、lal はどうなりますか? 怠け者で、最初にログを読むほうがよいでしょう。
2023/04/02 09:47:24.346491 ^[[22;36m INFO ^[[0mmsg header {
Csid:4 MsgLen:986 MsgTypeId:8 MsgStreamId:1 TimestampAbs:9323} - server_session.go:215
2023/04/02 09:47:24.346558 ^[[22;36m INFO ^[[0mmsg header {
Csid:4 MsgLen:950 MsgTypeId:8 MsgStreamId:1 TimestampAbs:9344} - server_session.go:215
2023/04/02 09:47:24.346605 ^[[22;36m INFO ^[[0mmsg header {
Csid:6 MsgLen:5 MsgTypeId:9 MsgStreamId:1 TimestampAbs:9320} - server_session.go:215
2023/04/02 09:47:24.346654 ^[[22;33m WARN ^[[0m[RTMP2MPEGTS1] rtmp msg too short, ignore. header={
Csid:6 MsgLen:5 MsgTypeId:9 MsgStreamId:1 TimestampAbs:9320}, payload=00000000 17 02 00 00 00 |.....|
- rtmp2mpegts.go:196
2023/04/02 09:47:24.346681 ^[[22;33m WARN ^[[0mrtmp msg too short, ignore. header={
Csid:6 MsgLen:5 MsgTypeId:9 MsgStreamId:1 TimestampAbs:9320}, payload=00000000 17 02 00 00 00 |.....|
- rtmp2rtsp.go:102
2023/04/02 09:47:24.346958 ^[[22;36m INFO ^[[0mmsg header {
Csid:3 MsgLen:34 MsgTypeId:20 MsgStreamId:0 TimestampAbs:0} - server_session.go:215
2023/04/02 09:47:24.346987 ^[[22;34mDEBUG ^[[0m[RTMPPUBSUB1] read command message, ignore it. cmd=FCUnpublish, header={
Csid:3 MsgLen:34 MsgTypeId:20 MsgStreamId:0 TimestampAbs:0}, b=len(core)=128, rpos=23, wpos=34, hex=00000000 05 02 00 07 74 65 73 74 31 31 30 |....test110|
- server_session.go:357
2023/04/02 09:47:24.347012 ^[[22;36m INFO ^[[0mmsg header {
Csid:3 MsgLen:34 MsgTypeId:20 MsgStreamId:0 TimestampAbs:0} - server_session.go:215
2023/04/02 09:47:24.347028 ^[[22;34mDEBUG ^[[0m[RTMPPUBSUB1] read command message, ignore it. cmd=deleteStream, header={
Csid:3 MsgLen:34 MsgTypeId:20 MsgStreamId:0 TimestampAbs:0}, b=len(core)=128, rpos=24, wpos=34, hex=00000000 05 00 3f f0 00 00 00 00 00 00 |..?.......|
- server_session.go:357
2023/04/02 09:47:24.347050 ^[[22;34mDEBUG ^[[0m[NAZACONN1] close once. err=EOF - connection.go:504
2023/04/02 09:47:24.347168 ^[[22;36m INFO ^[[0m[RTMPPUBSUB1] lifecycle dispose rtmp ServerSession. err=EOF - server_session.go:538
2023/04/02 09:47:24.347183 ^[[22;34mDEBUG ^[[0m[NAZACONN1] Close. - connection.go:376
2023/04/02 09:47:24.347199 ^[[22;34mDEBUG ^[[0m[GROUP1] [RTMPPUBSUB1] del rtmp PubSession from group. - group__in.go:318
2023/04/02 09:47:24.347303 ^[[22;36m INFO ^[[0m[HLSMUXER1] lifecycle dispose hls muxer. - muxer.go:126
2023/04/02 09:47:24.570509 ^[[22;36m INFO ^[[0merase inactive group. [GROUP1] - server_manager__.go:299
2023/04/02 09:47:24.570639 ^[[22;36m INFO ^[[0m[GROUP1] lifecycle dispose group. - group__.go:207
- 上記のログからわかるように、lal は FFmpeg から FCUnpublish、deleteStream などのコマンドを受け取りますが、lal はこれらのコマンドを無視しますが、下図に示すように、TCP 接続で EOF エラーが発生するまで待機し、エラー
- 具体的には、TCP エラーを処理してストリーミングを終了するためのコードを次の図に示します。
- ここまでで、ストリーミング サービスの関連ソース コードの学習は完了しました. lal の助けを借りて、rtmp ストリーミング サービスの詳細を初めて学びました. 基本的なスキルはしっかりしており、次の学習はさらに多くのことを学びます.次に、もう 1 つの基本機能である rtmp ストリーミングに挑戦してみましょう。