Skip to content

Commit 050bdde

Browse files
Fix AV1 encoder availability for custom RTMP streaming services. (#1690)
1 parent e0d4c05 commit 050bdde

2 files changed

Lines changed: 120 additions & 5 deletions

File tree

obs-studio-server/source/osn-encoders.cpp

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
#include <util/dstr.h>
2424
#include "utility.hpp"
2525

26+
static bool codecListContains(const char **codecs, const char *codec);
27+
static const char *getStreamOutputType(const obs_service_t *service);
2628
static bool isNvencAvailableForSimpleMode();
2729
static bool containerSupportsCodec(const std::string &container, const std::string &codec);
2830
static void convert_nvenc_h264_presets(obs_data_t *data);
@@ -104,18 +106,81 @@ bool osn::EncoderUtils::isCodecAvailableForService(const char *encoder, obs_serv
104106
auto supportedCodecs = obs_service_get_supported_video_codecs(service);
105107
auto encoderCodec = obs_get_encoder_codec(encoder);
106108

107-
if (!supportedCodecs || !encoderCodec)
109+
if (!encoderCodec)
108110
return false;
109111

110-
while (*supportedCodecs) {
111-
if (strcmp(*supportedCodecs, encoderCodec) == 0)
112+
if (supportedCodecs)
113+
return codecListContains(supportedCodecs, encoderCodec);
114+
115+
// Custom services do not expose codec lists, so mirror OBS and fall back to the output type.
116+
auto outputType = getStreamOutputType(service);
117+
if (!outputType)
118+
return false;
119+
120+
auto outputSupportedCodecs = obs_get_output_supported_video_codecs(outputType);
121+
if (!outputSupportedCodecs)
122+
return false;
123+
124+
auto splitOutputSupportedCodecs = strlist_split(outputSupportedCodecs, ';', false);
125+
bool supported = codecListContains((const char **)splitOutputSupportedCodecs, encoderCodec);
126+
strlist_free(splitOutputSupportedCodecs);
127+
128+
return supported;
129+
}
130+
131+
static bool codecListContains(const char **codecs, const char *codec)
132+
{
133+
if (!codecs || !codec)
134+
return false;
135+
136+
while (*codecs) {
137+
if (strcmp(*codecs, codec) == 0)
112138
return true;
113-
supportedCodecs++;
139+
codecs++;
114140
}
115141

116142
return false;
117143
}
118144

145+
// Resolves the OBS output type used by a streaming service.
146+
// Returns a non-owned output type ID, such as "rtmp_output", or nullptr if no compatible output is registered.
147+
static const char *getStreamOutputType(const obs_service_t *service)
148+
{
149+
const char *protocol = obs_service_get_protocol(service);
150+
151+
if (!protocol)
152+
return nullptr;
153+
154+
if (!obs_is_output_protocol_registered(protocol))
155+
return nullptr;
156+
157+
const char *output = obs_service_get_preferred_output_type(service);
158+
if (output && (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0)
159+
return output;
160+
161+
auto canUseOutput = [](const char *prot, const char *output, const char *prot_test1, const char *prot_test2 = nullptr) {
162+
return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) &&
163+
(obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0;
164+
};
165+
166+
if (canUseOutput(protocol, "rtmp_output", "RTMP", "RTMPS")) {
167+
return "rtmp_output";
168+
} else if (canUseOutput(protocol, "ffmpeg_hls_muxer", "HLS")) {
169+
return "ffmpeg_hls_muxer";
170+
} else if (canUseOutput(protocol, "ffmpeg_mpegts_muxer", "SRT", "RIST")) {
171+
return "ffmpeg_mpegts_muxer";
172+
}
173+
174+
auto returnFirstOutputId = [](void *data, const char *id) {
175+
const char **output = (const char **)data;
176+
177+
*output = id;
178+
return false;
179+
};
180+
obs_enum_output_types_with_protocol(protocol, &output, returnFirstOutputId);
181+
return output;
182+
}
183+
119184
bool osn::EncoderUtils::isEncoderCompatible(std::string encoderName, obs_service_t *service, bool simpleMode, bool recording, const std::string &container,
120185
int checkIndex)
121186
{
@@ -517,4 +582,4 @@ static void convert_nvenc_hevc_presets(obs_data_t *data)
517582
obs_data_set_string(data, "tune", "ll");
518583
obs_data_set_string(data, "multipass", "disabled");
519584
}
520-
}
585+
}

tests/osn-tests/src/test_osn_get_available_encoders.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,21 @@ import { ERecordingFormat } from '../osn';
99
import path = require('path');
1010

1111
const testName = 'osn-get-available-encoders';
12+
const av1EncoderNames = new Set([
13+
'ffmpeg_aom_av1',
14+
'ffmpeg_svt_av1',
15+
'obs_nvenc_av1_tex',
16+
'obs_qsv11_av1',
17+
'av1_texture_amf',
18+
]);
19+
20+
function getEncoderNames(encoders: { name: string }[]): string[] {
21+
return encoders.map(encoder => encoder.name);
22+
}
23+
24+
function getAv1EncoderNames(encoders: { name: string }[]): string[] {
25+
return getEncoderNames(encoders).filter(name => av1EncoderNames.has(name));
26+
}
1227

1328
describe(testName, () => {
1429
let obs: OBSHandler;
@@ -142,6 +157,41 @@ describe(testName, () => {
142157
osn.AdvancedStreamingFactory.destroy(stream);
143158
});
144159

160+
it('Get available AV1 encoders for custom RTMP streaming using output codec fallback', async () => {
161+
const youtubeService = osn.ServiceFactory.create('rtmp_common', 'youtube-service', {
162+
service: 'YouTube - RTMPS',
163+
server: 'rtmps://a.rtmps.youtube.com:443/live2',
164+
key: 'test',
165+
});
166+
const customService = osn.ServiceFactory.create('rtmp_custom', 'custom-service', {
167+
server: 'rtmps://a.rtmps.youtube.com:443/live2',
168+
key: 'test',
169+
});
170+
const youtubeStream = osn.AdvancedStreamingFactory.create();
171+
const customStream = osn.AdvancedStreamingFactory.create();
172+
173+
try {
174+
youtubeStream.service = youtubeService;
175+
customStream.service = customService;
176+
177+
const youtubeAv1Encoders = getAv1EncoderNames(youtubeStream.getAvailableEncoders());
178+
const customEncoderNames = getEncoderNames(customStream.getAvailableEncoders());
179+
180+
expect(youtubeAv1Encoders.length).to.be.greaterThan(0,
181+
'Test requires at least one registered AV1 encoder for YouTube');
182+
183+
for (const encoder of youtubeAv1Encoders) {
184+
expect(customEncoderNames).to.include(encoder,
185+
`Custom RTMP service should allow ${encoder} when the output supports AV1`);
186+
}
187+
} finally {
188+
osn.AdvancedStreamingFactory.destroy(customStream);
189+
osn.AdvancedStreamingFactory.destroy(youtubeStream);
190+
osn.ServiceFactory.destroy(customService);
191+
osn.ServiceFactory.destroy(youtubeService);
192+
}
193+
});
194+
145195
it('Get available encoders for simple recording', async () => {
146196
const recording = osn.SimpleRecordingFactory.create();
147197
expect(recording).to.not.equal(

0 commit comments

Comments
 (0)