Skip to content

Commit f0d5c9b

Browse files
committed
wip: inspired by nxtedition#16
1 parent 21ccd5e commit f0d5c9b

File tree

5 files changed

+197
-13
lines changed

5 files changed

+197
-13
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
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#include <common/array.h>
2+
#include <memory>
3+
4+
extern "C" {
5+
#include <libavutil/samplefmt.h>
6+
}
7+
8+
struct SwrContext;
9+
10+
namespace caspar::ffmpeg {
11+
12+
class AudioResampler
13+
{
14+
std::shared_ptr<SwrContext> ctx;
15+
16+
public:
17+
AudioResampler(int64_t sample_rate, AVSampleFormat in_sample_fmt);
18+
19+
AudioResampler(const AudioResampler&) = delete;
20+
AudioResampler& operator=(const AudioResampler&) = delete;
21+
22+
caspar::array<int32_t> convert(int frames, const void** src);
23+
};
24+
25+
}; // 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

+129-12
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,75 @@
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()
84+
{
85+
}
86+
87+
presentation_frame(presentation_frame&& other)
88+
noexcept : timestamp(other.timestamp),frame(std::move(other.frame))
89+
{
90+
}
91+
92+
presentation_frame(const presentation_frame&) = delete;
93+
presentation_frame& operator=(const presentation_frame&) = delete;
94+
95+
presentation_frame& operator=(presentation_frame&& rhs)
96+
{
97+
timestamp = rhs.timestamp;
98+
frame = std::move(rhs.frame);
99+
return *this;
100+
}
101+
102+
~presentation_frame() {}
103+
104+
void add_audio(core::mutable_frame audio) {
105+
if (has_audio) return;
106+
has_audio = true;
107+
108+
if (frame) {
109+
frame = core::draw_frame::over(frame, core::draw_frame(std::move (audio)));
110+
} else {
111+
frame = core::draw_frame(std::move (audio));
112+
}
113+
}
114+
115+
void add_video(core::draw_frame video) {
116+
if (has_video) return;
117+
has_video = true;
118+
119+
if (frame) {
120+
frame = core::draw_frame::over(frame, std::move (video));
121+
} else {
122+
frame = std::move (video);
123+
}
124+
}
125+
};
126+
127+
67128
class html_client
68129
: public CefClient
69130
, public CefRenderHandler
131+
, public CefAudioHandler
70132
, public CefLifeSpanHandler
71133
, public CefLoadHandler
72134
, public CefDisplayHandler
@@ -85,11 +147,14 @@ class html_client
85147
bool gpu_enabled_;
86148
tbb::concurrent_queue<std::wstring> javascript_before_load_;
87149
std::atomic<bool> loaded_;
88-
std::queue<std::pair<std::int_least64_t, core::draw_frame>> frames_;
150+
std::queue<presentation_frame> frames_;
151+
core::draw_frame last_generated_frame_;
89152
mutable std::mutex frames_mutex_;
90153
const size_t frames_max_size_ = 4;
91154
std::atomic<bool> closing_;
92155

156+
std::unique_ptr<ffmpeg::AudioResampler> audioResampler_;
157+
93158
core::draw_frame last_frame_;
94159
std::int_least64_t last_frame_time_;
95160

@@ -167,15 +232,15 @@ class html_client
167232

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

172237
if (follows_gap_in_frames && front_frame_is_too_young) {
173238
return false;
174239
}
175240
}
176241

177-
last_frame_time_ = frames_.front().first;
178-
last_frame_ = std::move(frames_.front().second);
242+
last_frame_time_ = frames_.front().timestamp;
243+
last_frame_ = std::move(frames_.front().frame);
179244
frames_.pop();
180245

181246
graph_->set_value("buffered-frames", (double)frames_.size() / frames_max_size_);
@@ -245,12 +310,7 @@ class html_client
245310
}
246311

247312
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-
}
313+
254314

255315
void GetViewRect(CefRefPtr<CefBrowser> browser, CefRect& rect) override
256316
{
@@ -302,7 +362,13 @@ class html_client
302362
{
303363
std::lock_guard<std::mutex> lock(frames_mutex_);
304364

305-
frames_.push(std::make_pair(now(), core::draw_frame(std::move(frame))));
365+
core::draw_frame new_frame = core::draw_frame(std::move(frame));
366+
last_generated_frame_ = new_frame;
367+
368+
presentation_frame wrapped_frame;
369+
wrapped_frame.add_video(std::move(new_frame));
370+
371+
frames_.push(std::move(wrapped_frame));
306372
while (frames_.size() > 4) {
307373
frames_.pop();
308374
graph_->set_tag(diagnostics::tag_severity::WARNING, "dropped-frame");
@@ -353,6 +419,8 @@ class html_client
353419

354420
CefRefPtr<CefRenderHandler> GetRenderHandler() override { return this; }
355421

422+
CefRefPtr<CefAudioHandler> GetAudioHandler() override { return this; }
423+
356424
CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }
357425

358426
CefRefPtr<CefLoadHandler> GetLoadHandler() override { return this; }
@@ -378,7 +446,7 @@ class html_client
378446

379447
{
380448
std::lock_guard<std::mutex> lock(frames_mutex_);
381-
frames_.push(std::make_pair(now(), core::draw_frame::empty()));
449+
frames_.push(presentation_frame());
382450
}
383451

384452
{
@@ -399,6 +467,55 @@ class html_client
399467
return false;
400468
}
401469

470+
bool GetAudioParameters(CefRefPtr<CefBrowser> browser, CefAudioParameters& params) override
471+
{
472+
params.channel_layout = CEF_CHANNEL_LAYOUT_7_1;
473+
params.sample_rate = format_desc_.audio_sample_rate;
474+
params.frames_per_buffer = format_desc_.audio_cadence[0];
475+
return format_desc_.audio_cadence.size() == 1; // TODO - handle 59.94
476+
}
477+
478+
void OnAudioStreamStarted(CefRefPtr<CefBrowser> browser, const CefAudioParameters& params, int channels) override
479+
{
480+
audioResampler_ = std::make_unique<ffmpeg::AudioResampler>(params.sample_rate, AV_SAMPLE_FMT_FLTP);
481+
}
482+
void OnAudioStreamPacket(CefRefPtr<CefBrowser> browser, const float** data, int samples, int64_t pts) override
483+
{
484+
if (!audioResampler_) return;
485+
486+
auto audio = audioResampler_->convert(samples, reinterpret_cast<const void**>(data));
487+
auto audio_frame = core::mutable_frame(this, {}, std::move(audio), core::pixel_format_desc());
488+
489+
{
490+
std::lock_guard<std::mutex> lock(frames_mutex_);
491+
if (frames_.empty()) {
492+
presentation_frame wrapped_frame;
493+
494+
wrapped_frame.add_audio(std::move(audio_frame));
495+
if (last_generated_frame_) {
496+
wrapped_frame.add_video(last_generated_frame_);
497+
}
498+
499+
frames_.push(std::move(wrapped_frame));
500+
} else {
501+
if (!frames_.back().has_audio) {
502+
frames_.back().add_audio(std::move(audio_frame));
503+
} else {
504+
presentation_frame wrapped_frame;
505+
wrapped_frame.add_audio(std::move(audio_frame));
506+
frames_.push(std::move(wrapped_frame));
507+
}
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)