OSSIA
Open Scenario System for Interactive Application
Loading...
Searching...
No Matches
libav.hpp
1#pragma once
2#include <ossia/detail/config.hpp>
3
4#if __has_include(<libavcodec/avcodec.h>) && \
5 __has_include(<libavformat/avformat.h>) && \
6 __has_include(<libavdevice/avdevice.h>) && \
7 __has_include(<libavutil/frame.h>) && \
8 __has_include(<libswresample/swresample.h>) && \
9 __has_include(<libswscale/swscale.h>)
10
11#define OSSIA_HAS_LIBAV 1
12#include <ossia/detail/flicks.hpp>
13
14extern "C" {
15#include <libavcodec/avcodec.h>
16#include <libavformat/avformat.h>
17#include <libavutil/channel_layout.h>
18#include <libavutil/version.h>
19#include <libswresample/swresample.h>
20}
21
22namespace ossia
23{
24// Try to find a flag that is likely not going to be taken by AVSEEK_FLAG_... ever
25static constexpr const int OSSIA_LIBAV_SEEK_ROUGH = 0b00000100000000000000000000000000;
26inline bool seek_to_flick(
27 AVFormatContext* format, AVCodecContext* codec, AVStream* stream, int64_t flicks,
28 int flags = 0)
29{
30 constexpr auto flicks_tb = AVRational{1, ossia::flicks_per_second<int64_t>};
31 constexpr auto av_tb = AVRational{1, AV_TIME_BASE};
32
33 const auto dts = av_rescale_q_rnd(flicks, flicks_tb, av_tb, AVRounding::AV_ROUND_DOWN);
34
35 avio_flush(format->pb);
36 avformat_flush(format);
37 if(codec)
38 avcodec_flush_buffers(codec);
39
40 if(flags & OSSIA_LIBAV_SEEK_ROUGH)
41 {
42 // Seems to work better for seeking for thumbnails
43 flags &= ~OSSIA_LIBAV_SEEK_ROUGH;
44 if(av_seek_frame(format, -1, dts, flags) < 0)
45 return false;
46 }
47 else
48 {
49 // Seems to work better for seeking for video & audio
50 if(avformat_seek_file(format, -1, INT64_MIN, dts, INT64_MAX, flags) < 0)
51 return false;
52 }
53
54 avio_flush(format->pb);
55 avformat_flush(format);
56 if(codec)
57 avcodec_flush_buffers(codec);
58
59 return true;
60}
61
62static inline int avstream_get_audio_channels(AVStream& stream) noexcept
63{
64#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 24, 100)
65 return stream.codecpar->ch_layout.nb_channels;
66#else
67 return stream.codecpar->channels;
68#endif
69}
70
71// Opens path over custom IO; returns nullptr if it does not handle that path.
72// io_owner/io_free hand back the streaming resources for cleanup() to release.
73using libav_open_hook
74 = AVFormatContext* (*)(const char* path, void*& io_owner, void (*&io_free)(void*));
75inline libav_open_hook& libav_custom_open() noexcept
76{
77 static libav_open_hook hook = nullptr;
78 return hook;
79}
80
81struct libav_handle
82{
83 AVFormatContext* format{};
84 AVStream* stream{};
85 AVCodecContext* codec{};
86 SwrContext* resample{};
87 void* io_owner{};
88 void (*io_free)(void*){};
89
90 libav_handle() = default;
91 libav_handle(const libav_handle& other) = delete;
92 libav_handle(libav_handle&& other)
93 {
94 format = other.format;
95 other.format = nullptr;
96 stream = other.stream;
97 other.stream = nullptr;
98 codec = other.codec;
99 other.codec = nullptr;
100 resample = other.resample;
101 other.resample = nullptr;
102 io_owner = other.io_owner;
103 other.io_owner = nullptr;
104 io_free = other.io_free;
105 other.io_free = nullptr;
106 }
107 libav_handle& operator=(const libav_handle& other) = delete;
108 libav_handle& operator=(libav_handle&& other)
109 {
110 format = other.format;
111 other.format = nullptr;
112 stream = other.stream;
113 other.stream = nullptr;
114 codec = other.codec;
115 other.codec = nullptr;
116 resample = other.resample;
117 other.resample = nullptr;
118 io_owner = other.io_owner;
119 other.io_owner = nullptr;
120 io_free = other.io_free;
121 other.io_free = nullptr;
122 return *this;
123 }
124 ~libav_handle()
125 {
126 if(format)
127 cleanup();
128 }
129
130 void cleanup()
131 {
132 stream = nullptr;
133 if(resample)
134 swr_free(&resample);
135 if(codec)
136 {
137 avcodec_free_context(&codec);
138 }
139 if(format)
140 {
141 avformat_close_input(&format);
142 avformat_free_context(format);
143 format = nullptr;
144 }
145 if(io_owner)
146 {
147 io_free(io_owner);
148 io_owner = nullptr;
149 io_free = nullptr;
150 }
151 }
152
153 void open(const std::string& path, int stream_index, int target_rate) noexcept
154 {
155 if(auto hook = libav_custom_open())
156 format = hook(path.c_str(), io_owner, io_free);
157
158 if(!format
159 && avformat_open_input(&format, path.c_str(), nullptr, nullptr) != 0)
160 {
161 cleanup();
162 return;
163 }
164 if(avformat_find_stream_info(format, nullptr) < 0)
165 {
166 cleanup();
167 return;
168 }
169 if(stream_index < 0 || stream_index >= int(format->nb_streams))
170 {
171 cleanup();
172 return;
173 }
174 if(format->streams[stream_index]->codecpar->codec_type != AVMEDIA_TYPE_AUDIO)
175 {
176 cleanup();
177 return;
178 }
179
180 stream = format->streams[stream_index];
181
182 auto cdc = avcodec_find_decoder(stream->codecpar->codec_id);
183 if(!cdc)
184 {
185 cleanup();
186 return;
187 }
188
189 codec = avcodec_alloc_context3(cdc);
190 if(avcodec_parameters_to_context(codec, stream->codecpar) < 0)
191 {
192 cleanup();
193 return;
194 }
195
196 if(avcodec_open2(codec, cdc, nullptr) < 0)
197 {
198 cleanup();
199 return;
200 }
201
202 AVSampleFormat out_sample_fmt{AV_SAMPLE_FMT_FLT};
203 AVSampleFormat in_sample_fmt{(AVSampleFormat)stream->codecpar->format};
204 int in_sample_rate = stream->codecpar->sample_rate;
205 int out_sample_rate = target_rate == 0 ? in_sample_rate : target_rate;
206
207#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 24, 100)
208 AVChannelLayout out_layout{stream->codecpar->ch_layout};
209 AVChannelLayout in_layout{stream->codecpar->ch_layout};
210
211 swr_alloc_set_opts2(
212 &resample, &out_layout, out_sample_fmt, out_sample_rate, &in_layout,
213 in_sample_fmt, in_sample_rate, 0, nullptr);
214#else
215 int64_t out_layout = stream->codecpar->channel_layout;
216 int64_t in_layout = stream->codecpar->channel_layout;
217
218 resample = swr_alloc_set_opts(
219 resample, out_layout, out_sample_fmt, out_sample_rate, in_layout, in_sample_fmt,
220 in_sample_rate, 0, nullptr);
221#endif
222 if(resample)
223 swr_init(resample);
224 }
225
226 int rate() const noexcept { return stream->codecpar->sample_rate; }
227 int channels() const noexcept { return avstream_get_audio_channels(*stream); }
228
229 int64_t totalPCMFrameCount() const noexcept
230 {
231 if(stream->duration > 0)
232 {
233 return stream->duration;
234 }
235 else if(format->duration > 0)
236 {
237 double seconds = format->duration / double(AV_TIME_BASE);
238 double frames = seconds * stream->codecpar->sample_rate;
239 return frames;
240 }
241 else if(stream->nb_frames > 0)
242 {
243 return av_rescale_q(
244 stream->nb_frames, stream->r_frame_rate,
245 AVRational{1, stream->codecpar->sample_rate});
246 }
247 else
248 {
249 return 0;
250 }
251 }
252
253 operator bool() const noexcept { return bool(format); }
254
255 void fetch(int64_t frame, int samples_to_write, auto func)
256 {
257 // First seek
258 ossia::seek_to_flick(
259 format, codec, stream,
260 ossia::flicks_per_second<double> * frame / stream->codecpar->sample_rate,
261 AVSEEK_FLAG_ANY);
262
263 const std::size_t channels = this->channels();
264 std::vector<float> tmp;
265 if(channels == 0)
266 return;
267
268 int processed = 0;
269 while(processed < samples_to_write)
270 {
271 // Need to fetch more data
272 {
273 auto packet = av_packet_alloc();
274 int ret{};
275 {
276 ret = av_read_frame(format, packet);
277
278 while(ret >= 0 && ret != AVERROR(EOF) && packet->stream_index != stream->index)
279 {
280 av_packet_unref(packet);
281 ret = av_read_frame(format, packet);
282 }
283 if(ret == AVERROR(EOF))
284 {
285 break;
286 }
287 }
288 if(ret < 0)
289 {
290 return;
291 }
292
293 ret = avcodec_send_packet(codec, packet);
294 if(ret == 0)
295 {
296 auto avframe = av_frame_alloc();
297 ret = avcodec_receive_frame(codec, avframe);
298 if(ret == 0)
299 {
300 const int av_frame_start = avframe->best_effort_timestamp;
301 const int samples = avframe->nb_samples;
302
303 // It's possible that we get a frame that is just after what we have asked, thus
304 // frame - av_frame_start can sometimes be negative
305 const int offset = (frame < av_frame_start) ? 0 : (frame - av_frame_start);
306 if(offset >= samples)
307 {
308 // ffmpeg didn't even manage to seek to the correct frame, we have to read another
309 av_frame_free(&avframe);
310 av_packet_unref(packet);
311 av_packet_free(&packet);
312 continue;
313 }
314
315 // The actual samples we can read here:
316 //samples = samples - (frame - av_frame_start);
317
318 tmp.resize(samples * channels);
319 float* out_ptr = tmp.data();
320
321 // Note: this function is only called with swr_convert's rate not changing
322 int read_samples = swr_convert(
323 resample, (uint8_t**)&out_ptr, samples,
324 (const uint8_t**)avframe->extended_data, samples);
325
326 auto end = tmp.data() + tmp.size();
327
328 read_samples -= offset;
329 if(read_samples <= 0)
330 {
331 av_frame_free(&avframe);
332 av_packet_unref(packet);
333 av_packet_free(&packet);
334 return;
335 }
336
337 out_ptr += offset * channels;
338
339 for(int i = 0; i < read_samples; i++)
340 {
341 func(out_ptr, end);
342
343 processed++;
344 if(processed == samples_to_write)
345 {
346 av_frame_free(&avframe);
347 av_packet_unref(packet);
348 av_packet_free(&packet);
349 return;
350 }
351
352 out_ptr += channels;
353 }
354
355 // We read an entire packet: set "frame" to the beginning of next packet
356 // so that we read it in full
357 frame = avframe->best_effort_timestamp + avframe->nb_samples;
358 }
359 av_frame_free(&avframe);
360 }
361 av_packet_unref(packet);
362 av_packet_free(&packet);
363 }
364 }
365 }
366};
367
368}
369#endif
Definition git_info.h:7