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