]> git.sesse.net Git - vlc/commitdiff
Added support for VDR recordings
authorTobias Güntner <fatbull@web.de>
Tue, 29 Jun 2010 23:36:13 +0000 (01:36 +0200)
committerJean-Baptiste Kempf <jb@videolan.org>
Tue, 7 Sep 2010 11:16:34 +0000 (13:16 +0200)
Signed-off-by: Jean-Baptiste Kempf <jb@videolan.org>
NEWS
modules/LIST
modules/access/Modules.am
modules/access/vdr.c [new file with mode: 0644]

diff --git a/NEWS b/NEWS
index e2de53ea8c3245a7cfdda8c66911713536e2230a..80e81220036f0447cb2d57f71c4f9ae0e8156265 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -12,6 +12,7 @@ Access
  * BDA: experimental support for ClearQam devices
  * DVB-S scanning support
  * DShow: support for freq and video standard selection
+ * Support for VDR recordings
 
 Codecs
  * You can now use ffmpeg-mt in conjunction with vlc
index aee05f2d93ce9cebbd690b54126f2da87212afe3..d75d4915c739e1816ffde80d87b6232b1492cec2 100644 (file)
@@ -34,6 +34,7 @@ $Id$
  * access_smb: SMB shares access module
  * access_tcp: TCP Network access module
  * access_udp: UDP Network access module
+ * access_vdr: VDR access module
  * adjust: Contrast/Hue/saturation/Brightness adjust module
  * adpcm: ADPCM audio decoder
  * aes3: aes3 decoder/packetizer
index 7aa0b9967bac97d32c5449eebda97424c6cb35f5..fba6dac5261d4111bc6983c4440c12b490458cb8 100644 (file)
@@ -50,6 +50,7 @@ SOURCES_access_sftp = sftp.c
 SOURCES_access_imem = imem.c
 SOURCES_access_avio = avio.c avio.h
 SOURCES_access_attachment = attachment.c
+SOURCES_access_vdr = vdr.c
 
 SOURCES_access_rar = rar/rar.c rar/rar.h rar/access.c
 SOURCES_stream_filter_rar = rar/rar.c rar/rar.h rar/stream.c
@@ -74,6 +75,7 @@ libvlc_LTLIBRARIES += \
        libaccess_attachment_plugin.la \
        libaccess_rar_plugin.la \
        libstream_filter_rar_plugin.la \
+       libaccess_vdr_plugin.la \
        $(NULL)
 
 libaccess_alsa_plugin_la_SOURCES = alsa.c
diff --git a/modules/access/vdr.c b/modules/access/vdr.c
new file mode 100644 (file)
index 0000000..ff8ca79
--- /dev/null
@@ -0,0 +1,957 @@
+/*****************************************************************************
+ * vdr.c: VDR recordings access plugin
+ *****************************************************************************
+ * Copyright (C) 2010 Tobias Güntner
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+/***
+VDR splits recordings into multiple files and stores each recording in a
+separate directory. If VLC opens a normal directory, the filesystem module
+will add all files to the playlist. If, however, VLC opens a VDR recording,
+this module will join all files within that directory and provide a single
+continuous stream instead.
+
+VDR recordings have either of two directory layouts:
+    1) PES format:
+        /path/to/0000-00-00.00.00.00.00.rec/
+            001.vdr, 002.vdr, 003.vdr, ...
+            index.vdr, info.vdr, marks.vdr, ...
+    2) TS format:
+        /path/to/0000-00-00.00.00.0.0.rec/
+            001.ts, 002.ts, 003.ts, ...
+            index, info, marks, ...
+See http://www.vdr-wiki.de/ and http://www.tvdr.de/ for more information.
+***/
+
+/*****************************************************************************
+ * Preamble
+ *****************************************************************************/
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#ifdef HAVE_SYS_TYPES_H
+#   include <sys/types.h>
+#endif
+#ifdef HAVE_SYS_STAT_H
+#   include <sys/stat.h>
+#endif
+#ifdef HAVE_FCNTL_H
+#   include <fcntl.h>
+#endif
+#ifdef HAVE_UNISTD_H
+#   include <unistd.h>
+#elif defined( WIN32 ) && !defined( UNDER_CE )
+#   include <io.h>
+#endif
+
+#include <ctype.h>
+#include <time.h>
+#include <errno.h>
+
+#if defined( WIN32 ) && !defined( UNDER_CE )
+#   undef lseek
+#   define lseek _lseeki64
+#endif
+
+#include <vlc_common.h>
+#include <vlc_plugin.h>
+#include <vlc_access.h>
+#include <vlc_input.h>
+#include <vlc_fs.h>
+#include <vlc_charset.h>
+#include <vlc_dialog.h>
+#include <vlc_configuration.h>
+
+/*****************************************************************************
+ * Module descriptor
+ *****************************************************************************/
+static int  Open ( vlc_object_t * );
+static void Close( vlc_object_t * );
+
+#define HELP_TEXT N_("Support for VDR recordings (http://www.tvdr.de/).")
+
+#define CACHING_TEXT N_("Caching value in ms")
+#define CACHING_LONGTEXT N_( \
+    "Caching value for files. This value should be set in milliseconds." )
+
+#define CHAPTER_OFFSET_TEXT N_("Chapter offset in ms")
+#define CHAPTER_OFFSET_LONGTEXT N_( \
+    "Move all chapters. This value should be set in milliseconds." )
+
+#define FPS_TEXT N_("Frame rate")
+#define FPS_LONGTEXT N_( \
+    "Default frame rate for chapter import." )
+
+vlc_module_begin ()
+    set_category( CAT_INPUT )
+    set_shortname( N_("VDR") )
+    set_help( HELP_TEXT )
+    set_subcategory( SUBCAT_INPUT_ACCESS )
+    set_description( N_("VDR recordings") )
+    add_integer( "vdr-caching", 5 * DEFAULT_PTS_DELAY / 1000, NULL,
+        CACHING_TEXT, CACHING_LONGTEXT, true )
+    add_integer( "vdr-chapter-offset", 0, NULL,
+        CHAPTER_OFFSET_TEXT, CHAPTER_OFFSET_LONGTEXT, true )
+    add_float_with_range( "vdr-fps", 25, 1, 1000, NULL,
+        FPS_TEXT, FPS_LONGTEXT, true )
+    set_capability( "access", 60 )
+    add_shortcut( "vdr" )
+    add_shortcut( "directory" )
+    add_shortcut( "dir" )
+    add_shortcut( "file" )
+    set_callbacks( Open, Close )
+vlc_module_end ()
+
+/*****************************************************************************
+ * Local prototypes, constants, structures
+ *****************************************************************************/
+
+TYPEDEF_ARRAY( uint64_t, size_array_t );
+
+struct access_sys_t
+{
+    /* file sizes of all parts */
+    size_array_t file_sizes;
+
+    /* index and fd of current open file */
+    unsigned i_current_file;
+    int fd;
+
+    /* meta data */
+    vlc_meta_t *p_meta;
+
+    /* cut marks */
+    input_title_t *p_marks;
+    float fps;
+
+    /* file format: true=TS, false=PES */
+    bool b_ts_format;
+};
+
+#define CURRENT_FILE_SIZE ARRAY_VAL(p_sys->file_sizes, p_sys->i_current_file)
+#define FILE_SIZE(pos)    ARRAY_VAL(p_sys->file_sizes, pos)
+#define FILE_COUNT        (unsigned)p_sys->file_sizes.i_size
+
+static int Control( access_t *, int, va_list );
+static ssize_t Read( access_t *p_access, uint8_t *p_buffer, size_t i_len );
+static int Seek( access_t *p_access, uint64_t i_pos);
+static void FindSeekpoint( access_t *p_access );
+static bool ScanDirectory( access_t *p_access, bool b_strict );
+static char *GetFilePath( access_t *p_access, unsigned i_file );
+static bool ImportNextFile( access_t *p_access );
+static bool SwitchFile( access_t *p_access, unsigned i_file );
+static void OptimizeForRead( int fd );
+static void UpdateFileSize( access_t *p_access );
+static int StatRelativeFile( access_t *p_access, const char *psz_file,
+                            struct stat *p_stat );
+static FILE *OpenRelativeFile( access_t *p_access, const char *psz_file );
+static bool ReadLine( char **ppsz_line, size_t *pi_size, FILE *p_file );
+static void ImportMeta( access_t *p_access );
+static void ImportMarks( access_t *p_access );
+static bool ReadIndexRecord( FILE *p_file, bool b_ts, int64_t i_frame,
+                            uint64_t *pi_offset, uint16_t *pi_file_num );
+static int64_t ParseFrameNumber( const char *psz_line, float fps );
+
+/*****************************************************************************
+ * Open a directory
+ *****************************************************************************/
+static int Open( vlc_object_t *p_this )
+{
+    access_t *p_access = (access_t*)p_this;
+
+    if( !p_access->psz_filepath )
+        return VLC_EGENERIC;
+
+    /* Some tests can be skipped if this module was explicitly requested.
+     * That way, the user can play "corrupt" recordings if necessary
+     * and we can avoid false positives in the general case. */
+    bool b_strict = strcmp( p_access->psz_access, "vdr" );
+
+    /* Do a quick test based on the directory extension to see if this
+     * directory might contain a VDR recording. We can be reasonably
+     * sure if ScanDirectory() actually finds files. */
+    if( b_strict )
+    {
+        const char *psz_ext = strrchr( p_access->psz_filepath, '.' );
+        if( !psz_ext || strcasecmp( psz_ext, ".rec" ) )
+            return VLC_EGENERIC;
+    }
+
+    /* Only directories can be recordings */
+    struct stat st;
+    if( vlc_stat( p_access->psz_filepath, &st ) ||
+        !S_ISDIR( st.st_mode ) )
+        return VLC_EGENERIC;
+
+    access_sys_t *p_sys;
+    STANDARD_READ_ACCESS_INIT;
+    p_sys->fd = -1;
+    p_sys->fps = var_InheritFloat( p_access, "vdr-fps" );
+    ARRAY_INIT( p_sys->file_sizes );
+
+    /* Import all files and prepare playback. */
+    if( !ScanDirectory( p_access, b_strict ) ||
+        !SwitchFile( p_access, 0 ) )
+    {
+        Close( p_this );
+        return VLC_EGENERIC;
+    }
+
+    return VLC_SUCCESS;
+}
+
+/*****************************************************************************
+ * Close files and free resources
+ *****************************************************************************/
+static void Close( vlc_object_t * p_this )
+{
+    access_t *p_access = (access_t*)p_this;
+    access_sys_t *p_sys = p_access->p_sys;
+
+    if( p_sys->fd != -1 )
+        close( p_sys->fd );
+    ARRAY_RESET( p_sys->file_sizes );
+
+    if( p_sys->p_meta )
+        vlc_meta_Delete( p_sys->p_meta );
+
+    vlc_input_title_Delete( p_sys->p_marks );
+    free( p_sys );
+}
+
+/*****************************************************************************
+ * Determine format and import files
+ *****************************************************************************/
+static bool ScanDirectory( access_t *p_access, bool b_strict )
+{
+    access_sys_t *p_sys = p_access->p_sys;
+
+    /* find first part and determine directory format */
+    p_sys->b_ts_format = true;
+    if( !ImportNextFile( p_access ) )
+    {
+        p_sys->b_ts_format = !p_sys->b_ts_format;
+        if( !ImportNextFile( p_access ) )
+            return false;
+    }
+
+    /* meta data and index should exist */
+    if( b_strict )
+    {
+        struct stat st;
+        if( StatRelativeFile( p_access, "info", &st ) ||
+            StatRelativeFile( p_access, "index", &st ) )
+            return false;
+    }
+
+    /* get all remaining parts */
+    while( ImportNextFile( p_access ) )
+        continue;
+
+    /* import meta data etc. */
+    ImportMeta( p_access );
+
+    /* cut marks depend on meta data and file sizes */
+    ImportMarks( p_access );
+
+    return true;
+}
+
+/*****************************************************************************
+ * Control input stream
+ *****************************************************************************/
+static int Control( access_t *p_access, int i_query, va_list args )
+{
+    access_sys_t *p_sys = p_access->p_sys;
+    input_title_t ***ppp_title;
+    int i;
+    int64_t *pi64;
+    vlc_meta_t *p_meta;
+
+    switch( i_query )
+    {
+        case ACCESS_CAN_SEEK:
+        case ACCESS_CAN_PAUSE:
+        case ACCESS_CAN_CONTROL_PACE:
+            *va_arg( args, bool* ) = true;
+            break;
+
+        case ACCESS_CAN_FASTSEEK:
+            /* Seek() can open files, so it might be "too slow" */
+            *va_arg( args, bool* ) = false;
+            break;
+
+        case ACCESS_GET_PTS_DELAY:
+            pi64 = va_arg( args, int64_t * );
+            *pi64 = var_InheritInteger( p_access, "vdr-caching" ) * INT64_C(1000);
+            break;
+
+        case ACCESS_SET_PAUSE_STATE:
+            /* nothing to do */
+            break;
+
+        case ACCESS_GET_TITLE_INFO:
+            /* return a copy of our seek points */
+            if( !p_sys->p_marks )
+                return VLC_EGENERIC;
+            ppp_title = va_arg( args, input_title_t*** );
+            *va_arg( args, int* ) = 1;
+            *ppp_title = malloc( sizeof( input_title_t** ) );
+            if( !*ppp_title )
+                return VLC_ENOMEM;
+            **ppp_title = vlc_input_title_Duplicate( p_sys->p_marks );
+            break;
+
+        case ACCESS_SET_TITLE:
+            /* ignore - only one title */
+            break;
+
+        case ACCESS_SET_SEEKPOINT:
+            i = va_arg( args, int );
+            /* Seek updates p_access->info */
+            return Seek( p_access, p_sys->p_marks->seekpoint[i]->i_byte_offset );
+
+        case ACCESS_GET_META:
+            if( !p_sys->p_meta )
+                return VLC_EGENERIC;
+            p_meta = va_arg( args, vlc_meta_t* );
+            vlc_meta_Merge( p_meta, p_sys->p_meta );
+            break;
+
+        case ACCESS_SET_PRIVATE_ID_STATE:
+        case ACCESS_GET_CONTENT_TYPE:
+            return VLC_EGENERIC;
+
+        default:
+            msg_Warn( p_access, "unimplemented query in control" );
+            return VLC_EGENERIC;
+    }
+    return VLC_SUCCESS;
+}
+
+/*****************************************************************************
+ * Read and concatenate files
+ *****************************************************************************/
+static ssize_t Read( access_t *p_access, uint8_t *p_buffer, size_t i_len )
+{
+    access_sys_t *p_sys = p_access->p_sys;
+
+    if( p_sys->fd == -1 )
+    {
+        /* no more data */
+        p_access->info.b_eof = true;
+        return 0;
+    }
+
+    ssize_t i_ret = read( p_sys->fd, p_buffer, i_len );
+
+    if( i_ret > 0 )
+    {
+        /* success */
+        p_access->info.i_pos += i_ret;
+        UpdateFileSize( p_access );
+        FindSeekpoint( p_access );
+        return i_ret;
+    }
+    else if( i_ret == 0 )
+    {
+        /* check for new files in case the recording is still active */
+        if( p_sys->i_current_file >= FILE_COUNT - 1 )
+            ImportNextFile( p_access );
+        /* play next file */
+        SwitchFile( p_access, p_sys->i_current_file + 1 );
+        return -1;
+    }
+    else if( errno == EINTR )
+    {
+        /* try again later */
+        return -1;
+    }
+    else
+    {
+        /* abort on read error */
+        msg_Err( p_access, "failed to read (%m)" );
+        dialog_Fatal( p_access, _("File reading failed"), "%s",
+                      _("VLC could not read the file.") );
+        SwitchFile( p_access, -1 );
+        return 0;
+    }
+}
+
+/*****************************************************************************
+ * Seek to a specific location in a file
+ *****************************************************************************/
+static int Seek( access_t *p_access, uint64_t i_pos )
+{
+    access_sys_t *p_sys = p_access->p_sys;
+
+    /* might happen if called by ACCESS_SET_SEEKPOINT */
+    i_pos = __MIN( i_pos, p_access->info.i_size );
+
+    p_access->info.i_pos = i_pos;
+    p_access->info.b_eof = false;
+
+    /* find correct chapter */
+    FindSeekpoint( p_access );
+
+    /* find correct file */
+    unsigned i_file = 0;
+    while( i_pos >= FILE_SIZE( i_file ) &&
+        i_file < FILE_COUNT - 1 )
+    {
+        i_pos -= FILE_SIZE( i_file );
+        i_file++;
+    }
+    if( !SwitchFile( p_access, i_file ) )
+        return VLC_EGENERIC;
+
+    /* adjust position within that file */
+    return lseek( p_sys->fd, i_pos, SEEK_SET ) != -1 ?
+        VLC_SUCCESS : VLC_EGENERIC;
+}
+
+/*****************************************************************************
+ * Change the chapter index to match the current position
+ *****************************************************************************/
+static void FindSeekpoint( access_t *p_access )
+{
+    access_sys_t *p_sys = p_access->p_sys;
+    if( !p_sys->p_marks )
+        return;
+
+    int i_new_seekpoint = p_access->info.i_seekpoint;
+    if( p_access->info.i_pos < (uint64_t)p_sys->p_marks->
+        seekpoint[ p_access->info.i_seekpoint ]->i_byte_offset )
+    {
+        /* i_pos moved backwards, start fresh */
+        i_new_seekpoint = 0;
+    }
+
+    /* only need to check the following seekpoints */
+    while( i_new_seekpoint + 1 < p_sys->p_marks->i_seekpoint &&
+        p_access->info.i_pos >= (uint64_t)p_sys->p_marks->
+        seekpoint[ i_new_seekpoint + 1 ]->i_byte_offset )
+    {
+        i_new_seekpoint++;
+    }
+
+    /* avoid unnecessary events */
+    if( p_access->info.i_seekpoint != i_new_seekpoint )
+    {
+        p_access->info.i_seekpoint = i_new_seekpoint;
+        p_access->info.i_update |= INPUT_UPDATE_SEEKPOINT;
+    }
+}
+
+/*****************************************************************************
+ * Returns the path of a certain part
+ *****************************************************************************/
+static char *GetFilePath( access_t *p_access, unsigned i_file )
+{
+    char *psz_path;
+    if( asprintf( &psz_path, p_access->p_sys->b_ts_format ?
+        "%s" DIR_SEP "%05u.ts" : "%s" DIR_SEP "%03u.vdr",
+        p_access->psz_filepath, i_file + 1 ) == -1 )
+        return NULL;
+    else
+        return psz_path;
+}
+
+/*****************************************************************************
+ * Check if another part exists and import it
+ *****************************************************************************/
+static bool ImportNextFile( access_t *p_access )
+{
+    access_sys_t *p_sys = p_access->p_sys;
+
+    char *psz_path = GetFilePath( p_access, FILE_COUNT );
+    if( !psz_path )
+        return false;
+
+    struct stat st;
+    if( vlc_stat( psz_path, &st ) )
+    {
+        msg_Dbg( p_access, "could not stat %s: %m", psz_path );
+        free( psz_path );
+        return false;
+    }
+    if( !S_ISREG( st.st_mode ) )
+    {
+        msg_Dbg( p_access, "%s is not a regular file", psz_path );
+        free( psz_path );
+        return false;
+    }
+    msg_Dbg( p_access, "%s exists", psz_path );
+    free( psz_path );
+
+    ARRAY_APPEND( p_sys->file_sizes, st.st_size );
+    p_access->info.i_size += st.st_size;
+    p_access->info.i_update |= INPUT_UPDATE_SIZE;
+
+    return true;
+}
+
+/*****************************************************************************
+ * Close the current file and open another
+ *****************************************************************************/
+static bool SwitchFile( access_t *p_access, unsigned i_file )
+{
+    access_sys_t *p_sys = p_access->p_sys;
+
+    /* requested file already open? */
+    if( p_sys->fd != -1 && p_sys->i_current_file == i_file )
+        return true;
+
+    /* close old file */
+    if( p_sys->fd != -1 )
+    {
+        close( p_sys->fd );
+        p_sys->fd = -1;
+    }
+
+    /* switch */
+    if( i_file >= FILE_COUNT )
+        return false;
+    p_sys->i_current_file = i_file;
+
+    /* open new file */
+    char *psz_path = GetFilePath( p_access, i_file );
+    if( !psz_path )
+        return false;
+    p_sys->fd = vlc_open( psz_path, O_RDONLY );
+
+    if( p_sys->fd == -1 )
+    {
+        msg_Err( p_access, "Failed to open %s: %m", psz_path );
+        goto error;
+    }
+
+    /* cannot handle anything except normal files */
+    struct stat st;
+    if( fstat( p_sys->fd, &st ) || !S_ISREG( st.st_mode ) )
+    {
+        msg_Err( p_access, "%s is not a regular file", psz_path );
+        goto error;
+    }
+
+    OptimizeForRead( p_sys->fd );
+
+    msg_Dbg( p_access, "opened %s", psz_path );
+    free( psz_path );
+    return true;
+
+error:
+    dialog_Fatal (p_access, _("File reading failed"), _("VLC could not"
+        " open the file \"%s\"."), psz_path);
+    if( p_sys->fd != -1 )
+    {
+        close( p_sys->fd );
+        p_sys->fd = -1;
+    }
+    free( psz_path );
+    return false;
+}
+
+/*****************************************************************************
+ * Some tweaks to speed up read()
+ *****************************************************************************/
+static void OptimizeForRead( int fd )
+{
+    /* cf. Open() in file access module */
+    VLC_UNUSED(fd);
+#ifdef HAVE_POSIX_FADVISE
+    posix_fadvise( fd, 0, 4096, POSIX_FADV_WILLNEED );
+    posix_fadvise( fd, 0, 0, POSIX_FADV_NOREUSE );
+#endif
+#ifdef HAVE_FCNTL
+#ifdef F_RDAHEAD
+    fcntl( fd, F_RDAHEAD, 1 );
+#endif
+#ifdef F_NOCACHE
+    fcntl( fd, F_NOCACHE, 1 );
+#endif
+#endif
+}
+
+/*****************************************************************************
+ * Fix size if the (last) part is still growing
+ *****************************************************************************/
+static void UpdateFileSize( access_t *p_access )
+{
+    access_sys_t *p_sys = p_access->p_sys;
+    struct stat st;
+
+    if( p_access->info.i_size >= p_access->info.i_pos )
+        return;
+
+    /* TODO: not sure if this can happen or what to do in this case */
+    if( fstat( p_sys->fd, &st ) )
+        return;
+    if( (uint64_t)st.st_size <= CURRENT_FILE_SIZE )
+        return;
+
+    p_access->info.i_size -= CURRENT_FILE_SIZE;
+    CURRENT_FILE_SIZE = st.st_size;
+    p_access->info.i_size += CURRENT_FILE_SIZE;
+    p_access->info.i_update |= INPUT_UPDATE_SIZE;
+}
+
+/*****************************************************************************
+ * Stat file relative to base directory
+ *****************************************************************************/
+static int StatRelativeFile( access_t *p_access, const char *psz_file,
+                              struct stat *p_stat )
+{
+    /* build path and add extension */
+    char *psz_path;
+    if( asprintf( &psz_path, "%s" DIR_SEP "%s%s",
+        p_access->psz_filepath, psz_file,
+        p_access->p_sys->b_ts_format ? "" : ".vdr" ) == -1 )
+        return -1;
+
+    int ret = vlc_stat( psz_path, p_stat );
+    if( ret )
+        msg_Dbg( p_access, "could not stat %s: %m", psz_path );
+    free( psz_path );
+
+    return ret;
+}
+
+/*****************************************************************************
+ * Open file relative to base directory for reading.
+ *****************************************************************************/
+static FILE *OpenRelativeFile( access_t *p_access, const char *psz_file )
+{
+    /* build path and add extension */
+    char *psz_path;
+    if( asprintf( &psz_path, "%s" DIR_SEP "%s%s",
+        p_access->psz_filepath, psz_file,
+        p_access->p_sys->b_ts_format ? "" : ".vdr" ) == -1 )
+        return NULL;
+
+    FILE *file = vlc_fopen( psz_path, "rb" );
+    if( !file )
+        msg_Warn( p_access, "Failed to open %s: %m", psz_path );
+    free( psz_path );
+
+    return file;
+}
+
+/*****************************************************************************
+ * Read a line of text. Returns false on error or EOF.
+ *****************************************************************************/
+static bool ReadLine( char **ppsz_line, size_t *pi_size, FILE *p_file )
+{
+    ssize_t read = getline( ppsz_line, pi_size, p_file );
+
+    if( read == -1 )
+    {
+        /* automatically free buffer on eof */
+        free( *ppsz_line );
+        *ppsz_line = NULL;
+        return false;
+    }
+
+    if( read > 0 && (*ppsz_line)[ read - 1 ] == '\n' )
+        (*ppsz_line)[ read - 1 ] = '\0';
+    EnsureUTF8( *ppsz_line );
+
+    return true;
+}
+
+/*****************************************************************************
+ * Import meta data
+ *****************************************************************************/
+static void ImportMeta( access_t *p_access )
+{
+    access_sys_t *p_sys = p_access->p_sys;
+
+    FILE *infofile = OpenRelativeFile( p_access, "info" );
+    if( !infofile )
+        return;
+
+    vlc_meta_t *p_meta = vlc_meta_New();
+    p_sys->p_meta = p_meta;
+    if( !p_meta )
+    {
+        fclose( infofile );
+        return;
+    }
+
+    char *line = NULL;
+    size_t line_len;
+    char *psz_title = NULL, *psz_smalltext = NULL, *psz_date = NULL;
+
+    while( ReadLine( &line, &line_len, infofile ) )
+    {
+        if( !isalpha( (unsigned char)line[0] ) || line[1] != ' ' )
+            continue;
+
+        char tag = line[0];
+        char *text = line + 2;
+
+        if( tag == 'C' )
+        {
+            char *psz_name = strchr( text, ' ' );
+            if( psz_name )
+            {
+                *psz_name = '\0';
+                vlc_meta_AddExtra( p_meta, "Channel", psz_name + 1 );
+            }
+            vlc_meta_AddExtra( p_meta, "Transponder", text );
+        }
+
+        else if( tag == 'E' )
+        {
+            unsigned i_id, i_start, i_length;
+            if( sscanf( text, "%u %u %u", &i_id, &i_start, &i_length ) == 3 )
+            {
+                char str[50];
+                struct tm tm;
+                time_t start = i_start;
+                localtime_r( &start, &tm );
+
+                /* TODO: locale */
+                strftime( str, sizeof(str), "%Y-%m-%d %H:%M", &tm );
+                vlc_meta_AddExtra( p_meta, "Date", str );
+                free( psz_date );
+                psz_date = strdup( str );
+
+                /* display in minutes */
+                i_length = ( i_length + 59 ) / 60;
+                snprintf( str, sizeof(str), "%u:%02u", i_length / 60, i_length % 60 );
+                vlc_meta_AddExtra( p_meta, "Duration", str );
+            }
+        }
+
+        else if( tag == 'T' )
+        {
+            free( psz_title );
+            psz_title = strdup( text );
+            vlc_meta_AddExtra( p_meta, "Title", text );
+        }
+
+        else if( tag == 'S' )
+        {
+            free( psz_smalltext );
+            psz_smalltext = strdup( text );
+            vlc_meta_AddExtra( p_meta, "Info", text );
+        }
+
+        else if( tag == 'D' )
+        {
+            for( char *p = text; *p; ++p )
+            {
+                if( *p == '|' )
+                    *p = '\n';
+            }
+            vlc_meta_SetDescription( p_meta, text );
+        }
+
+        /* FPS are required to convert between timestamps and frames */
+        else if( tag == 'F' )
+        {
+            float fps = atof( text );
+            if( fps >= 1 )
+                p_sys->fps = fps;
+            vlc_meta_AddExtra( p_meta, "Frame Rate", text );
+        }
+
+        else if( tag == 'P' )
+        {
+            vlc_meta_AddExtra( p_meta, "Priority", text );
+        }
+
+        else if( tag == 'L' )
+        {
+            vlc_meta_AddExtra( p_meta, "Lifetime", text );
+        }
+    }
+
+    /* create a meaningful title */
+    int i_len = 10 +
+        ( psz_title ? strlen( psz_title ) : 0 ) +
+        ( psz_smalltext ? strlen( psz_smalltext ) : 0 ) +
+        ( psz_date ? strlen( psz_date ) : 0 );
+    char *psz_display = malloc( i_len );
+
+    if( psz_display )
+    {
+        *psz_display = '\0';
+        if( psz_title )
+            strcat( psz_display, psz_title );
+        if( psz_title && psz_smalltext )
+            strcat( psz_display, " - " );
+        if( psz_smalltext )
+            strcat( psz_display, psz_smalltext );
+        if( ( psz_title || psz_smalltext ) && psz_date )
+        {
+            strcat( psz_display, " (" );
+            strcat( psz_display, psz_date );
+            strcat( psz_display, ")" );
+        }
+        if( *psz_display )
+            vlc_meta_SetTitle( p_meta, psz_display );
+    }
+
+    free( psz_display );
+    free( psz_title );
+    free( psz_smalltext );
+    free( psz_date );
+
+    fclose( infofile );
+}
+
+/*****************************************************************************
+ * Import cut marks and convert them to seekpoints (chapters).
+ *****************************************************************************/
+static void ImportMarks( access_t *p_access )
+{
+    access_sys_t *p_sys = p_access->p_sys;
+
+    FILE *marksfile = OpenRelativeFile( p_access, "marks" );
+    if( !marksfile )
+        return;
+
+    FILE *indexfile = OpenRelativeFile( p_access, "index" );
+    if( !indexfile )
+    {
+        fclose( marksfile );
+        return;
+    }
+
+    /* Put all cut marks in a "dummy" title */
+    input_title_t *p_marks = vlc_input_title_New();
+    if( !p_marks )
+    {
+        fclose( marksfile );
+        fclose( indexfile );
+        return;
+    }
+    p_marks->psz_name = strdup( _("VDR Cut Marks") );
+
+    /* offset for chapter positions */
+    int i_chapter_offset = p_sys->fps / 1000 *
+        var_InheritInteger( p_access, "vdr-chapter-offset" );
+
+    /* parse lines of the form "0:00:00.00 foobar" */
+    char *line = NULL;
+    size_t line_len;
+    while( ReadLine( &line, &line_len, marksfile ) )
+    {
+        int64_t i_frame = ParseFrameNumber( line, p_sys->fps );
+
+        /* move chapters (simple workaround for inaccurate cut marks) */
+        if( i_frame > -i_chapter_offset )
+            i_frame += i_chapter_offset;
+        else
+            i_frame = 0;
+
+        uint64_t i_offset;
+        uint16_t i_file_number;
+        if( !ReadIndexRecord( indexfile, p_sys->b_ts_format,
+            i_frame, &i_offset, &i_file_number ) )
+            continue;
+        if( i_file_number < 1 || i_file_number > FILE_COUNT )
+            continue;
+
+        /* add file sizes to get the "global" offset */
+        seekpoint_t *sp = vlc_seekpoint_New();
+        if( !sp )
+            continue;
+        sp->i_time_offset = i_frame * (int64_t)( CLOCK_FREQ / p_sys->fps );
+        sp->i_byte_offset = i_offset;
+        for( int i = 0; i + 1 < i_file_number; ++i )
+            sp->i_byte_offset += FILE_SIZE( i );
+        sp->psz_name = strdup( line );
+
+        TAB_APPEND( p_marks->i_seekpoint, p_marks->seekpoint, sp );
+    }
+
+    if( p_marks->i_seekpoint > 0 )
+    {
+        seekpoint_t *sp = vlc_seekpoint_New();
+        if( sp )
+        {
+            sp->i_byte_offset = 0;
+            sp->i_time_offset = 0;
+            sp->psz_name = strdup( _("Start") );
+            TAB_INSERT( p_marks->i_seekpoint, p_marks->seekpoint, sp, 0 );
+        }
+        p_sys->p_marks = p_marks;
+    }
+    else
+    {
+        vlc_input_title_Delete( p_marks );
+    }
+
+    fclose( marksfile );
+    fclose( indexfile );
+}
+
+/*****************************************************************************
+ * Lookup frame offset in index file
+ *****************************************************************************/
+static bool ReadIndexRecord( FILE *p_file, bool b_ts, int64_t i_frame,
+                            uint64_t *pi_offset, uint16_t *pi_file_num )
+{
+    uint8_t index_record[8];
+    if( fseek( p_file, sizeof(index_record) * i_frame, SEEK_SET ) != 0 )
+        return false;
+    if( fread( &index_record, sizeof(index_record), 1, p_file ) <= 0 )
+        return false;
+
+    /* VDR usually (only?) runs on little endian machines, but VLC has a
+     * broader audience. See recording.* in VDR source for data layout. */
+    if( b_ts )
+    {
+        uint64_t i_index_entry = GetQWLE( &index_record );
+        *pi_offset = i_index_entry & UINT64_C(0xFFFFFFFFFF);
+        *pi_file_num = i_index_entry >> 48;
+    }
+    else
+    {
+        *pi_offset = GetDWLE( &index_record );
+        *pi_file_num = index_record[5];
+    }
+
+    return true;
+}
+
+/*****************************************************************************
+ * Convert time stamp from file to frame number
+ *****************************************************************************/
+static int64_t ParseFrameNumber( const char *psz_line, float fps )
+{
+    unsigned h, m, s, f, n;
+
+    /* hour:min:sec.frame (frame is optional) */
+    n = sscanf( psz_line, "%u:%u:%u.%u", &h, &m, &s, &f );
+    if( n >= 3 )
+    {
+        if( n < 4 )
+            f = 1;
+        int64_t i_seconds = (int64_t)h * 3600 + (int64_t)m * 60 + s;
+        return (int64_t)( i_seconds * (double)fps ) + __MAX(1, f) - 1;
+    }
+
+    /* only a frame number */
+    int64_t i_frame = strtoll( psz_line, NULL, 10 );
+    return __MAX(1, i_frame) - 1;
+}