// // Copyright (c) 2013-2025 The SRS Authors // // SPDX-License-Identifier: MIT // #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; #define SRS_GB_MAX_RECOVER 16 #define SRS_GB_MAX_TIMEOUT 3 #define SRS_GB_LARGE_PACKET 1500 #define SRS_GB_SESSION_DRIVE_INTERVAL (300 * SRS_UTIME_MILLISECONDS) extern bool srs_is_rtcp(const uint8_t *data, size_t len); std::string srs_gb_session_state(SrsGbSessionState state) { switch (state) { case SrsGbSessionStateInit: return "Init"; case SrsGbSessionStateConnecting: return "Connecting"; case SrsGbSessionStateEstablished: return "Established"; default: return "Invalid"; } } std::string srs_gb_state(SrsGbSessionState ostate, SrsGbSessionState state) { return srs_fmt_sprintf("%s->%s", srs_gb_session_state(ostate).c_str(), srs_gb_session_state(state).c_str()); } SrsGbSession::SrsGbSession() : media_(new SrsGbMediaTcpConn()) { wrapper_ = NULL; owner_coroutine_ = NULL; owner_cid_ = NULL; muxer_ = new SrsGbMuxer(this); state_ = SrsGbSessionStateInit; connecting_starttime_ = 0; nn_timeout_ = 0; reinviting_starttime_ = 0; ppp_ = new SrsAlonePithyPrint(); startime_ = srs_time_now_realtime(); total_packs_ = 0; total_msgs_ = 0; total_recovered_ = 0; total_msgs_dropped_ = 0; total_reserved_ = 0; media_id_ = 0; media_msgs_ = 0; media_packs_ = 0; media_starttime_ = startime_; media_recovered_ = 0; media_msgs_dropped_ = 0; media_reserved_ = 0; cid_ = _srs_context->generate_id(); _srs_context->set_id(cid_); // Also change current coroutine cid as session's. } SrsGbSession::~SrsGbSession() { srs_freep(muxer_); srs_freep(ppp_); } void SrsGbSession::setup(SrsConfDirective *conf) { std::string output = _srs_config->get_stream_caster_output(conf); muxer_->setup(output); srs_trace("Session: Start output=%s", output.c_str()); } void SrsGbSession::setup_owner(SrsSharedResource *wrapper, ISrsInterruptable *owner_coroutine, ISrsContextIdSetter *owner_cid) { wrapper_ = wrapper; owner_coroutine_ = owner_coroutine; owner_cid_ = owner_cid; } void SrsGbSession::on_executor_done(ISrsInterruptable *executor) { owner_coroutine_ = NULL; } void SrsGbSession::on_ps_pack(SrsPackContext *ctx, SrsPsPacket *ps, const std::vector &msgs) { // Got a new context, that is new media transport. if (media_id_ != ctx->media_id_) { total_msgs_ += media_msgs_; total_packs_ += media_packs_; total_recovered_ += media_recovered_; total_msgs_dropped_ += media_msgs_dropped_; total_reserved_ += media_reserved_; media_msgs_ = media_packs_ = 0; media_recovered_ = media_msgs_dropped_ = 0; media_reserved_ = 0; } // Update data for current context. media_id_ = ctx->media_id_; media_packs_++; media_msgs_ += msgs.size(); media_starttime_ = ctx->media_startime_; media_recovered_ = ctx->media_nn_recovered_; media_msgs_dropped_ = ctx->media_nn_msgs_dropped_; media_reserved_ = ctx->media_reserved_; // Group all video in pack to a video frame, because only allows one video for each PS pack. SrsUniquePtr video(new SrsTsMessage()); for (vector::const_iterator it = msgs.begin(); it != msgs.end(); ++it) { SrsTsMessage *msg = *it; // Group all videos to one video. if (msg->sid_ == SrsTsPESStreamIdVideoCommon) { video->ps_helper_ = msg->ps_helper_; video->dts_ = msg->dts_; video->pts_ = msg->pts_; video->sid_ = msg->sid_; video->payload_->append(msg->payload_); continue; } // Directly mux audio message. srs_error_t err = muxer_->on_ts_message(msg); if (err != srs_success) { srs_warn("Muxer: Ignore audio err %s", srs_error_desc(err).c_str()); srs_freep(err); } } // Send the generated video message. if (video->payload_->length() > 0) { srs_error_t err = muxer_->on_ts_message(video.get()); if (err != srs_success) { srs_warn("Muxer: Ignore video err %s", srs_error_desc(err).c_str()); srs_freep(err); } } } void SrsGbSession::on_media_transport(SrsSharedResource media) { media_ = media; // Change id of SIP and all its child coroutines. media_->set_cid(cid_); } srs_error_t SrsGbSession::cycle() { srs_error_t err = srs_success; // Update all context id to cid of session. _srs_context->set_id(cid_); owner_cid_->set_cid(cid_); media_->set_cid(cid_); // Drive the session cycle. err = do_cycle(); // Interrupt the media transport when session terminated. media_->interrupt(); // success. if (err == srs_success) { srs_trace("client finished."); return err; } // It maybe success with message. if (srs_error_code(err) == ERROR_SUCCESS) { srs_trace("client finished %s.", srs_error_summary(err).c_str()); srs_freep(err); return err; } // client close peer. // TODO: FIXME: Only reset the error when client closed it. if (srs_is_client_gracefully_close(err)) { srs_warn("client disconnect peer. ret=%d", srs_error_code(err)); } else if (srs_is_server_gracefully_close(err)) { srs_warn("server disconnect. ret=%d", srs_error_code(err)); } else { srs_error("serve error %s", srs_error_desc(err).c_str()); } srs_freep(err); return srs_success; } srs_error_t SrsGbSession::do_cycle() { srs_error_t err = srs_success; while (true) { if (!owner_coroutine_) return err; if ((err = owner_coroutine_->pull()) != srs_success) { return srs_error_wrap(err, "pull"); } // Drive the state in a fixed interval. srs_usleep(SRS_GB_SESSION_DRIVE_INTERVAL); // Regular state, driven by state of transport. if ((err = drive_state()) != srs_success) { return srs_error_wrap(err, "drive"); } ppp_->elapse(); if (ppp_->can_print()) { int alive = srsu2msi(srs_time_now_realtime() - startime_) / 1000; int pack_alive = srsu2msi(srs_time_now_realtime() - media_starttime_) / 1000; srs_trace("Session: Alive=%ds, packs=%" PRId64 ", recover=%" PRId64 ", reserved=%" PRId64 ", msgs=%" PRId64 ", drop=%" PRId64 ", media(id=%u, alive=%ds, packs=%" PRId64 " recover=%" PRId64 ", reserved=%" PRId64 ", msgs=%" PRId64 ", drop=%" PRId64 ")", alive, (total_packs_ + media_packs_), (total_recovered_ + media_recovered_), (total_reserved_ + media_reserved_), (total_msgs_ + media_msgs_), (total_msgs_dropped_ + media_msgs_dropped_), media_id_, pack_alive, media_packs_, media_recovered_, media_reserved_, media_msgs_, media_msgs_dropped_); } } return err; } srs_error_t SrsGbSession::drive_state() { srs_error_t err = srs_success; #define SRS_GB_CHANGE_STATE_TO(state) \ { \ SrsGbSessionState ostate = set_state(state); \ srs_trace("Session: Change device=%s, state=%s", device_id_.c_str(), \ srs_gb_state(ostate, state_).c_str()); \ } if (state_ == SrsGbSessionStateInit) { // With external SIP server, we assume media connection is handled externally if (media_->is_connected()) { SRS_GB_CHANGE_STATE_TO(SrsGbSessionStateEstablished); } } if (state_ == SrsGbSessionStateEstablished) { // Simple state management - just check if media is still connected if (!media_->is_connected()) { SRS_GB_CHANGE_STATE_TO(SrsGbSessionStateInit); } } return err; } SrsGbSessionState SrsGbSession::set_state(SrsGbSessionState v) { SrsGbSessionState state = state_; state_ = v; return state; } const SrsContextId &SrsGbSession::get_id() { return cid_; } std::string SrsGbSession::desc() { return "GBS"; } SrsGbListener::SrsGbListener() { conf_ = NULL; media_listener_ = new SrsTcpListener(this); } SrsGbListener::~SrsGbListener() { srs_freep(conf_); srs_freep(media_listener_); } srs_error_t SrsGbListener::initialize(SrsConfDirective *conf) { srs_error_t err = srs_success; srs_freep(conf_); conf_ = conf->copy(); string ip = srs_net_address_any(); if (true) { int port = _srs_config->get_stream_caster_listen(conf); media_listener_->set_endpoint(ip, port)->set_label("GB-TCP"); } return err; } srs_error_t SrsGbListener::listen() { srs_error_t err = srs_success; if ((err = media_listener_->listen()) != srs_success) { return srs_error_wrap(err, "listen"); } if ((err = listen_api()) != srs_success) { return srs_error_wrap(err, "listen api"); } return err; } srs_error_t SrsGbListener::listen_api() { srs_error_t err = srs_success; ISrsHttpServeMux *mux = _srs_server->api_server(); if ((err = mux->handle("/gb/v1/publish/", new SrsGoApiGbPublish(conf_))) != srs_success) { return srs_error_wrap(err, "handle publish"); } return err; } void SrsGbListener::close() { } srs_error_t SrsGbListener::on_tcp_client(ISrsListener *listener, srs_netfd_t stfd) { srs_error_t err = srs_success; // Handle TCP connections. if (listener == media_listener_) { SrsGbMediaTcpConn *raw_conn = new SrsGbMediaTcpConn(); raw_conn->setup(stfd); SrsSharedResource *conn = new SrsSharedResource(raw_conn); _srs_gb_manager->add(conn, NULL); SrsExecutorCoroutine *executor = new SrsExecutorCoroutine(_srs_gb_manager, conn, raw_conn, raw_conn); raw_conn->setup_owner(conn, executor, executor); if ((err = executor->start()) != srs_success) { srs_freep(executor); return srs_error_wrap(err, "gb media"); } } else { srs_warn("GB: Ignore TCP client"); srs_close_stfd(stfd); } return err; } ISrsPsPackHandler::ISrsPsPackHandler() { } ISrsPsPackHandler::~ISrsPsPackHandler() { } SrsGbMediaTcpConn::SrsGbMediaTcpConn() { pack_ = new SrsPackContext(this); buffer_ = new uint8_t[65535]; conn_ = NULL; wrapper_ = NULL; owner_coroutine_ = NULL; owner_cid_ = NULL; cid_ = _srs_context->get_id(); session_ = NULL; connected_ = false; nn_rtcp_ = 0; } SrsGbMediaTcpConn::~SrsGbMediaTcpConn() { srs_freep(conn_); srs_freepa(buffer_); srs_freep(pack_); } void SrsGbMediaTcpConn::setup(srs_netfd_t stfd) { srs_freep(conn_); conn_ = new SrsTcpConnection(stfd); } void SrsGbMediaTcpConn::setup_owner(SrsSharedResource *wrapper, ISrsInterruptable *owner_coroutine, ISrsContextIdSetter *owner_cid) { wrapper_ = wrapper; owner_coroutine_ = owner_coroutine; owner_cid_ = owner_cid; } void SrsGbMediaTcpConn::on_executor_done(ISrsInterruptable *executor) { owner_coroutine_ = NULL; } bool SrsGbMediaTcpConn::is_connected() { return connected_; } void SrsGbMediaTcpConn::interrupt() { if (owner_coroutine_) owner_coroutine_->interrupt(); } void SrsGbMediaTcpConn::set_cid(const SrsContextId &cid) { if (owner_cid_) owner_cid_->set_cid(cid); cid_ = cid; } const SrsContextId &SrsGbMediaTcpConn::get_id() { return cid_; } std::string SrsGbMediaTcpConn::desc() { return "GB-Media-TCP"; } srs_error_t SrsGbMediaTcpConn::cycle() { srs_error_t err = do_cycle(); // Should disconnect the TCP connection when stop cycle, especially when we stop first. In this situation, the // connection won't be closed because it's shared by other objects. srs_freep(conn_); // Change state to disconnected. connected_ = false; srs_trace("PS: Media disconnect, code=%d", srs_error_code(err)); // success. if (err == srs_success) { srs_trace("client finished."); return err; } // It maybe success with message. if (srs_error_code(err) == ERROR_SUCCESS) { srs_trace("client finished%s.", srs_error_summary(err).c_str()); srs_freep(err); return err; } // client close peer. // TODO: FIXME: Only reset the error when client closed it. if (srs_is_client_gracefully_close(err)) { srs_warn("client disconnect peer. ret=%d", srs_error_code(err)); } else if (srs_is_server_gracefully_close(err)) { srs_warn("server disconnect. ret=%d", srs_error_code(err)); } else { srs_error("serve error %s", srs_error_desc(err).c_str()); } srs_freep(err); return srs_success; } srs_error_t SrsGbMediaTcpConn::do_cycle() { srs_error_t err = srs_success; // The PS context to decode all PS packets. SrsRecoverablePsContext context; // If bytes is not enough(defined by SRS_PS_MIN_REQUIRED), ignore. context.ctx_.set_detect_ps_integrity(true); // Previous left bytes, to parse in next loop. uint32_t reserved = 0; for (;;) { if (!owner_coroutine_) return err; if ((err = owner_coroutine_->pull()) != srs_success) { return srs_error_wrap(err, "pull"); } // RFC4571, 2 bytes length. uint16_t length = 0; if (true) { uint8_t lbuffer[2]; if ((err = conn_->read_fully(lbuffer, sizeof(lbuffer), NULL)) != srs_success) { return srs_error_wrap(err, "read"); } length = ((uint16_t)lbuffer[0]) << 8 | (uint16_t)lbuffer[1]; if (!length) { return srs_error_new(ERROR_GB_PS_MEDIA, "Invalid length"); } } if (length > SRS_GB_LARGE_PACKET) { const SrsPsDecodeHelper &h = context.ctx_.helper_; srs_warn("PS: Large length=%u, previous-seq=%u, previous-ts=%u", length, h.rtp_seq_, h.rtp_ts_); } // Read length of bytes of RTP packet. if ((err = conn_->read_fully(buffer_ + reserved, length, NULL)) != srs_success) { return srs_error_wrap(err, "read"); } // Drop all RTCP packets. if (srs_is_rtcp(buffer_ + reserved, length)) { nn_rtcp_++; srs_warn("PS: Drop RTCP packets nn=%d", nn_rtcp_); continue; } // If no session, try to finger out it. if (!session_) { SrsRtpPacket rtp; SrsBuffer b((char *)(buffer_ + reserved), length); if ((err = rtp.decode(&b)) != srs_success) { srs_warn("PS: Ignore packet length=%d for err %s", length, srs_error_desc(err).c_str()); srs_freep(err); // We ignore any error when decoding the RTP packet. continue; } if ((err = bind_session(rtp.header_.get_ssrc(), &session_)) != srs_success) { return srs_error_wrap(err, "bind session"); } } if (!session_) { srs_warn("PS: Ignore packet length=%d for no session", length); continue; // Ignore any media packet when no session. } // Show tips about the buffer to parse. if (reserved) { string bytes = srs_strings_dumps_hex((const char *)(buffer_ + reserved), length, 16); srs_trace("PS: Consume reserved=%dB, length=%d, bytes=[%s]", reserved, length, bytes.c_str()); } // Parse RTP over TCP, RFC4571. SrsBuffer b((char *)buffer_, length + reserved); if ((err = context.decode_rtp(&b, reserved, pack_)) != srs_success) { return srs_error_wrap(err, "decode pack"); } // There might some messages left to parse in next loop. reserved = b.left(); if (reserved > 128) { srs_warn("PS: Drop too many reserved=%d bytes", reserved); reserved = 0; // Avoid reserving too much data. } if (reserved) { string bytes = srs_strings_dumps_hex(b.head(), reserved, 16); srs_trace("PS: Reserved bytes for next loop, pos=%d, left=%d, total=%d, bytes=[%s]", b.pos(), b.left(), b.size(), bytes.c_str()); // Copy the bytes left to the start of buffer. Note that the left(reserved) bytes might be overlapped with // buffer, so we must use memmove not memcpy, see https://github.com/ossrs/srs/issues/3300#issuecomment-1352907075 memmove(buffer_, b.head(), reserved); pack_->media_reserved_++; } } return err; } srs_error_t SrsGbMediaTcpConn::on_ps_pack(SrsPsPacket *ps, const std::vector &msgs) { srs_error_t err = srs_success; // Change state to connected. if (!connected_) { connected_ = true; srs_trace("PS: Media connected"); } // Notify session about the media pack. session_->on_ps_pack(pack_, ps, msgs); // for (vector::const_iterator it = msgs.begin(); it != msgs.end(); ++it) { // SrsTsMessage* msg = *it; // uint8_t* p = (uint8_t*)msg->payload->bytes(); // srs_trace("PS: Handle message %s, dts=%" PRId64 ", payload=%dB, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x", // msg->is_video() ? "Video" : "Audio", msg->dts, msg->PES_packet_length, // p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7]); // } return err; } srs_error_t SrsGbMediaTcpConn::bind_session(uint32_t ssrc, SrsGbSession **psession) { srs_error_t err = srs_success; if (!ssrc) return err; // Find exists session for register, might be created by another object and still alive. SrsSharedResource *session = dynamic_cast *>(_srs_gb_manager->find_by_fast_id(ssrc)); if (!session) return err; SrsGbSession *raw_session = (*session).get(); srs_assert(raw_session); // Notice session to use current media connection. raw_session->on_media_transport(*wrapper_); *psession = raw_session; return err; } SrsMpegpsQueue::SrsMpegpsQueue() { nb_audios_ = nb_videos_ = 0; } SrsMpegpsQueue::~SrsMpegpsQueue() { std::map::iterator it; for (it = msgs_.begin(); it != msgs_.end(); ++it) { SrsMediaPacket *msg = it->second; srs_freep(msg); } msgs_.clear(); } srs_error_t SrsMpegpsQueue::push(SrsMediaPacket *msg) { srs_error_t err = srs_success; // TODO: FIXME: use right way. for (int i = 0; i < 10; i++) { if (msgs_.find(msg->timestamp_) == msgs_.end()) { break; } // adjust the ts, add 1ms. msg->timestamp_ += 1; if (i >= 100) { srs_warn("Muxer: free the msg for dts exists, dts=%" PRId64, msg->timestamp_); srs_freep(msg); return err; } } if (msg->is_audio()) { nb_audios_++; } if (msg->is_video()) { nb_videos_++; } msgs_[msg->timestamp_] = msg; return err; } SrsMediaPacket *SrsMpegpsQueue::dequeue() { // got 2+ videos and audios, ok to dequeue. bool av_ok = nb_videos_ >= 2 && nb_audios_ >= 2; // 100 videos about 30s, while 300 audios about 30s bool av_overflow = nb_videos_ > 100 || nb_audios_ > 300; if (av_ok || av_overflow) { std::map::iterator it = msgs_.begin(); SrsMediaPacket *msg = it->second; msgs_.erase(it); if (msg->is_audio()) { nb_audios_--; } if (msg->is_video()) { nb_videos_--; } return msg; } return NULL; } SrsGbMuxer::SrsGbMuxer(SrsGbSession *session) { sdk_ = NULL; session_ = session; avc_ = new SrsRawH264Stream(); h264_sps_changed_ = false; h264_pps_changed_ = false; h264_sps_pps_sent_ = false; hevc_ = new SrsRawHEVCStream(); vps_sps_pps_sent_ = false; vps_sps_pps_change_ = false; aac_ = new SrsRawAacStream(); queue_ = new SrsMpegpsQueue(); pprint_ = SrsPithyPrint::create_caster(); } SrsGbMuxer::~SrsGbMuxer() { close(); srs_freep(avc_); srs_freep(hevc_); srs_freep(aac_); srs_freep(queue_); srs_freep(pprint_); } void SrsGbMuxer::setup(std::string output) { output_ = output; } srs_error_t SrsGbMuxer::on_ts_message(SrsTsMessage *msg) { srs_error_t err = srs_success; SrsBuffer avs(msg->payload_->bytes(), msg->payload_->length()); if (msg->sid_ == SrsTsPESStreamIdVideoCommon) { if ((err = on_ts_video(msg, &avs)) != srs_success) { return srs_error_wrap(err, "ts: consume video"); } } else { if ((err = on_ts_audio(msg, &avs)) != srs_success) { return srs_error_wrap(err, "ts: consume audio"); } } return err; } srs_error_t SrsGbMuxer::on_ts_video(SrsTsMessage *msg, SrsBuffer *avs) { srs_error_t err = srs_success; // ensure rtmp connected. if ((err = connect()) != srs_success) { return srs_error_wrap(err, "connect"); } SrsPsDecodeHelper *h = (SrsPsDecodeHelper *)msg->ps_helper_; srs_assert(h && h->ctx_ && h->ps_); if (h->ctx_->video_stream_type_ == SrsTsStreamVideoH264) { if ((err = mux_h264(msg, avs)) != srs_success) { return srs_error_wrap(err, "mux h264"); } } else if (h->ctx_->video_stream_type_ == SrsTsStreamVideoHEVC) { if ((err = mux_h265(msg, avs)) != srs_success) { return srs_error_wrap(err, "mux hevc"); } } else { return srs_error_new(ERROR_STREAM_CASTER_TS_CODEC, "ts: unsupported stream codec=%d", h->ctx_->video_stream_type_); } return err; } srs_error_t SrsGbMuxer::mux_h264(SrsTsMessage *msg, SrsBuffer *avs) { srs_error_t err = srs_success; // ts tbn to flv tbn. uint32_t dts = (uint32_t)(msg->dts_ / 90); uint32_t pts = (uint32_t)(msg->dts_ / 90); // send each frame. while (!avs->empty()) { char *frame = NULL; int frame_size = 0; if ((err = avc_->annexb_demux(avs, &frame, &frame_size)) != srs_success) { return srs_error_wrap(err, "demux avc annexb"); } // 5bits, 7.3.1 NAL unit syntax, // ISO_IEC_14496-10-AVC-2003.pdf, page 44. // 7: SPS, 8: PPS, 5: I Frame, 1: P Frame SrsAvcNaluType nt = (SrsAvcNaluType)(frame[0] & 0x1f); // Ignore the nalu except video frames: // 7: SPS, 8: PPS, 5: I Frame, 1: P Frame, 6: SEI, 9: AUD if ( nt != SrsAvcNaluTypeSPS && nt != SrsAvcNaluTypePPS && nt != SrsAvcNaluTypeIDR && nt != SrsAvcNaluTypeNonIDR && nt != SrsAvcNaluTypeSEI && nt != SrsAvcNaluTypeAccessUnitDelimiter) { string bytes = srs_strings_dumps_hex(frame, frame_size, 4); srs_warn("GB: Ignore NALU nt=%d, frame=[%s]", nt, bytes.c_str()); return err; } if (nt == SrsAvcNaluTypeSEI || nt == SrsAvcNaluTypeAccessUnitDelimiter) { continue; } // for sps if (avc_->is_sps(frame, frame_size)) { std::string sps; if ((err = avc_->sps_demux(frame, frame_size, sps)) != srs_success) { return srs_error_wrap(err, "demux sps"); } if (h264_sps_ == sps) { continue; } h264_sps_changed_ = true; h264_sps_ = sps; if ((err = write_h264_sps_pps(dts, pts)) != srs_success) { return srs_error_wrap(err, "write sps/pps"); } continue; } // for pps if (avc_->is_pps(frame, frame_size)) { std::string pps; if ((err = avc_->pps_demux(frame, frame_size, pps)) != srs_success) { return srs_error_wrap(err, "demux pps"); } if (h264_pps_ == pps) { continue; } h264_pps_changed_ = true; h264_pps_ = pps; if ((err = write_h264_sps_pps(dts, pts)) != srs_success) { return srs_error_wrap(err, "write sps/pps"); } continue; } // ibp frame. // TODO: FIXME: we should group all frames to a rtmp/flv message from one ts message. srs_info("Muxer: demux avc ibp frame size=%d, dts=%d", frame_size, dts); if ((err = write_h264_ipb_frame(frame, frame_size, dts, pts)) != srs_success) { return srs_error_wrap(err, "write frame"); } } return err; } srs_error_t SrsGbMuxer::write_h264_sps_pps(uint32_t dts, uint32_t pts) { srs_error_t err = srs_success; // TODO: FIMXE: there exists bug, see following comments. // when sps or pps changed, update the sequence header, // for the pps maybe not changed while sps changed. // so, we must check when each video ts message frame parsed. if (!h264_sps_changed_ || !h264_pps_changed_) { return err; } // h264 raw to h264 packet. std::string sh; if ((err = avc_->mux_sequence_header(h264_sps_, h264_pps_, sh)) != srs_success) { return srs_error_wrap(err, "mux sequence header"); } // h264 packet to flv packet. int8_t frame_type = SrsVideoAvcFrameTypeKeyFrame; int8_t avc_packet_type = SrsVideoAvcFrameTraitSequenceHeader; char *flv = NULL; int nb_flv = 0; if ((err = avc_->mux_avc2flv(sh, frame_type, avc_packet_type, dts, pts, &flv, &nb_flv)) != srs_success) { return srs_error_wrap(err, "avc to flv"); } // the timestamp in rtmp message header is dts. uint32_t timestamp = dts; if ((err = rtmp_write_packet(SrsFrameTypeVideo, timestamp, flv, nb_flv)) != srs_success) { return srs_error_wrap(err, "write packet"); } // reset sps and pps. h264_sps_changed_ = false; h264_pps_changed_ = false; h264_sps_pps_sent_ = true; return err; } srs_error_t SrsGbMuxer::write_h264_ipb_frame(char *frame, int frame_size, uint32_t dts, uint32_t pts) { srs_error_t err = srs_success; // when sps or pps not sent, ignore the packet. if (!h264_sps_pps_sent_) { return srs_error_new(ERROR_H264_DROP_BEFORE_SPS_PPS, "drop for no sps/pps"); } // 5bits, 7.3.1 NAL unit syntax, // ISO_IEC_14496-10-AVC-2003.pdf, page 44. // 7: SPS, 8: PPS, 5: I Frame, 1: P Frame SrsAvcNaluType nal_unit_type = (SrsAvcNaluType)(frame[0] & 0x1f); // for IDR frame, the frame is keyframe. SrsVideoAvcFrameType frame_type = SrsVideoAvcFrameTypeInterFrame; if (nal_unit_type == SrsAvcNaluTypeIDR) { frame_type = SrsVideoAvcFrameTypeKeyFrame; } std::string ibp; if ((err = avc_->mux_ipb_frame(frame, frame_size, ibp)) != srs_success) { return srs_error_wrap(err, "mux frame"); } int8_t avc_packet_type = SrsVideoAvcFrameTraitNALU; char *flv = NULL; int nb_flv = 0; if ((err = avc_->mux_avc2flv(ibp, frame_type, avc_packet_type, dts, pts, &flv, &nb_flv)) != srs_success) { return srs_error_wrap(err, "mux avc to flv"); } // the timestamp in rtmp message header is dts. uint32_t timestamp = dts; return rtmp_write_packet(SrsFrameTypeVideo, timestamp, flv, nb_flv); } srs_error_t SrsGbMuxer::mux_h265(SrsTsMessage *msg, SrsBuffer *avs) { srs_error_t err = srs_success; // ts tbn to flv tbn. uint32_t dts = (uint32_t)(msg->dts_ / 90); uint32_t pts = (uint32_t)(msg->dts_ / 90); // send each frame. while (!avs->empty()) { char *frame = NULL; int frame_size = 0; if ((err = hevc_->annexb_demux(avs, &frame, &frame_size)) != srs_success) { return srs_error_wrap(err, "demux hevc annexb"); } // 6bits, 7.4.2.2 NAL unit header semantics // ITU-T-H.265-2021.pdf, page 85. // 32: VPS, 33: SPS, 34: PPS ... SrsHevcNaluType nt = SrsHevcNaluTypeParse(frame[0]); if (nt == SrsHevcNaluType_SEI || nt == SrsHevcNaluType_SEI_SUFFIX || nt == SrsHevcNaluType_ACCESS_UNIT_DELIMITER) { continue; } // for vps if (hevc_->is_vps(frame, frame_size)) { std::string vps; if ((err = hevc_->vps_demux(frame, frame_size, vps)) != srs_success) { return srs_error_wrap(err, "demux vps"); } if (h265_vps_ == vps) { continue; } vps_sps_pps_change_ = true; h265_vps_ = vps; if ((err = write_h265_vps_sps_pps(dts, pts)) != srs_success) { return srs_error_wrap(err, "write vps"); } continue; } // for sps if (hevc_->is_sps(frame, frame_size)) { std::string sps; if ((err = hevc_->sps_demux(frame, frame_size, sps)) != srs_success) { return srs_error_wrap(err, "demux sps"); } if (h265_sps_ == sps) { continue; } vps_sps_pps_change_ = true; h265_sps_ = sps; if ((err = write_h265_vps_sps_pps(dts, pts)) != srs_success) { return srs_error_wrap(err, "write sps"); } continue; } // for pps if (hevc_->is_pps(frame, frame_size)) { std::string pps; if ((err = hevc_->pps_demux(frame, frame_size, pps)) != srs_success) { return srs_error_wrap(err, "demux pps"); } if (h265_pps_ == pps) { continue; } vps_sps_pps_change_ = true; h265_pps_ = pps; if ((err = write_h265_vps_sps_pps(dts, pts)) != srs_success) { return srs_error_wrap(err, "write pps"); } continue; } // ibp frame. // TODO: FIXME: we should group all frames to a rtmp/flv message from one ts message. srs_info("Muxer: demux avc ibp frame size=%d, dts=%d", frame_size, dts); if ((err = write_h265_ipb_frame(frame, frame_size, dts, pts)) != srs_success) { return srs_error_wrap(err, "write frame"); } } return err; } srs_error_t SrsGbMuxer::write_h265_vps_sps_pps(uint32_t dts, uint32_t pts) { srs_error_t err = srs_success; if (!vps_sps_pps_change_) { return err; } if (h265_vps_.empty() || h265_sps_.empty() || h265_pps_.empty()) { return err; } std::vector h265_pps; h265_pps.push_back(h265_pps_); std::string sh; if ((err = hevc_->mux_sequence_header(h265_vps_, h265_sps_, h265_pps, sh)) != srs_success) { return srs_error_wrap(err, "hevc mux sequence header"); } // h265 packet to flv packet. int8_t frame_type = SrsVideoAvcFrameTypeKeyFrame; int8_t hevc_packet_type = SrsVideoAvcFrameTraitSequenceHeader; char *flv = NULL; int nb_flv = 0; if ((err = hevc_->mux_hevc2flv(sh, frame_type, hevc_packet_type, dts, pts, &flv, &nb_flv)) != srs_success) { return srs_error_wrap(err, "hevc to flv"); } // the timestamp in rtmp message header is dts. uint32_t timestamp = dts; if ((err = rtmp_write_packet(SrsFrameTypeVideo, timestamp, flv, nb_flv)) != srs_success) { return srs_error_wrap(err, "hevc write packet"); } // reset vps/sps/pps. vps_sps_pps_change_ = false; vps_sps_pps_sent_ = true; return err; } srs_error_t SrsGbMuxer::write_h265_ipb_frame(char *frame, int frame_size, uint32_t dts, uint32_t pts) { srs_error_t err = srs_success; // when sps or pps not sent, ignore the packet. if (!vps_sps_pps_sent_) { return srs_error_new(ERROR_H264_DROP_BEFORE_SPS_PPS, "drop for no vps/sps/pps"); } SrsHevcNaluType nt = SrsHevcNaluTypeParse(frame[0]); // F.3.29 intra random access point (IRAP) picture // ITU-T-H.265-2021.pdf, page 462. SrsVideoAvcFrameType frame_type = SrsVideoAvcFrameTypeInterFrame; if (SrsIsIRAP(nt)) { frame_type = SrsVideoAvcFrameTypeKeyFrame; } string ipb; if ((err = hevc_->mux_ipb_frame(frame, frame_size, ipb)) != srs_success) { return srs_error_wrap(err, "hevc mux ipb frame"); } int8_t hevc_packet_type = SrsVideoAvcFrameTraitNALU; char *flv = NULL; int nb_flv = 0; if ((err = hevc_->mux_hevc2flv(ipb, frame_type, hevc_packet_type, dts, pts, &flv, &nb_flv)) != srs_success) { return srs_error_wrap(err, "hevc to flv"); } // the timestamp in rtmp message header is dts. uint32_t timestamp = dts; if ((err = rtmp_write_packet(SrsFrameTypeVideo, timestamp, flv, nb_flv)) != srs_success) { return srs_error_wrap(err, "hevc write packet"); } return err; } srs_error_t SrsGbMuxer::on_ts_audio(SrsTsMessage *msg, SrsBuffer *avs) { srs_error_t err = srs_success; // ensure rtmp connected. if ((err = connect()) != srs_success) { return srs_error_wrap(err, "connect"); } // ts tbn to flv tbn. uint32_t dts = (uint32_t)(msg->dts_ / 90); // send each frame. while (!avs->empty()) { char *frame = NULL; int frame_size = 0; SrsRawAacStreamCodec codec; if ((err = aac_->adts_demux(avs, &frame, &frame_size, codec)) != srs_success) { return srs_error_wrap(err, "demux adts"); } // ignore invalid frame, // * atleast 1bytes for aac to decode the data. if (frame_size <= 0) { continue; } srs_info("Muxer: demux aac frame size=%d, dts=%d", frame_size, dts); // generate sh. if (aac_specific_config_.empty()) { std::string sh; if ((err = aac_->mux_sequence_header(&codec, sh)) != srs_success) { return srs_error_wrap(err, "mux sequence header"); } aac_specific_config_ = sh; codec.aac_packet_type_ = 0; if ((err = write_audio_raw_frame((char *)sh.data(), (int)sh.length(), &codec, dts)) != srs_success) { return srs_error_wrap(err, "write raw audio frame"); } } // audio raw data. codec.aac_packet_type_ = 1; if ((err = write_audio_raw_frame(frame, frame_size, &codec, dts)) != srs_success) { return srs_error_wrap(err, "write audio raw frame"); } } return err; } srs_error_t SrsGbMuxer::write_audio_raw_frame(char *frame, int frame_size, SrsRawAacStreamCodec *codec, uint32_t dts) { srs_error_t err = srs_success; char *data = NULL; int size = 0; if ((err = aac_->mux_aac2flv(frame, frame_size, codec, dts, &data, &size)) != srs_success) { return srs_error_wrap(err, "mux aac to flv"); } return rtmp_write_packet(SrsFrameTypeAudio, dts, data, size); } srs_error_t SrsGbMuxer::rtmp_write_packet(char type, uint32_t timestamp, char *data, int size) { srs_error_t err = srs_success; if ((err = connect()) != srs_success) { return srs_error_wrap(err, "connect"); } SrsRtmpCommonMessage *cmsg = NULL; if ((err = srs_rtmp_create_msg(type, timestamp, data, size, sdk_->sid(), &cmsg)) != srs_success) { return srs_error_wrap(err, "create message"); } SrsMediaPacket *msg = new SrsMediaPacket(); cmsg->to_msg(msg); srs_freep(cmsg); // push msg to queue. if ((err = queue_->push(msg)) != srs_success) { return srs_error_wrap(err, "push to queue"); } // for all ready msg, dequeue and send out. for (;;) { if ((msg = queue_->dequeue()) == NULL) { break; } if (pprint_->can_print()) { srs_trace("Muxer: send msg %s age=%d, dts=%" PRId64 ", size=%d", msg->is_audio() ? "A" : msg->is_video() ? "V" : "N", pprint_->age(), msg->timestamp_, msg->size()); } // send out encoded msg. if ((err = sdk_->send_and_free_message(msg)) != srs_success) { close(); return srs_error_wrap(err, "send messages"); } } return err; } srs_error_t SrsGbMuxer::connect() { srs_error_t err = srs_success; // Ignore when connected. if (sdk_) { return err; } // Cleanup the data before connect again. close(); string url = srs_strings_replace(output_, "[stream]", session_->device_id_); srs_trace("Muxer: Convert GB to RTMP %s", url.c_str()); srs_utime_t cto = SRS_CONSTS_RTMP_TIMEOUT; srs_utime_t sto = SRS_CONSTS_RTMP_PULSE; sdk_ = new SrsSimpleRtmpClient(url, cto, sto); if ((err = sdk_->connect()) != srs_success) { close(); return srs_error_wrap(err, "connect %s failed, cto=%dms, sto=%dms.", url.c_str(), srsu2msi(cto), srsu2msi(sto)); } if ((err = sdk_->publish(SRS_CONSTS_RTMP_PROTOCOL_CHUNK_SIZE)) != srs_success) { close(); return srs_error_wrap(err, "publish"); } return err; } void SrsGbMuxer::close() { srs_freep(sdk_); // Regenerate the AAC sequence header. aac_specific_config_ = ""; // Wait for the next AVC sequence header. h264_sps_pps_sent_ = false; h264_sps_ = ""; h264_pps_ = ""; } SrsPackContext::SrsPackContext(ISrsPsPackHandler *handler) { static uint32_t gid = 0; media_id_ = ++gid; media_startime_ = srs_time_now_realtime(); media_nn_recovered_ = 0; media_nn_msgs_dropped_ = 0; media_reserved_ = 0; ps_ = new SrsPsPacket(NULL); handler_ = handler; } SrsPackContext::~SrsPackContext() { clear(); srs_freep(ps_); } void SrsPackContext::clear() { for (vector::iterator it = msgs_.begin(); it != msgs_.end(); ++it) { SrsTsMessage *msg = *it; srs_freep(msg); } msgs_.clear(); } srs_error_t SrsPackContext::on_ts_message(SrsTsMessage *msg) { srs_error_t err = srs_success; SrsPsDecodeHelper *h = (SrsPsDecodeHelper *)msg->ps_helper_; srs_assert(h && h->ctx_ && h->ps_); // We got new pack header and an optional system header. // if (ps_->id_ != h->ps_->id_) { // stringstream ss; // if (h->ps_->has_pack_header_) ss << srs_fmt_sprintf(", clock=%" PRId64 ", rate=%d", h->ps_->system_clock_reference_base_, h->ps_->program_mux_rate_); // if (h->ps_->has_system_header_) ss << srs_fmt_sprintf(", rate_bound=%d, video_bound=%d, audio_bound=%d", h->ps_->rate_bound_, h->ps_->video_bound_, h->ps_->audio_bound_); // srs_trace("PS: New pack header=%d, system=%d%s", h->ps_->has_pack_header_, h->ps_->has_system_header_, ss.str().c_str()); //} // Correct DTS/PS to the last one. if (!msgs_.empty() && (!msg->dts_ || !msg->pts_)) { SrsTsMessage *last = msgs_.back(); if (!msg->dts_) msg->dts_ = last->dts_; if (!msg->pts_) msg->pts_ = last->pts_; } // uint8_t* p = (uint8_t*)msg->payload->bytes(); // srs_trace("PS: Got message %s, dts=%" PRId64 ", seq=%u, base=%" PRId64 ", payload=%dB, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x", // msg->is_video() ? "Video" : "Audio", msg->dts, h->rtp_seq_, h->ps_->system_clock_reference_base_, msg->PES_packet_length, // p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7]); // Notify about the previous pack. if (ps_->id_ != h->ps_->id_) { // Handle all messages of previous pack, note that we must free them. if (!msgs_.empty()) { err = handler_->on_ps_pack(ps_, msgs_); clear(); } // Directly copy the pack headers to current. *ps_ = *h->ps_; } // Store the message to current pack. msgs_.push_back(msg->detach()); return err; } void SrsPackContext::on_recover_mode(int nn_recover) { // Only update stat for the first time. if (nn_recover <= 1) { media_nn_recovered_++; } // Always update the stat for messages. if (!msgs_.empty()) { media_nn_msgs_dropped_ += msgs_.size(); clear(); } } SrsRecoverablePsContext::SrsRecoverablePsContext() { recover_ = 0; } SrsRecoverablePsContext::~SrsRecoverablePsContext() { } srs_error_t SrsRecoverablePsContext::decode_rtp(SrsBuffer *stream, int reserved, ISrsPsMessageHandler *handler) { srs_error_t err = srs_success; // Start to parse from the reserved bytes. stream->skip(reserved); SrsRtpPacket rtp; int pos = stream->pos(); if ((err = rtp.decode(stream)) != srs_success) { return enter_recover_mode(stream, handler, pos, srs_error_wrap(err, "decode rtp")); } SrsRtpRawPayload *rtp_raw = dynamic_cast(rtp.payload()); srs_assert(rtp_raw); // It must be a RTP RAW payload, by default. // If got reserved bytes, move to the start of payload. if (reserved) { // Move the reserved bytes to the start of payload, from which we should parse. char *src = stream->head() - stream->pos(); char *dst = stream->head() - reserved; memmove(dst, src, reserved); // The payload also should skip back to the reserved bytes. rtp_raw->payload_ -= reserved; rtp_raw->nn_payload_ += reserved; // The stream also skip back to the not parsed bytes. stream->skip(-1 * reserved); } SrsBuffer b((char *)rtp_raw->payload_, rtp_raw->nn_payload_); // srs_trace("GB: Got RTP length=%d, payload=%d, seq=%u, ts=%d", length, rtp_raw->nn_payload, rtp.header_.get_sequence(), rtp.header_.get_timestamp()); ctx_.helper_.rtp_seq_ = rtp.header_.get_sequence(); ctx_.helper_.rtp_ts_ = rtp.header_.get_timestamp(); ctx_.helper_.rtp_pt_ = rtp.header_.get_payload_type(); if ((err = decode(&b, handler)) != srs_success) { return srs_error_wrap(err, "decode"); } // Consume the stream, because there might be data left in stream. stream->skip(b.pos()); return err; } srs_error_t SrsRecoverablePsContext::decode(SrsBuffer *stream, ISrsPsMessageHandler *handler) { srs_error_t err = srs_success; // Ignore if empty packet. if (stream->empty()) return err; // For recover mode, we drop bytes util pack header(00 00 01 ba). if (recover_) { int pos = stream->pos(); if (!srs_skip_util_pack(stream)) { stream->skip(pos - stream->pos()); return enter_recover_mode(stream, handler, pos, srs_error_new(ERROR_GB_PS_HEADER, "no pack")); } quit_recover_mode(stream, handler); } // Got packet to decode. if ((err = ctx_.decode(stream, handler)) != srs_success) { return enter_recover_mode(stream, handler, stream->pos(), srs_error_wrap(err, "decode pack")); } return err; } srs_error_t SrsRecoverablePsContext::enter_recover_mode(SrsBuffer *stream, ISrsPsMessageHandler *handler, int pos, srs_error_t err) { // Enter recover mode. Increase the recover counter because we might fail for many times. recover_++; // Print the error information for debugging. int npos = stream->pos(); stream->skip(pos - stream->pos()); string bytes = srs_strings_dumps_hex(stream->head(), stream->left(), 8); SrsPsDecodeHelper &h = ctx_.helper_; uint16_t pack_seq = h.pack_first_seq_; uint16_t pack_msgs = h.pack_nn_msgs_; uint16_t lsopm = h.pack_pre_msg_last_seq_; SrsTsMessage *last = ctx_.last(); srs_warn("PS: Enter recover=%d, seq=%u, ts=%u, pt=%u, pack=%u, msgs=%u, lsopm=%u, last=%u/%u, bytes=[%s], pos=%d, left=%d for err %s", recover_, h.rtp_seq_, h.rtp_ts_, h.rtp_pt_, pack_seq, pack_msgs, lsopm, last->PES_packet_length_, last->payload_->length(), bytes.c_str(), npos, stream->left(), srs_error_desc(err).c_str()); // If RTP packet exceed SRS_GB_LARGE_PACKET, which is large packet, might be correct length and impossible to // recover, so we directly fail it and re-inivte. if (stream->size() > SRS_GB_LARGE_PACKET) { return srs_error_wrap(err, "no recover for large packet length=%dB", stream->size()); } // Sometimes, we're unable to recover it, so we limit the max retry. if (recover_ > SRS_GB_MAX_RECOVER) { return srs_error_wrap(err, "exceed max recover, pack=%u, pack-seq=%u, seq=%u", h.pack_id_, h.pack_first_seq_, h.rtp_seq_); } // Reap and dispose last incomplete message. SrsTsMessage *msg = ctx_.reap(); srs_freep(msg); // Skip all left bytes in buffer, reset error because recovered. stream->skip(stream->left()); srs_freep(err); // Notify handler to cleanup previous messages in pack. handler->on_recover_mode(recover_); return err; } void SrsRecoverablePsContext::quit_recover_mode(SrsBuffer *stream, ISrsPsMessageHandler *handler) { string bytes = srs_strings_dumps_hex(stream->head(), stream->left(), 8); srs_warn("PS: Quit recover=%d, seq=%u, bytes=[%s], pos=%d, left=%d", recover_, ctx_.helper_.rtp_seq_, bytes.c_str(), stream->pos(), stream->left()); recover_ = 0; } bool srs_skip_util_pack(SrsBuffer *stream) { while (stream->require(4)) { uint8_t *p = (uint8_t *)stream->head(); // When searching pack header from payload, mostly not zero. if (p[0] != 0x00 && p[1] != 0x00 && p[2] != 0x00 && p[3] != 0x00) { stream->skip(4); } else if (p[0] != 0x00 && p[1] != 0x00 && p[2] != 0x00) { stream->skip(3); } else if (p[0] != 0x00 && p[1] != 0x00) { stream->skip(2); } else { if (p[0] == 0x00 && p[1] == 0x00 && p[2] == 0x01 && p[3] == 0xba) { return true; } stream->skip(1); } } return false; } SrsGoApiGbPublish::SrsGoApiGbPublish(SrsConfDirective *conf) { conf_ = conf->copy(); } SrsGoApiGbPublish::~SrsGoApiGbPublish() { srs_freep(conf_); } srs_error_t SrsGoApiGbPublish::serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessage *r) { srs_error_t err = srs_success; SrsUniquePtr res(SrsJsonAny::object()); if ((err = do_serve_http(w, r, res.get())) != srs_success) { srs_warn("GB error %s", srs_error_desc(err).c_str()); res->set("code", SrsJsonAny::integer(srs_error_code(err))); res->set("desc", SrsJsonAny::str(srs_error_code_str(err).c_str())); srs_freep(err); return srs_api_response(w, r, res->dumps()); } return srs_api_response(w, r, res->dumps()); } srs_error_t SrsGoApiGbPublish::do_serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessage *r, SrsJsonObject *res) { srs_error_t err = srs_success; // For each GB session, we use short-term HTTP connection. w->header()->set("Connection", "Close"); // Parse req, the request json object, from body. SrsSharedPtr req; if (true) { string req_json; if ((err = r->body_read_all(req_json)) != srs_success) { return srs_error_wrap(err, "read body"); } SrsJsonAny *json = SrsJsonAny::loads(req_json); if (!json || !json->is_object()) { srs_freep(json); return srs_error_new(ERROR_HTTP_DATA_INVALID, "invalid body %s", req_json.c_str()); } req = SrsSharedPtr(json->to_object()); } // Fetch params from req object. SrsJsonAny *prop = NULL; if ((prop = req->ensure_property_string("id")) == NULL) { return srs_error_new(ERROR_HTTP_DATA_INVALID, "id required"); } string id = prop->to_str(); if ((prop = req->ensure_property_string("ssrc")) == NULL) { return srs_error_new(ERROR_HTTP_DATA_INVALID, "ssrc required"); } uint64_t ssrc = atoi(prop->to_str().c_str()); if ((err = bind_session(id, ssrc)) != srs_success) { return srs_error_wrap(err, "bind session"); } res->set("code", SrsJsonAny::integer(ERROR_SUCCESS)); int port = _srs_config->get_stream_caster_listen(conf_); res->set("port", SrsJsonAny::integer(port)); res->set("is_tcp", SrsJsonAny::boolean(true)); // only tcp supported srs_trace("GB publish id: %s, ssrc=%lu", id.c_str(), ssrc); return err; } srs_error_t SrsGoApiGbPublish::bind_session(std::string id, uint64_t ssrc) { srs_error_t err = srs_success; SrsSharedResource *session = NULL; session = dynamic_cast *>(_srs_gb_manager->find_by_id(id)); if (session) { return srs_error_new(ERROR_SYSTEM_STREAM_BUSY, "stream already exists"); } session = dynamic_cast *>(_srs_gb_manager->find_by_fast_id(ssrc)); if (session) { return srs_error_new(ERROR_SYSTEM_STREAM_BUSY, "ssrc already exists"); } // Create new GB session. SrsGbSession *raw_session = new SrsGbSession(); raw_session->setup(conf_); session = new SrsSharedResource(raw_session); _srs_gb_manager->add_with_id(id, session); _srs_gb_manager->add_with_fast_id(ssrc, session); SrsExecutorCoroutine *executor = new SrsExecutorCoroutine(_srs_gb_manager, session, raw_session, raw_session); raw_session->setup_owner(session, executor, executor); raw_session->device_id_ = id; if ((err = executor->start()) != srs_success) { srs_freep(executor); return srs_error_wrap(err, "gb session"); } return err; } SrsResourceManager *_srs_gb_manager = NULL;