Skip to content

Commit 43d0d34

Browse files
committed
feat: route audio from CEF
commit 7e2f134 Author: Julian Waller <[email protected]> Date: Fri Jan 5 16:36:53 2024 +0000 chore: format commit 91e9954 Author: Julian Waller <[email protected]> Date: Fri Jan 5 16:34:23 2024 +0000 wip: rework frame flow commit 0ce2bfd Author: Julian Waller <[email protected]> Date: Fri Jan 5 16:11:02 2024 +0000 wip: inspired by #16
1 parent 0acc8e0 commit 43d0d34

File tree

5 files changed

+211
-25
lines changed

5 files changed

+211
-25
lines changed

src/modules/ffmpeg/CMakeLists.txt

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ set(SOURCES
55
producer/av_producer.cpp
66
producer/av_input.cpp
77
util/av_util.cpp
8+
util/audio_resampler.cpp
89
producer/ffmpeg_producer.cpp
910
consumer/ffmpeg_consumer.cpp
1011

@@ -15,6 +16,7 @@ set(HEADERS
1516
producer/av_producer.h
1617
producer/av_input.h
1718
util/av_util.h
19+
util/audio_resampler.h
1820
producer/ffmpeg_producer.h
1921
consumer/ffmpeg_consumer.h
2022

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#include "audio_resampler.h"
2+
#include "av_assert.h"
3+
4+
extern "C" {
5+
#include <libavutil/samplefmt.h>
6+
#include <libswresample/swresample.h>
7+
}
8+
9+
namespace caspar::ffmpeg {
10+
11+
AudioResampler::AudioResampler(int64_t sample_rate, AVSampleFormat in_sample_fmt)
12+
: ctx(std::shared_ptr<SwrContext>(swr_alloc_set_opts(nullptr,
13+
AV_CH_LAYOUT_7POINT1,
14+
AV_SAMPLE_FMT_S32,
15+
sample_rate,
16+
AV_CH_LAYOUT_7POINT1,
17+
in_sample_fmt,
18+
sample_rate,
19+
0,
20+
nullptr),
21+
[](SwrContext* ptr) { swr_free(&ptr); }))
22+
{
23+
if (!ctx)
24+
FF_RET(AVERROR(ENOMEM), "swr_alloc_set_opts");
25+
26+
FF_RET(swr_init(ctx.get()), "swr_init");
27+
}
28+
29+
caspar::array<int32_t> AudioResampler::convert(int frames, const void** src)
30+
{
31+
auto result = caspar::array<int32_t>(frames * 8 * sizeof(int32_t));
32+
auto ptr = result.data();
33+
auto ret = swr_convert(ctx.get(), (uint8_t**)&ptr, frames, reinterpret_cast<const uint8_t**>(src), frames);
34+
35+
return result;
36+
}
37+
38+
}; // namespace caspar::ffmpeg
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#include <common/array.h>
2+
#include <memory>
3+
4+
#pragma once
5+
6+
extern "C" {
7+
#include <libavutil/samplefmt.h>
8+
}
9+
10+
struct SwrContext;
11+
12+
namespace caspar::ffmpeg {
13+
14+
class AudioResampler
15+
{
16+
std::shared_ptr<SwrContext> ctx;
17+
18+
public:
19+
AudioResampler(int64_t sample_rate, AVSampleFormat in_sample_fmt);
20+
21+
AudioResampler(const AudioResampler&) = delete;
22+
AudioResampler& operator=(const AudioResampler&) = delete;
23+
24+
caspar::array<int32_t> convert(int frames, const void** src);
25+
};
26+
27+
}; // namespace caspar::ffmpeg

src/modules/html/CMakeLists.txt

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ target_include_directories(html PRIVATE
2525
..
2626
../..
2727
${CEF_INCLUDE_PATH}
28-
)
28+
${FFMPEG_INCLUDE_PATH}
29+
)
30+
target_link_libraries(html ffmpeg)
2931

3032
set_target_properties(html PROPERTIES FOLDER modules)
3133
source_group(sources\\producer producer/*)

src/modules/html/producer/html_producer.cpp

+141-24
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,83 @@
6060
#include <queue>
6161
#include <utility>
6262

63+
#include <ffmpeg/util/audio_resampler.h>
64+
6365
#include "../html.h"
6466

6567
namespace caspar { namespace html {
6668

69+
inline std::int_least64_t now()
70+
{
71+
return std::chrono::duration_cast<std::chrono::milliseconds>(
72+
std::chrono::high_resolution_clock::now().time_since_epoch())
73+
.count();
74+
}
75+
76+
struct presentation_frame
77+
{
78+
std::int_least64_t timestamp = now();
79+
core::draw_frame frame = core::draw_frame::empty();
80+
bool has_video = false;
81+
bool has_audio = false;
82+
83+
explicit presentation_frame(core::draw_frame video = {})
84+
{
85+
if (video) {
86+
frame = std::move(video);
87+
has_video = true;
88+
}
89+
}
90+
91+
presentation_frame(presentation_frame&& other) noexcept
92+
: timestamp(other.timestamp)
93+
, frame(std::move(other.frame))
94+
{
95+
}
96+
97+
presentation_frame(const presentation_frame&) = delete;
98+
presentation_frame& operator=(const presentation_frame&) = delete;
99+
100+
presentation_frame& operator=(presentation_frame&& rhs)
101+
{
102+
timestamp = rhs.timestamp;
103+
frame = std::move(rhs.frame);
104+
return *this;
105+
}
106+
107+
~presentation_frame() {}
108+
109+
void add_audio(core::mutable_frame audio)
110+
{
111+
if (has_audio)
112+
return;
113+
has_audio = true;
114+
115+
if (frame) {
116+
frame = core::draw_frame::over(frame, core::draw_frame(std::move(audio)));
117+
} else {
118+
frame = core::draw_frame(std::move(audio));
119+
}
120+
}
121+
122+
void add_video(core::draw_frame video)
123+
{
124+
if (has_video)
125+
return;
126+
has_video = true;
127+
128+
if (frame) {
129+
frame = core::draw_frame::over(frame, std::move(video));
130+
} else {
131+
frame = std::move(video);
132+
}
133+
}
134+
};
135+
67136
class html_client
68137
: public CefClient
69138
, public CefRenderHandler
139+
, public CefAudioHandler
70140
, public CefLifeSpanHandler
71141
, public CefLoadHandler
72142
, public CefDisplayHandler
@@ -80,15 +150,18 @@ class html_client
80150
caspar::timer paint_timer_;
81151
caspar::timer test_timer_;
82152

83-
spl::shared_ptr<core::frame_factory> frame_factory_;
84-
core::video_format_desc format_desc_;
85-
bool gpu_enabled_;
86-
tbb::concurrent_queue<std::wstring> javascript_before_load_;
87-
std::atomic<bool> loaded_;
88-
std::queue<std::pair<std::int_least64_t, core::draw_frame>> frames_;
89-
mutable std::mutex frames_mutex_;
90-
const size_t frames_max_size_ = 4;
91-
std::atomic<bool> closing_;
153+
spl::shared_ptr<core::frame_factory> frame_factory_;
154+
core::video_format_desc format_desc_;
155+
bool gpu_enabled_;
156+
tbb::concurrent_queue<std::wstring> javascript_before_load_;
157+
std::atomic<bool> loaded_;
158+
std::queue<presentation_frame> frames_;
159+
core::draw_frame last_generated_frame_;
160+
mutable std::mutex frames_mutex_;
161+
const size_t frames_max_size_ = 4;
162+
std::atomic<bool> closing_;
163+
164+
std::unique_ptr<ffmpeg::AudioResampler> audioResampler_;
92165

93166
core::draw_frame last_frame_;
94167
std::int_least64_t last_frame_time_;
@@ -167,15 +240,15 @@ class html_client
167240

168241
// Check if the sole buffered frame is too young to have a partner field generated (with a tolerance)
169242
auto time_per_frame = (1000 * 1.5) / format_desc_.fps;
170-
auto front_frame_is_too_young = (now_time - frames_.front().first) < time_per_frame;
243+
auto front_frame_is_too_young = (now_time - frames_.front().timestamp) < time_per_frame;
171244

172245
if (follows_gap_in_frames && front_frame_is_too_young) {
173246
return false;
174247
}
175248
}
176249

177-
last_frame_time_ = frames_.front().first;
178-
last_frame_ = std::move(frames_.front().second);
250+
last_frame_time_ = frames_.front().timestamp;
251+
last_frame_ = std::move(frames_.front().frame);
179252
frames_.pop();
180253

181254
graph_->set_value("buffered-frames", (double)frames_.size() / frames_max_size_);
@@ -190,12 +263,13 @@ class html_client
190263
{
191264
if (!try_pop(field)) {
192265
graph_->set_tag(diagnostics::tag_severity::SILENT, "late-frame");
266+
return core::draw_frame::still(last_frame_);
267+
} else {
268+
return last_frame_;
193269
}
194-
195-
return last_frame_;
196270
}
197271

198-
core::draw_frame last_frame() const { return last_frame_; }
272+
core::draw_frame last_frame() const { return core::draw_frame::still(last_frame_); }
199273

200274
bool is_ready() const
201275
{
@@ -245,13 +319,6 @@ class html_client
245319
}
246320

247321
private:
248-
std::int_least64_t now()
249-
{
250-
return std::chrono::duration_cast<std::chrono::milliseconds>(
251-
std::chrono::high_resolution_clock::now().time_since_epoch())
252-
.count();
253-
}
254-
255322
void GetViewRect(CefRefPtr<CefBrowser> browser, CefRect& rect) override
256323
{
257324
CASPAR_ASSERT(CefCurrentlyOn(TID_UI));
@@ -302,7 +369,10 @@ class html_client
302369
{
303370
std::lock_guard<std::mutex> lock(frames_mutex_);
304371

305-
frames_.push(std::make_pair(now(), core::draw_frame(std::move(frame))));
372+
core::draw_frame new_frame = core::draw_frame(std::move(frame));
373+
last_generated_frame_ = new_frame;
374+
375+
frames_.push(presentation_frame(std::move(new_frame)));
306376
while (frames_.size() > 4) {
307377
frames_.pop();
308378
graph_->set_tag(diagnostics::tag_severity::WARNING, "dropped-frame");
@@ -353,6 +423,8 @@ class html_client
353423

354424
CefRefPtr<CefRenderHandler> GetRenderHandler() override { return this; }
355425

426+
CefRefPtr<CefAudioHandler> GetAudioHandler() override { return this; }
427+
356428
CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }
357429

358430
CefRefPtr<CefLoadHandler> GetLoadHandler() override { return this; }
@@ -378,7 +450,7 @@ class html_client
378450

379451
{
380452
std::lock_guard<std::mutex> lock(frames_mutex_);
381-
frames_.push(std::make_pair(now(), core::draw_frame::empty()));
453+
frames_.push(presentation_frame());
382454
}
383455

384456
{
@@ -399,6 +471,51 @@ class html_client
399471
return false;
400472
}
401473

474+
bool GetAudioParameters(CefRefPtr<CefBrowser> browser, CefAudioParameters& params) override
475+
{
476+
params.channel_layout = CEF_CHANNEL_LAYOUT_7_1;
477+
params.sample_rate = format_desc_.audio_sample_rate;
478+
params.frames_per_buffer = format_desc_.audio_cadence[0];
479+
return format_desc_.audio_cadence.size() == 1; // TODO - handle 59.94
480+
}
481+
482+
void OnAudioStreamStarted(CefRefPtr<CefBrowser> browser, const CefAudioParameters& params, int channels) override
483+
{
484+
audioResampler_ = std::make_unique<ffmpeg::AudioResampler>(params.sample_rate, AV_SAMPLE_FMT_FLTP);
485+
}
486+
void OnAudioStreamPacket(CefRefPtr<CefBrowser> browser, const float** data, int samples, int64_t pts) override
487+
{
488+
if (!audioResampler_)
489+
return;
490+
491+
auto audio = audioResampler_->convert(samples, reinterpret_cast<const void**>(data));
492+
auto audio_frame = core::mutable_frame(this, {}, std::move(audio), core::pixel_format_desc());
493+
494+
{
495+
std::lock_guard<std::mutex> lock(frames_mutex_);
496+
if (frames_.empty()) {
497+
presentation_frame wrapped_frame(last_generated_frame_);
498+
wrapped_frame.add_audio(std::move(audio_frame));
499+
500+
frames_.push(std::move(wrapped_frame));
501+
} else {
502+
if (!frames_.back().has_audio) {
503+
frames_.back().add_audio(std::move(audio_frame));
504+
} else {
505+
presentation_frame wrapped_frame(last_generated_frame_);
506+
wrapped_frame.add_audio(std::move(audio_frame));
507+
frames_.push(std::move(wrapped_frame));
508+
}
509+
}
510+
}
511+
}
512+
void OnAudioStreamStopped(CefRefPtr<CefBrowser> browser) override { audioResampler_ = nullptr; }
513+
void OnAudioStreamError(CefRefPtr<CefBrowser> browser, const CefString& message) override
514+
{
515+
CASPAR_LOG(info) << "[html_producer] OnAudioStreamError: \"" << message.ToString() << "\"";
516+
audioResampler_ = nullptr;
517+
}
518+
402519
void do_execute_javascript(const std::wstring& javascript)
403520
{
404521
html::begin_invoke([=] {

0 commit comments

Comments
 (0)