Skip to content

Commit 5daa184

Browse files
winlinvipclaude
andcommitted
Claude: Backport #4742 to 7.0: answer the stream video codec. v7.0.162
Backports #4742 (`c52c0f9a9`) to the SRS 7.0 release branch. Fixes #4738. Playing an H.265 stream over WebRTC without ?vcodec= answered H.264 and delivered no video. SrsRtcSource::init_for_play_before_publishing creates one placeholder track per codec, H.264 first, and negotiate_play_capability fell back to the first one whenever the client asked for no codec. A non-WebRTC publisher keeps those placeholder tracks, and the RTMP-to-RTC bridge builds RTP on the track of the codec it detects in the sequence header, so the player subscribed to the H.264 SSRC while every packet arrived on the H.265 one and SrsRtcPlayStream::send_packet dropped all of them. The rule is now the same for H.264, H.265 and AV1. Before publishing the codec of the stream is unknown, so the client decides and any codec it asks for is allowed. While publishing the stream decides: asking for no codec answers the codec of the stream, and asking for a different one is refused, because SRS never transcodes for WebRTC and that client could only ever get a black picture. SrsRtcRtpBuilder::initialize_video_track records the codec it selected on the source as bridge_video_codec_, which publish_video_codec returns, and on_unpublish clears it so the next player may choose again. For a WebRTC publisher, whose negotiated tracks replace the placeholders, the single remaining track description is the codec of the stream. Refuse a player with no matching track instead of answering audio only. The previous code left track_descs empty and built an answer without video, so the client waited for a track that never arrived; it now returns an SDP exchange error naming the codec. This also makes a VP9 request before publishing an error rather than silent audio. Add an AV1 placeholder track for play before publishing, so that a client can negotiate the codec that only a WebRTC publisher can produce. Inject the collaborators these paths reach: ISrsRtcSSRCGenerator is a new interface over SrsRtcSSRCGenerator, and SrsRtcSource and SrsRtcPlayerNegotiator now hold config, statistic, shared timer and generator pointers instead of reaching for the globals. ISrsAppConfig gains the five RTC getters the bridge uses. Without this the negotiation path cannot be exercised in a unit test. Cover the matrix in srs_utest_workflow_rtc_conn.cpp with MockRtcPlayScenario: play before publishing, an H.264 and an H.265 stream from the bridge, an AV1 stream from a WebRTC publisher, and an H.264-only client asking for an H.265 stream. The bridge cases assert the answered SSRC is the one the bridge sends with, which is what the reported failure got wrong. The source and test changes apply unchanged from 8.0. The version bump and changelog entry are the 7.0 ones, and the skills issue record in #4742 does not exist on this branch, so it is not backported. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent c9c9047 commit 5daa184

11 files changed

Lines changed: 555 additions & 21 deletions

‎internal/version/version.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ func VersionMinor() int {
1515
}
1616

1717
func VersionRevision() int {
18-
return 161
18+
return 162
1919
}
2020

2121
func Version() string {

‎trunk/doc/CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ The changelog for SRS.
77
<a name="v7-changes"></a>
88

99
## SRS 7.0 Changelog
10+
* v7.0, 2026-09-18, Merge [#4742](https://github.com/ossrs/srs/pull/4742): RTC: Answer the video codec of the stream for WebRTC play. v7.0.162 (#4742)
1011
* v7.0, 2026-09-17, Merge [#4741](https://github.com/ossrs/srs/pull/4741): Codec: Validate lengthSizeMinusOne to avoid abort on malformed SPS. v7.0.161 (#4741)
1112
* v7.0, 2026-08-20, Merge [#4729](https://github.com/ossrs/srs/pull/4729): SRT: Upgrade libsrt from 1.5.3 to 1.5.6 for CVE-2026-55868/55869. v7.0.160 (#4729)
1213
* v7.0, 2026-08-18, Merge [#4659](https://github.com/ossrs/srs/pull/4659): Proxy: Fix resource leak in ParseBody. v7.0.159 (#4659)

‎trunk/src/app/srs_app_config.hpp‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,11 @@ class ISrsAppConfig : public ISrsConfig
544544
virtual int get_time_jitter(std::string vhost) = 0;
545545
virtual bool get_mix_correct(std::string vhost) = 0;
546546
virtual bool try_annexb_first(std::string vhost) = 0;
547+
virtual bool get_rtc_keep_bframe(std::string vhost) = 0;
548+
virtual bool get_rtc_keep_avc_nalu_sei(std::string vhost) = 0;
549+
virtual bool get_rtc_server_merge_nalus() = 0;
550+
virtual srs_utime_t get_rtc_pli_for_rtmp(std::string vhost) = 0;
551+
virtual int get_rtc_opus_bitrate(std::string vhost) = 0;
547552
virtual bool get_vhost_is_edge(std::string vhost) = 0;
548553
virtual bool get_atc_auto(std::string vhost) = 0;
549554
virtual bool get_reduce_sequence_header(std::string vhost) = 0;

‎trunk/src/app/srs_app_rtc_conn.cpp‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3187,12 +3187,14 @@ SrsRtcPlayerNegotiator::SrsRtcPlayerNegotiator()
31873187
{
31883188
config_ = _srs_config;
31893189
rtc_sources_ = _srs_rtc_sources;
3190+
ssrc_generator_ = SrsRtcSSRCGenerator::instance();
31903191
}
31913192

31923193
SrsRtcPlayerNegotiator::~SrsRtcPlayerNegotiator()
31933194
{
31943195
config_ = NULL;
31953196
rtc_sources_ = NULL;
3197+
ssrc_generator_ = NULL;
31963198
}
31973199

31983200
bool srs_sdp_has_h264_profile(const SrsMediaPayloadType &payload_type, const string &profile)
@@ -3877,8 +3879,23 @@ srs_error_t SrsRtcPlayerNegotiator::negotiate_play_capability(SrsRtcUserConfig *
38773879
track_descs = source_audio_tracks;
38783880
} else if (remote_media_desc.is_video()) {
38793881
SrsVideoCodecId prefer_codec = srs_video_codec_str2id(ruc->vcodec_);
3882+
3883+
// While publishing, the codec of the stream decides, because SRS never transcodes for
3884+
// WebRTC. Answering any other codec delivers no media at all.
3885+
// @see https://github.com/ossrs/srs/issues/4738
3886+
SrsVideoCodecId stream_codec = source->publish_video_codec();
3887+
if (stream_codec != SrsVideoCodecIdReserved) {
3888+
if (prefer_codec == SrsVideoCodecIdReserved) {
3889+
prefer_codec = stream_codec;
3890+
} else if (prefer_codec != stream_codec) {
3891+
return srs_error_new(ERROR_RTC_SDP_EXCHANGE, "stream is %s, not the required %s",
3892+
srs_video_codec_id2str(stream_codec).c_str(), ruc->vcodec_.c_str());
3893+
}
3894+
}
3895+
3896+
// Before publishing, the codec of the stream is unknown, so the client decides. If it
3897+
// requires no codec, use the first track, see SrsRtcSource::init_for_play_before_publishing.
38803898
if (prefer_codec == SrsVideoCodecIdReserved) {
3881-
// Get the source codec if not specified.
38823899
std::vector<SrsRtcTrackDescription *> source_track_descs = source->get_track_desc("video", "");
38833900
if (!source_track_descs.empty()) {
38843901
SrsRtcTrackDescription *first_track = source_track_descs.at(0);
@@ -3959,6 +3976,14 @@ srs_error_t SrsRtcPlayerNegotiator::negotiate_play_capability(SrsRtcUserConfig *
39593976

39603977
track_descs = source->get_track_desc("video", "H264");
39613978
}
3979+
3980+
// Refuse the player rather than answering audio only without any error, which leaves
3981+
// the client waiting for a video track that never arrives.
3982+
// @see https://github.com/ossrs/srs/issues/4738
3983+
if (track_descs.empty()) {
3984+
return srs_error_new(ERROR_RTC_SDP_EXCHANGE, "no %s track in source",
3985+
srs_video_codec_id2str(prefer_codec).c_str());
3986+
}
39623987
}
39633988

39643989
for (int j = 0; j < (int)track_descs.size(); ++j) {
@@ -4007,7 +4032,7 @@ srs_error_t SrsRtcPlayerNegotiator::negotiate_play_capability(SrsRtcUserConfig *
40074032
// Otherwise, generate a new SSRC for each player.
40084033
// @see https://github.com/ossrs/srs/issues/3850
40094034
if (!keep_original_ssrc) {
4010-
track->ssrc_ = SrsRtcSSRCGenerator::instance()->generate_ssrc();
4035+
track->ssrc_ = ssrc_generator_->generate_ssrc();
40114036
}
40124037

40134038
// TODO: FIXME: set audio_payload rtcp_fbs_,

‎trunk/src/app/srs_app_rtc_conn.hpp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,7 @@ class SrsRtcPlayerNegotiator : public ISrsRtcPlayerNegotiator
10131013
SRS_DECLARE_PRIVATE: // clang-format on
10141014
ISrsAppConfig *config_;
10151015
ISrsRtcSourceManager *rtc_sources_;
1016+
ISrsRtcSSRCGenerator *ssrc_generator_;
10161017

10171018
public:
10181019
SrsRtcPlayerNegotiator();

‎trunk/src/app/srs_app_rtc_source.cpp‎

Lines changed: 91 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -393,10 +393,15 @@ SrsRtcSource::SrsRtcSource()
393393

394394
publish_stream_ = NULL;
395395
stream_desc_ = NULL;
396+
bridge_video_codec_ = SrsVideoCodecIdReserved;
396397

397398
req_ = NULL;
398399
rtc_bridge_ = NULL;
399400
circuit_breaker_ = _srs_circuit_breaker;
401+
config_ = _srs_config;
402+
stat_ = _srs_stat;
403+
shared_timer_ = _srs_shared_timer;
404+
ssrc_generator_ = SrsRtcSSRCGenerator::instance();
400405

401406
pli_for_rtmp_ = pli_elapsed_ = 0;
402407
// Initialize stream_die_at_ to current time to prevent newly created sources
@@ -423,6 +428,11 @@ SrsRtcSource::~SrsRtcSource()
423428
srs_trace("free rtc source id=[%s]", cid.c_str());
424429

425430
app_factory_ = NULL;
431+
circuit_breaker_ = NULL;
432+
config_ = NULL;
433+
stat_ = NULL;
434+
shared_timer_ = NULL;
435+
ssrc_generator_ = NULL;
426436
}
427437

428438
// CRITICAL: This method is called AFTER the source has been added to the source pool
@@ -498,14 +508,14 @@ void SrsRtcSource::init_for_play_before_publishing()
498508
audio_track_desc->type_ = "audio";
499509
audio_track_desc->id_ = "audio-" + rand.gen_str(8);
500510

501-
uint32_t audio_ssrc = SrsRtcSSRCGenerator::instance()->generate_ssrc();
511+
uint32_t audio_ssrc = ssrc_generator_->generate_ssrc();
502512
audio_track_desc->ssrc_ = audio_ssrc;
503513
audio_track_desc->direction_ = "recvonly";
504514

505515
audio_track_desc->media_ = new SrsAudioPayload(kAudioPayloadType, "opus", kAudioSamplerate, kAudioChannel);
506516
}
507517

508-
// video track descriptions - support both H.264 and H.265 for play before publishing
518+
// video track descriptions - support H.264, H.265 and AV1 for play before publishing
509519
// This allows clients to choose their preferred codec during SDP negotiation
510520
if (true) {
511521
// H.264 track description
@@ -515,7 +525,7 @@ void SrsRtcSource::init_for_play_before_publishing()
515525
h264_track_desc->type_ = "video";
516526
h264_track_desc->id_ = "video-h264-" + rand.gen_str(8);
517527

518-
uint32_t h264_ssrc = SrsRtcSSRCGenerator::instance()->generate_ssrc();
528+
uint32_t h264_ssrc = ssrc_generator_->generate_ssrc();
519529
h264_track_desc->ssrc_ = h264_ssrc;
520530
h264_track_desc->direction_ = "recvonly";
521531

@@ -533,7 +543,7 @@ void SrsRtcSource::init_for_play_before_publishing()
533543
h265_track_desc->type_ = "video";
534544
h265_track_desc->id_ = "video-h265-" + rand.gen_str(8);
535545

536-
uint32_t h265_ssrc = SrsRtcSSRCGenerator::instance()->generate_ssrc();
546+
uint32_t h265_ssrc = ssrc_generator_->generate_ssrc();
537547
h265_track_desc->ssrc_ = h265_ssrc;
538548
h265_track_desc->direction_ = "recvonly";
539549

@@ -543,6 +553,22 @@ void SrsRtcSource::init_for_play_before_publishing()
543553
h265_payload->set_h265_param_desc("level-id=156;profile-id=1;tier-flag=0;tx-mode=SRST");
544554
}
545555

556+
if (true) {
557+
// AV1 track description. Note that AV1 is published by WebRTC only, because the bridge
558+
// from RTMP or SRT supports H.264 and H.265 only.
559+
SrsRtcTrackDescription *av1_track_desc = new SrsRtcTrackDescription();
560+
stream_desc->video_track_descs_.push_back(av1_track_desc);
561+
562+
av1_track_desc->type_ = "video";
563+
av1_track_desc->id_ = "video-av1-" + rand.gen_str(8);
564+
565+
uint32_t av1_ssrc = ssrc_generator_->generate_ssrc();
566+
av1_track_desc->ssrc_ = av1_ssrc;
567+
av1_track_desc->direction_ = "recvonly";
568+
569+
av1_track_desc->media_ = new SrsVideoPayload(KVideoPayloadTypeAv1, "AV1", kVideoSamplerate);
570+
}
571+
546572
set_stream_desc(stream_desc.get());
547573
}
548574

@@ -703,14 +729,13 @@ srs_error_t SrsRtcSource::on_publish()
703729
}
704730

705731
// The PLI interval for RTC2RTMP.
706-
pli_for_rtmp_ = _srs_config->get_rtc_pli_for_rtmp(req_->vhost_);
732+
pli_for_rtmp_ = config_->get_rtc_pli_for_rtmp(req_->vhost_);
707733

708734
// @see SrsRtcSource::on_timer()
709-
_srs_shared_timer->timer100ms()->subscribe(this);
735+
shared_timer_->timer100ms()->subscribe(this);
710736
}
711737

712-
SrsStatistic *stat = _srs_stat;
713-
stat->on_stream_publish(req_, _source_id.c_str());
738+
stat_->on_stream_publish(req_, _source_id.c_str());
714739

715740
return err;
716741
}
@@ -729,6 +754,9 @@ void SrsRtcSource::on_unpublish()
729754
}
730755
_source_id = SrsContextId();
731756

757+
// The codec of the stream is unknown again, so the next player may choose any codec.
758+
bridge_video_codec_ = SrsVideoCodecIdReserved;
759+
732760
for (size_t i = 0; i < event_handlers_.size(); i++) {
733761
ISrsRtcSourceEventHandler *h = event_handlers_.at(i);
734762
h->on_unpublish();
@@ -737,14 +765,15 @@ void SrsRtcSource::on_unpublish()
737765
// free bridge resource
738766
if (rtc_bridge_) {
739767
// For SrsRtcSource::on_timer()
740-
_srs_shared_timer->timer100ms()->unsubscribe(this);
768+
if (shared_timer_) {
769+
shared_timer_->timer100ms()->unsubscribe(this);
770+
}
741771

742772
rtc_bridge_->on_unpublish();
743773
srs_freep(rtc_bridge_);
744774
}
745775

746-
SrsStatistic *stat = _srs_stat;
747-
stat->on_stream_close(req_);
776+
stat_->on_stream_close(req_);
748777

749778
// Destroy and cleanup source when no publishers and consumers.
750779
if (consumers_.empty()) {
@@ -857,6 +886,37 @@ std::vector<SrsRtcTrackDescription *> SrsRtcSource::get_track_desc(std::string t
857886
return track_descs;
858887
}
859888

889+
void SrsRtcSource::set_bridge_video_codec(SrsVideoCodecId codec)
890+
{
891+
bridge_video_codec_ = codec;
892+
}
893+
894+
SrsVideoCodecId SrsRtcSource::publish_video_codec()
895+
{
896+
// The bridge from RTMP or SRT keeps the tracks created for play before publishing, so the
897+
// codec of the stream is the one it detected in the video sequence header.
898+
if (bridge_video_codec_ != SrsVideoCodecIdReserved) {
899+
return bridge_video_codec_;
900+
}
901+
902+
if (!stream_desc_ || stream_desc_->video_track_descs_.empty()) {
903+
return SrsVideoCodecIdReserved;
904+
}
905+
906+
// A WebRTC publisher replaces the tracks created for play before publishing by the tracks it
907+
// negotiated, which describe exactly one codec, the codec of the stream. While the tracks for
908+
// play before publishing describe one track per supported codec, so the codec is still unknown.
909+
SrsVideoCodecId codec = SrsVideoCodecId(stream_desc_->video_track_descs_.at(0)->media_->codec(true));
910+
for (int i = 1; i < (int)stream_desc_->video_track_descs_.size(); i++) {
911+
SrsRtcTrackDescription *track_desc = stream_desc_->video_track_descs_.at(i);
912+
if (SrsVideoCodecId(track_desc->media_->codec(true)) != codec) {
913+
return SrsVideoCodecIdReserved;
914+
}
915+
}
916+
917+
return codec;
918+
}
919+
860920
srs_error_t SrsRtcSource::on_timer(srs_utime_t interval)
861921
{
862922
srs_error_t err = srs_success;
@@ -889,6 +949,7 @@ SrsRtcRtpBuilder::SrsRtcRtpBuilder(ISrsAppFactory *factory, ISrsRtpTarget *targe
889949
{
890950
rtp_target_ = target;
891951
source_ = source;
952+
config_ = _srs_config;
892953

893954
req_ = NULL;
894955
format_ = new SrsRtmpFormat();
@@ -920,6 +981,7 @@ SrsRtcRtpBuilder::~SrsRtcRtpBuilder()
920981
srs_freep(video_builder_);
921982

922983
app_factory_ = NULL;
984+
config_ = NULL;
923985
}
924986

925987
srs_error_t SrsRtcRtpBuilder::initialize_audio_track(SrsAudioCodecId codec)
@@ -970,6 +1032,11 @@ srs_error_t SrsRtcRtpBuilder::initialize_video_track(SrsVideoCodecId codec)
9701032
return srs_error_wrap(err, "initialize video builder");
9711033
}
9721034

1035+
// Now the codec of the stream is known, so the player must be answered with this codec, on the
1036+
// SSRC of this track, otherwise every packet we build is dropped.
1037+
// @see https://github.com/ossrs/srs/issues/4738
1038+
source_->set_bridge_video_codec(codec);
1039+
9731040
srs_trace("RTMP2RTC: Initialize video track with codec=%s, ssrc=%u, pt=%d",
9741041
codec_name.c_str(), video_ssrc, video_payload_type);
9751042

@@ -987,11 +1054,11 @@ srs_error_t SrsRtcRtpBuilder::initialize(ISrsRequest *r)
9871054
}
9881055

9891056
// Setup the SPS/PPS parsing strategy.
990-
format_->try_annexb_first_ = _srs_config->try_annexb_first(r->vhost_);
1057+
format_->try_annexb_first_ = config_->try_annexb_first(r->vhost_);
9911058

992-
keep_bframe_ = _srs_config->get_rtc_keep_bframe(req_->vhost_);
993-
keep_avc_nalu_sei_ = _srs_config->get_rtc_keep_avc_nalu_sei(req_->vhost_);
994-
merge_nalus_ = _srs_config->get_rtc_server_merge_nalus();
1059+
keep_bframe_ = config_->get_rtc_keep_bframe(req_->vhost_);
1060+
keep_avc_nalu_sei_ = config_->get_rtc_keep_avc_nalu_sei(req_->vhost_);
1061+
merge_nalus_ = config_->get_rtc_server_merge_nalus();
9951062
srs_trace("RTC bridge from RTMP, keep_bframe=%d, keep_avc_nalu_sei=%d, merge_nalus=%d",
9961063
keep_bframe_, keep_avc_nalu_sei_, merge_nalus_);
9971064

@@ -1110,7 +1177,7 @@ srs_error_t SrsRtcRtpBuilder::init_codec(SrsAudioCodecId codec)
11101177
codec_ = app_factory_->create_audio_transcoder();
11111178

11121179
// Initialize the codec according to the codec in stream.
1113-
int bitrate = _srs_config->get_rtc_opus_bitrate(req_->vhost_); // The output bitrate in bps.
1180+
int bitrate = config_->get_rtc_opus_bitrate(req_->vhost_); // The output bitrate in bps.
11141181
if ((err = codec_->initialize(codec, SrsAudioCodecIdOpus, kAudioChannel, kAudioSamplerate, bitrate)) != srs_success) {
11151182
return srs_error_wrap(err, "init codec=%d", codec);
11161183
}
@@ -3794,6 +3861,14 @@ SrsRtcSSRCGenerator::~SrsRtcSSRCGenerator()
37943861
{
37953862
}
37963863

3864+
ISrsRtcSSRCGenerator::ISrsRtcSSRCGenerator()
3865+
{
3866+
}
3867+
3868+
ISrsRtcSSRCGenerator::~ISrsRtcSSRCGenerator()
3869+
{
3870+
}
3871+
37973872
SrsRtcSSRCGenerator *SrsRtcSSRCGenerator::instance()
37983873
{
37993874
if (!instance_) {

0 commit comments

Comments
 (0)