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