Skip to content
Snippets Groups Projects
Verified Commit 0ea1013b authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

use rustfmt

parent 59c475e4
No related branches found
No related tags found
No related merge requests found
......@@ -17,12 +17,12 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
ffmpeg-dev = "0.3.8"
image = "0.23.0"
enum_primitive = "0.1.1"
failure = "0.1.6"
ffmpeg-dev = "0.3.8"
fraction = "0.6.2"
time = "0.2"
serde = { version = "1.0", features = ["derive"] }
image = "0.23.0"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
structopt = "0.3"
time = "0.2"
......@@ -23,7 +23,8 @@ impl<'a> AVFormatContext {
}
pub fn open_input(&mut self, path: &Path) -> Result<(), failure::Error> {
let path = path.to_str()
let path = path
.to_str()
.ok_or_else(|| failure::format_err!("Could not convert path to c string"))?;
let path = std::ffi::CString::new(path)
.map_err(|_| failure::format_err!("Could not convert path to c string"))?;
......@@ -37,7 +38,7 @@ impl<'a> AVFormatContext {
)
} {
0 => Ok(()),
_ => bail!("Could not open input")
_ => bail!("Could not open input"),
}
}
......@@ -46,45 +47,40 @@ impl<'a> AVFormatContext {
}
pub fn streams(&self) -> Vec<AVStream> {
Vec::from(
unsafe {
std::slice::from_raw_parts(
(*self.base).streams,
(*self.base).nb_streams as usize,
)
}
).iter().map(|stream| {
AVStream::new(unsafe { (*stream).as_mut() }.expect("not null"))
}).collect()
}
pub fn find_stream<P>(&self, predicate: P) -> Option<AVStream> where
P: FnMut(&AVStream) -> bool {
Vec::from(
unsafe {
std::slice::from_raw_parts(
(*self.base).streams,
(*self.base).nb_streams as usize,
)
}
).iter().map(|stream| {
AVStream::new(unsafe { (*stream).as_mut() }.expect("not null"))
}).find(predicate)
Vec::from(unsafe {
std::slice::from_raw_parts((*self.base).streams, (*self.base).nb_streams as usize)
})
.iter()
.map(|stream| AVStream::new(unsafe { (*stream).as_mut() }.expect("not null")))
.collect()
}
pub fn find_stream<P>(&self, predicate: P) -> Option<AVStream>
where
P: FnMut(&AVStream) -> bool,
{
Vec::from(unsafe {
std::slice::from_raw_parts((*self.base).streams, (*self.base).nb_streams as usize)
})
.iter()
.map(|stream| AVStream::new(unsafe { (*stream).as_mut() }.expect("not null")))
.find(predicate)
}
pub fn read_frame(&mut self, packet: &mut AVPacket) -> Result<(), failure::Error> {
match unsafe { ffi::av_read_frame(self.base, packet.base) } {
0 => Ok(()),
errno => Err(failure::format_err!("Error while decoding frame: {}", errno))
errno => Err(failure::format_err!(
"Error while decoding frame: {}",
errno
)),
}
}
pub fn duration(&self) -> Result<media_time::MediaTime, failure::Error> {
media_time::MediaTime::from_rational(
unsafe { (*self.base).duration }, Fraction::new(
1 as u64,
ffi::AV_TIME_BASE as u64,
),
unsafe { (*self.base).duration },
Fraction::new(1 as u64, ffi::AV_TIME_BASE as u64),
)
}
}
......@@ -96,7 +92,7 @@ impl Drop for AVFormatContext {
}
pub struct AVInputFormat<'a> {
base: &'a mut ffi::AVInputFormat
base: &'a mut ffi::AVInputFormat,
}
impl<'a> AVInputFormat<'a> {
......@@ -110,11 +106,13 @@ impl<'a> AVInputFormat<'a> {
if raw.is_null() {
Err(failure::format_err!("No mime type found"))
} else {
Ok(String::from(unsafe {
std::ffi::CStr::from_ptr(raw)
}.to_str().map_err(|err| {
Ok(String::from(
unsafe { std::ffi::CStr::from_ptr(raw) }
.to_str()
.map_err(|err| {
failure::format_err!("Could not convert mime type to string: {}", err)
})?))
})?,
))
}
}
......@@ -124,11 +122,13 @@ impl<'a> AVInputFormat<'a> {
if raw.is_null() {
Err(failure::format_err!("No mime type found"))
} else {
Ok(String::from(unsafe {
std::ffi::CStr::from_ptr(raw)
}.to_str().map_err(|err| {
Ok(String::from(
unsafe { std::ffi::CStr::from_ptr(raw) }
.to_str()
.map_err(|err| {
failure::format_err!("Could not convert mime type to string: {}", err)
})?))
})?,
))
}
}
......@@ -138,11 +138,13 @@ impl<'a> AVInputFormat<'a> {
if raw.is_null() {
Err(failure::format_err!("No mime type found"))
} else {
Ok(String::from(unsafe {
std::ffi::CStr::from_ptr(raw)
}.to_str().map_err(|err| {
Ok(String::from(
unsafe { std::ffi::CStr::from_ptr(raw) }
.to_str()
.map_err(|err| {
failure::format_err!("Could not convert mime type to string: {}", err)
})?))
})?,
))
}
}
......@@ -159,7 +161,7 @@ impl<'a> AVInputFormat<'a> {
_ => {
// Do nothing
}
}
},
"webm" => match stream_codec {
"vp8" | "vp9" | "av1" => {
return Ok(String::from("video/webm"));
......@@ -167,14 +169,18 @@ impl<'a> AVInputFormat<'a> {
_ => {
// Do nothing
}
}
},
_ => {
// Do nothing
}
}
}
return Err(failure::format_err!("Could not determine mime type: {} video in {} container", stream_codec, containers));
return Err(failure::format_err!(
"Could not determine mime type: {} video in {} container",
stream_codec,
containers
));
}
}
......@@ -193,19 +199,18 @@ impl AVBuffer {
}
pub fn empty() -> Self {
AVBuffer { base: std::ptr::null_mut(), size: 0 }
AVBuffer {
base: std::ptr::null_mut(),
size: 0,
}
}
pub fn data(&self) -> &[u8] {
unsafe {
std::slice::from_raw_parts(self.base, self.size)
}
unsafe { std::slice::from_raw_parts(self.base, self.size) }
}
pub fn data_mut(&mut self) -> &[u8] {
unsafe {
std::slice::from_raw_parts_mut(self.base, self.size)
}
unsafe { std::slice::from_raw_parts_mut(self.base, self.size) }
}
}
......@@ -258,10 +263,18 @@ impl AVFrame {
if base.is_null() {
bail!("avformat_alloc_frame() failed");
}
Ok(AVFrame { base, buffer: AVBuffer::empty() })
Ok(AVFrame {
base,
buffer: AVBuffer::empty(),
})
}
pub fn init(&mut self, width: i32, height: i32, format: AVPixelFormat) -> Result<(), failure::Error> {
pub fn init(
&mut self,
width: i32,
height: i32,
format: AVPixelFormat,
) -> Result<(), failure::Error> {
let mut base = unsafe { self.base.as_mut() }.expect("not null");
base.width = width;
......@@ -298,13 +311,16 @@ impl AVFrame {
pub fn format(&self) -> AVPixelFormat {
let base = unsafe { self.base.as_ref() }.expect("not null");
AVPixelFormat::from_i32(base.format)
.unwrap_or(AVPixelFormat::NONE)
AVPixelFormat::from_i32(base.format).unwrap_or(AVPixelFormat::NONE)
}
pub fn size(&self) -> usize {
unsafe {
ffi::avpicture_get_size(self.format() as ffi::AVPixelFormat, self.width(), self.height()) as usize
ffi::avpicture_get_size(
self.format() as ffi::AVPixelFormat,
self.width(),
self.height(),
) as usize
}
}
......@@ -353,17 +369,13 @@ impl AVFrame {
pub fn data(&self, index: usize) -> &[u8] {
let base = unsafe { self.base.as_ref() }.expect("not null");
unsafe {
std::slice::from_raw_parts(base.data[index], self.size())
}
unsafe { std::slice::from_raw_parts(base.data[index], self.size()) }
}
pub fn data_mut(&mut self, index: usize) -> &mut [u8] {
let base = unsafe { self.base.as_mut() }.expect("not null");
unsafe {
std::slice::from_raw_parts_mut(base.data[index], self.size())
}
unsafe { std::slice::from_raw_parts_mut(base.data[index], self.size()) }
}
}
......@@ -374,7 +386,7 @@ impl Drop for AVFrame {
}
pub struct AVStream<'a> {
base: &'a mut ffi::AVStream
base: &'a mut ffi::AVStream,
}
impl<'a> AVStream<'a> {
......@@ -393,7 +405,10 @@ impl<'a> AVStream<'a> {
)
}
pub fn timestamp(self: &AVStream<'a>, timestamp: i64) -> Result<media_time::MediaTime, failure::Error> {
pub fn timestamp(
self: &AVStream<'a>,
timestamp: i64,
) -> Result<media_time::MediaTime, failure::Error> {
media_time::MediaTime::from_rational(timestamp, self.time_base())
}
......@@ -428,7 +443,10 @@ impl<'a> AVStream<'a> {
}
pub fn codec_parameters(&self) -> AVCodecParameters {
AVCodecParameters::new(unsafe { self.base.codecpar.as_mut() }.expect("not null"), self)
AVCodecParameters::new(
unsafe { self.base.codecpar.as_mut() }.expect("not null"),
self,
)
}
}
......@@ -439,7 +457,10 @@ pub struct AVCodecParameters<'a> {
impl<'a> AVCodecParameters<'a> {
fn new(base: &'a mut ffi::AVCodecParameters, _: &'a AVStream) -> Self {
return AVCodecParameters { base, phantom: PhantomData };
return AVCodecParameters {
base,
phantom: PhantomData,
};
}
pub fn codec_type(&self) -> AVMediaType {
......@@ -456,7 +477,8 @@ impl<'a> AVCodecParameters<'a> {
pub fn find_decoder(&self) -> AVCodec {
AVCodec::new(
unsafe { ffi::avcodec_find_decoder(self.base.codec_id).as_mut() }.expect("Decoder not found"),
unsafe { ffi::avcodec_find_decoder(self.base.codec_id).as_mut() }
.expect("Decoder not found"),
self,
)
}
......@@ -469,11 +491,18 @@ pub struct AVCodec<'a> {
impl<'a> AVCodec<'a> {
fn new(base: &'a mut ffi::AVCodec, _: &'a AVCodecParameters) -> Self {
return AVCodec { base, phantom: PhantomData };
return AVCodec {
base,
phantom: PhantomData,
};
}
pub fn name(self: &AVCodec<'a>) -> std::string::String {
String::from(unsafe { std::ffi::CStr::from_ptr(self.base.name) }.to_str().unwrap())
String::from(
unsafe { std::ffi::CStr::from_ptr(self.base.name) }
.to_str()
.unwrap(),
)
}
}
......@@ -493,14 +522,20 @@ impl AVCodecContext {
pub fn in_packet(&mut self, packet: &mut AVPacket) -> Result<(), failure::Error> {
match unsafe { ffi::avcodec_send_packet(self.base, packet.base) } {
0 => Ok(()),
errno => Err(failure::format_err!("Error while loading paclet: {}", errno))
errno => Err(failure::format_err!(
"Error while loading paclet: {}",
errno
)),
}
}
pub fn out_frame(&mut self, frame: &mut AVFrame) -> Result<(), failure::Error> {
match unsafe { ffi::avcodec_receive_frame(self.base, frame.base) } {
0 => Ok(()),
errno => Err(failure::format_err!("Error while decoding frame: {}", errno))
errno => Err(failure::format_err!(
"Error while decoding frame: {}",
errno
)),
}
}
......@@ -565,10 +600,17 @@ pub struct SwsContext {
impl SwsContext {
pub fn new() -> Self {
SwsContext { base: std::ptr::null_mut() }
SwsContext {
base: std::ptr::null_mut(),
}
}
pub fn reinit(&mut self, source: &AVFrame, target: &AVFrame, scaler: SwsScaler) -> Result<(), failure::Error> {
pub fn reinit(
&mut self,
source: &AVFrame,
target: &AVFrame,
scaler: SwsScaler,
) -> Result<(), failure::Error> {
let base = unsafe {
ffi::sws_getCachedContext(
self.base,
......@@ -596,7 +638,13 @@ impl SwsContext {
self.scale_slice(source, target, 0, source.height())
}
pub fn scale_slice(&self, source: &AVFrame, target: &mut AVFrame, slice_from: i32, slice_to: i32) -> i32 {
pub fn scale_slice(
&self,
source: &AVFrame,
target: &mut AVFrame,
slice_from: i32,
slice_to: i32,
) -> i32 {
unsafe {
ffi::sws_scale(
self.base,
......
use ffmpeg_dev::sys as ffi;
use enum_primitive::*;
use ffmpeg_dev::sys as ffi;
enum_from_primitive! {
#[doc = " Pixel format."]
......
pub(crate) mod enums;
pub(crate) mod api;
pub(crate) mod enums;
use std::path::Path;
use failure::format_err;
use std::path::Path;
use crate::ffmpeg_api::api::*;
use crate::ffmpeg_api::enums::*;
use crate::util::media_time::*;
use crate::ingest::spritesheet::*;
use crate::util::media_time::*;
use crate::util::stream_metadata::*;
pub fn extract(
max_size: u32,
num_horizontal: u32, num_vertical: u32,
num_horizontal: u32,
num_vertical: u32,
frame_interval: MediaTime,
input_file: &Path,
output_folder: &Path,
) -> Result<(), failure::Error> {
let mut avformat_context = AVFormatContext::new().map_err(|error| {
format_err!("Could not allocate a context to process the video: {:?}", error)
})?;
avformat_context.open_input(input_file).map_err(|error| {
format_err!("Could not open video input: {:?}", error)
})?;
let mut avformat_context = AVFormatContext::new()
.map_err(|error| format_err!("Could not open video input: {}", error))?;
avformat_context
.open_input(input_file)
.map_err(|error| format_err!("Could not open video input: {}", error))?;
let duration = avformat_context.duration()?;
let spritesheet_path = output_folder.join("spritesheets");
std::fs::create_dir_all(&spritesheet_path)?;
let mut spritesheet_manager = SpritesheetManager::new(
max_size,
num_horizontal, num_vertical,
num_horizontal,
num_vertical,
frame_interval,
spritesheet_path,
"preview"
"preview",
);
let mut stream: AVStream = avformat_context.find_stream(|stream| {
stream.codec_parameters().codec_type() == AVMediaType::Video
}).ok_or_else(|| {
format_err!("Could not find video stream")
})?;
let mut stream: AVStream = avformat_context
.find_stream(|stream| stream.codec_parameters().codec_type() == AVMediaType::Video)
.ok_or_else(|| format_err!("Could not find video stream"))?;
stream.set_discard(AVDiscard::NonKey);
let index = stream.index();
......@@ -53,19 +52,19 @@ pub fn extract(
);
let mut metadata = StreamMetadata::new(
avformat_context.input_format().determine_mime(local_codec.name())?,
avformat_context
.input_format()
.determine_mime(local_codec.name())?,
duration,
codec_parameters.bit_rate() / 1000
codec_parameters.bit_rate() / 1000,
);
let mut output_frame = AVFrame::new().map_err(|error| {
format_err!("Could not create output frame: {:?}", error)
})?;
let mut output_frame =
AVFrame::new().map_err(|error| format_err!("Could not create output frame: {}", error))?;
if codec_parameters.codec_type() == AVMediaType::Video {
let mut codec_context = AVCodecContext::new(&local_codec).map_err(|error| {
format_err!("Could not init codec context: {:?}", error)
})?;
let mut codec_context = AVCodecContext::new(&local_codec)
.map_err(|error| format_err!("Could not init codec context: {}", error))?;
codec_context.set_parameters(&codec_parameters);
codec_context.open(&local_codec);
......@@ -73,21 +72,19 @@ pub fn extract(
codec_context.set_skip_idct(AVDiscard::NonKey);
codec_context.set_skip_frame(AVDiscard::NonKey);
let mut packet = AVPacket::new().map_err(|error| {
format_err!("Could not init temporary packet: {:?}", error)
})?;
let mut packet = AVPacket::new()
.map_err(|error| format_err!("Could not init temporary packet: {}", error))?;
let mut frame = AVFrame::new().map_err(|error| {
format_err!("Could not create input frame: {:?}", error)
})?;
let mut frame = AVFrame::new()
.map_err(|error| format_err!("Could not create input frame: {}", error))?;
let mut scale_context = SwsContext::new();
while avformat_context.read_frame(&mut packet).is_ok() {
if packet.stream_index() == index {
codec_context.in_packet(&mut packet).map_err(|error| {
format_err!("Could not load packet: {:?}", error)
})?;
codec_context
.in_packet(&mut packet)
.map_err(|error| format_err!("Could not load packet: {}", error))?;
while codec_context.out_frame(&mut frame).is_ok() {
let timestamp = MediaTime::from_rational(frame.pts(), time_base)?;
......@@ -100,21 +97,22 @@ pub fn extract(
if spritesheet_manager.fulfils_frame_interval(timestamp) {
if !spritesheet_manager.initialized() {
spritesheet_manager.initialize(frame.width() as u32, frame.height() as u32);
spritesheet_manager
.initialize(frame.width() as u32, frame.height() as u32);
metadata.set_frame_size(frame.width(), frame.height());
output_frame.init(
output_frame
.init(
spritesheet_manager.sprite_width() as i32,
spritesheet_manager.sprite_height() as i32,
AVPixelFormat::RGB24,
).map_err(|error| {
format_err!("Could not init output frame: {:?}", error)
)
.map_err(|error| {
format_err!("Could not init output frame: {}", error)
})?;
scale_context.reinit(
&frame,
&output_frame,
SwsScaler::FastBilinear,
).map_err(|error| {
format_err!("Could not reinit scale context: {:?}", error)
scale_context
.reinit(&frame, &output_frame, SwsScaler::FastBilinear)
.map_err(|error| {
format_err!("Could not reinit scale context: {}", error)
})?;
}
......@@ -126,9 +124,8 @@ pub fn extract(
output_frame.width() as u32,
output_frame.height() as u32,
output_frame.data(0).to_vec(),
).ok_or_else(|| {
format_err!("Could not process frame")
})?
)
.ok_or_else(|| format_err!("Could not process frame"))?,
)?;
}
}
......@@ -139,11 +136,9 @@ pub fn extract(
spritesheet_manager.save()?;
}
metadata.save(
output_folder.join("metadata.json")
).map_err(|error| {
format_err!("Could not write stream metadata: {}", error)
})?;
metadata
.save(output_folder.join("metadata.json"))
.map_err(|error| format_err!("Could not write stream metadata: {}", error))?;
Ok(())
}
use std::path::PathBuf;
use image::{RgbImage, ImageBuffer};
use failure::{bail, format_err};
use image::{ImageBuffer, RgbImage};
use crate::util::media_time::MediaTime;
use crate::util::webvtt::{WebVTTFile, WebVTTCue};
use crate::util::webvtt::{WebVTTCue, WebVTTFile};
pub struct SpritesheetManager {
num_horizontal: u32,
......@@ -23,7 +23,14 @@ pub struct SpritesheetManager {
}
impl SpritesheetManager {
pub fn new<T: AsRef<str>, U: Into<PathBuf>>(max_side: u32, num_horizontal: u32, num_vertical: u32, frame_interval: MediaTime, output_path: U, name: T) -> SpritesheetManager {
pub fn new<T: AsRef<str>, U: Into<PathBuf>>(
max_side: u32,
num_horizontal: u32,
num_vertical: u32,
frame_interval: MediaTime,
output_path: U,
name: T,
) -> SpritesheetManager {
SpritesheetManager {
num_horizontal,
num_vertical,
......@@ -94,22 +101,24 @@ impl SpritesheetManager {
self.current_image == 0 || timestamp - self.last_timestamp > self.frame_interval
}
pub fn add_image(&mut self, timestamp: MediaTime, image: RgbImage) -> Result<(), failure::Error> {
pub fn add_image(
&mut self,
timestamp: MediaTime,
image: RgbImage,
) -> Result<(), failure::Error> {
if image.width() != self.sprite_width || image.height() != self.sprite_height {
bail!(
"Wrong image size: {}x{}, but expected {}x{}",
image.width(), image.height(),
self.sprite_width, self.sprite_height
image.width(),
image.height(),
self.sprite_width,
self.sprite_height
)
}
let x = self.x(self.current_image);
let y = self.y(self.current_image);
image::imageops::overlay(
&mut self.spritesheet,
&image,
x, y,
);
image::imageops::overlay(&mut self.spritesheet, &image, x, y);
if self.current_image != 0 {
self.end_frame(timestamp);
......@@ -142,22 +151,22 @@ impl SpritesheetManager {
}
fn save_spritesheet(&mut self) -> Result<(), failure::Error> {
self.spritesheet.save(
self.output_path.join(format!("{}_{}.jpg", self.name, self.spritesheet_index(self.current_image)))
).map_err(|error| {
format_err!("Could not write spritesheet: {}", error)
})?;
self.spritesheet
.save(self.output_path.join(format!(
"{}_{}.jpg",
self.name,
self.spritesheet_index(self.current_image)
)))
.map_err(|error| format_err!("Could not write spritesheet: {}", error))?;
self.reinit_buffer();
Ok(())
}
pub fn save(&mut self) -> Result<(), failure::Error> {
self.save_spritesheet()?;
self.metadata.save(
self.output_path.join(format!("{}.vtt", self.name))
).map_err(|error| {
format_err!("Could not write spritesheet metadata: {}", error)
})?;
self.metadata
.save(self.output_path.join(format!("{}.vtt", self.name)))
.map_err(|error| format_err!("Could not write spritesheet metadata: {}", error))?;
Ok(())
}
}
......@@ -34,7 +34,7 @@ fn main() -> Result<(), failure::Error> {
options.num_vertical,
MediaTime::from_seconds(options.frame_interval),
Path::new(&options.input),
Path::new(&options.output)
Path::new(&options.output),
)?;
Ok(())
......
use fraction::Fraction;
use failure::format_err;
use fraction::Fraction;
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct MediaTime(time::Duration);
impl MediaTime {
pub fn from_rational(timestamp: i64, base: Fraction) -> Result<MediaTime, failure::Error> {
let num: u64 = *base.numer().ok_or_else(|| {
format_err!("time base of unusable format")
})?;
let den: u64 = *base.denom().ok_or_else(|| {
format_err!("time base of unusable format")
})?;
let num: u64 = *base
.numer()
.ok_or_else(|| format_err!("time base of unusable format"))?;
let den: u64 = *base
.denom()
.ok_or_else(|| format_err!("time base of unusable format"))?;
Ok(MediaTime(time::Duration::milliseconds(
(1000 * timestamp as i128 * num as i128 / den as i128) as i64
(1000 * timestamp as i128 * num as i128 / den as i128) as i64,
)))
}
......
use std::fs::File;
use std::path::Path;
use std::io::BufWriter;
use std::path::Path;
use serde::{Deserialize, Serialize};
......@@ -17,7 +17,11 @@ pub struct StreamMetadata {
}
impl StreamMetadata {
pub fn new<T: AsRef<str>>(content_type: T, duration: MediaTime, bitrate: i64) -> StreamMetadata {
pub fn new<T: AsRef<str>>(
content_type: T,
duration: MediaTime,
bitrate: i64,
) -> StreamMetadata {
StreamMetadata {
content_type: String::from(content_type.as_ref()),
duration: duration.seconds(),
......
......@@ -7,7 +7,7 @@ use std::string::String;
use crate::util::media_time::MediaTime;
pub struct WebVTTFile {
cues: Vec<WebVTTCue>
cues: Vec<WebVTTCue>,
}
pub struct WebVTTCue {
......@@ -18,9 +18,7 @@ pub struct WebVTTCue {
impl WebVTTFile {
pub fn new() -> WebVTTFile {
WebVTTFile {
cues: Vec::new()
}
WebVTTFile { cues: Vec::new() }
}
pub fn add(&mut self, cue: WebVTTCue) {
......@@ -41,7 +39,11 @@ impl WebVTTFile {
impl WebVTTCue {
pub fn new(start: MediaTime, end: MediaTime, payload: String) -> WebVTTCue {
WebVTTCue { start, end, payload }
WebVTTCue {
start,
end,
payload,
}
}
fn save(&self, writer: &mut LineWriter<File>) -> Result<(), std::io::Error> {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment