Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: route audio from CEF #1517

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/modules/ffmpeg/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ set(SOURCES
producer/av_producer.cpp
producer/av_input.cpp
util/av_util.cpp
util/audio_resampler.cpp
producer/ffmpeg_producer.cpp
consumer/ffmpeg_consumer.cpp

Expand All @@ -15,6 +16,7 @@ set(HEADERS
producer/av_producer.h
producer/av_input.h
util/av_util.h
util/audio_resampler.h
producer/ffmpeg_producer.h
consumer/ffmpeg_consumer.h

Expand Down
38 changes: 38 additions & 0 deletions src/modules/ffmpeg/util/audio_resampler.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#include "audio_resampler.h"
#include "av_assert.h"

extern "C" {
#include <libavutil/samplefmt.h>
#include <libswresample/swresample.h>
}

namespace caspar::ffmpeg {

AudioResampler::AudioResampler(int64_t sample_rate, AVSampleFormat in_sample_fmt)
: ctx(std::shared_ptr<SwrContext>(swr_alloc_set_opts(nullptr,
AV_CH_LAYOUT_7POINT1,
AV_SAMPLE_FMT_S32,
sample_rate,
AV_CH_LAYOUT_7POINT1,
in_sample_fmt,
sample_rate,
0,
nullptr),
[](SwrContext* ptr) { swr_free(&ptr); }))
{
if (!ctx)
FF_RET(AVERROR(ENOMEM), "swr_alloc_set_opts");

FF_RET(swr_init(ctx.get()), "swr_init");
}

caspar::array<int32_t> AudioResampler::convert(int frames, const void** src)
{
auto result = caspar::array<int32_t>(frames * 8 * sizeof(int32_t));
auto ptr = result.data();
swr_convert(ctx.get(), (uint8_t**)&ptr, frames, reinterpret_cast<const uint8_t**>(src), frames);

return result;
}

}; // namespace caspar::ffmpeg
27 changes: 27 additions & 0 deletions src/modules/ffmpeg/util/audio_resampler.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#include <common/array.h>
#include <memory>

#pragma once

extern "C" {
#include <libavutil/samplefmt.h>
}

struct SwrContext;

namespace caspar::ffmpeg {

class AudioResampler
{
std::shared_ptr<SwrContext> ctx;

public:
AudioResampler(int64_t sample_rate, AVSampleFormat in_sample_fmt);

AudioResampler(const AudioResampler&) = delete;
AudioResampler& operator=(const AudioResampler&) = delete;

caspar::array<int32_t> convert(int frames, const void** src);
};

}; // namespace caspar::ffmpeg
4 changes: 3 additions & 1 deletion src/modules/html/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ target_include_directories(html PRIVATE
..
../..
${CEF_INCLUDE_PATH}
)
${FFMPEG_INCLUDE_PATH}
)
target_link_libraries(html ffmpeg)

set_target_properties(html PROPERTIES FOLDER modules)
source_group(sources\\producer producer/*)
Expand Down
165 changes: 141 additions & 24 deletions src/modules/html/producer/html_producer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,83 @@
#include <queue>
#include <utility>

#include <ffmpeg/util/audio_resampler.h>

#include "../html.h"

namespace caspar { namespace html {

inline std::int_least64_t now()
{
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::high_resolution_clock::now().time_since_epoch())
.count();
}

struct presentation_frame
{
std::int_least64_t timestamp = now();
core::draw_frame frame = core::draw_frame::empty();
bool has_video = false;
bool has_audio = false;

explicit presentation_frame(core::draw_frame video = {})
{
if (video) {
frame = std::move(video);
has_video = true;
}
}

presentation_frame(presentation_frame&& other) noexcept
: timestamp(other.timestamp)
, frame(std::move(other.frame))
{
}

presentation_frame(const presentation_frame&) = delete;
presentation_frame& operator=(const presentation_frame&) = delete;

presentation_frame& operator=(presentation_frame&& rhs)
{
timestamp = rhs.timestamp;
frame = std::move(rhs.frame);
return *this;
}

~presentation_frame() {}

void add_audio(core::mutable_frame audio)
{
if (has_audio)
return;
has_audio = true;

if (frame) {
frame = core::draw_frame::over(frame, core::draw_frame(std::move(audio)));
} else {
frame = core::draw_frame(std::move(audio));
}
}

void add_video(core::draw_frame video)
{
if (has_video)
return;
has_video = true;

if (frame) {
frame = core::draw_frame::over(frame, std::move(video));
} else {
frame = std::move(video);
}
}
};

class html_client
: public CefClient
, public CefRenderHandler
, public CefAudioHandler
, public CefLifeSpanHandler
, public CefLoadHandler
, public CefDisplayHandler
Expand All @@ -80,15 +150,18 @@ class html_client
caspar::timer paint_timer_;
caspar::timer test_timer_;

spl::shared_ptr<core::frame_factory> frame_factory_;
core::video_format_desc format_desc_;
bool gpu_enabled_;
tbb::concurrent_queue<std::wstring> javascript_before_load_;
std::atomic<bool> loaded_;
std::queue<std::pair<std::int_least64_t, core::draw_frame>> frames_;
mutable std::mutex frames_mutex_;
const size_t frames_max_size_ = 4;
std::atomic<bool> closing_;
spl::shared_ptr<core::frame_factory> frame_factory_;
core::video_format_desc format_desc_;
bool gpu_enabled_;
tbb::concurrent_queue<std::wstring> javascript_before_load_;
std::atomic<bool> loaded_;
std::queue<presentation_frame> frames_;
core::draw_frame last_generated_frame_;
mutable std::mutex frames_mutex_;
const size_t frames_max_size_ = 4;
std::atomic<bool> closing_;

std::unique_ptr<ffmpeg::AudioResampler> audioResampler_;

core::draw_frame last_frame_;
std::int_least64_t last_frame_time_;
Expand Down Expand Up @@ -167,15 +240,15 @@ class html_client

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

if (follows_gap_in_frames && front_frame_is_too_young) {
return false;
}
}

last_frame_time_ = frames_.front().first;
last_frame_ = std::move(frames_.front().second);
last_frame_time_ = frames_.front().timestamp;
last_frame_ = std::move(frames_.front().frame);
frames_.pop();

graph_->set_value("buffered-frames", (double)frames_.size() / frames_max_size_);
Expand All @@ -190,12 +263,13 @@ class html_client
{
if (!try_pop(field)) {
graph_->set_tag(diagnostics::tag_severity::SILENT, "late-frame");
return core::draw_frame::still(last_frame_);
} else {
return last_frame_;
}

return last_frame_;
}

core::draw_frame last_frame() const { return last_frame_; }
core::draw_frame last_frame() const { return core::draw_frame::still(last_frame_); }

bool is_ready() const
{
Expand Down Expand Up @@ -245,13 +319,6 @@ class html_client
}

private:
std::int_least64_t now()
{
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::high_resolution_clock::now().time_since_epoch())
.count();
}

void GetViewRect(CefRefPtr<CefBrowser> browser, CefRect& rect) override
{
CASPAR_ASSERT(CefCurrentlyOn(TID_UI));
Expand Down Expand Up @@ -302,7 +369,10 @@ class html_client
{
std::lock_guard<std::mutex> lock(frames_mutex_);

frames_.push(std::make_pair(now(), core::draw_frame(std::move(frame))));
core::draw_frame new_frame = core::draw_frame(std::move(frame));
last_generated_frame_ = new_frame;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Julusian Could you talk me through how the move semantics works here? As far as I can tell, the draw_frame assignment operator moves the content of new_frame to last_generated_frame_ which would leave new_frame invalid.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can tell, the draw_frame assignment operator moves the content of new_frame to last_generated_frame_ which would leave new_frame invalid.

The parameter of the assignment operator is not a reference, so will be a copy of new_frame making it safe to mutate. You have made me question this now, but this same pattern has been in place for draw_frame for years, with the core::frame_producer doing it with any draw_frame that gets produced by every producer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I see it now. I missed the implicit call to the copy constructor during the assignment. It looks OK.


frames_.push(presentation_frame(std::move(new_frame)));
while (frames_.size() > 4) {
frames_.pop();
graph_->set_tag(diagnostics::tag_severity::WARNING, "dropped-frame");
Expand Down Expand Up @@ -353,6 +423,8 @@ class html_client

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

CefRefPtr<CefAudioHandler> GetAudioHandler() override { return this; }

CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }

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

{
std::lock_guard<std::mutex> lock(frames_mutex_);
frames_.push(std::make_pair(now(), core::draw_frame::empty()));
frames_.push(presentation_frame());
}

{
Expand All @@ -399,6 +471,51 @@ class html_client
return false;
}

bool GetAudioParameters(CefRefPtr<CefBrowser> browser, CefAudioParameters& params) override
{
params.channel_layout = CEF_CHANNEL_LAYOUT_7_1;
params.sample_rate = format_desc_.audio_sample_rate;
params.frames_per_buffer = format_desc_.audio_cadence[0];
return format_desc_.audio_cadence.size() == 1; // TODO - handle 59.94
}

void OnAudioStreamStarted(CefRefPtr<CefBrowser> browser, const CefAudioParameters& params, int channels) override
{
audioResampler_ = std::make_unique<ffmpeg::AudioResampler>(params.sample_rate, AV_SAMPLE_FMT_FLTP);
}
void OnAudioStreamPacket(CefRefPtr<CefBrowser> browser, const float** data, int samples, int64_t pts) override
{
if (!audioResampler_)
return;

auto audio = audioResampler_->convert(samples, reinterpret_cast<const void**>(data));
auto audio_frame = core::mutable_frame(this, {}, std::move(audio), core::pixel_format_desc());

{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This crashes on Linux. If i comment out this entire block, it runs.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this is from the hardcoded 8 in

auto result = caspar::array<int32_t>(frames * 8 * sizeof(int32_t));
?
I appear to have opened this PR a few hours before I made a change to support 16 channel audio 28c4dcd

That would explain the slowness/broken audio you are experiencing too

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try that

std::lock_guard<std::mutex> lock(frames_mutex_);
if (frames_.empty()) {
presentation_frame wrapped_frame(last_generated_frame_);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I don't understand how this would work. When you construct a presentation_frame from a draw_frame, you move the draw_frame into the presentation_frame. Doesn't that invalidate last_generated_frame_?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the other question, the constructor parameter isnt a r-value or l-value, so I think it is fine

wrapped_frame.add_audio(std::move(audio_frame));

frames_.push(std::move(wrapped_frame));
} else {
if (!frames_.back().has_audio) {
frames_.back().add_audio(std::move(audio_frame));
} else {
presentation_frame wrapped_frame(last_generated_frame_);
wrapped_frame.add_audio(std::move(audio_frame));
frames_.push(std::move(wrapped_frame));
}
}
}
}
void OnAudioStreamStopped(CefRefPtr<CefBrowser> browser) override { audioResampler_ = nullptr; }
void OnAudioStreamError(CefRefPtr<CefBrowser> browser, const CefString& message) override
{
CASPAR_LOG(info) << "[html_producer] OnAudioStreamError: \"" << message.ToString() << "\"";
audioResampler_ = nullptr;
}

void do_execute_javascript(const std::wstring& javascript)
{
html::begin_invoke([=] {
Expand Down