Práctica cinco de medios de transmisión de Golang: lectura del código fuente del servicio de transmisión push lal

Bienvenido a mi GitHub

Aquí hay una clasificación y un resumen de todos los trabajos originales de Xinchen (incluido el código fuente de soporte): https://github.com/zq2599/blog_demos

Enlaces a la serie "Golang Streaming Media Combat"

  1. Experimente el proyecto de código abierto lal
  2. Volver a la fuente
  3. Retwittear y grabar
  4. Lectura del código fuente de inicio de Lalserver

Preparación

  • Lo que este artículo es para aprender es el código de función de lalserver que procesa la transmisión rtmp, por lo que es necesario comprender el protocolo rtmp, al menos los conceptos básicos de los comandos de protocolo de enlace, fragmento, mensaje, tipo de mensaje y amf0. ya es muy bueno en Internet Enriquecido, no me extenderé aquí, solo proporcione un wiki como referencia: https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol
  • almacén de origen lalserver: https://github.com/q191201771/lal
  • Cuando hay muchas lógicas y ramas en el código fuente, puede combinar el registro lal para determinar la secuencia de ejecución real y hacer que el contenido de la salida del registro sea más rico. Por lo tanto, la transmisión se enviará aquí primero y se obtendrá el registro de lal. La operación push se ha descrito en "Experiencia Open Source Ejecutado en el artículo , el comando es el siguiente
./ffmpeg \
-re \
-stream_loop -1 \
-i ../videos/sample.mp4 \
-c copy \
-f flv \
'rtmp://127.0.0.1:1935/live/test110'

Resumen de este artículo

  • Transmisión push-pull, que es la función básica de la tecnología de transmisión de medios, este artículo comprende la implementación específica de la función push-stream mediante la lectura del código fuente de lal
  • Esta vez estoy aprendiendo el código fuente del servidor de streaming rtmp.En general, el proceso de procesamiento de streaming es el siguiente
  1. Recibir conexión TCP
  2. Apretón de manos
  3. Recibir paquete de fragmentos y mensaje de formulario
  4. Procesar mensajes por separado según el tipo de mensaje
  5. Para el mensaje de tipo amf0, también se procesa por separado según los diferentes comandos amf0 6. Procesamiento de datos de audio y video
  6. Operaciones relacionadas al finalizar la transmisión
  • A continuación, aprendamos juntos el código fuente de transmisión de lal y usemos esto para obtener una comprensión profunda de la función de transmisión.

La entrada para manejar las solicitudes de transmisión de rtmp

  • Por el lado de lalserver, el punto de partida es que el servidor rtmp recibe una conexión TCP remota (puerto 1935 por defecto), veamos como lo maneja lal, es decir, la lógica de procesamiento del servidor rtmp, el código está en lal/pkg /rtmp/ servidor.ir
func (server *Server) RunLoop() error {
    
    
	for {
    
    
		conn, err := server.ln.Accept()
		if err != nil {
    
    
			return err
		}
		go server.handleTcpConnect(conn)
	}
}
  • Como se puede ver en el código anterior, cada vez que se recibe una conexión TCP, se utiliza el método handleTcpConnect para procesar la conexión en una corrutina. El método handleTcpConnect no involucra detalles, y el contenido es muy simple: cree un objeto ServerSession para el Conexión TCP y entrega el procesamiento específico al objeto ServerSession implementado
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)
	}
}
  • Como se puede ver en el código anterior, la lógica comercial principal está en el método RunLoop del objeto ServerSession.Después de la expansión, se muestra de la siguiente manera, primero se dan la mano (apretón de manos) y luego ejecutan la lógica específica (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
}

Apretón de manos

  • Antes de mirar el código de protocolo de enlace, revise el proceso de protocolo de enlace rtmp
    inserte la descripción de la imagen aquí
  • Mirando el código fuente del apretón de manos, es claro de un vistazo, pero el código es ligeramente diferente de la imagen: en el código de lal, S0S1S2 se envía en ráfagas
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
}
  • ¿Quiere ver la lógica específica de leer los datos de C0, C1 y C2? Como se muestra en la figura a continuación, la lectura de io más común es solo
    inserte la descripción de la imagen aquí

Leer y procesar fragmentos

  • Después de que el protocolo de enlace sea exitoso, el siguiente es el método lal/pkg/rtmp/server_session.go#runReadLoop, que llama directamente a lal/pkg/rtmp/chunk_composer.go#RunLoop, recuerde el segundo parámetro de entrada de RunLoop, que se obtiene de el fragmento El método de devolución de llamada después del mensaje completo (es decir, lal/pkg/rtmp/server_session.go#doMsg) también es un punto clave
func (s *ServerSession) runReadLoop() error {
    
    
	return s.chunkComposer.RunLoop(s.conn, s.doMsg)
}
  • El código central real ha llegado Después de que el cliente de transmisión y el servidor lal se dan la mano con éxito, toda la lógica de la operación de transmisión está aquí: lal/pkg/rtmp/chunk_composer.go#RunLoop, el código aquí se divide en varias partes, y el código es un poco largo, no lo publicaré más, veamos algunas lógicas clave
  • Lo primero que ve es un bucle infinito.Después de procesar un paquete de fragmentos, continúe procesando el siguiente paquete de fragmentos.
    inserte la descripción de la imagen aquí
  • Determine el mensaje correspondiente al paquete actual de acuerdo con el id de vapor de fragmento (csid), que consta de uno o más paquetes de fragmentos
    inserte la descripción de la imagen aquí
  • El siguiente párrafo es más importante. Cada mensaje tiene su propio csid, que a su vez corresponde a un objeto de flujo. Todos los paquetes correspondientes al mensaje se almacenan en las variables miembro del flujo y se registra la longitud guardada. Cuando el contenido guardado longitud alcanza la longitud del mensaje, significa que se han obtenido todos los datos correspondientes al mensaje y se puede ejecutar el código para procesar el mensaje
    inserte la descripción de la imagen aquí
  • El método Flush indicado por la flecha roja 2 en la figura anterior en realidad no tiene las operaciones de lectura y escritura de la memoria o el disco duro, sino que solo modifica la variable de posición, lo que indica que aquellos en el búfer son contenido oficial del mensaje. aprendiendo
  • Después de obtener el mensaje completo del fragmento, el siguiente paso es ejecutar la lógica de procesamiento del mensaje, como se muestra en la figura a continuación, la razón por la que hay dos códigos de devolución de llamada es porque si el tipo de mensaje es un mensaje agregado (Mensaje agregado , 22), significa que un mensaje obtenido en el fragmento es en realidad varios mensajes, que deben dividirse y recuperarse uno por uno para su procesamiento.
    inserte la descripción de la imagen aquí
  • Lo anterior es la lógica de procesamiento del fragmento. Ahora que se ha obtenido el mensaje completo del fragmento, es hora de observar la lógica de procesamiento del mensaje.

procesar mensajes

  • Como se puede ver en la figura anterior, el código para procesar mensajes es cb(stream) apuntado por la flecha roja , y el código correspondiente real es server_session.go#doMsg
  • El código de doMsg es simple y claro, y se realizan diferentes operaciones según los diferentes tipos de mensajes.
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
}
  • Entonces, la pregunta es, para aprender el conocimiento de la transmisión, ¿tengo que leer el código lógico de procesamiento de cada mensaje? Parece un poco demasiado, y leer en este orden no conoce el orden o la relación de dependencia entre los mensajes, así que necesito ser perezoso en este momento...
  • Para comprender la situación específica de la interacción del protocolo durante la transmisión, modifique ligeramente el método doMsg, agregue la línea de código con la flecha amarilla en la figura a continuación y luego compile y ejecute
    inserte la descripción de la imagen aquí
  • Use FFmpeg para hacer una transmisión push nuevamente y obtener un nuevo registro
  • El registro después de la transmisión es el siguiente (a través del comando grep, solo mire la línea de registro anterior)
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
  • Veamos la explicación del tipo de mensaje en la wiki.
    inserte la descripción de la imagen aquí
  • Combinando el protocolo y los registros, se puede ver que después de que comienza la transmisión, excepto por el segundo fragmento de configuración, los demás son principalmente mensajes de comando RTMP codificados por AMF0, así como paquetes de datos de audio y video.
  • Hay una gran cantidad de mensajes con MsgTypeId igual a 20 en el registro, y el número hexadecimal correspondiente es 0x14, que es el comando AMF0. Por lo tanto, se debe enfocar la lógica de procesamiento de dichos mensajes. Se puede ver en el código doMsg que indica que el método para procesar dichos mensajes es doCommandMessage
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
}
  • Los métodos de procesamiento de cada comando, doConnect , doCreateStream , doPublish y doPlay tienen registros que representan sus propias características en su interior. Por lo tanto, según el contenido del registro, es fácil ordenar el orden de los comandos recibidos durante la transmisión, de la siguiente manera :
connect 
-> 
releaseStream 
-> 
FCPublish 
-> 
createStream
-> 
publish
-> 
MetaDzta
-> 
然后就是音频视频的chunk包
  • Abra el método doConnect para ver qué hizo lalserver después de recibir el comando de conexión de FFmpeg, de la siguiente manera, se puede ver que appName (en vivo en este ejemplo) y tcUrl (rtmp://127.0.0.1 en este ejemplo) se obtienen de los parámetros del comando de conexión: 1935/live), y luego responder continuamente mensajes a FFmpeg en forma de comandos
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
}
  • Una vez que se completa la conexión, es el comando createStream. El método doCreateStream correspondiente es el siguiente. Es muy simple, es decir, para responder de inmediato, y el tipo de mensaje sigue siendo createStream. Piénselo también. La información relacionada con la transmisión está lista en el lado lal y también se recibe el comando para crear la transmisión.
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
}
  • El siguiente paso es el comando de publicación, el código no se publicará, principalmente para obtener el nombre de la transmisión, responder en Estado, establecer el tiempo de espera de la conexión, etc. Además, si hay un observador, también enviará publicación. eventos relacionados con él y consumir el evento de publicación El código también merece una mirada En lal/pkg/logic/server_manager__.go#OnNewRtmpPubSession, a continuación, se puede ver que incluye principalmente autenticación, procesamiento de grupo relacionado con el flujo , y notificación de monitoreo externo Además, si la función de grabación está habilitada en la configuración, en el método group.AddRtmpPubSession, se realizarán las operaciones de inicialización pertinentes
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
}
  • El siguiente es el procesamiento de mensajes del tipo Metadatos. El mensaje contiene parámetros detallados del flujo que se enviará, como el ancho y el alto, la velocidad de bits, la velocidad de fotogramas, etc. El método correspondiente es doDataMessageAmf0. Lo más importante de este método es para ejecutar Notificado, el método correspondiente está en lal/pkg/logic/group__core_streaming.go#OnReadRtmpAvMsg,
  • El método OnReadRtmpAvMsg llamará a broadcastByRtmpMsg, que es el punto clave. Comenzará a enviar transmisiones de acuerdo con los suscriptores guardados en el grupo, para que todos los suscriptores puedan obtener parámetros detallados del flujo de medios (el código aquí es muy complicado e involucra diferentes protocolos (grabación, retuiteo, streaming, etc.), en la escena del streaming, este código se puede centrar en
    inserte la descripción de la imagen aquí
  • Espere hasta que se respondan todos los comandos anteriores, lo que significa que el trabajo de preparación se ha completado y puede esperar la llegada de los datos del flujo de medios.

Manejo de mensajes de audio y video.

  • Regrese al método doMsg para ver el procesamiento de los mensajes de audio y video, como se muestra en la figura a continuación, primero realice una verificación básica y luego transfiéralo a lal/pkg/logic/group__core_streaming.go#OnReadRtmpAvMsg para su procesamiento.
    inserte la descripción de la imagen aquí
  • Es el método OnReadRtmpAvMsg nuevamente. Era el que respondía a los metadatos antes. Ahora sigue siendo el que maneja los mensajes de audio y video. Entonces es fácil leer el código, y todavía se entrega al método broadcastByRtmpMsg para su procesamiento.
  • En broadcastByRtmpMsg, concéntrese en lal/pkg/remux/gop_cache.go#Feed, que almacenará en caché el encabezado de secuencia y el contenido del fotograma clave.
  • En el método broadcastByRtmpMsg, hay dos códigos más importantes en este artículo (creo que sí)
  • En primer lugar, como se muestra en la figura a continuación, si se trata de una nueva solicitud de transmisión, los atributos de transmisión multimedia se escribirán en el extremo de la transmisión, y el cuadro clave y su información de encabezado de secuencia se almacenarán en caché, el resultado es que el extremo de la transmisión tiene acaba de establecer una conexión Puede obtener el cuadro clave (obtener rápidamente el efecto del primer cuadro), sin esperar el último cuadro clave empujado por el transmisor, después de todo, un gop puede llevar mucho tiempo
    inserte la descripción de la imagen aquí
  • El segundo código clave es como se muestra en la figura a continuación, llamando a write2RtmpSubSessions para reenviar los mensajes de audio y video recibidos esta vez.
    inserte la descripción de la imagen aquí
  • Expanda write2RtmpSubSessions, de repente se dio cuenta, creo personalmente, este es el código central de la transmisión push-pull, escriba cada dato de audio y video recibido directamente desde la conexión TCP del final de la transmisión (expanda el método session.Write para ver)
func (group *Group) write2RtmpSubSessions(b []byte) {
    
    
	for session := range group.rtmpSubSessionSet {
    
    
		if session.IsFresh || session.ShouldWaitVideoKeyFrame {
    
    
			continue
		}
		_ = session.Write(b)
	}
}

Manejo del final de la transmisión

  • Si usa ctrl+c para finalizar la transmisión FFmpeg, ¿qué pasará con lal? Es mejor ser perezoso y leer el registro primero.
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
  • Se puede ver en el registro anterior que lal recibirá FCUnpublish, deleteStream y otros comandos de FFmpeg, pero lal ignorará estos comandos, como se muestra en la figura a continuación, pero espere hasta que ocurra el error EOF en la conexión TCP, comience desde el error
    inserte la descripción de la imagen aquí
  • Específicamente, el código para manejar los errores de TCP y finalizar la transmisión se muestra en la figura a continuación.
    inserte la descripción de la imagen aquí
  • Hasta ahora, se completó el estudio del código fuente relevante del servicio de transmisión. Con la ayuda de lal, aprendí los detalles del servicio de transmisión rtmp por primera vez. Las habilidades básicas son sólidas y el próximo aprendizaje será más A continuación, desafiemos otra función básica: transmisión rtmp

No estás solo, Xinchen Original está contigo todo el camino

  1. serie Java
  2. serie primavera
  3. Serie Docker
  4. serie kubernetes
  5. Serie de base de datos + middleware
  6. Serie DevOps

Supongo que te gusta

Origin blog.csdn.net/boling_cavalry/article/details/129912667
Recomendado
Clasificación