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