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