input.c 15.6 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, 1999, 2000 VideoLAN
Jean-Marc Dressler's avatar
   
Jean-Marc Dressler committed
7
 * $Id: input.c,v 1.83 2001/02/18 03:32:02 polux 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
#include "defs.h"

31
32
33
34
35
36
37
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
Michel Kaempf's avatar
Michel Kaempf committed
38

39
40
41
42
#ifdef STATS
#   include <sys/times.h>
#endif

Michel Kaempf's avatar
Michel Kaempf committed
43
#include "config.h"
44
45
#include "common.h"
#include "threads.h"
Michel Kaempf's avatar
Michel Kaempf committed
46
#include "mtime.h"
Sam Hocevar's avatar
   
Sam Hocevar committed
47
#include "modules.h"
48

49
#include "intf_msg.h"
Sam Hocevar's avatar
   
Sam Hocevar committed
50
#include "intf_plst.h"
51

52
53
54
#include "stream_control.h"
#include "input_ext-intf.h"
#include "input_ext-dec.h"
Michel Lespinasse's avatar
Yop,    
Michel Lespinasse committed
55

56
#include "input.h"
Sam Hocevar's avatar
   
Sam Hocevar committed
57
58
59
#include "interface.h"

#include "main.h"
Michel Kaempf's avatar
Michel Kaempf committed
60

61
/*****************************************************************************
Michel Kaempf's avatar
Michel Kaempf committed
62
 * Local prototypes
63
 *****************************************************************************/
Sam Hocevar's avatar
   
Sam Hocevar committed
64
65
66
67
68
static void RunThread       ( input_thread_t *p_input );
static  int InitThread      ( input_thread_t *p_input );
static void ErrorThread     ( input_thread_t *p_input );
static void DestroyThread   ( input_thread_t *p_input );
static void EndThread       ( input_thread_t *p_input );
Michel Kaempf's avatar
Michel Kaempf committed
69

70
/*****************************************************************************
71
 * input_CreateThread: creates a new input thread
72
 *****************************************************************************
73
74
75
76
 * 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.
77
 *****************************************************************************/
Sam Hocevar's avatar
   
Sam Hocevar committed
78
input_thread_t *input_CreateThread ( playlist_item_t *p_item, int *pi_status )
Michel Kaempf's avatar
Michel Kaempf committed
79
{
80
81
82
    input_thread_t *    p_input;                        /* thread descriptor */
    int                 i_status;                           /* thread status */

83
84
85
    /* Allocate descriptor */
    p_input = (input_thread_t *)malloc( sizeof(input_thread_t) );
    if( p_input == NULL )
Michel Kaempf's avatar
Michel Kaempf committed
86
    {
Sam Hocevar's avatar
   
Sam Hocevar committed
87
88
        intf_ErrMsg( "input error: can't allocate input thread (%s)",
                     strerror(errno) );
Michel Kaempf's avatar
Michel Kaempf committed
89
90
        return( NULL );
    }
91
92
93
94

    /* Initialize thread properties */
    p_input->b_die              = 0;
    p_input->b_error            = 0;
Sam Hocevar's avatar
   
Sam Hocevar committed
95
96
97
98
99
    p_input->b_eof              = 0;

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

100
    /* I have never understood that stuff --Meuuh */
101
102
    p_input->pi_status          = (pi_status != NULL) ? pi_status : &i_status;
    *p_input->pi_status         = THREAD_CREATE;
Michel Kaempf's avatar
Michel Kaempf committed
103

104
    /* Initialize stream description */
105
106
    p_input->stream.i_es_number = 0;
    p_input->stream.i_selected_es_number = 0;
107
    p_input->stream.i_pgrm_number = 0;
108
    p_input->stream.i_new_status = p_input->stream.i_new_rate = 0;
109
    p_input->stream.i_seek = NO_SEEK;
Michel Kaempf's avatar
Michel Kaempf committed
110

111
112
113
114
115
    /* 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;
    p_input->stream.control.b_bw = 0;
Michel Kaempf's avatar
Michel Kaempf committed
116

Sam Hocevar's avatar
   
Sam Hocevar committed
117
118
    /* Initialize default settings for spawned decoders */
    p_input->p_default_aout = p_main->p_aout;
Sam Hocevar's avatar
   
Sam Hocevar committed
119
    p_input->p_default_vout = p_main->p_vout;
Sam Hocevar's avatar
   
Sam Hocevar committed
120

Michel Kaempf's avatar
Michel Kaempf committed
121
    /* Create thread and set locks. */
122
    vlc_mutex_init( &p_input->stream.stream_lock );
123
    vlc_cond_init( &p_input->stream.stream_wait );
124
125
126
    vlc_mutex_init( &p_input->stream.control.control_lock );
    if( vlc_thread_create( &p_input->thread_id, "input", (void *) RunThread,
                           (void *) p_input ) )
Michel Kaempf's avatar
Michel Kaempf committed
127
    {
Sam Hocevar's avatar
   
Sam Hocevar committed
128
129
        intf_ErrMsg( "input error: can't create input thread (%s)",
                     strerror(errno) );
Michel Kaempf's avatar
Michel Kaempf committed
130
131
132
        free( p_input );
        return( NULL );
    }
133

134
135
136
137
    /* If status is NULL, wait until the thread is created */
    if( pi_status == NULL )
    {
        do
138
        {
139
            msleep( THREAD_SLEEP );
140
        } while( (i_status != THREAD_READY) && (i_status != THREAD_ERROR)
Sam Hocevar's avatar
   
Sam Hocevar committed
141
                && (i_status != THREAD_FATAL) );
142
143
        if( i_status != THREAD_READY )
        {
144
145
            return( NULL );
        }
146
    }
Michel Kaempf's avatar
Michel Kaempf committed
147
148
149
    return( p_input );
}

150
/*****************************************************************************
Michel Kaempf's avatar
Michel Kaempf committed
151
 * input_DestroyThread: mark an input thread as zombie
152
 *****************************************************************************
Michel Kaempf's avatar
Michel Kaempf committed
153
 * This function should not return until the thread is effectively cancelled.
154
 *****************************************************************************/
155
void input_DestroyThread( input_thread_t *p_input, int *pi_status )
Michel Kaempf's avatar
Michel Kaempf committed
156
{
157
    int         i_status;                                   /* thread status */
158
159
160

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

163
164
    /* Request thread destruction */
    p_input->b_die = 1;
Michel Kaempf's avatar
Michel Kaempf committed
165

166
167
168
169
170
    /* Make the thread exit of an eventual vlc_cond_wait() */
    vlc_mutex_lock( &p_input->stream.stream_lock );
    vlc_cond_signal( &p_input->stream.stream_wait );
    vlc_mutex_unlock( &p_input->stream.stream_lock );

171
172
173
174
175
176
    /* If status is NULL, wait until thread has been destroyed */
    if( pi_status == NULL )
    {
        do
        {
            msleep( THREAD_SLEEP );
177
178
        } while ( (i_status != THREAD_OVER) && (i_status != THREAD_ERROR)
                  && (i_status != THREAD_FATAL) );
179
    }
Michel Kaempf's avatar
Michel Kaempf committed
180
181
}

182
/*****************************************************************************
183
 * RunThread: main thread loop
184
 *****************************************************************************
185
 * Thread in charge of processing the network packets and demultiplexing.
186
 *****************************************************************************/
187
static void RunThread( input_thread_t *p_input )
Michel Kaempf's avatar
Michel Kaempf committed
188
{
189
190
    data_packet_t *         pp_packets[INPUT_READ_ONCE];
    int                     i_error, i;
Michel Kaempf's avatar
Michel Kaempf committed
191

Sam Hocevar's avatar
   
Sam Hocevar committed
192
193
194
195
196
197
198
199
200
201
    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
202

Sam Hocevar's avatar
   
Sam Hocevar committed
203
    while( !p_input->b_die && !p_input->b_error && !p_input->b_eof )
204
205
    {

Sam Hocevar's avatar
   
Sam Hocevar committed
206
#ifdef STATS
Sam Hocevar's avatar
   
Sam Hocevar committed
207
        p_input->c_loops++;
Sam Hocevar's avatar
   
Sam Hocevar committed
208
209
#endif

210
        vlc_mutex_lock( &p_input->stream.stream_lock );
211
        if( p_input->stream.i_seek != NO_SEEK )
Sam Hocevar's avatar
   
Sam Hocevar committed
212
        {
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
            if( p_input->stream.b_seekable && p_input->pf_seek != NULL )
            {
                p_input->pf_seek( p_input, p_input->stream.i_seek );

                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;
                }
            }
229
            p_input->stream.i_seek = NO_SEEK;
Sam Hocevar's avatar
   
Sam Hocevar committed
230
        }
231
        vlc_mutex_unlock( &p_input->stream.stream_lock );
Sam Hocevar's avatar
   
Sam Hocevar committed
232
233

        i_error = p_input->pf_read( p_input, pp_packets );
Sam Hocevar's avatar
   
Sam Hocevar committed
234

Sam Hocevar's avatar
   
Sam Hocevar committed
235
236
237
238
239
        /* Demultiplex read packets. */
        for( i = 0; i < INPUT_READ_ONCE && pp_packets[i] != NULL; i++ )
        {
            p_input->pf_demux( p_input, pp_packets[i] );
        }
Sam Hocevar's avatar
   
Sam Hocevar committed
240

Sam Hocevar's avatar
   
Sam Hocevar committed
241
242
243
        if( i_error )
        {
            if( i_error == 1 )
Sam Hocevar's avatar
Sam Hocevar committed
244
            {
Sam Hocevar's avatar
   
Sam Hocevar committed
245
246
247
248
                /* End of file - we do not set b_die because only the
                 * interface is allowed to do so. */
                intf_WarnMsg( 1, "End of file reached" );
                p_input->b_eof = 1;
Sam Hocevar's avatar
   
Sam Hocevar committed
249
            }
Sam Hocevar's avatar
   
Sam Hocevar committed
250
            else
Sam Hocevar's avatar
   
Sam Hocevar committed
251
            {
Sam Hocevar's avatar
   
Sam Hocevar committed
252
                p_input->b_error = 1;
Sam Hocevar's avatar
Sam Hocevar committed
253
            }
254
255
256
        }
    }

Sam Hocevar's avatar
   
Sam Hocevar committed
257
    if( p_input->b_error || p_input->b_eof )
258
259
260
    {
        ErrorThread( p_input );
    }
261

262
    EndThread( p_input );
Sam Hocevar's avatar
   
Sam Hocevar committed
263
264
265

    DestroyThread( p_input );

266
    intf_DbgMsg("Thread end");
267
268
}

269
/*****************************************************************************
Sam Hocevar's avatar
   
Sam Hocevar committed
270
 * InitThread: init the input Thread
271
 *****************************************************************************/
Sam Hocevar's avatar
   
Sam Hocevar committed
272
static int InitThread( input_thread_t * p_input )
Michel Kaempf's avatar
Michel Kaempf committed
273
274
275
{

#ifdef STATS
276
277
278
279
280
281
    /* Initialize statistics */
    p_input->c_loops                    = 0;
    p_input->c_bytes                    = 0;
    p_input->c_payload_bytes            = 0;
    p_input->c_packets_read             = 0;
    p_input->c_packets_trashed          = 0;
Michel Kaempf's avatar
Michel Kaempf committed
282
#endif
Sam Hocevar's avatar
Sam Hocevar committed
283

Sam Hocevar's avatar
   
Sam Hocevar committed
284
    p_input->p_input_module = module_Need( p_main->p_bank,
Sam Hocevar's avatar
   
Sam Hocevar committed
285
286
                                           MODULE_CAPABILITY_INPUT,
                                           (probedata_t *)p_input );
Sam Hocevar's avatar
   
Sam Hocevar committed
287
288

    if( p_input->p_input_module == NULL )
289
    {
Sam Hocevar's avatar
   
Sam Hocevar committed
290
        intf_ErrMsg( "input error: no suitable input module" );
Sam Hocevar's avatar
   
Sam Hocevar committed
291
292
        module_Unneed( p_main->p_bank, p_input->p_input_module );
        return( -1 );
Michel Kaempf's avatar
Michel Kaempf committed
293
    }
294

Sam Hocevar's avatar
   
Sam Hocevar committed
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
#define f p_input->p_input_module->p_functions->input.functions.input
    p_input->pf_init          = f.pf_init;
    p_input->pf_open          = f.pf_open;
    p_input->pf_close         = f.pf_close;
    p_input->pf_end           = f.pf_end;
    p_input->pf_read          = f.pf_read;
    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;
#undef f

    p_input->pf_open( p_input );
311

Sam Hocevar's avatar
   
Sam Hocevar committed
312
    if( p_input->b_error )
313
    {
Sam Hocevar's avatar
   
Sam Hocevar committed
314
315
        /* We barfed -- exit nicely */
        p_input->pf_close( p_input );
Sam Hocevar's avatar
   
Sam Hocevar committed
316
        module_Unneed( p_main->p_bank, p_input->p_input_module );
Sam Hocevar's avatar
   
Sam Hocevar committed
317
        return( -1 );
318
    }
Sam Hocevar's avatar
   
Sam Hocevar committed
319
320

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

Sam Hocevar's avatar
   
Sam Hocevar committed
322
    *p_input->pi_status = THREAD_READY;
Sam Hocevar's avatar
   
Sam Hocevar committed
323
324

    return( 0 );
Michel Kaempf's avatar
Michel Kaempf committed
325
326
}

327
/*****************************************************************************
328
 * ErrorThread: RunThread() error loop
329
 *****************************************************************************
330
 * This function is called when an error occured during thread main's loop.
331
 *****************************************************************************/
332
static void ErrorThread( input_thread_t *p_input )
Michel Kaempf's avatar
Michel Kaempf committed
333
{
334
    while( !p_input->b_die )
Michel Kaempf's avatar
Michel Kaempf committed
335
    {
336
337
        /* Sleep a while */
        msleep( INPUT_IDLE_SLEEP );
Michel Kaempf's avatar
Michel Kaempf committed
338
339
340
    }
}

341
/*****************************************************************************
342
 * EndThread: end the input thread
343
 *****************************************************************************/
344
static void EndThread( input_thread_t * p_input )
345
{
346
    int *       pi_status;                                  /* thread status */
347

348
349
350
    /* Store status */
    pi_status = p_input->pi_status;
    *pi_status = THREAD_END;
Sam Hocevar's avatar
Sam Hocevar committed
351

Sam Hocevar's avatar
   
Sam Hocevar committed
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
#ifdef STATS
    {
        struct tms cpu_usage;
        times( &cpu_usage );

        intf_Msg("input stats: cpu usage (user: %d, system: %d)",
                 cpu_usage.tms_utime, cpu_usage.tms_stime);
    }
#endif

    /* 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
368
369
370
    /* Close stream */
    p_input->pf_close( p_input );

Sam Hocevar's avatar
   
Sam Hocevar committed
371
    /* Release modules */
Sam Hocevar's avatar
   
Sam Hocevar committed
372
    module_Unneed( p_main->p_bank, p_input->p_input_module );
Sam Hocevar's avatar
   
Sam Hocevar committed
373

Sam Hocevar's avatar
   
Sam Hocevar committed
374
375
376
377
378
379
380
381
382
383
384
}

/*****************************************************************************
 * 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
385

Henri Fallon's avatar
   
Henri Fallon committed
386
387
    /* Destroy Mutex locks */
    vlc_mutex_destroy( &p_input->stream.control.control_lock );
Henri Fallon's avatar
   
Henri Fallon committed
388
    vlc_mutex_destroy( &p_input->stream.stream_lock );
Henri Fallon's avatar
   
Henri Fallon committed
389
    
390
    /* Free input structure */
391
    free( p_input );
392

393
394
    /* Update status */
    *pi_status = THREAD_OVER;
Sam Hocevar's avatar
Sam Hocevar committed
395
}
396

Sam Hocevar's avatar
Sam Hocevar committed
397
/*****************************************************************************
Sam Hocevar's avatar
   
Sam Hocevar committed
398
 * input_FileOpen : open a file descriptor
Sam Hocevar's avatar
Sam Hocevar committed
399
 *****************************************************************************/
Sam Hocevar's avatar
   
Sam Hocevar committed
400
void input_FileOpen( input_thread_t * p_input )
Michel Kaempf's avatar
Michel Kaempf committed
401
{
402
    struct stat         stat_info;
Sam Hocevar's avatar
   
Sam Hocevar committed
403
404
405
    int                 i_stat;

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

Sam Hocevar's avatar
   
Sam Hocevar committed
407
408
409
    /* FIXME: this code ought to be in the plugin so that code can
     * be shared with the *_Probe function */
    if( ( i_stat = stat( psz_name, &stat_info ) ) == (-1) )
Sam Hocevar's avatar
   
Sam Hocevar committed
410
    {
Sam Hocevar's avatar
   
Sam Hocevar committed
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
        int i_size = strlen( psz_name );

        if( ( i_size > 4 )
            && !strncasecmp( psz_name, "dvd:", 4 ) )
        {
            /* get rid of the 'dvd:' stuff and try again */
            psz_name += 4;
            i_stat = stat( psz_name, &stat_info );
	}
	else if( ( i_size > 5 )
                 && !strncasecmp( psz_name, "file:", 5 ) )
        {
            /* get rid of the 'file:' stuff and try again */
            psz_name += 5;
            i_stat = stat( psz_name, &stat_info );
	}

	if( i_stat == (-1) )
        {
            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
435
436
437
438
439
440
441
442
443
444
445
446
447
    }

    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)
         || S_ISBLK(stat_info.st_mode) )
    {
        p_input->stream.b_seekable = 1;
        p_input->stream.i_size = stat_info.st_size;
    }
Jean-Marc Dressler's avatar
   
Jean-Marc Dressler committed
448
449
450
451
452
    else if( S_ISFIFO(stat_info.st_mode)
#ifndef SYS_BEOS
             || S_ISSOCK(stat_info.st_mode)
#endif
             )
Sam Hocevar's avatar
Sam Hocevar committed
453
    {
454
455
        p_input->stream.b_seekable = 0;
        p_input->stream.i_size = 0;
Benoit Steiner's avatar
   
Benoit Steiner committed
456
457
458
    }
    else
    {
459
        vlc_mutex_unlock( &p_input->stream.stream_lock );
Sam Hocevar's avatar
   
Sam Hocevar committed
460
        intf_ErrMsg( "input error: unknown file type for `%s'",
Sam Hocevar's avatar
   
Sam Hocevar committed
461
                     psz_name );
Sam Hocevar's avatar
   
Sam Hocevar committed
462
463
464
        p_input->b_error = 1;
        return;
    }
465

Sam Hocevar's avatar
   
Sam Hocevar committed
466
467
468
    p_input->stream.i_tell = 0;
    vlc_mutex_unlock( &p_input->stream.stream_lock );

Sam Hocevar's avatar
   
Sam Hocevar committed
469
470
    intf_Msg( "input: opening %s", p_input->p_source );
    if( (p_input->i_handle = open( psz_name,
Sam Hocevar's avatar
   
Sam Hocevar committed
471
472
                                   /*O_NONBLOCK | O_LARGEFILE*/0 )) == (-1) )
    {
Sam Hocevar's avatar
   
Sam Hocevar committed
473
        intf_ErrMsg( "input error: cannot open file (%s)", strerror(errno) );
Sam Hocevar's avatar
   
Sam Hocevar committed
474
475
        p_input->b_error = 1;
        return;
Michel Kaempf's avatar
Michel Kaempf committed
476
477
478
    }

}
Stéphane Borel's avatar
Stéphane Borel committed
479
480

/*****************************************************************************
Sam Hocevar's avatar
   
Sam Hocevar committed
481
 * input_FileClose : close a file descriptor
Stéphane Borel's avatar
Stéphane Borel committed
482
 *****************************************************************************/
Sam Hocevar's avatar
   
Sam Hocevar committed
483
void input_FileClose( input_thread_t * p_input )
Stéphane Borel's avatar
Stéphane Borel committed
484
{
Sam Hocevar's avatar
   
Sam Hocevar committed
485
    close( p_input->i_handle );
Stéphane Borel's avatar
Stéphane Borel committed
486

Sam Hocevar's avatar
   
Sam Hocevar committed
487
    return;
Stéphane Borel's avatar
Stéphane Borel committed
488
}
Sam Hocevar's avatar
   
Sam Hocevar committed
489