capi.rs 18.1 KB
Newer Older
1
2
3
4
5
//! # C API for rav1e
//!
//! [rav1e](https://github.com/xiph/rav1e/) is an [AV1](https://aomediacodec.github.io/av1-spec/)
//! encoder written in [Rust](https://rust-lang.org)
//!
6
//! This is the C-compatible API
7

Luca Barbato's avatar
Luca Barbato committed
8
9
10
11
use std::slice;
use std::sync::Arc;

use std::ffi::CStr;
12
use std::ffi::CString;
Luca Barbato's avatar
Luca Barbato committed
13
14
15
use std::os::raw::c_char;
use std::os::raw::c_int;

16
17
18
use libc::size_t;
use libc::ptrdiff_t;

19
use num_derive::*;
20
21
use num_traits::cast::FromPrimitive;

22
23
use crate::prelude as rav1e;

24
25
26
/// Raw video Frame
///
/// It can be allocated throught rav1e_frame_new(), populated using rav1e_frame_fill_plane()
27
/// and freed using rav1e_frame_unref().
28
pub struct Frame(Arc<rav1e::Frame<u16>>);
Luca Barbato's avatar
Luca Barbato committed
29

30
#[repr(C)]
31
#[derive(Copy, Clone, Debug, FromPrimitive)]
32
33
34
35
36
37
38
39
40
41
42
43
44
pub enum EncoderStatus {
    /// Normal operation
    Success = 0,
    /// The encoder needs more data to produce an output Packet
    /// May be emitted by `Context::receive_packet`  when frame reordering is enabled.
    NeedMoreData,
    /// There are enough Frames queue
    /// May be emitted by `Context::send_frame` when the input queue is constrained
    EnoughData,
    /// The encoder already produced the number of frames requested
    /// May be emitted by `Context::receive_packet` after a flush request had been processed
    /// or the frame limit had been reached.
    LimitReached,
45
46
    /// A Frame had been encoded but not emitted yet
    Encoded,
47
48
    /// Generic fatal error
    Failure = -1,
Derek Buitenhuis's avatar
Derek Buitenhuis committed
49
50
51
52
    /// A frame was encoded in the first pass of a 2-pass encode, but its stats
    /// data was not retrieved with rav1e_twopass_out(), or not enough stats data was
    /// provided in the second pass of a 2-pass encode to encode the next frame.
    NotReady = -2,
53
54
55
56
57
58
59
60
61
62
}

impl From<Option<rav1e::EncoderStatus>> for EncoderStatus {
    fn from(status: Option<rav1e::EncoderStatus>) -> Self {
        match status {
            None => EncoderStatus::Success,
            Some(s) => match s {
                rav1e::EncoderStatus::NeedMoreData => EncoderStatus::NeedMoreData,
                rav1e::EncoderStatus::EnoughData => EncoderStatus::EnoughData,
                rav1e::EncoderStatus::LimitReached => EncoderStatus::LimitReached,
63
                rav1e::EncoderStatus::Encoded => EncoderStatus::Encoded,
64
                rav1e::EncoderStatus::Failure => EncoderStatus::Failure,
Derek Buitenhuis's avatar
Derek Buitenhuis committed
65
                rav1e::EncoderStatus::NotReady => EncoderStatus::NotReady,
66
67
68
69
            }
        }
    }
}
70
71
72
73
74
75

/// Encoder configuration
///
/// Instantiate it using rav1e_config_default() and fine-tune it using
/// rav1e_config_parse().
///
76
/// Use rav1e_config_unref() to free its memory.
77
pub struct Config {
Luca Barbato's avatar
Luca Barbato committed
78
79
80
    cfg: rav1e::Config,
}

81
82
83
84
85
/// Encoder context
///
/// Contains the encoding state, it is created by rav1e_context_new() using an
/// Encoder configuration.
///
86
/// Use rav1e_context_unref() to free its memory.
87
pub struct Context {
88
    ctx: rav1e::Context<u16>,
89
    last_err: Option<rav1e::EncoderStatus>,
Luca Barbato's avatar
Luca Barbato committed
90
91
}

92
type FrameType = rav1e::FrameType;
Luca Barbato's avatar
Luca Barbato committed
93

94
95
96
97
/// Encoded Packet
///
/// The encoded packets are retrieved using rav1e_receive_packet().
///
98
/// Use rav1e_packet_unref() to free its memory.
Luca Barbato's avatar
Luca Barbato committed
99
#[repr(C)]
100
101
pub struct Packet {
    /// Encoded data buffer
Luca Barbato's avatar
Luca Barbato committed
102
    pub data: *const u8,
103
    /// Encoded data buffer size
104
    pub len: size_t,
105
    /// Frame sequence number
106
    pub input_frameno: u64,
107
108
    /// Frame type
    pub frame_type: FrameType,
Luca Barbato's avatar
Luca Barbato committed
109
110
}

111
112
113
114
115
116
117
type PixelRange=rav1e::PixelRange;
type ChromaSamplePosition=rav1e::ChromaSamplePosition;
type ChromaSampling=rav1e::ChromaSampling;
type MatrixCoefficients=rav1e::MatrixCoefficients;
type ColorPrimaries=rav1e::ColorPrimaries;
type TransferCharacteristics=rav1e::TransferCharacteristics;
type Rational=rav1e::Rational;
Luca Barbato's avatar
Luca Barbato committed
118
119

#[no_mangle]
120
pub unsafe extern "C" fn rav1e_config_default() -> *mut Config {
Luca Barbato's avatar
Luca Barbato committed
121
    let cfg = rav1e::Config {
122
        enc: rav1e::EncoderConfig::default(),
123
        threads: 0,
Luca Barbato's avatar
Luca Barbato committed
124
125
    };

126
    let c = Box::new(Config {
Luca Barbato's avatar
Luca Barbato committed
127
128
129
130
131
132
        cfg,
    });

    Box::into_raw(c)
}

133

134
135
136
137
138
139
140
141
142
143
/// Set the time base of the stream
///
/// Needed for rate control.
#[no_mangle]
pub unsafe extern "C" fn rav1e_config_set_time_base(cfg: *mut Config,
                                                    time_base: Rational)
{
    (*cfg).cfg.enc.time_base = time_base
}

144
145
146
147
148
149
/// Set pixel format of the stream.
///
/// Supported values for subsampling and chromapos are defined by the
/// enum types RaChromaSampling and RaChromaSamplePosition respectively.
/// Valid values for fullrange are 0 and 1.
///
150
/// Returns a negative value on error or 0.
151
152
153
154
155
156
157
158
159
160
161
162
163
#[no_mangle]
pub unsafe extern "C" fn rav1e_config_set_pixel_format(cfg: *mut Config,
                                                       bit_depth: u8,
                                                       subsampling: ChromaSampling,
                                                       chroma_pos: ChromaSamplePosition,
                                                       pixel_range: PixelRange
) -> c_int {
    if bit_depth != 8 && bit_depth != 10 && bit_depth != 12 {
        return -1
    }
    (*cfg).cfg.enc.bit_depth = bit_depth as usize;

    let subsampling_val = std::mem::transmute::<ChromaSampling, i32>(subsampling);
Luca Barbato's avatar
Luca Barbato committed
164
    if ChromaSampling::from_i32(subsampling_val).is_none() {
165
166
167
168
169
        return -1
    }
    (*cfg).cfg.enc.chroma_sampling = subsampling;

    let chroma_pos_val = std::mem::transmute::<ChromaSamplePosition, i32>(chroma_pos);
Luca Barbato's avatar
Luca Barbato committed
170
    if ChromaSamplePosition::from_i32(chroma_pos_val).is_none() {
171
172
173
174
175
        return -1
    }
    (*cfg).cfg.enc.chroma_sample_position = chroma_pos;

    let pixel_range_val = std::mem::transmute::<PixelRange, i32>(pixel_range);
Luca Barbato's avatar
Luca Barbato committed
176
    if PixelRange::from_i32(pixel_range_val).is_none() {
177
178
179
180
181
182
183
        return -1;
    }
    (*cfg).cfg.enc.pixel_range = pixel_range;

    0
}

184
185
186
187
188
189
190
/// Set color properties of the stream.
///
/// Supported values are defined by the enum types
/// RaMatrixCoefficients, RaColorPrimaries, and RaTransferCharacteristics
/// respectively.
///
/// Return a negative value on error or 0.
Luca Barbato's avatar
Luca Barbato committed
191
192
193
194
#[no_mangle]
pub unsafe extern "C" fn rav1e_config_set_color_description(cfg: *mut Config,
                                                            matrix: MatrixCoefficients,
                                                            primaries: ColorPrimaries,
195
196
                                                            transfer: TransferCharacteristics
) -> c_int {
197
    (*cfg).cfg.enc.color_description = Some(rav1e::ColorDescription {
Luca Barbato's avatar
Luca Barbato committed
198
199
200
201
        matrix_coefficients: matrix,
        color_primaries: primaries,
        transfer_characteristics: transfer,
    });
202
203

    if (*cfg).cfg.enc.color_description.is_some() { 0 } else { -1 }
Luca Barbato's avatar
Luca Barbato committed
204
205
}

206
207
208
/// Set the content light level information for HDR10 streams.
///
/// Return a negative value on error or 0.
209
210
211
#[no_mangle]
pub unsafe extern "C" fn rav1e_config_set_content_light(cfg: *mut Config,
                                                        max_content_light_level: u16,
212
213
                                                        max_frame_average_light_level: u16
) -> c_int {
214
    (*cfg).cfg.enc.content_light = Some(rav1e::ContentLight {
215
216
217
        max_content_light_level,
        max_frame_average_light_level,
    });
218
219

    if (*cfg).cfg.enc.content_light.is_some() { 0 } else { -1 }
220
221
}

222
223
224
225
226
227
228
/// Set the mastering display information for HDR10 streams.
///
/// primaries and white_point arguments are RaPoint, containing 0.16 fixed point values.
/// max_luminance is a 24.8 fixed point value.
/// min_luminance is a 18.14 fixed point value.
///
/// Returns a negative value on error or 0.
229
230
#[no_mangle]
pub unsafe extern "C" fn rav1e_config_set_mastering_display(cfg: *mut Config,
231
232
                                                            primaries: [rav1e::Point; 3],
                                                            white_point: rav1e::Point,
233
                                                            max_luminance: u32,
234
235
                                                            min_luminance: u32
) -> c_int {
236
    (*cfg).cfg.enc.mastering_display = Some(rav1e::MasteringDisplay {
237
238
239
240
241
        primaries,
        white_point,
        max_luminance,
        min_luminance,
    });
242
243

    if (*cfg).cfg.enc.mastering_display.is_some() { 0 } else { -1 }
244
245
}

Luca Barbato's avatar
Luca Barbato committed
246
#[no_mangle]
247
pub unsafe extern "C" fn rav1e_config_unref(cfg: *mut Config) {
248
249
250
    if !cfg.is_null() {
        let _ = Box::from_raw(cfg);
    }
Luca Barbato's avatar
Luca Barbato committed
251
252
}

253
254
255
256
257
258
259
260
261
262
263
264
unsafe fn option_match(
    cfg: *mut Config,
    key: *const c_char,
    value: *const c_char
) -> Result<(), ()> {
    let key = CStr::from_ptr(key).to_str().map_err(|_| ())?;
    let value = CStr::from_ptr(value).to_str().map_err(|_| ())?;
    let enc = &mut(*cfg).cfg.enc;

    match key {
        "width" => enc.width = value.parse().map_err(|_| ())?,
        "height" => enc.height = value.parse().map_err(|_| ())?,
265
        "speed" => enc.speed_settings = rav1e::SpeedSettings::from_preset(value.parse().map_err(|_| ())?),
266
267
268

        "threads" => (*cfg).cfg.threads = value.parse().map_err(|_| ())?,

Derek Buitenhuis's avatar
Derek Buitenhuis committed
269
270
271
        "tile_rows_log2" => enc.tile_rows_log2 = value.parse().map_err(|_| ())?,
        "tile_cols_log2" => enc.tile_cols_log2 = value.parse().map_err(|_| ())?,

272
273
        "tune" => enc.tune = value.parse().map_err(|_| ())?,
        "quantizer" => enc.quantizer = value.parse().map_err(|_| ())?,
Derek Buitenhuis's avatar
Derek Buitenhuis committed
274
        "bitrate" => enc.bitrate = value.parse().map_err(|_| ())?,
275
276
277

        "key_frame_interval" => enc.max_key_frame_interval = value.parse().map_err(|_| ())?,
        "min_key_frame_interval" => enc.min_key_frame_interval = value.parse().map_err(|_| ())?,
278
        "reservoir_frame_delay" => enc.reservoir_frame_delay = Some(value.parse().map_err(|_| ())?),
279
280
281
282
283
284
285
286
        "low_latency" => enc.low_latency = value.parse().map_err(|_| ())?,

        _ => return Err(())
    }

    Ok(())
}

287
/// Set a configuration parameter using its key and value as string.
288
289
290
291
292
293
///
/// Available keys and values
/// - "quantizer": 0-255, default 100
/// - "speed": 0-10, default 3
/// - "tune": "psnr"-"psychovisual", default "psnr"
///
294
/// Return a negative value on error or 0.
Luca Barbato's avatar
Luca Barbato committed
295
296
#[no_mangle]
pub unsafe extern "C" fn rav1e_config_parse(
297
    cfg: *mut Config,
Luca Barbato's avatar
Luca Barbato committed
298
299
300
    key: *const c_char,
    value: *const c_char,
) -> c_int {
301
    if option_match(cfg, key, value) == Ok(()) { 0 } else { -1 }
Luca Barbato's avatar
Luca Barbato committed
302
303
}

304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
/// Set a configuration parameter using its key and value as integer.
///
/// Available keys and values are the same as rav1e_config_parse()
///
/// Return a negative value on error or 0.
#[no_mangle]
pub unsafe extern "C" fn rav1e_config_parse_int(
    cfg: *mut Config,
    key: *const c_char,
    value: c_int,
) -> c_int {
    let val = CString::new(value.to_string()).unwrap();
    if option_match(cfg, key, val.as_ptr()) == Ok(()) { 0 } else { -1 }
}

319
320
321
/// Generate a new encoding context from a populated encoder configuration
///
/// Multiple contexts can be generated through it.
Luca Barbato's avatar
Luca Barbato committed
322
#[no_mangle]
323
324
pub unsafe extern "C" fn rav1e_context_new(cfg: *const Config) -> *mut Context {
    let ctx = Context {
Luca Barbato's avatar
Luca Barbato committed
325
326
327
328
329
330
331
332
        ctx: (*cfg).cfg.new_context(),
        last_err: None,
    };

    Box::into_raw(Box::new(ctx))
}

#[no_mangle]
333
pub unsafe extern "C" fn rav1e_context_unref(ctx: *mut Context) {
334
335
336
    if !ctx.is_null() {
        let _ = Box::from_raw(ctx);
    }
Luca Barbato's avatar
Luca Barbato committed
337
338
}

339
340
341
342
/// Produce a new frame from the encoding context
///
/// It must be populated using rav1e_frame_fill_plane().
///
343
/// The frame is reference counted and must be released passing it to rav1e_frame_unref(),
344
/// see rav1e_send_frame().
Luca Barbato's avatar
Luca Barbato committed
345
#[no_mangle]
346
pub unsafe extern "C" fn rav1e_frame_new(ctx: *const Context) -> *mut Frame {
Luca Barbato's avatar
Luca Barbato committed
347
    let f = (*ctx).ctx.new_frame();
348
    let frame = Box::new(Frame(f));
Luca Barbato's avatar
Luca Barbato committed
349
350
351
352
353

    Box::into_raw(frame)
}

#[no_mangle]
354
pub unsafe extern "C" fn rav1e_frame_unref(frame: *mut Frame) {
355
356
357
    if !frame.is_null() {
        let _ = Box::from_raw(frame);
    }
Luca Barbato's avatar
Luca Barbato committed
358
}
Derek Buitenhuis's avatar
Derek Buitenhuis committed
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423

/// Retrieve the first-pass data of a two-pass encode for the frame that was
/// just encoded. This should be called BEFORE every call to rav1e_receive_packet()
/// (including the very first one), even if no packet was produced by the
/// last call to rav1e_receive_packet, if any (i.e., RA_ENCODER_STATUS_ENCODED
/// was returned). It needs to be called once more after
/// RA_ENCODER_STATUS_LIMIT_REACHED is returned, to retrieve the header that
/// should be written to the front of the stats file (overwriting the
/// placeholder header that was emitted at the start of encoding).
///
/// It is still safe to call this function when rav1e_receive_packet() returns any
/// other error. It will return NULL instead of returning a duplicate copy
/// of the previous frame's data.
///
/// Must be freed with rav1e_twopass_unref().
#[no_mangle]
pub unsafe extern "C" fn rav1e_twopass_out(ctx: *mut Context, buf_size: *mut size_t) -> *mut u8 {
    let buf = (*ctx).ctx.twopass_out();

    if buf.is_none() {
        return std::ptr::null_mut();
    }

    let v = buf.unwrap().to_vec();
    *buf_size = v.len();
    Box::into_raw(v.into_boxed_slice()) as *mut u8
}

#[no_mangle]
pub unsafe extern fn rav1e_twopass_unref(buf: *mut u8) {
    if !buf.is_null() {
        let _ = Box::from_raw(buf);
    }
}

/// Ask how many bytes of the stats file are needed before the next frame
/// of the second pass in a two-pass encode can be encoded. This is a lower
/// bound (more might be required), but if 0 is returned, then encoding can
/// proceed. This is just a hint to the application, and does not need to
/// be called for encoding the second pass to work, so long as the
/// application continues to provide more data to rav1e_twopass_in() in a loop
/// until rav1e_twopass_in() returns 0.
#[no_mangle]
pub unsafe extern "C" fn rav1e_twopass_bytes_needed(ctx: *mut Context) -> size_t {
    (*ctx).ctx.twopass_bytes_needed() as size_t
}

/// Provide stats data produced in the first pass of a two-pass encode to the
/// second pass. On success this returns the number of bytes of that data
/// which were consumed. When encoding the second pass of a two-pass encode,
/// this should be called repeatedly in a loop before every call to
/// rav1e_receive_packet() (including the very first one) until no bytes are
/// consumed, or until twopass_bytes_needed() returns 0. Returns -1 on failure.
#[no_mangle]
pub unsafe extern "C" fn rav1e_twopass_in(ctx: *mut Context, buf: *mut u8, buf_size: size_t) -> c_int {
    let buf_slice = slice::from_raw_parts(buf, buf_size as usize);
    let r = (*ctx).ctx.twopass_in(buf_slice);
    match r {
        Ok(v) => v as c_int,
        Err(v) => {
            (*ctx).last_err = Some(v);
            -1
        },
    }
}
Luca Barbato's avatar
Luca Barbato committed
424

425
426
427
428
/// Send the frame for encoding
///
/// The function increases the frame internal reference count and it can be passed multiple
/// times to different rav1e_send_frame().
429
430
431
432
433
///
/// Returns:
/// - `0` on success,
/// - `> 0` if the input queue is full
/// - `< 0` on unrecoverable failure
Luca Barbato's avatar
Luca Barbato committed
434
#[no_mangle]
435
pub unsafe extern "C" fn rav1e_send_frame(ctx: *mut Context, frame: *const Frame) -> EncoderStatus {
Luca Barbato's avatar
Luca Barbato committed
436
437
438
439
440
441
    let frame = if frame.is_null() {
        None
    } else {
        Some((*frame).0.clone())
    };

442
    let ret = (*ctx)
Luca Barbato's avatar
Luca Barbato committed
443
444
445
        .ctx
        .send_frame(frame)
        .map(|_v| {
446
            None
447
        }).unwrap_or_else(|e| {
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
            Some(e)
        });

    (*ctx).last_err = ret;

    ret.into()
}

/// Return the last encoder status
#[no_mangle]
pub unsafe extern "C" fn rav1e_last_status(ctx: *const Context) -> EncoderStatus {
    (*ctx).last_err.into()
}

/// Return a string matching the EncooderStatus variant.
#[no_mangle]
pub unsafe extern "C" fn rav1e_status_to_str(status: EncoderStatus) -> *mut c_char {
465
466
467
468
    if EncoderStatus::from_i32(std::mem::transmute(status)).is_none() {
        return std::ptr::null_mut();
    }

469
    let status = format!("{:?}", status);
470
471
472
    let cbuf = CString::new(status).unwrap();
    let len = cbuf.as_bytes_with_nul().len();
    let ret = libc::malloc(len);
473

474
475
476
477
478
479
    if !ret.is_null() {
        let cptr = cbuf.as_ptr() as *const libc::c_void;
        libc::memcpy(ret, cptr, len);
    }

    ret as *mut c_char
Luca Barbato's avatar
Luca Barbato committed
480
481
}

482
/// Receive encoded data
483
484
485
486
487
///
/// Returns:
/// - `0` on success
/// - `> 0` if additional frame data is required
/// - `< 0` on unrecoverable failure
Luca Barbato's avatar
Luca Barbato committed
488
489
#[no_mangle]
pub unsafe extern "C" fn rav1e_receive_packet(
490
491
    ctx: *mut Context,
    pkt: *mut *mut Packet,
492
493
) -> EncoderStatus {
    let ret = (*ctx)
Luca Barbato's avatar
Luca Barbato committed
494
495
496
        .ctx
        .receive_packet()
        .map(|p| {
497
            let rav1e::Packet { data, input_frameno, frame_type, .. } = p;
498
499
            let len  = data.len();
            let data = Box::into_raw(data.into_boxed_slice()) as *const u8;
500
            let packet = Packet {
501
502
                data,
                len,
503
                input_frameno,
504
                frame_type,
Luca Barbato's avatar
Luca Barbato committed
505
506
            };
            *pkt = Box::into_raw(Box::new(packet));
507
            None
508
        }).unwrap_or_else(|e| {
509
510
511
512
513
514
            Some(e)
        });

    (*ctx).last_err = ret;

    ret.into()
Luca Barbato's avatar
Luca Barbato committed
515
516
517
}

#[no_mangle]
518
pub unsafe extern fn rav1e_packet_unref(pkt: *mut Packet) {
519
    if !pkt.is_null() {
520
521
        let pkt = Box::from_raw(pkt);
        let _ = Box::from_raw(pkt.data as *mut u8);
522
    }
Luca Barbato's avatar
Luca Barbato committed
523
524
}

525
526
527
/// Produce a sequence header matching the current encoding context
///
/// Its format is compatible with the AV1 Matroska and ISOBMFF specification.
528
529
///
/// Use rav1e_container_sequence_header_unref() to free it.
530
#[no_mangle]
531
pub unsafe extern fn rav1e_container_sequence_header(ctx: *mut Context, buf_size: *mut size_t) -> *mut u8 {
532
533
    let buf = (*ctx).ctx.container_sequence_header();

534
    *buf_size = buf.len();
535
536
537
    Box::into_raw(buf.into_boxed_slice()) as *mut u8
}

538
539
#[no_mangle]
pub unsafe extern fn rav1e_container_sequence_header_unref(sequence: *mut u8) {
540
541
542
    if !sequence.is_null() {
        let _ = Box::from_raw(sequence);
    }
543
544
}

545
546
/// Fill a frame plane
///
547
/// Currently the frame contains 3 planes, the first is luminance followed by
548
/// chrominance.
549
550
///
/// The data is copied and this function has to be called for each plane.
551
552
553
554
555
556
557
///
/// frame: A frame provided by rav1e_frame_new()
/// plane: The index of the plane starting from 0
/// data: The data to be copied
/// data_len: Lenght of the buffer
/// stride: Plane line in bytes, including padding
/// bytewidth: Number of bytes per component, either 1 or 2
Luca Barbato's avatar
Luca Barbato committed
558
559
#[no_mangle]
pub unsafe extern "C" fn rav1e_frame_fill_plane(
560
    frame: *mut Frame,
561
    plane: c_int,
Luca Barbato's avatar
Luca Barbato committed
562
    data: *const u8,
563
564
565
    data_len: size_t,
    stride: ptrdiff_t,
    bytewidth: c_int,
Luca Barbato's avatar
Luca Barbato committed
566
) {
567
568
    let f = &mut (*frame).0;
    let input = Arc::make_mut(f);
569
    let data_slice = slice::from_raw_parts(data, data_len as usize);
Luca Barbato's avatar
Luca Barbato committed
570

571
    input.planes[plane as usize].copy_from_raw_u8(data_slice, stride as usize, bytewidth as usize);
Luca Barbato's avatar
Luca Barbato committed
572
}