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