MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
FFmpegCameraReader.cpp
Go to the documentation of this file.
2
4
7
9
10extern "C" {
11#include <libavcodec/avcodec.h>
12#include <libavdevice/avdevice.h>
13#include <libavformat/avformat.h>
14#include <libavutil/dict.h>
15#include <libavutil/frame.h>
16#include <libavutil/imgutils.h>
17#include <libswscale/swscale.h>
18}
19
20namespace MayaFlux::IO {
21
23 : m_demux(std::make_shared<FFmpegDemuxContext>())
24 , m_video(std::make_shared<VideoStreamContext>())
25{
26}
27
29{
30 m_standalone_reader_id = reader_id;
31
33 .get_service<Registry::Service::IOService>()) {
34
35 m_standalone_service = std::make_shared<Registry::Service::IOService>();
36 m_standalone_service->request_frame = [this](uint64_t reader_id) {
37 if (reader_id == m_standalone_reader_id)
39 };
40
43 [this]() -> void* { return m_standalone_service.get(); });
44
45 m_owns_service = true;
46 }
47}
48
58
59#ifdef MAYAFLUX_PLATFORM_MACOS
60double macos_camera_max_fps(const std::string& device_name);
61#endif
62
63namespace {
64
65 constexpr std::string_view PREFERRED_INPUT_FORMAT =
66#ifdef MAYAFLUX_PLATFORM_MACOS
67 "";
68#else
69 "mjpeg";
70#endif
71
72 struct OpenHints {
73 bool format;
74 bool size;
75 bool fps;
76 };
77
78 AVDictionary* build_options(const CameraConfig& config,
79 const std::string& input_format, const OpenHints& hints)
80 {
81 AVDictionary* opts = nullptr;
82
83 if (hints.fps && config.target_fps > 0.0)
84 av_dict_set(&opts, "framerate", std::to_string(config.target_fps).c_str(), 0);
85#ifdef MAYAFLUX_PLATFORM_MACOS
86 else if (const double native = macos_camera_max_fps(config.device_name); native > 0.0)
87 av_dict_set(&opts, "framerate", std::to_string(native).c_str(), 0);
88#endif
89
90 if (hints.size && config.target_width > 0 && config.target_height > 0) {
91 const std::string size_str = std::to_string(config.target_width)
92 + "x"
93 + std::to_string(config.target_height);
94 av_dict_set(&opts, "video_size", size_str.c_str(), 0);
95 }
96
97 if (hints.format && !input_format.empty()) {
98#if defined(MAYAFLUX_PLATFORM_LINUX)
99 av_dict_set(&opts, "input_format", input_format.c_str(), 0);
100#elif defined(MAYAFLUX_PLATFORM_WINDOWS)
101 const bool raw = av_get_pix_fmt(input_format.c_str()) != AV_PIX_FMT_NONE;
102 av_dict_set(&opts, raw ? "pixel_format" : "vcodec", input_format.c_str(), 0);
103#endif
104 }
105
106 return opts;
107 }
108
109}
110
112{
113 close();
114
115 const std::string fmt_name = config.format_override.empty()
116 ? std::string(CAMERA_FORMAT)
117 : config.format_override;
118
119 const std::string input_format = config.input_format.empty()
120 ? std::string(PREFERRED_INPUT_FORMAT)
121 : config.input_format;
122
123 constexpr std::array<OpenHints, 4> ladder { {
124 { .format = true, .size = true, .fps = true },
125 { .format = false, .size = true, .fps = true },
126 { .format = false, .size = true, .fps = false },
127 { .format = false, .size = false, .fps = false },
128 } };
129
131
132 for (size_t step = 0; step < ladder.size(); ++step) {
133 const OpenHints& hints = ladder.at(step);
134 if (hints.format && input_format.empty())
135 continue;
136
137 AVDictionary* opts = build_options(config, input_format, hints);
138 const bool opened = m_demux->open_device(config.device_name, fmt_name, &opts);
139 av_dict_free(&opts);
140
141 if (!opened) {
142 m_last_error = "Device open failed: " + m_demux->last_error();
143 continue;
144 }
145
146 if (!m_video->open_device(*m_demux,
147 hints.size ? config.target_width : 0,
148 hints.size ? config.target_height : 0,
149 config.pixel_format)) {
150 m_last_error = "Video stream open failed: " + m_video->last_error();
151 m_demux->close();
152 continue;
153 }
154
155 m_sws_buf.clear();
156 m_scaler_ready = false;
157
158 if (step > (input_format.empty() ? 1U : 0U)) {
160 "FFmpegCameraReader::open: '{}' did not accept the requested mode, "
161 "opened with fallback step {}",
162 config.device_name, step + 1);
163 }
164
166 "FFmpegCameraReader: opened '{}' via {}: {}x{} @{:.1f}fps (scaler deferred)",
167 config.device_name, fmt_name,
168 m_video->width, m_video->height, m_video->frame_rate);
169
170 return true;
171 }
172
174 "FFmpegCameraReader::open: {}", m_last_error);
175 return false;
176}
177
178std::vector<CameraMode> FFmpegCameraReader::preset_modes() const
179{
180 return {
181 { .label = "Device default" },
182 { .label = "640 x 480 @ 30 fps", .width = 640, .height = 480, .fps = 30.0 },
183 { .label = "1280 x 720 @ 30 fps", .width = 1280, .height = 720, .fps = 30.0 },
184 { .label = "1920 x 1080 @ 30 fps", .width = 1920, .height = 1080, .fps = 30.0 },
185 { .label = "1920 x 1080 @ 60 fps (high-end cameras)", .width = 1920, .height = 1080, .fps = 60.0, .high_end = true },
186 { .label = "2560 x 1440 @ 30 fps (high-end cameras)", .width = 2560, .height = 1440, .fps = 30.0, .high_end = true },
187 };
188}
189
191{
193
194 std::unique_lock lock(m_ctx_mutex);
195 m_video->close();
196 m_demux->close();
197 m_scaler_ready = false;
198 m_sws_buf.clear();
199 m_sws_buf.shrink_to_fit();
200 m_last_error.clear();
201}
202
204{
205 std::shared_lock lock(m_ctx_mutex);
206 return m_demux->is_open() && m_video->is_codec_valid();
207}
208
209std::shared_ptr<Kakshya::CameraContainer>
211{
212 std::shared_lock lock(m_ctx_mutex);
213 if (!m_video->is_codec_valid()) {
214 m_last_error = "Cannot create container: reader not open";
215 return nullptr;
216 }
217
219 if (!fmt) {
220 m_last_error = "Requested pixel format has no ImageFormat equivalent";
221 return nullptr;
222 }
223
224 return std::make_shared<Kakshya::CameraContainer>(
225 m_video->out_width,
226 m_video->out_height,
227 *fmt,
228 m_video->frame_rate);
229}
230
232 const std::shared_ptr<Kakshya::CameraContainer>& container)
233{
234 if (!container)
235 return false;
236
237 std::shared_lock lock(m_ctx_mutex);
238 if (!m_video->is_codec_valid())
239 return false;
240
241 uint8_t* dest = container->mutable_frame_ptr();
242 if (!dest)
243 return false;
244
245 AVPacket* pkt = av_packet_alloc();
246 if (!pkt)
247 return false;
248
249 AVFrame* frame = av_frame_alloc();
250 if (!frame) {
251 av_packet_free(&pkt);
252 return false;
253 }
254
255 bool got_frame = false;
256
257 while (!got_frame) {
258 int ret = av_read_frame(m_demux->format_context, pkt);
259 if (ret < 0)
260 break;
261
262 if (pkt->stream_index != m_video->stream_index) {
263 av_packet_unref(pkt);
264 continue;
265 }
266
267 ret = avcodec_send_packet(m_video->codec_context, pkt);
268 av_packet_unref(pkt);
269
270 if (ret < 0 && ret != AVERROR(EAGAIN))
271 break;
272
273 ret = avcodec_receive_frame(m_video->codec_context, frame);
274 if (ret == AVERROR(EAGAIN))
275 continue;
276 if (ret < 0)
277 break;
278
279 if (!m_scaler_ready) {
280 if (!m_video->rebuild_scaler_from_frame(
281 frame,
282 static_cast<uint32_t>(frame->width),
283 static_cast<uint32_t>(frame->height))) {
285 "FFmpegCameraReader: scaler init failed: {}",
286 m_video->last_error());
287 break;
288 }
289 const size_t buf_bytes = static_cast<size_t>(m_video->out_linesize) * m_video->out_height;
290 m_sws_buf.assign(buf_bytes, 0);
291 m_scaler_ready = true;
292 }
293
294 uint8_t* sws_dst[1] = { m_sws_buf.data() };
295 int sws_stride[1] = { m_video->out_linesize };
296
297 sws_scale(m_video->sws_context,
298 frame->data, frame->linesize,
299 0, static_cast<int>(m_video->height),
300 sws_dst, sws_stride);
301
302 const int packed_stride = static_cast<int>(m_video->out_width * m_video->out_bytes_per_pixel);
303
304 if (m_video->out_linesize == packed_stride) {
305 std::memcpy(dest, m_sws_buf.data(),
306 static_cast<size_t>(packed_stride) * m_video->out_height);
307 } else {
308 for (uint32_t row = 0; row < m_video->out_height; ++row) {
309 std::memcpy(
310 dest + static_cast<size_t>(row) * packed_stride,
311 m_sws_buf.data() + static_cast<size_t>(row) * m_video->out_linesize,
312 static_cast<size_t>(packed_stride));
313 }
314 }
315
316 container->mark_ready_for_processing(true);
317 got_frame = true;
318 }
319
320 av_frame_free(&frame);
321 av_packet_free(&pkt);
322
323 return got_frame;
324}
325
327{
328 std::shared_lock lock(m_ctx_mutex);
329 return m_video->out_width;
330}
331
333{
334 std::shared_lock lock(m_ctx_mutex);
335 return m_video->out_height;
336}
337
339{
340 std::shared_lock lock(m_ctx_mutex);
341 return m_video->frame_rate;
342}
343
345 const std::shared_ptr<Kakshya::CameraContainer>& container)
346{
347 m_container_ref = container;
349}
350
352{
353 {
354 std::lock_guard lock(m_decode_mutex);
355 m_frame_requested.store(true, std::memory_order_relaxed);
356 }
357 m_decode_cv.notify_one();
358}
359
360const std::string& FFmpegCameraReader::last_error() const
361{
362 return m_last_error;
363}
364
366{
368
369 m_decode_stop.store(false);
370 m_decode_active.store(true);
372}
373
375{
376 if (!m_decode_active.load())
377 return;
378
379 m_decode_stop.store(true);
380 m_decode_cv.notify_all();
381
382 if (m_decode_thread.joinable())
383 m_decode_thread.join();
384
385 m_decode_active.store(false);
386}
387
389{
390 while (!m_decode_stop.load()) {
391 {
392 std::unique_lock lock(m_decode_mutex);
393 m_decode_cv.wait(lock, [this] {
394 return m_decode_stop.load()
395 || m_frame_requested.load(std::memory_order_relaxed);
396 });
397 }
398
399 if (m_decode_stop.load())
400 break;
401
402 m_frame_requested.store(false, std::memory_order_relaxed);
403
404 auto container = m_container_ref.lock();
405 if (container)
406 pull_frame(container);
407 }
408
409 m_decode_active.store(false);
410}
411
412} // namespace MayaFlux::IO
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
bool format
bool size
bool fps
std::shared_ptr< FFmpegDemuxContext > m_demux
void setup_io_service(uint64_t reader_id) override
Setup an IOService for this reader with the given reader_id.
uint32_t height() const override
Negotiated output height in pixels.
std::vector< CameraMode > preset_modes() const override
Capture modes this source offers as defaults, ordered from safest to most demanding.
std::weak_ptr< Kakshya::CameraContainer > m_container_ref
void pull_frame_all() override
Signal the source to pull one frame.
std::shared_ptr< VideoStreamContext > m_video
int m_requested_pixel_format
Pixel format requested at open() time, as an AVPixelFormat int.
const std::string & last_error() const override
Last error string, empty if no error.
uint32_t width() const override
Negotiated output width in pixels.
std::shared_ptr< Registry::Service::IOService > m_standalone_service
void close() override
Release device, decode, and scratch resources.
bool is_open() const override
True if the device is open and ready to deliver frames.
bool pull_frame(const std::shared_ptr< Kakshya::CameraContainer > &container) override
Produce one frame into the container.
std::shared_ptr< Kakshya::CameraContainer > create_container() const override
Create a CameraContainer sized to the negotiated device resolution.
void set_container(const std::shared_ptr< Kakshya::CameraContainer > &container) override
Store a weak reference to the container for IOService dispatch.
double frame_rate() const override
Negotiated frame rate in fps.
bool open(const CameraConfig &config) override
Open a camera device using the supplied config.
RAII owner of a single AVFormatContext and associated demux state.
RAII owner of one video stream's codec and pixel-format scaler state.
void register_service(ServiceFactory factory)
Register a backend service capability.
static BackendRegistry & instance()
Get the global registry instance.
void unregister_service()
Unregister a service.
std::optional< Portal::Graphics::ImageFormat > to_image_format(int av_pixel_format)
Map an AVPixelFormat to the Portal ImageFormat backing it.
@ FileIO
Filesystem I/O operations.
@ Runtime
General runtime operations (default fallback)
@ IO
Networking, file handling, streaming.
int pixel_format
Target AVPixelFormat as int; negative selects AV_PIX_FMT_RGBA.
std::string device_name
Platform device string.
std::string format_override
Leave empty to use CAMERA_FORMAT for current platform.
std::string input_format
Capture format or codec (e.g. "mjpeg"); empty selects mjpeg where the platform supports it.
uint32_t target_width
Requested width in pixels.
uint32_t target_height
Requested height in pixels.
Configuration for opening a camera device via FFmpeg.
Backend IO streaming service interface.
Definition IOService.hpp:18