input.c 40.2 KB
Newer Older
1
2
/*****************************************************************************
 * input.c: input thread
Michel Kaempf's avatar
Michel Kaempf committed
3
4
 * Read an MPEG2 stream, demultiplex and parse it before sending it to
 * decoders.
5
 *****************************************************************************
6
 * Copyright (C) 1998-2001 VideoLAN
Sam Hocevar's avatar
   
Sam Hocevar committed
7
 * $Id: input.c,v 1.168 2002/01/07 02:12:29 sam Exp $
8
 *
9
 * Authors: Christophe Massiot <massiot@via.ecp.fr>
10
11
12
13
14
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
15
 * 
16
17
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
19
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
20
 *
21
22
23
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111, USA.
24
 *****************************************************************************/
Michel Kaempf's avatar
Michel Kaempf committed
25

26
/*****************************************************************************
Michel Kaempf's avatar
Michel Kaempf committed
27
 * Preamble
28
 *****************************************************************************/
29
30
31
32
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
Sam Hocevar's avatar
   
Sam Hocevar committed
33

Sam Hocevar's avatar
   
Sam Hocevar committed
34
35
#include <videolan/vlc.h>

Sam Hocevar's avatar
   
Sam Hocevar committed
36
#ifdef HAVE_UNISTD_H
Sam Hocevar's avatar
   
Sam Hocevar committed
37
#   include <unistd.h>
Sam Hocevar's avatar
   
Sam Hocevar committed
38
#elif defined( _MSC_VER ) && defined( _WIN32 )
Sam Hocevar's avatar
   
Sam Hocevar committed
39
#   include <io.h>
Sam Hocevar's avatar
   
Sam Hocevar committed
40
41
#endif

42
#include <string.h>
Sam Hocevar's avatar
   
Sam Hocevar committed
43
44
#include <errno.h>

Sam Hocevar's avatar
   
Sam Hocevar committed
45
46
47
#ifdef STRNCASECMP_IN_STRINGS_H
#   include <strings.h>
#endif
48
49

#ifdef WIN32
Sam Hocevar's avatar
   
Sam Hocevar committed
50
#   include <winsock2.h>
51
#   include <ws2tcpip.h>
Sam Hocevar's avatar
   
Sam Hocevar committed
52
53
54
55
#elif !defined( SYS_BEOS ) && !defined( SYS_NTO )
#   include <netdb.h>                                         /* hostent ... */
#   include <sys/socket.h>
#   include <netinet/in.h>
Sam Hocevar's avatar
   
Sam Hocevar committed
56
57
58
#   ifdef HAVE_ARPA_INET_H
#       include <arpa/inet.h>                    /* inet_ntoa(), inet_aton() */
#   endif
Sam Hocevar's avatar
   
Sam Hocevar committed
59
#endif
Henri Fallon's avatar
   
Henri Fallon committed
60

61
62
#ifdef HAVE_SYS_TIMES_H
#   include <sys/times.h>
63
#endif
64

Sam Hocevar's avatar
   
Sam Hocevar committed
65
#include "netutils.h"
66

Sam Hocevar's avatar
   
Sam Hocevar committed
67
#include "intf_playlist.h"
68

69
70
71
#include "stream_control.h"
#include "input_ext-intf.h"
#include "input_ext-dec.h"
72
#include "input_ext-plugins.h"
Michel Lespinasse's avatar
Yop,    
Michel Lespinasse committed
73

Sam Hocevar's avatar
   
Sam Hocevar committed
74
75
#include "interface.h"

76
/*****************************************************************************
Michel Kaempf's avatar
Michel Kaempf committed
77
 * Local prototypes
78
 *****************************************************************************/
Sam Hocevar's avatar
   
Sam Hocevar committed
79
80
81
static void RunThread       ( input_thread_t *p_input );
static  int InitThread      ( input_thread_t *p_input );
static void ErrorThread     ( input_thread_t *p_input );
82
static void CloseThread     ( input_thread_t *p_input );
Sam Hocevar's avatar
   
Sam Hocevar committed
83
84
static void DestroyThread   ( input_thread_t *p_input );
static void EndThread       ( input_thread_t *p_input );
Michel Kaempf's avatar
Michel Kaempf committed
85

Sam Hocevar's avatar
   
Sam Hocevar committed
86
static void FileOpen        ( input_thread_t *p_input );
Christophe Massiot's avatar
Christophe Massiot committed
87
static void StdOpen         ( input_thread_t *p_input );
Sam Hocevar's avatar
   
Sam Hocevar committed
88
static void FileClose       ( input_thread_t *p_input );
Sam Hocevar's avatar
   
Sam Hocevar committed
89
#if !defined( SYS_BEOS ) && !defined( SYS_NTO )
Sam Hocevar's avatar
   
Sam Hocevar committed
90
static void NetworkOpen     ( input_thread_t *p_input );
91
static void HTTPOpen        ( input_thread_t *p_input );
Sam Hocevar's avatar
   
Sam Hocevar committed
92
static void NetworkClose    ( input_thread_t *p_input );
Sam Hocevar's avatar
   
Sam Hocevar committed
93
#endif
Sam Hocevar's avatar
   
Sam Hocevar committed
94

Sam Hocevar's avatar
   
Sam Hocevar committed
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/*****************************************************************************
 * input_InitBank: initialize the input bank.
 *****************************************************************************/
void input_InitBank ( void )
{
    p_input_bank->i_count = 0;

    /* XXX: Workaround for old interface modules */
    p_input_bank->pp_input[0] = NULL;

    vlc_mutex_init( &p_input_bank->lock );
}

/*****************************************************************************
 * input_EndBank: empty the input bank.
 *****************************************************************************
 * This function ends all unused inputs and empties the bank in
 * case of success.
 *****************************************************************************/
void input_EndBank ( void )
{
    /* Ask all remaining video outputs to die */
    while( p_input_bank->i_count )
    {
        input_DestroyThread(
                p_input_bank->pp_input[ --p_input_bank->i_count ], NULL );
    }

    vlc_mutex_destroy( &p_input_bank->lock );
}

126
/*****************************************************************************
127
 * input_CreateThread: creates a new input thread
128
 *****************************************************************************
129
130
131
132
 * This function creates a new input, and returns a pointer
 * to its description. On error, it returns NULL.
 * If pi_status is NULL, then the function will block until the thread is ready.
 * If not, it will be updated using one of the THREAD_* constants.
133
 *****************************************************************************/
Sam Hocevar's avatar
   
Sam Hocevar committed
134
input_thread_t *input_CreateThread ( playlist_item_t *p_item, int *pi_status )
Michel Kaempf's avatar
Michel Kaempf committed
135
{
136
137
138
    input_thread_t *    p_input;                        /* thread descriptor */
    int                 i_status;                           /* thread status */

139
140
141
    /* Allocate descriptor */
    p_input = (input_thread_t *)malloc( sizeof(input_thread_t) );
    if( p_input == NULL )
Michel Kaempf's avatar
Michel Kaempf committed
142
    {
Sam Hocevar's avatar
   
Sam Hocevar committed
143
144
        intf_ErrMsg( "input error: can't allocate input thread (%s)",
                     strerror(errno) );
Michel Kaempf's avatar
Michel Kaempf committed
145
146
        return( NULL );
    }
147
148
149
150

    /* Initialize thread properties */
    p_input->b_die              = 0;
    p_input->b_error            = 0;
Sam Hocevar's avatar
   
Sam Hocevar committed
151
152
153
154
155
    p_input->b_eof              = 0;

    /* Set target */
    p_input->p_source           = p_item->psz_name;

156
    /* I have never understood that stuff --Meuuh */
157
158
    p_input->pi_status          = (pi_status != NULL) ? pi_status : &i_status;
    *p_input->pi_status         = THREAD_CREATE;
Michel Kaempf's avatar
Michel Kaempf committed
159

160
    /* Initialize stream description */
161
162
    p_input->stream.i_es_number = 0;
    p_input->stream.i_selected_es_number = 0;
163
    p_input->stream.i_pgrm_number = 0;
164
    p_input->stream.i_new_status = p_input->stream.i_new_rate = 0;
165
    p_input->stream.b_new_mute = MUTE_NO_CHANGE;
Christophe Massiot's avatar
Christophe Massiot committed
166
    p_input->stream.i_mux_rate = 0;
Michel Kaempf's avatar
Michel Kaempf committed
167

168
    /* no stream, no area */
Stéphane Borel's avatar
   
Stéphane Borel committed
169
170
    p_input->stream.i_area_nb = 0;
    p_input->stream.pp_areas = NULL;
171
    p_input->stream.p_selected_area = NULL;
172
    p_input->stream.p_new_area = NULL;
Sam Hocevar's avatar
   
Sam Hocevar committed
173
174

    /* By default there is one area in a stream */
Stéphane Borel's avatar
   
Stéphane Borel committed
175
    input_AddArea( p_input );
176
    p_input->stream.p_selected_area = p_input->stream.pp_areas[0];
Stéphane Borel's avatar
   
Stéphane Borel committed
177

178
179
180
181
    /* Initialize stream control properties. */
    p_input->stream.control.i_status = PLAYING_S;
    p_input->stream.control.i_rate = DEFAULT_RATE;
    p_input->stream.control.b_mute = 0;
182
183
184
185
    p_input->stream.control.b_grayscale = main_GetIntVariable(
                            VOUT_GRAYSCALE_VAR, VOUT_GRAYSCALE_DEFAULT );
    p_input->stream.control.i_smp = main_GetIntVariable(
                            VDEC_SMP_VAR, VDEC_SMP_DEFAULT );
Michel Kaempf's avatar
Michel Kaempf committed
186

Sam Hocevar's avatar
   
Sam Hocevar committed
187
188
    intf_WarnMsg( 1, "input: playlist item `%s'", p_input->p_source );

Sam Hocevar's avatar
   
Sam Hocevar committed
189
    /* Create thread. */
190
191
    if( vlc_thread_create( &p_input->thread_id, "input", (void *) RunThread,
                           (void *) p_input ) )
Michel Kaempf's avatar
Michel Kaempf committed
192
    {
Sam Hocevar's avatar
   
Sam Hocevar committed
193
194
        intf_ErrMsg( "input error: can't create input thread (%s)",
                     strerror(errno) );
Michel Kaempf's avatar
Michel Kaempf committed
195
196
197
        free( p_input );
        return( NULL );
    }
198

199
200
201
202
    /* If status is NULL, wait until the thread is created */
    if( pi_status == NULL )
    {
        do
203
        {
204
            msleep( THREAD_SLEEP );
205
        } while( (i_status != THREAD_READY) && (i_status != THREAD_ERROR)
Sam Hocevar's avatar
   
Sam Hocevar committed
206
                && (i_status != THREAD_FATAL) );
207
208
        if( i_status != THREAD_READY )
        {
209
210
            return( NULL );
        }
211
    }
Michel Kaempf's avatar
Michel Kaempf committed
212
213
214
    return( p_input );
}

215
/*****************************************************************************
Michel Kaempf's avatar
Michel Kaempf committed
216
 * input_DestroyThread: mark an input thread as zombie
217
 *****************************************************************************
Michel Kaempf's avatar
Michel Kaempf committed
218
 * This function should not return until the thread is effectively cancelled.
219
 *****************************************************************************/
220
void input_DestroyThread( input_thread_t *p_input, int *pi_status )
Michel Kaempf's avatar
Michel Kaempf committed
221
{
222
    int         i_status;                                   /* thread status */
223
224
225

    /* Set status */
    p_input->pi_status = (pi_status != NULL) ? pi_status : &i_status;
226
227
    *p_input->pi_status = THREAD_DESTROY;

228
229
    /* Request thread destruction */
    p_input->b_die = 1;
Michel Kaempf's avatar
Michel Kaempf committed
230

Sam Hocevar's avatar
   
Sam Hocevar committed
231
    /* Make the thread exit from a possible vlc_cond_wait() */
232
233
234
235
    vlc_mutex_lock( &p_input->stream.stream_lock );
    vlc_cond_signal( &p_input->stream.stream_wait );
    vlc_mutex_unlock( &p_input->stream.stream_lock );

236
237
238
239
240
241
    /* If status is NULL, wait until thread has been destroyed */
    if( pi_status == NULL )
    {
        do
        {
            msleep( THREAD_SLEEP );
242
243
        } while ( (i_status != THREAD_OVER) && (i_status != THREAD_ERROR)
                  && (i_status != THREAD_FATAL) );
244
    }
Michel Kaempf's avatar
Michel Kaempf committed
245
246
}

247
/*****************************************************************************
248
 * RunThread: main thread loop
249
 *****************************************************************************
250
 * Thread in charge of processing the network packets and demultiplexing.
251
 *****************************************************************************/
252
static void RunThread( input_thread_t *p_input )
Michel Kaempf's avatar
Michel Kaempf committed
253
{
Sam Hocevar's avatar
   
Sam Hocevar committed
254
255
256
257
258
259
260
261
262
    if( InitThread( p_input ) )
    {
        /* If we failed, wait before we are killed, and exit */
        *p_input->pi_status = THREAD_ERROR;
        p_input->b_error = 1;
        ErrorThread( p_input );
        DestroyThread( p_input );
        return;
    }
Michel Kaempf's avatar
Michel Kaempf committed
263

264
    /* initialization is complete */
265
266
267
268
    vlc_mutex_lock( &p_input->stream.stream_lock );
    p_input->stream.b_changed = 1;
    vlc_mutex_unlock( &p_input->stream.stream_lock );

Sam Hocevar's avatar
   
Sam Hocevar committed
269
270
    while( !p_input->b_die && !p_input->b_error && !p_input->b_eof )
    {
271
272
273
        data_packet_t * p_data;
        int i_count, i;

Sam Hocevar's avatar
   
Sam Hocevar committed
274
        p_input->c_loops++;
Sam Hocevar's avatar
   
Sam Hocevar committed
275

276
        vlc_mutex_lock( &p_input->stream.stream_lock );
277

278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
        if( p_input->stream.p_new_program )
        {
            if( p_input->pf_set_program != NULL )
            {

                p_input->pf_set_program( p_input, 
                        p_input->stream.p_new_program );

                for( i = 0; i < p_input->stream.i_pgrm_number; i++ )
                {
                    pgrm_descriptor_t * p_pgrm
                                            = p_input->stream.pp_programs[i];
                    /* Escape all decoders for the stream discontinuity they
                     * will encounter. */
                    input_EscapeDiscontinuity( p_input, p_pgrm );

                    /* Reinitialize synchro. */
                    p_pgrm->i_synchro_state = SYNCHRO_REINIT;
                }
            }
            p_input->stream.p_new_program = NULL;
        }
        
301
302
        if( p_input->stream.p_new_area )
        {
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
            if( p_input->stream.b_seekable && p_input->pf_set_area != NULL )
            {

                p_input->pf_set_area( p_input, p_input->stream.p_new_area );

                for( i = 0; i < p_input->stream.i_pgrm_number; i++ )
                {
                    pgrm_descriptor_t * p_pgrm
                                            = p_input->stream.pp_programs[i];
                    /* Escape all decoders for the stream discontinuity they
                     * will encounter. */
                    input_EscapeDiscontinuity( p_input, p_pgrm );

                    /* Reinitialize synchro. */
                    p_pgrm->i_synchro_state = SYNCHRO_REINIT;
                }
            }
320
321
322
            p_input->stream.p_new_area = NULL;
        }

323
        if( p_input->stream.p_selected_area->i_seek != NO_SEEK )
Sam Hocevar's avatar
   
Sam Hocevar committed
324
        {
325
326
            if( p_input->stream.b_seekable && p_input->pf_seek != NULL )
            {
327
328
                p_input->pf_seek( p_input,
                                  p_input->stream.p_selected_area->i_seek );
329
330
331
332
333
334
335
336
337
338
339
340
341

                for( i = 0; i < p_input->stream.i_pgrm_number; i++ )
                {
                    pgrm_descriptor_t * p_pgrm
                                            = p_input->stream.pp_programs[i];
                    /* Escape all decoders for the stream discontinuity they
                     * will encounter. */
                    input_EscapeDiscontinuity( p_input, p_pgrm );

                    /* Reinitialize synchro. */
                    p_pgrm->i_synchro_state = SYNCHRO_REINIT;
                }
            }
342
            p_input->stream.p_selected_area->i_seek = NO_SEEK;
Sam Hocevar's avatar
   
Sam Hocevar committed
343
        }
344

345
346
347
348
349
350
351
352
353
354
355
356
        if( p_input->stream.p_removed_es )
        {
            input_UnselectES( p_input, p_input->stream.p_removed_es );
            p_input->stream.p_removed_es = NULL;
        }

        if( p_input->stream.p_newly_selected_es )
        {
            input_SelectES( p_input, p_input->stream.p_newly_selected_es );
            p_input->stream.p_newly_selected_es = NULL;
        }

357
358
359
360
361
362
363
364
365
366
367
368
369
370
        if( p_input->stream.b_new_mute != MUTE_NO_CHANGE )
        {
            if( p_input->stream.b_new_mute )
            {
                input_EscapeAudioDiscontinuity( p_input );
            }

            vlc_mutex_lock( &p_input->stream.control.control_lock );
            p_input->stream.control.b_mute = p_input->stream.b_new_mute;
            vlc_mutex_unlock( &p_input->stream.control.control_lock );

            p_input->stream.b_new_mute = MUTE_NO_CHANGE;
        }

371
        vlc_mutex_unlock( &p_input->stream.stream_lock );
Sam Hocevar's avatar
   
Sam Hocevar committed
372

373
        i_count = p_input->pf_read( p_input, &p_data );
Sam Hocevar's avatar
   
Sam Hocevar committed
374

Sam Hocevar's avatar
   
Sam Hocevar committed
375
        /* Demultiplex read packets. */
376
        while( p_data != NULL )
Sam Hocevar's avatar
   
Sam Hocevar committed
377
        {
378
379
380
            data_packet_t * p_next = p_data->p_next;
            p_data->p_next = NULL;

381
            p_input->stream.c_packets_read++;
382
383
384
            p_input->pf_demux( p_input, p_data );

            p_data = p_next;
Sam Hocevar's avatar
   
Sam Hocevar committed
385
        }
Sam Hocevar's avatar
   
Sam Hocevar committed
386

387
        if( i_count == 0 && p_input->stream.b_seekable )
Sam Hocevar's avatar
   
Sam Hocevar committed
388
        {
389
390
391
392
393
394
395
396
            /* End of file - we do not set b_die because only the
             * interface is allowed to do so. */
            intf_WarnMsg( 3, "input: EOF reached" );
            p_input->b_eof = 1;
        }
        else if( i_count < 0 )
        {
            p_input->b_error = 1;
397
398
399
        }
    }

Sam Hocevar's avatar
   
Sam Hocevar committed
400
    if( p_input->b_error || p_input->b_eof )
401
402
403
    {
        ErrorThread( p_input );
    }
404

405
    EndThread( p_input );
Sam Hocevar's avatar
   
Sam Hocevar committed
406
407
408

    DestroyThread( p_input );

Sam Hocevar's avatar
   
Sam Hocevar committed
409
    intf_DbgMsg("input: Thread end");
410
411
}

412
/*****************************************************************************
Sam Hocevar's avatar
   
Sam Hocevar committed
413
 * InitThread: init the input Thread
414
 *****************************************************************************/
Sam Hocevar's avatar
   
Sam Hocevar committed
415
static int InitThread( input_thread_t * p_input )
Michel Kaempf's avatar
Michel Kaempf committed
416
{
417
418
    /* Initialize statistics */
    p_input->c_loops                    = 0;
419
420
    p_input->stream.c_packets_read      = 0;
    p_input->stream.c_packets_trashed   = 0;
421
    p_input->p_stream                   = NULL;
Sam Hocevar's avatar
Sam Hocevar committed
422

Sam Hocevar's avatar
   
Sam Hocevar committed
423
424
425
426
427
    /* Set locks. */
    vlc_mutex_init( &p_input->stream.stream_lock );
    vlc_cond_init( &p_input->stream.stream_wait );
    vlc_mutex_init( &p_input->stream.control.control_lock );

428
    /* Find appropriate module. */
Sam Hocevar's avatar
   
Sam Hocevar committed
429
    p_input->p_input_module = module_Need( MODULE_CAPABILITY_INPUT,
Sam Hocevar's avatar
   
Sam Hocevar committed
430
431
                                 main_GetPszVariable( INPUT_METHOD_VAR, NULL ),
                                 (probedata_t *)p_input );
Sam Hocevar's avatar
   
Sam Hocevar committed
432
433

    if( p_input->p_input_module == NULL )
434
    {
Sam Hocevar's avatar
   
Sam Hocevar committed
435
436
        intf_ErrMsg( "input error: no suitable input module for `%s'",
                     p_input->p_source );
Sam Hocevar's avatar
   
Sam Hocevar committed
437
        return( -1 );
Michel Kaempf's avatar
Michel Kaempf committed
438
    }
439

Sam Hocevar's avatar
   
Sam Hocevar committed
440
441
442
#define f p_input->p_input_module->p_functions->input.functions.input
    p_input->pf_init          = f.pf_init;
    p_input->pf_end           = f.pf_end;
443
    p_input->pf_init_bit_stream= f.pf_init_bit_stream;
Sam Hocevar's avatar
   
Sam Hocevar committed
444
    p_input->pf_read          = f.pf_read;
445
    p_input->pf_set_area      = f.pf_set_area;
446
    p_input->pf_set_program   = f.pf_set_program;
Sam Hocevar's avatar
   
Sam Hocevar committed
447
448
449
450
451
452
453
    p_input->pf_demux         = f.pf_demux;
    p_input->pf_new_packet    = f.pf_new_packet;
    p_input->pf_new_pes       = f.pf_new_pes;
    p_input->pf_delete_packet = f.pf_delete_packet;
    p_input->pf_delete_pes    = f.pf_delete_pes;
    p_input->pf_rewind        = f.pf_rewind;
    p_input->pf_seek          = f.pf_seek;
Sam Hocevar's avatar
   
Sam Hocevar committed
454

Sam Hocevar's avatar
   
Sam Hocevar committed
455
456
457
458
459
    if( f.pf_open != NULL )
    {
        f.pf_open( p_input );
        p_input->stream.i_method = INPUT_METHOD_DVD;
    }
460
#if !defined( SYS_BEOS ) && !defined( SYS_NTO )
461
    /* FIXME : this is waaaay too kludgy */
Sam Hocevar's avatar
   
Sam Hocevar committed
462
463
464
465
    else if( ( strlen( p_input->p_source ) >= 10
               && !strncasecmp( p_input->p_source, "udpstream:", 10 ) )
               || ( strlen( p_input->p_source ) >= 4
                     && !strncasecmp( p_input->p_source, "udp:", 4 ) ) )
466
467
468
469
470
    {
        /* Network stream */
        NetworkOpen( p_input );
        p_input->stream.i_method = INPUT_METHOD_NETWORK;
    }
Sam Hocevar's avatar
   
Sam Hocevar committed
471
472
    else if( ( strlen( p_input->p_source ) > 5 )
               && !strncasecmp( p_input->p_source, "http:", 5 ) )
473
474
475
476
477
    {
        /* HTTP stream */
        HTTPOpen( p_input );
        p_input->stream.i_method = INPUT_METHOD_NETWORK;
    }
478
#endif
Sam Hocevar's avatar
   
Sam Hocevar committed
479
480
    else if( ( strlen( p_input->p_source ) == 1 )
               && *p_input->p_source == '-' )
Christophe Massiot's avatar
Christophe Massiot committed
481
482
483
484
    {
        /* Stdin */
        StdOpen( p_input );
    }
485
486
487
488
489
490
    else
    {
        /* File input */
        FileOpen( p_input );
        p_input->stream.i_method = INPUT_METHOD_FILE;
    }
Sam Hocevar's avatar
   
Sam Hocevar committed
491
#undef f
492

Sam Hocevar's avatar
   
Sam Hocevar committed
493
    if( p_input->b_error )
494
    {
Sam Hocevar's avatar
   
Sam Hocevar committed
495
        /* We barfed -- exit nicely */
Sam Hocevar's avatar
   
Sam Hocevar committed
496
        module_Unneed( p_input->p_input_module );
Sam Hocevar's avatar
   
Sam Hocevar committed
497
        return( -1 );
498
    }
Sam Hocevar's avatar
   
Sam Hocevar committed
499
500

    p_input->pf_init( p_input );
Sam Hocevar's avatar
   
Sam Hocevar committed
501

502
503
504
    if( p_input->b_error )
    {
        /* We barfed -- exit nicely */
Sam Hocevar's avatar
   
Sam Hocevar committed
505
        CloseThread( p_input );
Sam Hocevar's avatar
   
Sam Hocevar committed
506
        module_Unneed( p_input->p_input_module );
507
508
509
        return( -1 );
    }

Sam Hocevar's avatar
   
Sam Hocevar committed
510
    *p_input->pi_status = THREAD_READY;
Sam Hocevar's avatar
   
Sam Hocevar committed
511
512

    return( 0 );
Michel Kaempf's avatar
Michel Kaempf committed
513
514
}

515
/*****************************************************************************
516
 * ErrorThread: RunThread() error loop
517
 *****************************************************************************
518
 * This function is called when an error occured during thread main's loop.
519
 *****************************************************************************/
520
static void ErrorThread( input_thread_t *p_input )
Michel Kaempf's avatar
Michel Kaempf committed
521
{
522
    while( !p_input->b_die )
Michel Kaempf's avatar
Michel Kaempf committed
523
    {
524
525
        /* Sleep a while */
        msleep( INPUT_IDLE_SLEEP );
Michel Kaempf's avatar
Michel Kaempf committed
526
527
528
    }
}

529
/*****************************************************************************
530
 * EndThread: end the input thread
531
 *****************************************************************************/
532
static void EndThread( input_thread_t * p_input )
533
{
534
    int *       pi_status;                                  /* thread status */
535

536
537
538
    /* Store status */
    pi_status = p_input->pi_status;
    *pi_status = THREAD_END;
Sam Hocevar's avatar
Sam Hocevar committed
539

540
    if( p_main->b_stats )
Sam Hocevar's avatar
   
Sam Hocevar committed
541
    {
542
#ifdef HAVE_SYS_TIMES_H
543
544
        /* Display statistics */
        struct tms  cpu_usage;
Sam Hocevar's avatar
   
Sam Hocevar committed
545
546
        times( &cpu_usage );

547
548
549
        intf_StatMsg( "input stats: %d loops consuming user: %d, system: %d",
                      p_input->c_loops,
                      cpu_usage.tms_utime, cpu_usage.tms_stime );
550
551
552
#else
        intf_StatMsg( "input stats: %d loops", p_input->c_loops );
#endif
553
554

        input_DumpStream( p_input );
Sam Hocevar's avatar
   
Sam Hocevar committed
555
556
557
558
559
560
561
562
    }

    /* Free all ES and destroy all decoder threads */
    input_EndStream( p_input );

    /* Free demultiplexer's data */
    p_input->pf_end( p_input );

Sam Hocevar's avatar
   
Sam Hocevar committed
563
564
565
566
567
568
569
570
571
572
573
574
575
576
    /* Close the input method */
    CloseThread( p_input );

    /* Release modules */
    module_Unneed( p_input->p_input_module );
}

/*****************************************************************************
 * CloseThread: close the target
 *****************************************************************************/
static void CloseThread( input_thread_t * p_input )
{
#define f p_input->p_input_module->p_functions->input.functions.input

Sam Hocevar's avatar
   
Sam Hocevar committed
577
578
579
580
    if( f.pf_close != NULL )
    {
        f.pf_close( p_input );
    }
581
#if !defined( SYS_BEOS ) && !defined( SYS_NTO )
Sam Hocevar's avatar
   
Sam Hocevar committed
582
    /* Close stream */
Sam Hocevar's avatar
   
Sam Hocevar committed
583
584
585
586
    else if( ( strlen( p_input->p_source ) > 10
               && !strncasecmp( p_input->p_source, "udpstream:", 10 ) )
               || ( strlen( p_input->p_source ) > 4
                     && !strncasecmp( p_input->p_source, "udp:", 4 ) ) )
587
588
589
    {
        NetworkClose( p_input );
    }
Sam Hocevar's avatar
   
Sam Hocevar committed
590
591
    else if( ( strlen( p_input->p_source ) > 5 )
               && !strncasecmp( p_input->p_source, "http:", 5 ) )
592
593
594
    {
        NetworkClose( p_input );
    }
595
#endif
596
597
598
599
    else
    {
        FileClose( p_input );
    }
Sam Hocevar's avatar
   
Sam Hocevar committed
600
#undef f
Sam Hocevar's avatar
   
Sam Hocevar committed
601
602
603
604
605
606
607
608
609
610
611
}

/*****************************************************************************
 * DestroyThread: destroy the input thread
 *****************************************************************************/
static void DestroyThread( input_thread_t * p_input )
{
    int *       pi_status;                                  /* thread status */

    /* Store status */
    pi_status = p_input->pi_status;
Sam Hocevar's avatar
   
Sam Hocevar committed
612

Henri Fallon's avatar
   
Henri Fallon committed
613
614
    /* Destroy Mutex locks */
    vlc_mutex_destroy( &p_input->stream.control.control_lock );
Henri Fallon's avatar
   
Henri Fallon committed
615
    vlc_mutex_destroy( &p_input->stream.stream_lock );
Henri Fallon's avatar
   
Henri Fallon committed
616
    
617
    /* Free input structure */
618
    free( p_input );
619

620
621
    /* Update status */
    *pi_status = THREAD_OVER;
Sam Hocevar's avatar
Sam Hocevar committed
622
}
623

Christophe Massiot's avatar
Christophe Massiot committed
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
/*****************************************************************************
 * StdOpen : open standard input
 *****************************************************************************/
static void StdOpen( input_thread_t * p_input )
{
    vlc_mutex_lock( &p_input->stream.stream_lock );

    /* Suppose we can control the pace - this won't work in some cases ! */
    p_input->stream.b_pace_control = 1;

    p_input->stream.b_seekable = 0;
    p_input->stream.p_selected_area->i_size = 0;
    p_input->stream.p_selected_area->i_tell = 0;
    vlc_mutex_unlock( &p_input->stream.stream_lock );

    intf_WarnMsg( 2, "input: opening stdin" );
    p_input->i_handle = 0;
}

Sam Hocevar's avatar
Sam Hocevar committed
643
/*****************************************************************************
Sam Hocevar's avatar
   
Sam Hocevar committed
644
 * FileOpen : open a file descriptor
Sam Hocevar's avatar
Sam Hocevar committed
645
 *****************************************************************************/
Sam Hocevar's avatar
   
Sam Hocevar committed
646
static void FileOpen( input_thread_t * p_input )
Michel Kaempf's avatar
Michel Kaempf committed
647
{
648
    struct stat         stat_info;
Sam Hocevar's avatar
   
Sam Hocevar committed
649
650
651
    int                 i_stat;

    char *psz_name = p_input->p_source;
Michel Kaempf's avatar
Michel Kaempf committed
652

Sam Hocevar's avatar
   
Sam Hocevar committed
653
    if( ( i_stat = stat( psz_name, &stat_info ) ) == (-1) )
Sam Hocevar's avatar
   
Sam Hocevar committed
654
    {
Sam Hocevar's avatar
   
Sam Hocevar committed
655
656
        int i_size = strlen( psz_name );

Stéphane Borel's avatar
Stéphane Borel committed
657
658
659
660
661
662
663
        if( ( i_size > 8 )
            && !strncasecmp( psz_name, "dvdread:", 8 ) )
        {
            /* get rid of the 'dvdread:' stuff and try again */
            psz_name += 8;
            i_stat = stat( psz_name, &stat_info );
        }
664
        else if( ( i_size > 4 )
Sam Hocevar's avatar
   
Sam Hocevar committed
665
666
667
668
669
            && !strncasecmp( psz_name, "dvd:", 4 ) )
        {
            /* get rid of the 'dvd:' stuff and try again */
            psz_name += 4;
            i_stat = stat( psz_name, &stat_info );
Henri Fallon's avatar
   
Henri Fallon committed
670
        }
671
672
        else if( ( i_size > 4 )
                 && !strncasecmp( psz_name, "vcd:", 4 ) )
673
674
675
676
677
        {
            /* get rid of the 'vcd:' stuff and try again */
            psz_name += 4;
            i_stat = stat( psz_name, &stat_info );
        }
Sam Hocevar's avatar
   
Sam Hocevar committed
678
        else if( ( i_size > 5 )
Sam Hocevar's avatar
   
Sam Hocevar committed
679
680
681
682
683
                 && !strncasecmp( psz_name, "file:", 5 ) )
        {
            /* get rid of the 'file:' stuff and try again */
            psz_name += 5;
            i_stat = stat( psz_name, &stat_info );
Henri Fallon's avatar
   
Henri Fallon committed
684
        }
Sam Hocevar's avatar
   
Sam Hocevar committed
685

Sam Hocevar's avatar
   
Sam Hocevar committed
686
        if( i_stat == (-1) )
Sam Hocevar's avatar
   
Sam Hocevar committed
687
688
689
690
691
692
        {
            intf_ErrMsg( "input error: cannot stat() file `%s' (%s)",
                         psz_name, strerror(errno));
            p_input->b_error = 1;
            return;
        }
Sam Hocevar's avatar
   
Sam Hocevar committed
693
694
695
696
697
698
699
700
    }

    vlc_mutex_lock( &p_input->stream.stream_lock );

    /* If we are here we can control the pace... */
    p_input->stream.b_pace_control = 1;

    if( S_ISREG(stat_info.st_mode) || S_ISCHR(stat_info.st_mode)
Sam Hocevar's avatar
   
Sam Hocevar committed
701
         || S_ISBLK(stat_info.st_mode) )
Sam Hocevar's avatar
   
Sam Hocevar committed
702
703
    {
        p_input->stream.b_seekable = 1;
704
        p_input->stream.p_selected_area->i_size = stat_info.st_size;
Sam Hocevar's avatar
   
Sam Hocevar committed
705
    }
Jean-Marc Dressler's avatar
   
Jean-Marc Dressler committed
706
    else if( S_ISFIFO(stat_info.st_mode)
Sam Hocevar's avatar
   
Sam Hocevar committed
707
#if !defined( SYS_BEOS ) && !defined( WIN32 )
Jean-Marc Dressler's avatar
   
Jean-Marc Dressler committed
708
709
710
             || S_ISSOCK(stat_info.st_mode)
#endif
             )
Sam Hocevar's avatar
Sam Hocevar committed
711
    {
712
        p_input->stream.b_seekable = 0;
713
        p_input->stream.p_selected_area->i_size = 0;
Benoit Steiner's avatar
   
Benoit Steiner committed
714
715
716
    }
    else
    {
717
        vlc_mutex_unlock( &p_input->stream.stream_lock );
Sam Hocevar's avatar
   
Sam Hocevar committed
718
        intf_ErrMsg( "input error: unknown file type for `%s'",
Sam Hocevar's avatar
   
Sam Hocevar committed
719
                     psz_name );
Sam Hocevar's avatar
   
Sam Hocevar committed
720
721
722
        p_input->b_error = 1;
        return;
    }
723

724
    p_input->stream.p_selected_area->i_tell = 0;
Sam Hocevar's avatar
   
Sam Hocevar committed
725
726
    vlc_mutex_unlock( &p_input->stream.stream_lock );

Sam Hocevar's avatar
   
Sam Hocevar committed
727
    intf_WarnMsg( 2, "input: opening file `%s'", p_input->p_source );
Sam Hocevar's avatar
   
Sam Hocevar committed
728
    if( (p_input->i_handle = open( psz_name,
Sam Hocevar's avatar
   
Sam Hocevar committed
729
730
                                   /*O_NONBLOCK | O_LARGEFILE*/0 )) == (-1) )
    {
Sam Hocevar's avatar
   
Sam Hocevar committed
731
        intf_ErrMsg( "input error: cannot open file (%s)", strerror(errno) );
Sam Hocevar's avatar
   
Sam Hocevar committed
732
733
        p_input->b_error = 1;
        return;
Michel Kaempf's avatar
Michel Kaempf committed
734
735
736
    }

}
Stéphane Borel's avatar
Stéphane Borel committed
737
738

/*****************************************************************************
Sam Hocevar's avatar
   
Sam Hocevar committed
739
 * FileClose : close a file descriptor
Stéphane Borel's avatar
Stéphane Borel committed
740
 *****************************************************************************/
Sam Hocevar's avatar
   
Sam Hocevar committed
741
static void FileClose( input_thread_t * p_input )
Stéphane Borel's avatar
Stéphane Borel committed
742
{
Sam Hocevar's avatar
   
Sam Hocevar committed
743
    intf_WarnMsg( 2, "input: closing file `%s'", p_input->p_source );
Sam Hocevar's avatar
   
Sam Hocevar committed
744

Sam Hocevar's avatar
   
Sam Hocevar committed
745
    close( p_input->i_handle );
Stéphane Borel's avatar
Stéphane Borel committed
746

Sam Hocevar's avatar
   
Sam Hocevar committed
747
    return;
Stéphane Borel's avatar
Stéphane Borel committed
748
}
Sam Hocevar's avatar
   
Sam Hocevar committed
749

750
#if !defined( SYS_BEOS ) && !defined( SYS_NTO )
Henri Fallon's avatar
   
Henri Fallon committed
751
/*****************************************************************************
Sam Hocevar's avatar
   
Sam Hocevar committed
752
 * NetworkOpen : open a network socket 
Henri Fallon's avatar
   
Henri Fallon committed
753
 *****************************************************************************/
Sam Hocevar's avatar
   
Sam Hocevar committed
754
static void NetworkOpen( input_thread_t * p_input )
Henri Fallon's avatar
   
Henri Fallon committed
755
{
Henri Fallon's avatar
   
Henri Fallon committed
756
    char                *psz_server = NULL;
757
758
759
    char                *psz_bind = NULL;
    int                 i_server_port = 0;
    int                 i_bind_port = 0;
Sam Hocevar's avatar
   
Sam Hocevar committed
760
    int                 i_opt;
761
    int                 i_opt_size;
Sam Hocevar's avatar
   
Sam Hocevar committed
762
    struct sockaddr_in  sock;
763

764
765
    /* Get the remote server. Syntax is :
     * udp[stream]:[/][/][serveraddr[:serverport]][@[bindaddr]:[bindport]] */
Sam Hocevar's avatar
   
Sam Hocevar committed
766
767
    if( p_input->p_source != NULL )
    {
768
769
770
        char * psz_parser = p_input->p_source;
        char * psz_server_port = NULL;
        char * psz_bind_port = NULL;
Sam Hocevar's avatar
   
Sam Hocevar committed
771
772

        /* Skip the protocol name */
773
        while( *psz_parser && *psz_parser != ':' )
Sam Hocevar's avatar
   
Sam Hocevar committed
774
        {
775
            psz_parser++;
Sam Hocevar's avatar
   
Sam Hocevar committed
776
777
778
        }

        /* Skip the "://" part */
779
        while( *psz_parser && (*psz_parser == ':' || *psz_parser == '/') )
Sam Hocevar's avatar
   
Sam Hocevar committed
780
        {
781
            psz_parser++;
Sam Hocevar's avatar
   
Sam Hocevar committed
782
783
        }

784
        if( *psz_parser && *psz_parser != '@' )
Sam Hocevar's avatar
   
Sam Hocevar committed
785
        {
786
787
            /* Found server */
            psz_server = psz_parser;
Sam Hocevar's avatar
   
Sam Hocevar committed
788

789
            while( *psz_parser && *psz_parser != ':' && *psz_parser != '@' )
Sam Hocevar's avatar
   
Sam Hocevar committed
790
            {
791
                psz_parser++;
Sam Hocevar's avatar
   
Sam Hocevar committed
792
793
            }

794
            if( *psz_parser == ':' )
Sam Hocevar's avatar
   
Sam Hocevar committed
795
            {
796
797
798
799
                /* Found server port */
                *psz_parser = '\0'; /* Terminate server name */
                psz_parser++;
                psz_server_port = psz_parser;
Sam Hocevar's avatar
   
Sam Hocevar committed
800

801
                while( *psz_parser && *psz_parser != '@' )
802
                {
803
                    psz_parser++;
804
                }
805
806
            }
        }
807

808
809
810
811
812
813
814
815
816
817
818
819
        if( *psz_parser == '@' )
        {
            /* Found bind address or bind port */
            *psz_parser = '\0'; /* Terminate server port or name if necessary */
            psz_parser++;

            if( *psz_parser && *psz_parser != ':' )
            {
                /* Found bind address */
                psz_bind = psz_parser;

                while( *psz_parser && *psz_parser != ':' )
820
                {
821
                    psz_parser++;
822
                }
Sam Hocevar's avatar
   
Sam Hocevar committed
823
            }
824
825
826
827
828
829
830
831
832

            if( *psz_parser == ':' )
            {
                /* Found bind port */
                *psz_parser = '\0'; /* Terminate bind address if necessary */
                psz_parser++;

                psz_bind_port = psz_parser;
            }
Sam Hocevar's avatar
   
Sam Hocevar committed
833
        }
834
835
836

        /* Convert ports format */
        if( psz_server_port != NULL )
Sam Hocevar's avatar
   
Sam Hocevar committed
837
        {
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
            i_server_port = strtol( psz_server_port, &psz_parser, 10 );
            if( *psz_parser )
            {
                intf_ErrMsg( "input error: cannot parse server port near %s",
                             psz_parser );
                p_input->b_error = 1;
                return;
            }
        }

        if( psz_bind_port != NULL )
        {
            i_bind_port = strtol( psz_bind_port, &psz_parser, 10 );
            if( *psz_parser )
            {
                intf_ErrMsg( "input error: cannot parse bind port near %s",
                             psz_parser );
                p_input->b_error = 1;
                return;
            }
Sam Hocevar's avatar
   
Sam Hocevar committed
858
859
        }
    }
Sam Hocevar's avatar
   
Sam Hocevar committed
860
861
862
863
864
    else
    {
        /* This is required or NetworkClose will never be called */
        p_input->p_source = "ts: network input";
    }
Sam Hocevar's avatar
   
Sam Hocevar committed
865
866

    /* Check that we got a valid port */
867
    if( i_bind_port == 0 )
868
    {
869
        i_bind_port = main_GetIntVariable( INPUT_PORT_VAR, INPUT_PORT_DEFAULT );
870
871
    }

872
873
    intf_WarnMsg( 2, "input: server=%s:%d local=%s:%d",
                     psz_server, i_server_port, psz_bind, i_bind_port );
874

Henri Fallon's avatar
   
Henri Fallon committed
875
876
877
878
879
    /* Open a SOCK_DGRAM (UDP) socket, in the AF_INET domain, automatic (0)
     * protocol */
    p_input->i_handle = socket( AF_INET, SOCK_DGRAM, 0 );
    if( p_input->i_handle == -1 )
    {
Sam Hocevar's avatar
   
Sam Hocevar committed
880
        intf_ErrMsg( "input error: can't create socket (%s)", strerror(errno) );
Henri Fallon's avatar
   
Henri Fallon committed
881
882
883
884
885
        p_input->b_error = 1;
        return;
    }

    /* We may want to reuse an already used socket */
Sam Hocevar's avatar
   
Sam Hocevar committed
886
    i_opt = 1;
Henri Fallon's avatar
   
Henri Fallon committed
887
    if( setsockopt( p_input->i_handle, SOL_SOCKET, SO_REUSEADDR,
888
                    (void *) &i_opt, sizeof( i_opt ) ) == -1 )
Henri Fallon's avatar
   
Henri Fallon committed
889
    {
Sam Hocevar's avatar
   
Sam Hocevar committed
890
891
        intf_ErrMsg( "input error: can't configure socket (SO_REUSEADDR: %s)",
                     strerror(errno));
Henri Fallon's avatar
   
Henri Fallon committed
892
893
894
895
896
897
898
        close( p_input->i_handle );
        p_input->b_error = 1;
        return;
    }

    /* Increase the receive buffer size to 1/2MB (8Mb/s during 1/2s) to avoid
     * packet loss caused by scheduling problems */
Sam Hocevar's avatar
   
Sam Hocevar committed
899
900
    i_opt = 0x80000;
    if( setsockopt( p_input->i_handle, SOL_SOCKET, SO_RCVBUF,
901
                    (void *) &i_opt, sizeof( i_opt ) ) == -1 )
Henri Fallon's avatar
   
Henri Fallon committed
902
    {
903
        intf_WarnMsg( 1, "input warning: can't configure socket (SO_RCVBUF: %s)", 
Sam Hocevar's avatar
   
Sam Hocevar committed
904
                         strerror(errno));
Henri Fallon's avatar
   
Henri Fallon committed
905
906
    }

907
908
909
910
911
912
913
914
    /* Check if we really got what we have asked for, because Linux, etc.
     * will silently limit the max buffer size to net.core.rmem_max which
     * is typically only 65535 bytes */
    i_opt = 0;
    i_opt_size = sizeof( i_opt );
    if( getsockopt( p_input->i_handle, SOL_SOCKET, SO_RCVBUF,
                    (void*) &i_opt, &i_opt_size ) == -1 )
    {
915
        intf_WarnMsg( 1, "input warning: can't query socket (SO_RCVBUF: %s)", 
Sam Hocevar's avatar
   
Sam Hocevar committed
916
                         strerror(errno));
917
    }
Sam Hocevar's avatar
   
Sam Hocevar committed
918
    else if( i_opt < 0x80000 )
919
    {
Sam Hocevar's avatar
   
Sam Hocevar committed
920
921
        intf_WarnMsg( 1, "input warning: socket buffer size is 0x%x"
                         " instead of 0x%x", i_opt, 0x80000 );
922
923
    }

Henri Fallon's avatar
   
Henri Fallon committed
924
    /* Build the local socket */
925
    if ( network_BuildAddr( &sock, psz_bind, i_bind_port ) == -1 )
Henri Fallon's avatar
   
Henri Fallon committed
926
    {
Sam Hocevar's avatar
   
Sam Hocevar committed
927
        intf_ErrMsg( "input error: can't build local address" );
Henri Fallon's avatar
   
Henri Fallon committed
928
929
930
931
        close( p_input->i_handle );
        p_input->b_error = 1;
        return;
    }
Sam Hocevar's avatar
   
Sam Hocevar committed
932

Henri Fallon's avatar
   
Henri Fallon committed
933
    /* Bind it */
Sam Hocevar's avatar
   
Sam Hocevar committed
934
935
    if( bind( p_input->i_handle, (struct sockaddr *)&sock, 
              sizeof( sock ) ) < 0 )
Henri Fallon's avatar
   
Henri Fallon committed
936
    {
Sam Hocevar's avatar
   
Sam Hocevar committed
937
        intf_ErrMsg( "input error: can't bind socket (%s)", strerror(errno) );
Henri Fallon's avatar
   
Henri Fallon committed
938
939
940
941
942
        close( p_input->i_handle );
        p_input->b_error = 1;
        return;
    }

943
944
945
946
947
948
949
950
951
952
953
    /* Allow broadcast reception if we bound on INADDR_ANY */
    if( psz_bind == NULL )
    {
        i_opt = 1;
        if( setsockopt( p_input->i_handle, SOL_SOCKET, SO_BROADCAST,
                        (void*) &i_opt, sizeof( i_opt ) ) == -1 )
        {
            intf_WarnMsg( 1, "input warning: can't configure socket (SO_BROADCAST: %s)", 
                             strerror(errno));
        }
    }
954

955
956
    /* Join the multicast group if the socket is a multicast address */
#ifndef IN_MULTICAST
957
958
959
960
#   define IN_MULTICAST(a)         IN_CLASSD(a)
#endif

    if( IN_MULTICAST( ntohl(sock.sin_addr.s_addr) ) )
961
962
963
    {
        struct ip_mreq imr;

964
965
        imr.imr_interface.s_addr = INADDR_ANY;
        imr.imr_multiaddr.s_addr = sock.sin_addr.s_addr;
966
        if( setsockopt( p_input->i_handle, IPPROTO_IP, IP_ADD_MEMBERSHIP,
967
968
969
970
                        (char*)&imr, sizeof(struct ip_mreq) ) == -1 )
        {
            intf_ErrMsg( "input error: failed to join IP multicast group (%s)",
                         strerror(errno) );
971
            close( p_input->i_handle );
972
973
974
975
            p_input->b_error = 1;
            return;
        }
    }
976
    
977
    if( psz_server != NULL )
Henri Fallon's avatar
   
Henri Fallon committed
978
    {
979
980
981
982