]> git.sesse.net Git - vlc/blob - modules/access/vdr.c
access: http: only warn on deflate errors
[vlc] / modules / access / vdr.c
1 /*****************************************************************************
2  * vdr.c: VDR recordings access plugin
3  *****************************************************************************
4  * Copyright (C) 2010 Tobias Güntner
5  *
6  * This program is free software; you can redistribute it and/or modify it
7  * under the terms of the GNU Lesser General Public License as published by
8  * the Free Software Foundation; either version 2.1 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with this program; if not, write to the Free Software Foundation,
18  * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
19  *****************************************************************************/
20
21 /***
22 VDR splits recordings into multiple files and stores each recording in a
23 separate directory. If VLC opens a normal directory, the filesystem module
24 will add all files to the playlist. If, however, VLC opens a VDR recording,
25 this module will join all files within that directory and provide a single
26 continuous stream instead.
27
28 VDR recordings have either of two directory layouts:
29     1) PES format:
30         /path/to/0000-00-00.00.00.00.00.rec/
31             001.vdr, 002.vdr, 003.vdr, ...
32             index.vdr, info.vdr, marks.vdr, ...
33     2) TS format:
34         /path/to/0000-00-00.00.00.0-0.rec/
35             00001.ts, 00002.ts, 00003.ts, ...
36             index, info, marks, ...
37 See http://www.vdr-wiki.de/ and http://www.tvdr.de/ for more information.
38 ***/
39
40 /*****************************************************************************
41  * Preamble
42  *****************************************************************************/
43
44 #ifdef HAVE_CONFIG_H
45 # include "config.h"
46 #endif
47
48 #include <sys/types.h>
49 #include <sys/stat.h>
50 #include <fcntl.h>
51 #ifdef HAVE_UNISTD_H
52 #   include <unistd.h>
53 #elif defined( WIN32 )
54 #   include <io.h>
55 #endif
56
57 #include <ctype.h>
58 #include <time.h>
59 #include <errno.h>
60
61 #include <vlc_common.h>
62 #include <vlc_plugin.h>
63 #include <vlc_access.h>
64 #include <vlc_input.h>
65 #include <vlc_fs.h>
66 #include <vlc_charset.h>
67 #include <vlc_dialog.h>
68 #include <vlc_configuration.h>
69
70 /*****************************************************************************
71  * Module descriptor
72  *****************************************************************************/
73 static int  Open ( vlc_object_t * );
74 static void Close( vlc_object_t * );
75
76 #define HELP_TEXT N_("Support for VDR recordings (http://www.tvdr.de/).")
77
78 #define CHAPTER_OFFSET_TEXT N_("Chapter offset in ms")
79 #define CHAPTER_OFFSET_LONGTEXT N_( \
80     "Move all chapters. This value should be set in milliseconds." )
81
82 #define FPS_TEXT N_("Frame rate")
83 #define FPS_LONGTEXT N_( \
84     "Default frame rate for chapter import." )
85
86 vlc_module_begin ()
87     set_category( CAT_INPUT )
88     set_shortname( N_("VDR") )
89     set_help( HELP_TEXT )
90     set_subcategory( SUBCAT_INPUT_ACCESS )
91     set_description( N_("VDR recordings") )
92     add_integer( "vdr-chapter-offset", 0,
93         CHAPTER_OFFSET_TEXT, CHAPTER_OFFSET_LONGTEXT, true )
94     add_float_with_range( "vdr-fps", 25, 1, 1000,
95         FPS_TEXT, FPS_LONGTEXT, true )
96     set_capability( "access", 60 )
97     add_shortcut( "vdr" )
98     add_shortcut( "directory" )
99     add_shortcut( "dir" )
100     add_shortcut( "file" )
101     set_callbacks( Open, Close )
102 vlc_module_end ()
103
104 /*****************************************************************************
105  * Local prototypes, constants, structures
106  *****************************************************************************/
107
108 /* minimum chapter size in seconds */
109 #define MIN_CHAPTER_SIZE 5
110
111 TYPEDEF_ARRAY( uint64_t, size_array_t );
112
113 struct access_sys_t
114 {
115     /* file sizes of all parts */
116     size_array_t file_sizes;
117
118     /* index and fd of current open file */
119     unsigned i_current_file;
120     int fd;
121
122     /* meta data */
123     vlc_meta_t *p_meta;
124
125     /* cut marks */
126     input_title_t *p_marks;
127     float fps;
128
129     /* file format: true=TS, false=PES */
130     bool b_ts_format;
131 };
132
133 #define CURRENT_FILE_SIZE ARRAY_VAL(p_sys->file_sizes, p_sys->i_current_file)
134 #define FILE_SIZE(pos)    ARRAY_VAL(p_sys->file_sizes, pos)
135 #define FILE_COUNT        (unsigned)p_sys->file_sizes.i_size
136
137 static int Control( access_t *, int, va_list );
138 static ssize_t Read( access_t *p_access, uint8_t *p_buffer, size_t i_len );
139 static int Seek( access_t *p_access, uint64_t i_pos);
140 static void FindSeekpoint( access_t *p_access );
141 static bool ScanDirectory( access_t *p_access );
142 static char *GetFilePath( access_t *p_access, unsigned i_file );
143 static bool ImportNextFile( access_t *p_access );
144 static bool SwitchFile( access_t *p_access, unsigned i_file );
145 static void OptimizeForRead( int fd );
146 static void UpdateFileSize( access_t *p_access );
147 static FILE *OpenRelativeFile( access_t *p_access, const char *psz_file );
148 static bool ReadLine( char **ppsz_line, size_t *pi_size, FILE *p_file );
149 static void ImportMeta( access_t *p_access );
150 static void ImportMarks( access_t *p_access );
151 static bool ReadIndexRecord( FILE *p_file, bool b_ts, int64_t i_frame,
152                             uint64_t *pi_offset, uint16_t *pi_file_num );
153 static int64_t ParseFrameNumber( const char *psz_line, float fps );
154 static const char *BaseName( const char *psz_path );
155
156 /*****************************************************************************
157  * Open a directory
158  *****************************************************************************/
159 static int Open( vlc_object_t *p_this )
160 {
161     access_t *p_access = (access_t*)p_this;
162
163     if( !p_access->psz_filepath )
164         return VLC_EGENERIC;
165
166     /* Some tests can be skipped if this module was explicitly requested.
167      * That way, the user can play "corrupt" recordings if necessary
168      * and we can avoid false positives in the general case. */
169     bool b_strict = strcmp( p_access->psz_access, "vdr" );
170
171     /* Do a quick test based on the directory name to see if this
172      * directory might contain a VDR recording. We can be reasonably
173      * sure if ScanDirectory() actually finds files. */
174     if( b_strict )
175     {
176         char psz_extension[4];
177         int i_length = 0;
178         const char *psz_name = BaseName( p_access->psz_filepath );
179         if( sscanf( psz_name, "%*u-%*u-%*u.%*u.%*u.%*u%*[-.]%*u.%3s%n",
180             psz_extension, &i_length ) != 1 || strcasecmp( psz_extension, "rec" ) ||
181             ( psz_name[i_length] != DIR_SEP_CHAR && psz_name[i_length] != '\0' ) )
182             return VLC_EGENERIC;
183     }
184
185     /* Only directories can be recordings */
186     struct stat st;
187     if( vlc_stat( p_access->psz_filepath, &st ) ||
188         !S_ISDIR( st.st_mode ) )
189         return VLC_EGENERIC;
190
191     access_sys_t *p_sys;
192     STANDARD_READ_ACCESS_INIT;
193     p_sys->fd = -1;
194     p_sys->fps = var_InheritFloat( p_access, "vdr-fps" );
195     ARRAY_INIT( p_sys->file_sizes );
196
197     /* Import all files and prepare playback. */
198     if( !ScanDirectory( p_access ) ||
199         !SwitchFile( p_access, 0 ) )
200     {
201         Close( p_this );
202         return VLC_EGENERIC;
203     }
204
205     free( p_access->psz_demux );
206     p_access->psz_demux = strdup( "ts" );
207     return VLC_SUCCESS;
208 }
209
210 /*****************************************************************************
211  * Close files and free resources
212  *****************************************************************************/
213 static void Close( vlc_object_t * p_this )
214 {
215     access_t *p_access = (access_t*)p_this;
216     access_sys_t *p_sys = p_access->p_sys;
217
218     if( p_sys->fd != -1 )
219         close( p_sys->fd );
220     ARRAY_RESET( p_sys->file_sizes );
221
222     if( p_sys->p_meta )
223         vlc_meta_Delete( p_sys->p_meta );
224
225     vlc_input_title_Delete( p_sys->p_marks );
226     free( p_sys );
227 }
228
229 /*****************************************************************************
230  * Determine format and import files
231  *****************************************************************************/
232 static bool ScanDirectory( access_t *p_access )
233 {
234     access_sys_t *p_sys = p_access->p_sys;
235
236     /* find first part and determine directory format */
237     p_sys->b_ts_format = true;
238     if( !ImportNextFile( p_access ) )
239     {
240         p_sys->b_ts_format = !p_sys->b_ts_format;
241         if( !ImportNextFile( p_access ) )
242             return false;
243     }
244
245     /* get all remaining parts */
246     while( ImportNextFile( p_access ) )
247         continue;
248
249     /* import meta data etc. */
250     ImportMeta( p_access );
251
252     /* cut marks depend on meta data and file sizes */
253     ImportMarks( p_access );
254
255     return true;
256 }
257
258 /*****************************************************************************
259  * Control input stream
260  *****************************************************************************/
261 static int Control( access_t *p_access, int i_query, va_list args )
262 {
263     access_sys_t *p_sys = p_access->p_sys;
264     input_title_t ***ppp_title;
265     int i;
266     int64_t *pi64;
267     vlc_meta_t *p_meta;
268
269     switch( i_query )
270     {
271         case ACCESS_CAN_SEEK:
272         case ACCESS_CAN_FASTSEEK:
273         case ACCESS_CAN_PAUSE:
274         case ACCESS_CAN_CONTROL_PACE:
275             *va_arg( args, bool* ) = true;
276             break;
277
278         case ACCESS_GET_PTS_DELAY:
279             pi64 = va_arg( args, int64_t * );
280             *pi64 = INT64_C(1000)
281                   * var_InheritInteger( p_access, "file-caching" );
282             break;
283
284         case ACCESS_SET_PAUSE_STATE:
285             /* nothing to do */
286             break;
287
288         case ACCESS_GET_TITLE_INFO:
289             /* return a copy of our seek points */
290             if( !p_sys->p_marks )
291                 return VLC_EGENERIC;
292             ppp_title = va_arg( args, input_title_t*** );
293             *va_arg( args, int* ) = 1;
294             *ppp_title = malloc( sizeof( input_title_t** ) );
295             if( !*ppp_title )
296                 return VLC_ENOMEM;
297             **ppp_title = vlc_input_title_Duplicate( p_sys->p_marks );
298             break;
299
300         case ACCESS_SET_TITLE:
301             /* ignore - only one title */
302             break;
303
304         case ACCESS_SET_SEEKPOINT:
305             i = va_arg( args, int );
306             /* Seek updates p_access->info */
307             return Seek( p_access, p_sys->p_marks->seekpoint[i]->i_byte_offset );
308
309         case ACCESS_GET_META:
310             if( !p_sys->p_meta )
311                 return VLC_EGENERIC;
312             p_meta = va_arg( args, vlc_meta_t* );
313             vlc_meta_Merge( p_meta, p_sys->p_meta );
314             break;
315
316         case ACCESS_SET_PRIVATE_ID_STATE:
317         case ACCESS_GET_CONTENT_TYPE:
318             return VLC_EGENERIC;
319
320         default:
321             msg_Warn( p_access, "unimplemented query in control" );
322             return VLC_EGENERIC;
323     }
324     return VLC_SUCCESS;
325 }
326
327 /*****************************************************************************
328  * Read and concatenate files
329  *****************************************************************************/
330 static ssize_t Read( access_t *p_access, uint8_t *p_buffer, size_t i_len )
331 {
332     access_sys_t *p_sys = p_access->p_sys;
333
334     if( p_sys->fd == -1 )
335     {
336         /* no more data */
337         p_access->info.b_eof = true;
338         return 0;
339     }
340
341     ssize_t i_ret = read( p_sys->fd, p_buffer, i_len );
342
343     if( i_ret > 0 )
344     {
345         /* success */
346         p_access->info.i_pos += i_ret;
347         UpdateFileSize( p_access );
348         FindSeekpoint( p_access );
349         return i_ret;
350     }
351     else if( i_ret == 0 )
352     {
353         /* check for new files in case the recording is still active */
354         if( p_sys->i_current_file >= FILE_COUNT - 1 )
355             ImportNextFile( p_access );
356         /* play next file */
357         SwitchFile( p_access, p_sys->i_current_file + 1 );
358         return -1;
359     }
360     else if( errno == EINTR )
361     {
362         /* try again later */
363         return -1;
364     }
365     else
366     {
367         /* abort on read error */
368         msg_Err( p_access, "failed to read (%m)" );
369         dialog_Fatal( p_access, _("File reading failed"), "%s (%m)",
370                       _("VLC could not read the file.") );
371         SwitchFile( p_access, -1 );
372         return 0;
373     }
374 }
375
376 /*****************************************************************************
377  * Seek to a specific location in a file
378  *****************************************************************************/
379 static int Seek( access_t *p_access, uint64_t i_pos )
380 {
381     access_sys_t *p_sys = p_access->p_sys;
382
383     /* might happen if called by ACCESS_SET_SEEKPOINT */
384     i_pos = __MIN( i_pos, p_access->info.i_size );
385
386     p_access->info.i_pos = i_pos;
387     p_access->info.b_eof = false;
388
389     /* find correct chapter */
390     FindSeekpoint( p_access );
391
392     /* find correct file */
393     unsigned i_file = 0;
394     while( i_file < FILE_COUNT - 1 &&
395            i_pos >= FILE_SIZE( i_file ) )
396     {
397         i_pos -= FILE_SIZE( i_file );
398         i_file++;
399     }
400     if( !SwitchFile( p_access, i_file ) )
401         return VLC_EGENERIC;
402
403     /* adjust position within that file */
404     return lseek( p_sys->fd, i_pos, SEEK_SET ) != -1 ?
405         VLC_SUCCESS : VLC_EGENERIC;
406 }
407
408 /*****************************************************************************
409  * Change the chapter index to match the current position
410  *****************************************************************************/
411 static void FindSeekpoint( access_t *p_access )
412 {
413     access_sys_t *p_sys = p_access->p_sys;
414     if( !p_sys->p_marks )
415         return;
416
417     int i_new_seekpoint = p_access->info.i_seekpoint;
418     if( p_access->info.i_pos < (uint64_t)p_sys->p_marks->
419         seekpoint[ p_access->info.i_seekpoint ]->i_byte_offset )
420     {
421         /* i_pos moved backwards, start fresh */
422         i_new_seekpoint = 0;
423     }
424
425     /* only need to check the following seekpoints */
426     while( i_new_seekpoint + 1 < p_sys->p_marks->i_seekpoint &&
427         p_access->info.i_pos >= (uint64_t)p_sys->p_marks->
428         seekpoint[ i_new_seekpoint + 1 ]->i_byte_offset )
429     {
430         i_new_seekpoint++;
431     }
432
433     /* avoid unnecessary events */
434     if( p_access->info.i_seekpoint != i_new_seekpoint )
435     {
436         p_access->info.i_seekpoint = i_new_seekpoint;
437         p_access->info.i_update |= INPUT_UPDATE_SEEKPOINT;
438     }
439 }
440
441 /*****************************************************************************
442  * Returns the path of a certain part
443  *****************************************************************************/
444 static char *GetFilePath( access_t *p_access, unsigned i_file )
445 {
446     char *psz_path;
447     if( asprintf( &psz_path, p_access->p_sys->b_ts_format ?
448         "%s" DIR_SEP "%05u.ts" : "%s" DIR_SEP "%03u.vdr",
449         p_access->psz_filepath, i_file + 1 ) == -1 )
450         return NULL;
451     else
452         return psz_path;
453 }
454
455 /*****************************************************************************
456  * Check if another part exists and import it
457  *****************************************************************************/
458 static bool ImportNextFile( access_t *p_access )
459 {
460     access_sys_t *p_sys = p_access->p_sys;
461
462     char *psz_path = GetFilePath( p_access, FILE_COUNT );
463     if( !psz_path )
464         return false;
465
466     struct stat st;
467     if( vlc_stat( psz_path, &st ) )
468     {
469         msg_Dbg( p_access, "could not stat %s: %m", psz_path );
470         free( psz_path );
471         return false;
472     }
473     if( !S_ISREG( st.st_mode ) )
474     {
475         msg_Dbg( p_access, "%s is not a regular file", psz_path );
476         free( psz_path );
477         return false;
478     }
479     msg_Dbg( p_access, "%s exists", psz_path );
480     free( psz_path );
481
482     ARRAY_APPEND( p_sys->file_sizes, st.st_size );
483     p_access->info.i_size += st.st_size;
484
485     return true;
486 }
487
488 /*****************************************************************************
489  * Close the current file and open another
490  *****************************************************************************/
491 static bool SwitchFile( access_t *p_access, unsigned i_file )
492 {
493     access_sys_t *p_sys = p_access->p_sys;
494
495     /* requested file already open? */
496     if( p_sys->fd != -1 && p_sys->i_current_file == i_file )
497         return true;
498
499     /* close old file */
500     if( p_sys->fd != -1 )
501     {
502         close( p_sys->fd );
503         p_sys->fd = -1;
504     }
505
506     /* switch */
507     if( i_file >= FILE_COUNT )
508         return false;
509     p_sys->i_current_file = i_file;
510
511     /* open new file */
512     char *psz_path = GetFilePath( p_access, i_file );
513     if( !psz_path )
514         return false;
515     p_sys->fd = vlc_open( psz_path, O_RDONLY );
516
517     if( p_sys->fd == -1 )
518     {
519         msg_Err( p_access, "Failed to open %s: %m", psz_path );
520         goto error;
521     }
522
523     /* cannot handle anything except normal files */
524     struct stat st;
525     if( fstat( p_sys->fd, &st ) || !S_ISREG( st.st_mode ) )
526     {
527         msg_Err( p_access, "%s is not a regular file", psz_path );
528         goto error;
529     }
530
531     OptimizeForRead( p_sys->fd );
532
533     msg_Dbg( p_access, "opened %s", psz_path );
534     free( psz_path );
535     return true;
536
537 error:
538     dialog_Fatal (p_access, _("File reading failed"), _("VLC could not"
539         " open the file \"%s\". (%m)"), psz_path);
540     if( p_sys->fd != -1 )
541     {
542         close( p_sys->fd );
543         p_sys->fd = -1;
544     }
545     free( psz_path );
546     return false;
547 }
548
549 /*****************************************************************************
550  * Some tweaks to speed up read()
551  *****************************************************************************/
552 static void OptimizeForRead( int fd )
553 {
554     /* cf. Open() in file access module */
555     VLC_UNUSED(fd);
556 #ifdef HAVE_POSIX_FADVISE
557     posix_fadvise( fd, 0, 4096, POSIX_FADV_WILLNEED );
558     posix_fadvise( fd, 0, 0, POSIX_FADV_NOREUSE );
559 #endif
560 #ifdef F_RDAHEAD
561     fcntl( fd, F_RDAHEAD, 1 );
562 #endif
563 #ifdef F_NOCACHE
564     fcntl( fd, F_NOCACHE, 0 );
565 #endif
566 }
567
568 /*****************************************************************************
569  * Fix size if the (last) part is still growing
570  *****************************************************************************/
571 static void UpdateFileSize( access_t *p_access )
572 {
573     access_sys_t *p_sys = p_access->p_sys;
574     struct stat st;
575
576     if( p_access->info.i_size >= p_access->info.i_pos )
577         return;
578
579     /* TODO: not sure if this can happen or what to do in this case */
580     if( fstat( p_sys->fd, &st ) )
581         return;
582     if( (uint64_t)st.st_size <= CURRENT_FILE_SIZE )
583         return;
584
585     p_access->info.i_size -= CURRENT_FILE_SIZE;
586     CURRENT_FILE_SIZE = st.st_size;
587     p_access->info.i_size += CURRENT_FILE_SIZE;
588 }
589
590 /*****************************************************************************
591  * Open file relative to base directory for reading.
592  *****************************************************************************/
593 static FILE *OpenRelativeFile( access_t *p_access, const char *psz_file )
594 {
595     /* build path and add extension */
596     char *psz_path;
597     if( asprintf( &psz_path, "%s" DIR_SEP "%s%s",
598         p_access->psz_filepath, psz_file,
599         p_access->p_sys->b_ts_format ? "" : ".vdr" ) == -1 )
600         return NULL;
601
602     FILE *file = vlc_fopen( psz_path, "rb" );
603     if( !file )
604         msg_Warn( p_access, "Failed to open %s: %m", psz_path );
605     free( psz_path );
606
607     return file;
608 }
609
610 /*****************************************************************************
611  * Read a line of text. Returns false on error or EOF.
612  *****************************************************************************/
613 static bool ReadLine( char **ppsz_line, size_t *pi_size, FILE *p_file )
614 {
615     ssize_t read = getline( ppsz_line, pi_size, p_file );
616
617     if( read == -1 )
618     {
619         /* automatically free buffer on eof */
620         free( *ppsz_line );
621         *ppsz_line = NULL;
622         return false;
623     }
624
625     if( read > 0 && (*ppsz_line)[ read - 1 ] == '\n' )
626         (*ppsz_line)[ read - 1 ] = '\0';
627     EnsureUTF8( *ppsz_line );
628
629     return true;
630 }
631
632 /*****************************************************************************
633  * Import meta data
634  *****************************************************************************/
635 static void ImportMeta( access_t *p_access )
636 {
637     access_sys_t *p_sys = p_access->p_sys;
638
639     FILE *infofile = OpenRelativeFile( p_access, "info" );
640     if( !infofile )
641         return;
642
643     vlc_meta_t *p_meta = vlc_meta_New();
644     p_sys->p_meta = p_meta;
645     if( !p_meta )
646     {
647         fclose( infofile );
648         return;
649     }
650
651     char *line = NULL;
652     size_t line_len;
653     char *psz_title = NULL, *psz_smalltext = NULL, *psz_date = NULL;
654
655     while( ReadLine( &line, &line_len, infofile ) )
656     {
657         if( !isalpha( (unsigned char)line[0] ) || line[1] != ' ' )
658             continue;
659
660         char tag = line[0];
661         char *text = line + 2;
662
663         if( tag == 'C' )
664         {
665             char *psz_name = strchr( text, ' ' );
666             if( psz_name )
667             {
668                 *psz_name = '\0';
669                 vlc_meta_AddExtra( p_meta, "Channel", psz_name + 1 );
670             }
671             vlc_meta_AddExtra( p_meta, "Transponder", text );
672         }
673
674         else if( tag == 'E' )
675         {
676             unsigned i_id, i_start, i_length;
677             if( sscanf( text, "%u %u %u", &i_id, &i_start, &i_length ) == 3 )
678             {
679                 char str[50];
680                 struct tm tm;
681                 time_t start = i_start;
682                 localtime_r( &start, &tm );
683
684                 /* TODO: locale */
685                 strftime( str, sizeof(str), "%Y-%m-%d %H:%M", &tm );
686                 vlc_meta_AddExtra( p_meta, "Date", str );
687                 free( psz_date );
688                 psz_date = strdup( str );
689
690                 /* display in minutes */
691                 i_length = ( i_length + 59 ) / 60;
692                 snprintf( str, sizeof(str), "%u:%02u", i_length / 60, i_length % 60 );
693                 vlc_meta_AddExtra( p_meta, "Duration", str );
694             }
695         }
696
697         else if( tag == 'T' )
698         {
699             free( psz_title );
700             psz_title = strdup( text );
701             vlc_meta_AddExtra( p_meta, "Title", text );
702         }
703
704         else if( tag == 'S' )
705         {
706             free( psz_smalltext );
707             psz_smalltext = strdup( text );
708             vlc_meta_AddExtra( p_meta, "Info", text );
709         }
710
711         else if( tag == 'D' )
712         {
713             for( char *p = text; *p; ++p )
714             {
715                 if( *p == '|' )
716                     *p = '\n';
717             }
718             vlc_meta_SetDescription( p_meta, text );
719         }
720
721         /* FPS are required to convert between timestamps and frames */
722         else if( tag == 'F' )
723         {
724             float fps = atof( text );
725             if( fps >= 1 )
726                 p_sys->fps = fps;
727             vlc_meta_AddExtra( p_meta, "Frame Rate", text );
728         }
729
730         else if( tag == 'P' )
731         {
732             vlc_meta_AddExtra( p_meta, "Priority", text );
733         }
734
735         else if( tag == 'L' )
736         {
737             vlc_meta_AddExtra( p_meta, "Lifetime", text );
738         }
739     }
740
741     /* create a meaningful title */
742     int i_len = 10 +
743         ( psz_title ? strlen( psz_title ) : 0 ) +
744         ( psz_smalltext ? strlen( psz_smalltext ) : 0 ) +
745         ( psz_date ? strlen( psz_date ) : 0 );
746     char *psz_display = malloc( i_len );
747
748     if( psz_display )
749     {
750         *psz_display = '\0';
751         if( psz_title )
752             strcat( psz_display, psz_title );
753         if( psz_title && psz_smalltext )
754             strcat( psz_display, " - " );
755         if( psz_smalltext )
756             strcat( psz_display, psz_smalltext );
757         if( ( psz_title || psz_smalltext ) && psz_date )
758         {
759             strcat( psz_display, " (" );
760             strcat( psz_display, psz_date );
761             strcat( psz_display, ")" );
762         }
763         if( *psz_display )
764             vlc_meta_SetTitle( p_meta, psz_display );
765     }
766
767     free( psz_display );
768     free( psz_title );
769     free( psz_smalltext );
770     free( psz_date );
771
772     fclose( infofile );
773 }
774
775 /*****************************************************************************
776  * Import cut marks and convert them to seekpoints (chapters).
777  *****************************************************************************/
778 static void ImportMarks( access_t *p_access )
779 {
780     access_sys_t *p_sys = p_access->p_sys;
781
782     FILE *marksfile = OpenRelativeFile( p_access, "marks" );
783     if( !marksfile )
784         return;
785
786     FILE *indexfile = OpenRelativeFile( p_access, "index" );
787     if( !indexfile )
788     {
789         fclose( marksfile );
790         return;
791     }
792
793     /* get the length of this recording (index stores 8 bytes per frame) */
794     struct stat st;
795     if( fstat( fileno( indexfile ), &st ) )
796     {
797         fclose( marksfile );
798         fclose( indexfile );
799         return;
800     }
801     int64_t i_frame_count = st.st_size / 8;
802
803     /* Put all cut marks in a "dummy" title */
804     input_title_t *p_marks = vlc_input_title_New();
805     if( !p_marks )
806     {
807         fclose( marksfile );
808         fclose( indexfile );
809         return;
810     }
811     p_marks->psz_name = strdup( _("VDR Cut Marks") );
812     p_marks->i_length = i_frame_count * (int64_t)( CLOCK_FREQ / p_sys->fps );
813     p_marks->i_size = p_access->info.i_size;
814
815     /* offset for chapter positions */
816     int i_chapter_offset = p_sys->fps / 1000 *
817         var_InheritInteger( p_access, "vdr-chapter-offset" );
818
819     /* minimum chapter size in frames */
820     int i_min_chapter_size = p_sys->fps * MIN_CHAPTER_SIZE;
821
822     /* the last chapter started at this frame (init to 0 so
823      * we skip useless chapters near the beginning as well) */
824     int64_t i_prev_chapter = 0;
825
826     /* parse lines of the form "0:00:00.00 foobar" */
827     char *line = NULL;
828     size_t line_len;
829     while( ReadLine( &line, &line_len, marksfile ) )
830     {
831         int64_t i_frame = ParseFrameNumber( line, p_sys->fps );
832
833         /* skip chapters which are near the end or too close to each other */
834         if( i_frame - i_prev_chapter < i_min_chapter_size ||
835             i_frame >= i_frame_count - i_min_chapter_size )
836             continue;
837         i_prev_chapter = i_frame;
838
839         /* move chapters (simple workaround for inaccurate cut marks) */
840         if( i_frame > -i_chapter_offset )
841             i_frame += i_chapter_offset;
842         else
843             i_frame = 0;
844
845         uint64_t i_offset;
846         uint16_t i_file_number;
847         if( !ReadIndexRecord( indexfile, p_sys->b_ts_format,
848             i_frame, &i_offset, &i_file_number ) )
849             continue;
850         if( i_file_number < 1 || i_file_number > FILE_COUNT )
851             continue;
852
853         /* add file sizes to get the "global" offset */
854         seekpoint_t *sp = vlc_seekpoint_New();
855         if( !sp )
856             continue;
857         sp->i_time_offset = i_frame * (int64_t)( CLOCK_FREQ / p_sys->fps );
858         sp->i_byte_offset = i_offset;
859         for( int i = 0; i + 1 < i_file_number; ++i )
860             sp->i_byte_offset += FILE_SIZE( i );
861         sp->psz_name = strdup( line );
862
863         TAB_APPEND( p_marks->i_seekpoint, p_marks->seekpoint, sp );
864     }
865
866     /* add a chapter at the beginning if missing */
867     if( p_marks->i_seekpoint > 0 && p_marks->seekpoint[0]->i_byte_offset > 0 )
868     {
869         seekpoint_t *sp = vlc_seekpoint_New();
870         if( sp )
871         {
872             sp->i_byte_offset = 0;
873             sp->i_time_offset = 0;
874             sp->psz_name = strdup( _("Start") );
875             TAB_INSERT( p_marks->i_seekpoint, p_marks->seekpoint, sp, 0 );
876         }
877     }
878
879     if( p_marks->i_seekpoint > 0 )
880         p_sys->p_marks = p_marks;
881     else
882         vlc_input_title_Delete( p_marks );
883
884     fclose( marksfile );
885     fclose( indexfile );
886 }
887
888 /*****************************************************************************
889  * Lookup frame offset in index file
890  *****************************************************************************/
891 static bool ReadIndexRecord( FILE *p_file, bool b_ts, int64_t i_frame,
892                             uint64_t *pi_offset, uint16_t *pi_file_num )
893 {
894     uint8_t index_record[8];
895     if( fseek( p_file, sizeof(index_record) * i_frame, SEEK_SET ) != 0 )
896         return false;
897     if( fread( &index_record, sizeof(index_record), 1, p_file ) <= 0 )
898         return false;
899
900     /* VDR usually (only?) runs on little endian machines, but VLC has a
901      * broader audience. See recording.* in VDR source for data layout. */
902     if( b_ts )
903     {
904         uint64_t i_index_entry = GetQWLE( &index_record );
905         *pi_offset = i_index_entry & UINT64_C(0xFFFFFFFFFF);
906         *pi_file_num = i_index_entry >> 48;
907     }
908     else
909     {
910         *pi_offset = GetDWLE( &index_record );
911         *pi_file_num = index_record[5];
912     }
913
914     return true;
915 }
916
917 /*****************************************************************************
918  * Convert time stamp from file to frame number
919  *****************************************************************************/
920 static int64_t ParseFrameNumber( const char *psz_line, float fps )
921 {
922     unsigned h, m, s, f, n;
923
924     /* hour:min:sec.frame (frame is optional) */
925     n = sscanf( psz_line, "%u:%u:%u.%u", &h, &m, &s, &f );
926     if( n >= 3 )
927     {
928         if( n < 4 )
929             f = 1;
930         int64_t i_seconds = (int64_t)h * 3600 + (int64_t)m * 60 + s;
931         return (int64_t)( i_seconds * (double)fps ) + __MAX(1, f) - 1;
932     }
933
934     /* only a frame number */
935     int64_t i_frame = strtoll( psz_line, NULL, 10 );
936     return __MAX(1, i_frame) - 1;
937 }
938
939 /*****************************************************************************
940  * Return the last path component (including trailing separators)
941  *****************************************************************************/
942 static const char *BaseName( const char *psz_path )
943 {
944     const char *psz_name = psz_path + strlen( psz_path );
945
946     /* skip superfluous separators at the end */
947     while( psz_name > psz_path && psz_name[-1] == DIR_SEP_CHAR )
948         --psz_name;
949
950     /* skip last component */
951     while( psz_name > psz_path && psz_name[-1] != DIR_SEP_CHAR )
952         --psz_name;
953
954     return psz_name;
955 }