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