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