Commit f2c5927d authored by Laurent Aimar's avatar Laurent Aimar
Browse files

*ALL : ameliorations ( i hope ;)

parent 86614936
......@@ -2,7 +2,7 @@
* avi.c : AVI file Stream input module for vlc
*****************************************************************************
* Copyright (C) 2001 VideoLAN
* $Id: avi.c,v 1.12 2002/05/05 17:20:49 fenrir Exp $
* $Id: avi.c,v 1.13 2002/05/06 22:02:32 fenrir Exp $
* Authors: Laurent Aimar <fenrir@via.ecp.fr>
*
* This program is free software; you can redistribute it and/or modify
......@@ -286,7 +286,8 @@ static void __AVI_GetIndex( input_thread_t *p_input )
intf_WarnMsg( 1, "input init: loading index" );
for(;;)
{
if( ((i_read = input_Peek( p_input, &p_buff, 16*1024 )) < 16 )
i_read = __MIN( 16*1024, p_idx1->i_size - i_totalentry *16);
if( ((i_read = input_Peek( p_input, &p_buff, i_read )) < 16 )
||( i_totalentry *16 >= p_idx1->i_size ) )
{
intf_WarnMsg( 1,"input info: read %d idx chunk", i_totalentry );
......@@ -306,7 +307,7 @@ static void __AVI_GetIndex( input_thread_t *p_input )
if( (__AVI_ParseStreamHeader( index.i_id, &i_number, &i_type ) == 0)
&&(i_number < p_avi_demux->i_streams )
&&(p_avi_demux->pp_info[i_number]->i_cat ==
__AVIGetESTypeFromTwoCC( i_type )))
__AVIGetESTypeFromTwoCC( i_type )))
{
__AVI_AddEntryIndex( p_avi_demux->pp_info[i_number],
&index );
......@@ -321,7 +322,7 @@ static int __AVI_SeekToChunk( input_thread_t *p_input, AVIStreamInfo_t *p_info )
demux_data_avi_file_t *p_avi_demux;
p_avi_demux = (demux_data_avi_file_t*)p_input->p_demux_data;
if( (p_info->p_index != NULL)&&(p_info->i_idxposc < p_info->i_idxnb) )
if( (p_info->p_index )&&(p_info->i_idxposc < p_info->i_idxnb) )
{
off_t i_pos;
i_pos = (off_t)p_info->p_index[p_info->i_idxposc].i_offset +
......@@ -817,8 +818,7 @@ static int AVIInit( input_thread_t *p_input )
}
p_input->stream.p_selected_program->b_is_ok = 1;
vlc_mutex_unlock( &p_input->stream.stream_lock );
return( 0 );
}
......@@ -843,8 +843,9 @@ static mtime_t AVI_GetPTS( AVIStreamInfo_t *p_info )
if( p_info->header.i_samplesize != 0 )
{
i_pts = (mtime_t)( (double)1000000.0 *
((double)p_info->p_index[p_info->i_idxposc].i_lengthtotal +
(double)p_info->i_idxposb ) *
(double)(p_info->p_index[p_info->i_idxposc].i_lengthtotal +
p_info->i_idxposb -
p_info->p_index[p_info->header.i_initialframes].i_lengthtotal)*
(double)p_info->header.i_scale /
(double)p_info->header.i_rate /
(double)p_info->header.i_samplesize );
......@@ -852,7 +853,8 @@ static mtime_t AVI_GetPTS( AVIStreamInfo_t *p_info )
else
{
i_pts = (mtime_t)( (double)1000000.0 *
(double)p_info->i_idxposc *
(double)(p_info->i_idxposc
- p_info->header.i_initialframes )*
(double)p_info->header.i_scale /
(double)p_info->header.i_rate);
}
......@@ -890,6 +892,7 @@ static int __AVI_NextIndexEntry( input_thread_t *p_input,
the same things two time */
/* search for the less advance stream and parse from it for all streams*/
p_info_tmp = p_info;
/* XXX XXX XXX change to take the stream the more advanced */
for( i = 0; i < p_avi_demux->i_streams; i++ )
{
......@@ -1050,16 +1053,11 @@ static int __AVI_GoToStreamBytes( input_thread_t *p_input,
while( p_info->p_index[p_info->i_idxposc].i_lengthtotal +
p_info->p_index[p_info->i_idxposc].i_length <= i_byte)
{
intf_WarnMsg( 1,"read 0");
if( __AVI_NextIndexEntry( p_input, p_info ) != 0)
{
return( -1 );
}
intf_WarnMsg( 1,"read 1");
}
}
else
{
......@@ -1210,25 +1208,25 @@ static pes_packet_t *__AVI_ReadStreamBytesInPES( input_thread_t *p_input,
static __inline__ mtime_t __AVI_PTSToChunk( AVIStreamInfo_t *p_info,
mtime_t i_pts )
{
return( (mtime_t)((mtime_t)i_pts *
(mtime_t)p_info->header.i_rate /
(mtime_t)p_info->header.i_scale /
(mtime_t)1000000.0 ) );
return( (mtime_t)((double)i_pts *
(double)p_info->header.i_rate /
(double)p_info->header.i_scale /
(double)1000000.0 ) );
}
static __inline__ mtime_t __AVI_PTSToByte( AVIStreamInfo_t *p_info,
mtime_t i_pts )
{
return( (mtime_t)((mtime_t)i_pts *
(mtime_t)p_info->header.i_samplesize *
(mtime_t)p_info->header.i_rate /
(mtime_t)p_info->header.i_scale /
(mtime_t)1000000.0 ) );
return( (mtime_t)((double)i_pts *
(double)p_info->header.i_samplesize *
(double)p_info->header.i_rate /
(double)p_info->header.i_scale /
(double)1000000.0 ) );
}
/* try to realign after a seek */
static int __AVI_ReAlign( input_thread_t *p_input )
static int AVI_ReAlign( input_thread_t *p_input )
{
u32 u32_pos;
off_t i_pos;
......@@ -1245,16 +1243,24 @@ static int __AVI_ReAlign( input_thread_t *p_input )
i_pos = (off_t)u32_pos - (off_t)p_info->i_idxoffset;
if( i_pos <= p_info->p_index[0].i_offset )
if( i_pos <= p_info->p_index[p_info->header.i_initialframes].i_offset )
{
/* before beginning of stream */
if( !p_info->header.i_samplesize )
{
__AVI_GoToStreamChunk( p_input, p_info, 0 );
__AVI_GoToStreamChunk( p_input,
p_info,
p_info->header.i_initialframes );
}
else
{
__AVI_GoToStreamBytes( p_input, p_info, 0 );
__AVI_GoToStreamChunk( p_input,
p_info,
p_info->header.i_initialframes );
__AVI_GoToStreamBytes( p_input,
p_info,
p_info->p_index[p_info->header.i_initialframes].i_lengthtotal);
}
return( 0 );
}
......@@ -1276,7 +1282,7 @@ static int __AVI_ReAlign( input_thread_t *p_input )
}
/* now find in what chunk we are */
while( ( i_pos < p_info->p_index[p_info->i_idxposc].i_offset )
&&( p_info->i_idxposc > 0 ) )
&&( p_info->i_idxposc > p_info->header.i_initialframes ) )
{
/* search before i_idxposc */
p_info->i_idxposc--;
......@@ -1307,7 +1313,7 @@ static int __AVI_ReAlign( input_thread_t *p_input )
}
else
{
while( ( p_info->i_idxposc > 0 ) &&
while( ( p_info->i_idxposc > p_info->header.i_initialframes ) &&
(!(p_info->p_index[p_info->i_idxposc].i_flags&AVIIF_KEYFRAME)) )
{
p_info->i_idxposc--;
......@@ -1321,7 +1327,7 @@ static int __AVI_ReAlign( input_thread_t *p_input )
/* update i_date and */
/* make difference between audio and video pts as little as possible */
static void __AVI_SynchroReInit( input_thread_t *p_input )
static void AVI_SynchroReInit( input_thread_t *p_input )
{
demux_data_avi_file_t *p_avi_demux;
......@@ -1345,11 +1351,8 @@ static void __AVI_SynchroReInit( input_thread_t *p_input )
if( p_avi_demux->p_info_audio->header.i_samplesize == 0 )
{
int i_chunk = __AVI_PTSToChunk( p_avi_demux->p_info_audio,
AVI_GetPTS( p_avi_demux->p_info_video ));
if( i_chunk < 0 )
{
i_chunk = 0;
}
AVI_GetPTS( p_avi_demux->p_info_video )) +
p_avi_demux->p_info_audio->header.i_initialframes ;
__AVI_GoToStreamChunk( p_input,
p_avi_demux->p_info_audio,
i_chunk );
......@@ -1357,18 +1360,15 @@ static void __AVI_SynchroReInit( input_thread_t *p_input )
else
{
int i_byte = __AVI_PTSToByte( p_avi_demux->p_info_audio,
AVI_GetPTS( p_avi_demux->p_info_video ) );
if( i_byte < 0 )
{
i_byte = 0;
}
AVI_GetPTS( p_avi_demux->p_info_video ) ) +
p_avi_demux->p_info_audio->p_index[p_avi_demux->p_info_audio->header.i_initialframes].i_lengthtotal;
__AVI_GoToStreamBytes( p_input,
p_avi_demux->p_info_audio,
i_byte );
}
}
}
}
static pes_packet_t *AVI_GetFrameInPES( input_thread_t *p_input,
......@@ -1384,22 +1384,17 @@ static pes_packet_t *AVI_GetFrameInPES( input_thread_t *p_input,
p_avi_demux = (demux_data_avi_file_t*)p_input->p_demux_data;
if( ( !p_info)||(i_dpts < 0 ) )
if( ( !p_info)||(i_dpts < 1000 ) )
{
return( NULL ) ;
}
/* if i_pts is too small use 50 ms */
if( i_dpts <= 50000)
{
i_dpts = 50000; /* 50 ms by default */
}
if( !p_info->header.i_samplesize )
{
/* stream is chunk based , easy */
int i_chunk = __MAX( __AVI_PTSToChunk( p_info, i_dpts) , 1 );
int i_chunk = __AVI_PTSToChunk( p_info, i_dpts);
/* at least one frame */
i_chunk = __MIN( 20, i_chunk ); /* but no more than 20 */
/* i_chunk = __MIN( 50, i_chunk ); */ /* but no more than 20 */
/* read them */
p_pes_first = NULL;
for( i = 0; i < i_chunk; i++ )
......@@ -1427,9 +1422,13 @@ static pes_packet_t *AVI_GetFrameInPES( input_thread_t *p_input,
else
{
/* stream is byte based */
int i_byte = __MAX( __AVI_PTSToByte( p_info, i_dpts), 1024 );
int i_byte = __AVI_PTSToByte( p_info, i_dpts);
if( !i_byte )
{
return( NULL );
}
/* at least one Kbyte */
i_byte = __MIN( 1024*1000, i_byte ); /* but no more than 1000ko */
/*i_byte = __MIN( 1024*1000, i_byte ); *//* but no more than 1000ko */
i_pts = AVI_GetPTS( p_info );
p_pes = __AVI_ReadStreamBytesInPES( p_input, p_info, i_byte);
......@@ -1469,7 +1468,7 @@ static void AVI_DecodePES( AVIStreamInfo_t *p_info,
(mtime_t)DEFAULT_RATE;
input_DecodePES( p_info->p_es->p_decoder_fifo, p_pes );
p_pes = p_pes_next;
} while( p_pes != NULL );
} while( p_pes );
}
......@@ -1540,7 +1539,7 @@ static int AVIDemux( input_thread_t *p_input )
if( !p_info_master )
{
intf_ErrMsg( "input error: stream selected" );
intf_ErrMsg( "input error: no stream selected" );
return( -1 );
}
......@@ -1548,7 +1547,7 @@ static int AVIDemux( input_thread_t *p_input )
if( (input_ClockManageControl( p_input, p_input->stream.p_selected_program,
(mtime_t)0) == PAUSE_S) )
{
__AVI_SynchroReInit( p_input ); /* resynchro, and make pts audio
AVI_SynchroReInit( p_input ); /* resynchro, and make pts audio
and video egual */
p_avi_demux->i_rate = DEFAULT_RATE;
}
......@@ -1556,13 +1555,13 @@ static int AVIDemux( input_thread_t *p_input )
if( p_input->stream.p_selected_program->i_synchro_state == SYNCHRO_REINIT )
{
/* wait until buffer is empty */
msleep( 2*DEFAULT_PTS_DELAY );
msleep( 3*DEFAULT_PTS_DELAY );
/*realign audio and video stream to the good pts*/
if( __AVI_ReAlign( p_input ) != 0 )
if( AVI_ReAlign( p_input ) != 0 )
{
return( 0 ); /* assume EOF */
}
__AVI_SynchroReInit( p_input ); /* resynchro, and make pts audio
AVI_SynchroReInit( p_input ); /* resynchro, and make pts audio
and video egual */
p_avi_demux->i_rate = DEFAULT_RATE;
}
......@@ -1571,7 +1570,7 @@ static int AVIDemux( input_thread_t *p_input )
if( (p_input->stream.control.i_rate != p_avi_demux->i_rate)
&&(p_info_master->i_cat == VIDEO_ES) )
{
msleep( 2*DEFAULT_PTS_DELAY );
msleep( 3*DEFAULT_PTS_DELAY );
p_avi_demux->i_rate = p_input->stream.control.i_rate;
p_avi_demux->i_date = mdate() + DEFAULT_PTS_DELAY
- AVI_GetPTS( p_info_master ) *
......@@ -1595,13 +1594,13 @@ static int AVIDemux( input_thread_t *p_input )
if( p_info_master->b_selected )
{
p_info_master->b_selected = 0;
__AVI_SynchroReInit( p_input ); /* resynchro, and make pts audio
AVI_SynchroReInit( p_input ); /* resynchro, and make pts audio
and video equal */
}
if( ( p_info_slave )&&( p_info_slave->b_selected ) )
{
p_info_slave->b_selected = 0;
__AVI_SynchroReInit( p_input );
AVI_SynchroReInit( p_input );
}
/* get audio and video frame */
......@@ -1613,7 +1612,7 @@ static int AVIDemux( input_thread_t *p_input )
p_pes_slave = AVI_GetFrameInPES( p_input,
p_info_slave,
AVI_GetPTS( p_info_master ) -
AVI_GetPTS( p_info_slave)+10000);
AVI_GetPTS( p_info_slave));
/* decode it first because video will make us wait */
AVI_DecodePES( p_info_slave,
p_pes_slave,
......
......@@ -2,7 +2,7 @@
* ffmpeg.c: video decoder using ffmpeg library
*****************************************************************************
* Copyright (C) 1999-2001 VideoLAN
* $Id: ffmpeg.c,v 1.4 2002/05/05 17:20:49 fenrir Exp $
* $Id: ffmpeg.c,v 1.5 2002/05/06 22:02:32 fenrir Exp $
*
* Authors: Laurent Aimar <fenrir@via.ecp.fr>
*
......@@ -156,7 +156,7 @@ static pes_packet_t *__PES_GET( decoder_fifo_t *p_fifo )
vlc_mutex_lock( &p_fifo->data_lock );
/* if fifo is emty wait */
while( p_fifo->p_first == NULL )
while( !p_fifo->p_first )
{
if( p_fifo->b_die )
{
......@@ -184,12 +184,12 @@ static void __PES_NEXT( decoder_fifo_t *p_fifo )
p_fifo->p_first = p_next;
p_fifo->i_depth--;
if( p_fifo->p_first == NULL )
if( !p_fifo->p_first )
{
/* No PES in the fifo */
/* pp_last no longer valid */
p_fifo->pp_last = &p_fifo->p_first;
while( p_fifo->p_first == NULL )
while( !p_fifo->p_first )
{
vlc_cond_signal( &p_fifo->data_wait );
vlc_cond_wait( &p_fifo->data_wait, &p_fifo->data_lock );
......@@ -218,7 +218,7 @@ static void __PACKET_NEXT( videodec_thread_t *p_vdec )
do
{
p_vdec->p_data = p_vdec->p_data->p_next;
if( p_vdec->p_data == NULL )
if( !p_vdec->p_data )
{
__PES_NEXT( p_vdec->p_fifo );
if( p_vdec->p_fifo->b_die )
......@@ -249,20 +249,24 @@ static void __PACKET_FILL( videodec_thread_t *p_vdec )
static __inline__ void __ConvertAVPictureToPicture( AVPicture *p_avpicture,
picture_t *p_picture )
{
int i_plane, i_line;
int i_plane, i_line, i_inc;
u8 *p_dest,*p_src;
for( i_plane = 0; i_plane < p_picture->i_planes; i_plane++ )
for( i_plane = 0; i_plane < __MIN(p_picture->i_planes, 3); i_plane++ )
{
p_dest = p_picture->p[i_plane].p_pixels;
p_src = p_avpicture->data[i_plane];
if( (p_dest == NULL)||( p_src == NULL)||(i_plane >= 3) ) { return; }
if( ( !p_dest )||( !p_src ))
{
return;
}
i_inc = __MIN( p_picture->p[i_plane].i_pitch,
p_avpicture->linesize[i_plane] );
for( i_line = 0; i_line < p_picture->p[i_plane].i_lines; i_line++ )
{
FAST_MEMCPY( p_dest,
p_src,
__MIN( p_picture->p[i_plane].i_pitch,
p_avpicture->linesize[i_plane] ) );
p_src,
i_inc );
p_dest += p_picture->p[i_plane].i_pitch;
p_src += p_avpicture->linesize[i_plane];
}
......@@ -278,14 +282,12 @@ static __inline__ u32 __FfmpegChromaToFourCC( int i_ffmpegchroma )
return FOURCC_I420;
case( PIX_FMT_RGB24 ):
return FOURCC_RV24;
case( PIX_FMT_BGR24 ):
return 0; /* FIXME pas trouv ds video.h */
case( PIX_FMT_YUV422P ):
return FOURCC_Y422;
case( PIX_FMT_YUV444P ):
return 0; /* FIXME pas trouv FOURCC_IYU2; */
case( PIX_FMT_BGR24 ):
default:
return 0;
return( 0 );
}
}
......@@ -383,11 +385,11 @@ static int InitThread( videodec_thread_t *p_vdec )
break;
case( MSMPEG4_VIDEO_ES):
p_vdec->p_codec = avcodec_find_decoder( CODEC_ID_MSMPEG4 );
p_vdec->psz_namecodec = "MS MPEG-4";
p_vdec->psz_namecodec = "MS MPEG-4/divx";
break;
case( MPEG4_VIDEO_ES):
p_vdec->p_codec = avcodec_find_decoder( CODEC_ID_MPEG4 );
p_vdec->psz_namecodec = "MPEG-4";
p_vdec->psz_namecodec = "MPEG-4/opendivx";
break;
default:
p_vdec->p_codec = NULL;
......@@ -419,7 +421,17 @@ static int InitThread( videodec_thread_t *p_vdec )
intf_WarnMsg( 1, "vdec info: ffmpeg codec (%s) started",
p_vdec->psz_namecodec );
}
/* destroy each p_vout */
vlc_mutex_lock( &p_vout_bank->lock );
if( p_vout_bank->i_count != 0 )
{
vlc_mutex_unlock( &p_vout_bank->lock );
vout_DestroyThread( p_vout_bank->pp_vout[ 0 ], NULL );
p_vout_bank->i_count--;
p_vout_bank->pp_vout[ 0 ] = NULL;
vlc_mutex_lock( &p_vout_bank->lock );
}
vlc_mutex_unlock( &p_vout_bank->lock );
__PACKET_REINIT( p_vdec );
return( 0 );
}
......@@ -445,10 +457,15 @@ static void EndThread( videodec_thread_t *p_vdec )
p_vdec->psz_namecodec);
}
if( p_vdec->p_vout != NULL )
if( p_vout_bank->i_count != 0 )
{
vout_DestroyThread( p_vdec->p_vout, NULL );
vlc_mutex_unlock( &p_vout_bank->lock );
vout_DestroyThread( p_vout_bank->pp_vout[ 0 ], NULL );
p_vout_bank->i_count--;
p_vout_bank->pp_vout[ 0 ] = NULL;
vlc_mutex_lock( &p_vout_bank->lock );
}
vlc_mutex_unlock( &p_vout_bank->lock );
free( p_vdec );
}
......@@ -459,7 +476,6 @@ static void DecodeThread( videodec_thread_t *p_vdec )
int b_gotpicture;
int b_convert;
mtime_t i_pts;
pes_packet_t *p_pes;
AVPicture avpicture; /* ffmpeg picture */
u32 i_chroma;
picture_t *p_picture; /* videolan picture */
......@@ -467,16 +483,8 @@ static void DecodeThread( videodec_thread_t *p_vdec )
give it to ffmpeg decoder
and send the image to the output */
/* when we have the first image we create the video output */
/* int avcodec_decode_video(AVCodecContext *avctx, AVPicture *picture,
int *got_picture_ptr,
UINT8 *buf, int buf_size);
typedef struct AVPicture
{
UINT8 *data[3];
int linesize[3];
} AVPicture;
*/
i_pts = -1 ;
i_pts = 0 ;
do
{
__PACKET_FILL( p_vdec );
......@@ -485,7 +493,7 @@ static void DecodeThread( videodec_thread_t *p_vdec )
return;
}
/* save pts */
if( i_pts < 0 ) {i_pts = __PES_GET( p_vdec->p_fifo )->i_pts;}
if( !i_pts ) {i_pts = __PES_GET( p_vdec->p_fifo )->i_pts;}
i_len = avcodec_decode_video( p_vdec->p_context,
&avpicture,
......@@ -495,7 +503,7 @@ static void DecodeThread( videodec_thread_t *p_vdec )
if( i_len < 0 )
{
intf_WarnMsg( 1, "vdec error: cannot decode one frame (%d bytes)",
intf_WarnMsg( 3, "vdec error: cannot decode one frame (%d bytes)",
p_vdec->i_data_size );
__PES_NEXT( p_vdec->p_fifo );
__PACKET_REINIT( p_vdec );
......@@ -505,8 +513,7 @@ static void DecodeThread( videodec_thread_t *p_vdec )
p_vdec->p_buff += i_len;
} while( !b_gotpicture );
i_chroma =__FfmpegChromaToFourCC( p_vdec->p_context->pix_fmt );
if( i_chroma == 0 )
if( !(i_chroma =__FfmpegChromaToFourCC( p_vdec->p_context->pix_fmt ) ) )
{
b_convert = 1;
i_chroma = FOURCC_I420;
......@@ -517,17 +524,12 @@ static void DecodeThread( videodec_thread_t *p_vdec )
}
/* Send decoded frame to vout */
if( p_vdec->p_vout == NULL )
if( !p_vdec->p_vout )
{
/* FIXME FIXME faire ca comme il faut avec :
* pp_vout_bank
* bon aspect, ds avi pas definie mais pour le reste a voir ...
*/
/* create vout */
/* ffmpeg set it for our with some codec */
if( (p_vdec->format.i_width == 0)||(p_vdec->format.i_height == 0) )
if( (!p_vdec->format.i_width )||(!p_vdec->format.i_height) )
{
p_vdec->format.i_width = p_vdec->p_context->width;
p_vdec->format.i_height = p_vdec->p_context->height;
......@@ -537,12 +539,6 @@ static void DecodeThread( videodec_thread_t *p_vdec )
p_vdec->format.i_height;
p_vdec->i_chroma = i_chroma;
intf_WarnMsg( 1, "vdec info: creating vout %dx%d chroma %4.4s %s",
p_vdec->format.i_width,
p_vdec->format.i_height,
(char*)&p_vdec->i_chroma,
b_convert ? "(with convertion)" : "" );
p_vdec->p_vout = vout_CreateThread(
NULL,
p_vdec->format.i_width,
......@@ -550,12 +546,17 @@ static void DecodeThread( videodec_thread_t *p_vdec )
p_vdec->i_chroma,
p_vdec->i_aspect );
if( p_vdec->p_vout == NULL )
if( !p_vdec->p_vout )
{
intf_ErrMsg( "vdec error: can't open vout, aborting" );
p_vdec->p_fifo->b_error = 1;
return;
}
vlc_mutex_lock( &p_vout_bank->lock );
p_vout_bank->pp_vout[ 0 ] = p_vdec->p_vout;
p_vout_bank->i_count++;
vlc_mutex_unlock( &p_vout_bank->lock );
}
while( (p_picture = vout_CreatePicture( p_vdec->p_vout,
......@@ -571,7 +572,7 @@ static void DecodeThread( videodec_thread_t *p_vdec )
msleep( VOUT_OUTMEM_SLEEP );
}
if( b_convert == 1 )
if( b_convert )
{
/* we convert in a supported format */
int i_status;
......@@ -610,5 +611,5 @@ static void DecodeThread( videodec_thread_t *p_vdec )
vout_DisplayPicture( p_vdec->p_vout, p_picture );
return;
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment