Skip to content

Commit 0e9f1a4

Browse files
Julusianniklaspandersson
authored andcommitted
Capture audio from CEF
1 parent 9445ce1 commit 0e9f1a4

File tree

5 files changed

+199
-29
lines changed

5 files changed

+199
-29
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,41 @@
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(int sample_rate, AVSampleFormat in_sample_fmt)
12+
{
13+
AVChannelLayout channel_layout = AV_CHANNEL_LAYOUT_7POINT1;
14+
AVChannelLayout channel_layout_out = AV_CHANNEL_LAYOUT_HEXADECAGONAL;
15+
16+
SwrContext* raw_ctx = nullptr;
17+
FF(swr_alloc_set_opts2(&raw_ctx,
18+
&channel_layout_out,
19+
AV_SAMPLE_FMT_S32,
20+
sample_rate,
21+
&channel_layout,
22+
in_sample_fmt,
23+
sample_rate,
24+
0,
25+
nullptr));
26+
27+
ctx = std::shared_ptr<SwrContext>(raw_ctx, [](SwrContext* ptr) { swr_free(&ptr); });
28+
29+
FF_RET(swr_init(ctx.get()), "swr_init");
30+
}
31+
32+
caspar::array<int32_t> AudioResampler::convert(int frames, const void** src)
33+
{
34+
auto result = caspar::array<int32_t>(frames * 16 * sizeof(int32_t));
35+
auto ptr = result.data();
36+
swr_convert(ctx.get(), (uint8_t**)&ptr, frames, reinterpret_cast<const uint8_t**>(src), frames);
37+
38+
return result;
39+
}
40+
41+
}; // 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(int 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

+126-28
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,53 @@
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;
79+
core::draw_frame frame;
80+
81+
explicit presentation_frame(core::draw_frame frame = {}, std::int_least64_t ts = now()) noexcept
82+
: timestamp(ts)
83+
, frame(std::move(frame))
84+
{
85+
}
86+
87+
presentation_frame(presentation_frame&& other) noexcept
88+
: timestamp(other.timestamp)
89+
, frame(std::move(other.frame))
90+
{
91+
}
92+
93+
presentation_frame(const presentation_frame&) = delete;
94+
presentation_frame& operator=(const presentation_frame&) = delete;
95+
96+
presentation_frame& operator=(presentation_frame&& rhs) noexcept
97+
{
98+
timestamp = rhs.timestamp;
99+
frame = std::move(rhs.frame);
100+
return *this;
101+
}
102+
103+
~presentation_frame() {}
104+
};
105+
67106
class html_client
68107
: public CefClient
69108
, public CefRenderHandler
109+
, public CefAudioHandler
70110
, public CefLifeSpanHandler
71111
, public CefLoadHandler
72112
, public CefDisplayHandler
@@ -80,16 +120,21 @@ class html_client
80120
caspar::timer paint_timer_;
81121
caspar::timer test_timer_;
82122

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_;
92-
123+
spl::shared_ptr<core::frame_factory> frame_factory_;
124+
core::video_format_desc format_desc_;
125+
bool gpu_enabled_;
126+
tbb::concurrent_queue<std::wstring> javascript_before_load_;
127+
std::atomic<bool> loaded_;
128+
std::queue<presentation_frame> frames_;
129+
std::queue<presentation_frame> audio_frames_;
130+
mutable std::mutex frames_mutex_;
131+
mutable std::mutex audio_frames_mutex_;
132+
const size_t frames_max_size_ = 4;
133+
std::atomic<bool> closing_;
134+
135+
std::unique_ptr<ffmpeg::AudioResampler> audioResampler_;
136+
137+
core::draw_frame last_video_frame_;
93138
core::draw_frame last_frame_;
94139
std::int_least64_t last_frame_time_;
95140

@@ -146,8 +191,21 @@ class html_client
146191

147192
bool try_pop(const core::video_field field)
148193
{
194+
bool result = false;
149195
std::lock_guard<std::mutex> lock(frames_mutex_);
150196

197+
core::draw_frame audio_frame;
198+
uint64_t audio_frame_timestamp = 0;
199+
200+
{
201+
std::lock_guard<std::mutex> audio_lock(audio_frames_mutex_);
202+
if (!audio_frames_.empty()) {
203+
audio_frame_timestamp = audio_frames_.front().timestamp;
204+
audio_frame = core::draw_frame(std::move(audio_frames_.front().frame));
205+
audio_frames_.pop();
206+
}
207+
}
208+
151209
if (!frames_.empty()) {
152210
/*
153211
* CEF in gpu-enabled mode only sends frames when something changes, and interlaced channels
@@ -167,35 +225,43 @@ class html_client
167225

168226
// Check if the sole buffered frame is too young to have a partner field generated (with a tolerance)
169227
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;
228+
auto front_frame_is_too_young = (now_time - frames_.front().timestamp) < time_per_frame;
171229

172230
if (follows_gap_in_frames && front_frame_is_too_young) {
173231
return false;
174232
}
175233
}
176234

177-
last_frame_time_ = frames_.front().first;
178-
last_frame_ = std::move(frames_.front().second);
235+
last_frame_time_ = frames_.front().timestamp;
236+
last_video_frame_ = std::move(frames_.front().frame);
237+
last_frame_ = last_video_frame_;
179238
frames_.pop();
180239

181240
graph_->set_value("buffered-frames", (double)frames_.size() / frames_max_size_);
182241

183-
return true;
242+
result = true;
184243
}
185244

186-
return false;
245+
if (audio_frame) {
246+
last_frame_time_ = audio_frame_timestamp;
247+
last_frame_ = core::draw_frame::over(last_video_frame_, audio_frame);
248+
result = true;
249+
}
250+
251+
return result;
187252
}
188253

189254
core::draw_frame receive(const core::video_field field)
190255
{
191256
if (!try_pop(field)) {
192257
graph_->set_tag(diagnostics::tag_severity::SILENT, "late-frame");
258+
return core::draw_frame::still(last_frame_);
259+
} else {
260+
return last_frame_;
193261
}
194-
195-
return last_frame_;
196262
}
197263

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

200266
bool is_ready() const
201267
{
@@ -245,13 +311,6 @@ class html_client
245311
}
246312

247313
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-
255314
void GetViewRect(CefRefPtr<CefBrowser> browser, CefRect& rect) override
256315
{
257316
CASPAR_ASSERT(CefCurrentlyOn(TID_UI));
@@ -302,8 +361,10 @@ class html_client
302361
{
303362
std::lock_guard<std::mutex> lock(frames_mutex_);
304363

305-
frames_.push(std::make_pair(now(), core::draw_frame(std::move(frame))));
306-
while (frames_.size() > 4) {
364+
core::draw_frame new_frame = core::draw_frame(std::move(frame));
365+
366+
frames_.push(presentation_frame(std::move(new_frame)));
367+
while (frames_.size() > frames_max_size_) {
307368
frames_.pop();
308369
graph_->set_tag(diagnostics::tag_severity::WARNING, "dropped-frame");
309370
}
@@ -353,6 +414,8 @@ class html_client
353414

354415
CefRefPtr<CefRenderHandler> GetRenderHandler() override { return this; }
355416

417+
CefRefPtr<CefAudioHandler> GetAudioHandler() override { return this; }
418+
356419
CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }
357420

358421
CefRefPtr<CefLoadHandler> GetLoadHandler() override { return this; }
@@ -378,7 +441,7 @@ class html_client
378441

379442
{
380443
std::lock_guard<std::mutex> lock(frames_mutex_);
381-
frames_.push(std::make_pair(now(), core::draw_frame::empty()));
444+
frames_.push(presentation_frame());
382445
}
383446

384447
{
@@ -399,6 +462,41 @@ class html_client
399462
return false;
400463
}
401464

465+
bool GetAudioParameters(CefRefPtr<CefBrowser> browser, CefAudioParameters& params) override
466+
{
467+
params.channel_layout = CEF_CHANNEL_LAYOUT_7_1;
468+
params.sample_rate = format_desc_.audio_sample_rate;
469+
params.frames_per_buffer = format_desc_.audio_cadence[0];
470+
return format_desc_.audio_cadence.size() == 1; // TODO - handle 59.94
471+
}
472+
473+
void OnAudioStreamStarted(CefRefPtr<CefBrowser> browser, const CefAudioParameters& params, int channels) override
474+
{
475+
audioResampler_ = std::make_unique<ffmpeg::AudioResampler>(params.sample_rate, AV_SAMPLE_FMT_FLTP);
476+
}
477+
void OnAudioStreamPacket(CefRefPtr<CefBrowser> browser, const float** data, int samples, int64_t pts) override
478+
{
479+
if (!audioResampler_)
480+
return;
481+
482+
auto audio = audioResampler_->convert(samples, reinterpret_cast<const void**>(data));
483+
auto audio_frame = core::mutable_frame(this, {}, std::move(audio), core::pixel_format_desc());
484+
485+
{
486+
std::lock_guard<std::mutex> lock(audio_frames_mutex_);
487+
while (audio_frames_.size() >= frames_max_size_) {
488+
audio_frames_.pop();
489+
}
490+
audio_frames_.push(presentation_frame(core::draw_frame(std::move(audio_frame))));
491+
}
492+
}
493+
void OnAudioStreamStopped(CefRefPtr<CefBrowser> browser) override { audioResampler_ = nullptr; }
494+
void OnAudioStreamError(CefRefPtr<CefBrowser> browser, const CefString& message) override
495+
{
496+
CASPAR_LOG(info) << "[html_producer] OnAudioStreamError: \"" << message.ToString() << "\"";
497+
audioResampler_ = nullptr;
498+
}
499+
402500
void do_execute_javascript(const std::wstring& javascript)
403501
{
404502
html::begin_invoke([=] {

0 commit comments

Comments
 (0)