Skip to content

Commit 0ce2bfd

Browse files
committed
wip: inspired by nxtedition#16
1 parent e8dd522 commit 0ce2bfd

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
@@ -61,13 +61,75 @@
6161
#include <queue>
6262
#include <utility>
6363

64+
#include <ffmpeg/util/audio_resampler.h>
65+
6466
#include "../html.h"
6567

6668
namespace caspar { namespace html {
6769

70+
inline std::int_least64_t now()
71+
{
72+
return std::chrono::duration_cast<std::chrono::milliseconds>(
73+
std::chrono::high_resolution_clock::now().time_since_epoch())
74+
.count();
75+
}
76+
77+
struct presentation_frame
78+
{
79+
std::int_least64_t timestamp = now();
80+
core::draw_frame frame = core::draw_frame::empty();
81+
bool has_video = false;
82+
bool has_audio = false;
83+
84+
explicit presentation_frame()
85+
{
86+
}
87+
88+
presentation_frame(presentation_frame&& other)
89+
noexcept : timestamp(other.timestamp),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)
97+
{
98+
timestamp = rhs.timestamp;
99+
frame = std::move(rhs.frame);
100+
return *this;
101+
}
102+
103+
~presentation_frame() {}
104+
105+
void add_audio(core::mutable_frame audio) {
106+
if (has_audio) return;
107+
has_audio = true;
108+
109+
if (frame) {
110+
frame = core::draw_frame::over(frame, core::draw_frame(std::move (audio)));
111+
} else {
112+
frame = core::draw_frame(std::move (audio));
113+
}
114+
}
115+
116+
void add_video(core::draw_frame video) {
117+
if (has_video) return;
118+
has_video = true;
119+
120+
if (frame) {
121+
frame = core::draw_frame::over(frame, std::move (video));
122+
} else {
123+
frame = std::move (video);
124+
}
125+
}
126+
};
127+
128+
68129
class html_client
69130
: public CefClient
70131
, public CefRenderHandler
132+
, public CefAudioHandler
71133
, public CefLifeSpanHandler
72134
, public CefLoadHandler
73135
, public CefDisplayHandler
@@ -86,11 +148,14 @@ class html_client
86148
bool gpu_enabled_;
87149
tbb::concurrent_queue<std::wstring> javascript_before_load_;
88150
std::atomic<bool> loaded_;
89-
std::queue<std::pair<std::int_least64_t, core::draw_frame>> frames_;
151+
std::queue<presentation_frame> frames_;
152+
core::draw_frame last_generated_frame_;
90153
mutable std::mutex frames_mutex_;
91154
const size_t frames_max_size_ = 4;
92155
std::atomic<bool> closing_;
93156

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

@@ -168,15 +233,15 @@ class html_client
168233

169234
// Check if the sole buffered frame is too young to have a partner field generated (with a tolerance)
170235
auto time_per_frame = (1000 * 1.5) / format_desc_.fps;
171-
auto front_frame_is_too_young = (now_time - frames_.front().first) < time_per_frame;
236+
auto front_frame_is_too_young = (now_time - frames_.front().timestamp) < time_per_frame;
172237

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

178-
last_frame_time_ = frames_.front().first;
179-
last_frame_ = std::move(frames_.front().second);
243+
last_frame_time_ = frames_.front().timestamp;
244+
last_frame_ = std::move(frames_.front().frame);
180245
frames_.pop();
181246

182247
graph_->set_value("buffered-frames", (double)frames_.size() / frames_max_size_);
@@ -246,12 +311,7 @@ class html_client
246311
}
247312

248313
private:
249-
std::int_least64_t now()
250-
{
251-
return std::chrono::duration_cast<std::chrono::milliseconds>(
252-
std::chrono::high_resolution_clock::now().time_since_epoch())
253-
.count();
254-
}
314+
255315

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

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

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

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

359427
CefRefPtr<CefLoadHandler> GetLoadHandler() override { return this; }
@@ -379,7 +447,7 @@ class html_client
379447

380448
{
381449
std::lock_guard<std::mutex> lock(frames_mutex_);
382-
frames_.push(std::make_pair(now(), core::draw_frame::empty()));
450+
frames_.push(presentation_frame());
383451
}
384452

385453
{
@@ -400,6 +468,55 @@ class html_client
400468
return false;
401469
}
402470

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

0 commit comments

Comments
 (0)