]> git.sesse.net Git - vlc/blob - modules/access/vdr.c
72de3478acb21236d8e2b1f5666a0e9c9679882d
[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
483     return true;
484 }
485
486 /*****************************************************************************
487  * Close the current file and open another
488  *****************************************************************************/
489 static bool SwitchFile( access_t *p_access, unsigned i_file )
490 {
491     access_sys_t *p_sys = p_access->p_sys;
492
493     /* requested file already open? */
494     if( p_sys->fd != -1 && p_sys->i_current_file == i_file )
495         return true;
496
497     /* close old file */
498     if( p_sys->fd != -1 )
499     {
500         close( p_sys->fd );
501         p_sys->fd = -1;
502     }
503
504     /* switch */
505     if( i_file >= FILE_COUNT )
506         return false;
507     p_sys->i_current_file = i_file;
508
509     /* open new file */
510     char *psz_path = GetFilePath( p_access, i_file );
511     if( !psz_path )
512         return false;
513     p_sys->fd = vlc_open( psz_path, O_RDONLY );
514
515     if( p_sys->fd == -1 )
516     {
517         msg_Err( p_access, "Failed to open %s: %m", psz_path );
518         goto error;
519     }
520
521     /* cannot handle anything except normal files */
522     struct stat st;
523     if( fstat( p_sys->fd, &st ) || !S_ISREG( st.st_mode ) )
524     {
525         msg_Err( p_access, "%s is not a regular file", psz_path );
526         goto error;
527     }
528
529     OptimizeForRead( p_sys->fd );
530
531     msg_Dbg( p_access, "opened %s", psz_path );
532     free( psz_path );
533     return true;
534
535 error:
536     dialog_Fatal (p_access, _("File reading failed"), _("VLC could not"
537         " open the file \"%s\". (%m)"), psz_path);
538     if( p_sys->fd != -1 )
539     {
540         close( p_sys->fd );
541         p_sys->fd = -1;
542     }
543     free( psz_path );
544     return false;
545 }
546
547 /*****************************************************************************
548  * Some tweaks to speed up read()
549  *****************************************************************************/
550 static void OptimizeForRead( int fd )
551 {
552     /* cf. Open() in file access module */
553     VLC_UNUSED(fd);
554 #ifdef HAVE_POSIX_FADVISE
555     posix_fadvise( fd, 0, 4096, POSIX_FADV_WILLNEED );
556     posix_fadvise( fd, 0, 0, POSIX_FADV_NOREUSE );
557 #endif
558 #ifdef F_RDAHEAD
559     fcntl( fd, F_RDAHEAD, 1 );
560 #endif
561 #ifdef F_NOCACHE
562     fcntl( fd, F_NOCACHE, 0 );
563 #endif
564 }
565
566 /*****************************************************************************
567  * Fix size if the (last) part is still growing
568  *****************************************************************************/
569 static void UpdateFileSize( access_t *p_access )
570 {
571     access_sys_t *p_sys = p_access->p_sys;
572     struct stat st;
573
574     if( p_access->info.i_size >= p_access->info.i_pos )
575         return;
576
577     /* TODO: not sure if this can happen or what to do in this case */
578     if( fstat( p_sys->fd, &st ) )
579         return;
580     if( (uint64_t)st.st_size <= CURRENT_FILE_SIZE )
581         return;
582
583     p_access->info.i_size -= CURRENT_FILE_SIZE;
584     CURRENT_FILE_SIZE = st.st_size;
585     p_access->info.i_size += CURRENT_FILE_SIZE;
586 }
587
588 /*****************************************************************************
589  * Open file relative to base directory for reading.
590  *****************************************************************************/
591 static FILE *OpenRelativeFile( access_t *p_access, const char *psz_file )
592 {
593     /* build path and add extension */
594     char *psz_path;
595     if( asprintf( &psz_path, "%s" DIR_SEP "%s%s",
596         p_access->psz_filepath, psz_file,
597         p_access->p_sys->b_ts_format ? "" : ".vdr" ) == -1 )
598         return NULL;
599
600     FILE *file = vlc_fopen( psz_path, "rb" );
601     if( !file )
602         msg_Warn( p_access, "Failed to open %s: %m", psz_path );
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_access->info.i_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 }