a1e353a51e882f927bea53b7999270b7fbf250bd
[vlc.git] / modules / access_filter / timeshift.c
1 /*****************************************************************************
2  * timeshift.c: access filter implementing timeshifting capabilities
3  *****************************************************************************
4  * Copyright (C) 2005 the VideoLAN team
5  * $Id$
6  *
7  * Authors: Laurent Aimar <fenrir@via.ecp.fr>
8  *          Gildas Bazin <gbazin@videolan.org>
9  *
10  * This program is free software; you can redistribute it and/or modify
11  * it under the terms of the GNU General Public License as published by
12  * the Free Software Foundation; either version 2 of the License, or
13  * (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU General Public License for more details.
19  *
20  * You should have received a copy of the GNU General Public License
21  * along with this program; if not, write to the Free Software
22  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
23  *****************************************************************************/
24
25 /*****************************************************************************
26  * Preamble
27  *****************************************************************************/
28 #ifdef HAVE_CONFIG_H
29 # include "config.h"
30 #endif
31
32 #include <vlc_common.h>
33 #include <vlc_plugin.h>
34
35 #include <errno.h>
36
37 #include <vlc_access.h>
38 #include <vlc_charset.h>
39 #include <vlc_input.h>
40
41 #include <unistd.h>
42
43 #ifdef WIN32
44 #  include <direct.h>                                        /* _wgetcwd  */
45 #endif
46
47 /*****************************************************************************
48  * Module descriptor
49  *****************************************************************************/
50 static int  Open ( vlc_object_t * );
51 static void Close( vlc_object_t * );
52
53 #define GRANULARITY_TEXT N_("Timeshift granularity")
54 /// \bug [String] typo
55 #define GRANULARITY_LONGTEXT N_( "This is the size of the temporary files " \
56   "that will be used to store the timeshifted streams." )
57 #define DIR_TEXT N_("Timeshift directory")
58 #define DIR_LONGTEXT N_( "Directory used to store the timeshift temporary " \
59   "files." )
60 #define FORCE_TEXT N_("Force use of the timeshift module")
61 #define FORCE_LONGTEXT N_("Force use of the timeshift module even if the " \
62   "access declares that it can control pace or pause." )
63
64 vlc_module_begin();
65     set_shortname( N_("Timeshift") );
66     set_description( N_("Timeshift") );
67     set_category( CAT_INPUT );
68     set_subcategory( SUBCAT_INPUT_ACCESS_FILTER );
69     set_capability( "access_filter", 0 );
70     add_shortcut( "timeshift" );
71     set_callbacks( Open, Close );
72
73     add_integer( "timeshift-granularity", 50, NULL, GRANULARITY_TEXT,
74                  GRANULARITY_LONGTEXT, true );
75     add_directory( "timeshift-dir", 0, 0, DIR_TEXT, DIR_LONGTEXT, false );
76     add_bool( "timeshift-force", false, NULL, FORCE_TEXT, FORCE_LONGTEXT,
77               false );
78 vlc_module_end();
79
80 /*****************************************************************************
81  * Local prototypes
82  *****************************************************************************/
83
84 static int      Seek( access_t *, int64_t );
85 static block_t *Block  ( access_t *p_access );
86 static int      Control( access_t *, int i_query, va_list args );
87 static void*    Thread ( vlc_object_t *p_this );
88 static int      WriteBlockToFile( access_t *p_access, block_t *p_block );
89 static block_t *ReadBlockFromFile( access_t *p_access );
90 static char    *GetTmpFilePath( access_t *p_access );
91
92 #define TIMESHIFT_FIFO_MAX (10*1024*1024)
93 #define TIMESHIFT_FIFO_MIN (TIMESHIFT_FIFO_MAX/4)
94 #define TMP_FILE_MAX 256
95
96 typedef struct ts_entry_t
97 {
98     FILE *file;
99     struct ts_entry_t *p_next;
100
101 } ts_entry_t;
102
103 struct access_sys_t
104 {
105     block_fifo_t *p_fifo;
106
107     unsigned  i_files;
108     unsigned  i_file_size;
109     unsigned  i_write_size;
110
111     ts_entry_t *p_read_list;
112     ts_entry_t **pp_read_last;
113     ts_entry_t *p_write_list;
114     ts_entry_t **pp_write_last;
115
116     char *psz_filename_base;
117     char *psz_filename;
118
119     int64_t i_data;
120 };
121
122 /*****************************************************************************
123  * Open:
124  *****************************************************************************/
125 static int Open( vlc_object_t *p_this )
126 {
127     access_t *p_access = (access_t*)p_this;
128     access_t *p_src = p_access->p_source;
129     access_sys_t *p_sys;
130     bool b_bool;
131
132     var_Create( p_access, "timeshift-force", VLC_VAR_BOOL|VLC_VAR_DOINHERIT );
133     if( var_GetBool( p_access, "timeshift-force" ) )
134     {
135         msg_Dbg( p_access, "Forcing use of timeshift even if access can control pace or pause" );
136     }
137     else
138     {
139         /* Only work with not pace controled access */
140         if( access_Control( p_src, ACCESS_CAN_CONTROL_PACE, &b_bool ) ||
141             b_bool )
142         {
143             msg_Dbg( p_src, "ACCESS_CAN_CONTROL_PACE: timeshift useless" );
144             return VLC_EGENERIC;
145         }
146         /* Refuse access that can be paused */
147         if( access_Control( p_src, ACCESS_CAN_PAUSE, &b_bool ) || b_bool )
148         {
149             msg_Dbg( p_src, "ACCESS_CAN_PAUSE: timeshift useless" );
150             return VLC_EGENERIC;
151         }
152     }
153
154     /* */
155     p_access->pf_read = NULL;
156     p_access->pf_block = Block;
157     p_access->pf_seek = Seek;
158     p_access->pf_control = Control;
159     p_access->info = p_src->info;
160
161     p_access->p_sys = p_sys = malloc( sizeof( access_sys_t ) );
162     if( !p_sys )
163         return VLC_ENOMEM;
164
165     /* */
166     p_sys->p_fifo = block_FifoNew();
167     p_sys->i_write_size = 0;
168     p_sys->i_files = 0;
169     p_sys->i_data = 0;
170
171     p_sys->p_read_list = NULL;
172     p_sys->pp_read_last = &p_sys->p_read_list;
173     p_sys->p_write_list = NULL;
174     p_sys->pp_write_last = &p_sys->p_write_list;
175
176     var_Create( p_access, "timeshift-dir",
177                 VLC_VAR_DIRECTORY | VLC_VAR_DOINHERIT );
178     var_Create( p_access, "timeshift-granularity",
179                 VLC_VAR_INTEGER | VLC_VAR_DOINHERIT );
180     p_sys->i_file_size = var_GetInteger( p_access, "timeshift-granularity" );
181     if( p_sys->i_file_size < 1 ) p_sys->i_file_size = 1;
182     p_sys->i_file_size *= 1024 * 1024; /* In MBytes */
183
184     p_sys->psz_filename_base = GetTmpFilePath( p_access );
185     p_sys->psz_filename = malloc( strlen( p_sys->psz_filename_base ) + 1000 );
186
187     if( vlc_thread_create( p_access, "timeshift thread", Thread,
188                            VLC_THREAD_PRIORITY_LOW, false ) )
189     {
190         Close( p_this );
191         msg_Err( p_access, "cannot spawn timeshift access thread" );
192         return VLC_EGENERIC;
193     }
194
195     return VLC_SUCCESS;
196 }
197
198 /*****************************************************************************
199  * Close:
200  *****************************************************************************/
201 static void Close( vlc_object_t *p_this )
202 {
203     access_t     *p_access = (access_t*)p_this;
204     access_sys_t *p_sys = p_access->p_sys;
205     ts_entry_t *p_entry;
206     unsigned i;
207
208     msg_Dbg( p_access, "timeshift close called" );
209     vlc_thread_join( p_access );
210
211     for( p_entry = p_sys->p_write_list; p_entry; )
212     {
213         ts_entry_t *p_next = p_entry->p_next;
214         fclose( p_entry->file );
215         free( p_entry );
216         p_entry = p_next;
217     }
218     for( p_entry = p_sys->p_read_list; p_entry; )
219     {
220         ts_entry_t *p_next = p_entry->p_next;
221         fclose( p_entry->file );
222         free( p_entry );
223         p_entry = p_next;
224     }
225     for( i = 0; i < p_sys->i_files; i++ )
226     {
227         sprintf( p_sys->psz_filename, "%s%i.dat",
228                  p_sys->psz_filename_base, i );
229         unlink( p_sys->psz_filename );
230     }
231
232     free( p_sys->psz_filename );
233     free( p_sys->psz_filename_base );
234     block_FifoRelease( p_sys->p_fifo );
235     free( p_sys );
236 }
237
238 /*****************************************************************************
239  *
240  *****************************************************************************/
241 static block_t *Block( access_t *p_access )
242 {
243     access_sys_t *p_sys = p_access->p_sys;
244     access_t *p_src = p_access->p_source;
245     block_t *p_block = NULL;
246
247     /* Update info (we probably ought to be time caching that as well) */
248     if( p_src->info.i_update & INPUT_UPDATE_META )
249     {
250         p_src->info.i_update &= ~INPUT_UPDATE_META;
251         p_access->info.i_update |= INPUT_UPDATE_META;
252     }
253
254     /* Get data from timeshift fifo */
255     if( !p_access->info.b_eof )
256         p_block = block_FifoGet( p_sys->p_fifo );
257
258     if( p_block && !p_block->i_buffer ) /* Used to signal EOF */
259     { block_Release( p_block ); p_block = 0; }
260
261     if( p_block )
262     {
263         p_sys->i_data -= p_block->i_buffer;
264         return p_block;
265     }
266
267     p_access->info.b_eof = p_src->info.b_eof;
268     return NULL;
269 }
270
271 /*****************************************************************************
272  *
273  *****************************************************************************/
274 static void* Thread( vlc_object_t* p_this )
275 {
276     access_t *p_access = (access_t*)p_this;
277     access_sys_t *p_sys = p_access->p_sys;
278     access_t     *p_src = p_access->p_source;
279     block_t      *p_block;
280
281     int canc = vlc_savecancel ();
282     while( vlc_object_alive (p_access) )
283     {
284         /* Get a new block from the source */
285         if( p_src->pf_block )
286         {
287             p_block = p_src->pf_block( p_src );
288         }
289         else
290         {
291             int i_read;
292
293             if( ( p_block = block_New( p_access, 2048 ) ) == NULL ) break;
294
295             i_read = p_src->pf_read( p_src, p_block->p_buffer, 2048 );
296             if( i_read <= 0 )
297             {
298               block_Release( p_block );
299               p_block = NULL;
300             }
301             p_block->i_buffer = i_read;
302         }
303
304         if( p_block == NULL )
305         {
306           if( p_src->info.b_eof ) break;
307           msleep( 10000 );
308           continue;
309         }
310
311         p_sys->i_data += p_block->i_buffer;
312
313         /* Write block */
314         if( !p_sys->p_write_list && !p_sys->p_read_list &&
315             block_FifoSize( p_sys->p_fifo ) < TIMESHIFT_FIFO_MAX )
316         {
317             /* If there isn't too much timeshifted data,
318              * write directly to FIFO */
319             block_FifoPut( p_sys->p_fifo, p_block );
320             continue;
321         }
322
323         WriteBlockToFile( p_access, p_block );
324         block_Release( p_block );
325
326         /* Read from file to fill up the fifo */
327         while( block_FifoSize( p_sys->p_fifo ) < TIMESHIFT_FIFO_MIN &&
328                vlc_object_alive (p_access) )
329         {
330             p_block = ReadBlockFromFile( p_access );
331             if( !p_block ) break;
332
333             block_FifoPut( p_sys->p_fifo, p_block );
334         }
335     }
336
337     msg_Dbg( p_access, "timeshift: no more input data" );
338
339     while( vlc_object_alive (p_access) &&
340            (p_sys->p_read_list || block_FifoSize( p_sys->p_fifo ) ) )
341     {
342         /* Read from file to fill up the fifo */
343         while( block_FifoSize( p_sys->p_fifo ) < TIMESHIFT_FIFO_MIN &&
344                vlc_object_alive (p_access) && p_sys->p_read_list )
345         {
346             p_block = ReadBlockFromFile( p_access );
347             if( !p_block ) break;
348
349             block_FifoPut( p_sys->p_fifo, p_block );
350         }
351
352         msleep( 100000 );
353     }
354
355     msg_Dbg( p_access, "timeshift: EOF" );
356     p_src->info.b_eof = true;
357
358     /* Send dummy packet to avoid deadlock in Block() */
359     block_FifoPut( p_sys->p_fifo, block_New( p_access, 0 ) );
360     vlc_restorecancel (canc);
361     return NULL;
362 }
363
364 /*****************************************************************************
365  * NextFileWrite:
366  *****************************************************************************/
367 static void NextFileWrite( access_t *p_access )
368 {
369     access_sys_t *p_sys = p_access->p_sys;
370     ts_entry_t   *p_next;
371
372     if( !p_sys->p_write_list )
373     {
374         p_sys->i_write_size = 0;
375         return;
376     }
377
378     p_next = p_sys->p_write_list->p_next;
379
380     /* Put written file in read list */
381     if( p_sys->i_write_size < p_sys->i_file_size )
382         ftruncate( fileno( p_sys->p_write_list->file ), p_sys->i_write_size );
383
384     fseek( p_sys->p_write_list->file, 0, SEEK_SET );
385     *p_sys->pp_read_last = p_sys->p_write_list;
386     p_sys->pp_read_last = &p_sys->p_write_list->p_next;
387     p_sys->p_write_list->p_next = 0;
388
389     /* Switch to next file to write */
390     p_sys->p_write_list = p_next;
391     if( !p_sys->p_write_list ) p_sys->pp_write_last = &p_sys->p_write_list;
392
393     p_sys->i_write_size = 0;
394 }
395
396 /*****************************************************************************
397  * NextFileRead:
398  *****************************************************************************/
399 static void NextFileRead( access_t *p_access )
400 {
401     access_sys_t *p_sys = p_access->p_sys;
402     ts_entry_t   *p_next;
403
404     if( !p_sys->p_read_list ) return;
405
406     p_next = p_sys->p_read_list->p_next;
407
408     /* Put read file in write list */
409     fseek( p_sys->p_read_list->file, 0, SEEK_SET );
410     *p_sys->pp_write_last = p_sys->p_read_list;
411     p_sys->pp_write_last = &p_sys->p_read_list->p_next;
412     p_sys->p_read_list->p_next = 0;
413
414     /* Switch to next file to read */
415     p_sys->p_read_list = p_next;
416     if( !p_sys->p_read_list ) p_sys->pp_read_last = &p_sys->p_read_list;
417 }
418
419 /*****************************************************************************
420  * WriteBlockToFile:
421  *****************************************************************************/
422 static int WriteBlockToFile( access_t *p_access, block_t *p_block )
423 {
424     access_sys_t *p_sys = p_access->p_sys;
425     int i_write, i_buffer;
426
427     if( p_sys->i_write_size == p_sys->i_file_size ) NextFileWrite( p_access );
428
429     /* Open new file if necessary */
430     if( !p_sys->p_write_list )
431     {
432         FILE *file;
433
434         sprintf( p_sys->psz_filename, "%s%u.dat",
435                  p_sys->psz_filename_base, p_sys->i_files );
436         file = utf8_fopen( p_sys->psz_filename, "w+b" );
437
438         if( !file && p_sys->i_files < 2 )
439         {
440             /* We just can't work with less than 2 buffer files */
441             msg_Err( p_access, "cannot open temporary file '%s' (%m)",
442                      p_sys->psz_filename );
443             return VLC_EGENERIC;
444         }
445         else if( !file ) return VLC_EGENERIC;
446
447         p_sys->p_write_list = malloc( sizeof(ts_entry_t) );
448         p_sys->p_write_list->p_next = 0;
449         p_sys->p_write_list->file = file;
450         p_sys->pp_write_last = &p_sys->p_write_list->p_next;
451
452         p_sys->i_files++;
453     }
454
455     /* Write to file */
456     i_buffer = __MIN( p_block->i_buffer,
457                       p_sys->i_file_size - p_sys->i_write_size );
458
459     i_write = fwrite( p_block->p_buffer, 1, i_buffer,
460                       p_sys->p_write_list->file );
461
462     if( i_write > 0 ) p_sys->i_write_size += i_write;
463
464     //p_access->info.i_size += i_write;
465     //p_access->info.i_update |= INPUT_UPDATE_SIZE;
466
467     if( i_write < i_buffer )
468     {
469         /* Looks like we're short of space */
470
471         if( !p_sys->p_write_list->p_next )
472         {
473             msg_Warn( p_access, "no more space, overwritting old data" );
474             NextFileRead( p_access );
475             NextFileRead( p_access );
476         }
477
478         /* Make sure we switch to next file in write list */
479         p_sys->i_write_size = p_sys->i_file_size;
480     }
481
482     p_block->p_buffer += i_write;
483     p_block->i_buffer -= i_write;
484
485     /* Check if we have some data left */
486     if( p_block->i_buffer ) return WriteBlockToFile( p_access, p_block );
487
488     return VLC_SUCCESS;
489 }
490
491 /*****************************************************************************
492  * ReadBlockFromFile:
493  *****************************************************************************/
494 static block_t *ReadBlockFromFile( access_t *p_access )
495 {
496     access_sys_t *p_sys = p_access->p_sys;
497     block_t *p_block;
498
499     if( !p_sys->p_read_list && p_sys->p_write_list )
500     {
501         /* Force switching to next write file, that should
502          * give us something to read */
503         NextFileWrite( p_access );
504     }
505
506     if( !p_sys->p_read_list ) return 0;
507
508     p_block = block_New( p_access, 4096 );
509     p_block->i_buffer = fread( p_block->p_buffer, 1, 4096,
510                                p_sys->p_read_list->file );
511
512     if( p_block->i_buffer == 0 ) NextFileRead( p_access );
513
514     //p_access->info.i_size -= p_block->i_buffer;
515     //p_access->info.i_update |= INPUT_UPDATE_SIZE;
516
517     return p_block;
518 }
519
520 /*****************************************************************************
521  * Seek: seek to a specific location in a file
522  *****************************************************************************/
523 static int Seek( access_t *p_access, int64_t i_pos )
524 {
525     //access_sys_t *p_sys = p_access->p_sys;
526     (void)p_access;
527     (void)i_pos;
528     return VLC_SUCCESS;
529 }
530
531 /*****************************************************************************
532  *
533  *****************************************************************************/
534 static int Control( access_t *p_access, int i_query, va_list args )
535 {
536     bool   *pb_bool;
537     int          *pi_int;
538
539     switch( i_query )
540     {
541     case ACCESS_CAN_SEEK:
542     case ACCESS_CAN_FASTSEEK:
543         pb_bool = (bool*)va_arg( args, bool* );
544         *pb_bool = true;
545         break;
546
547     case ACCESS_CAN_CONTROL_PACE:   /* Not really true */
548     case ACCESS_CAN_PAUSE:
549         pb_bool = (bool*)va_arg( args, bool* );
550         *pb_bool = true;
551         break;
552
553     case ACCESS_GET_MTU:
554         pi_int = (int*)va_arg( args, int * );
555         *pi_int = 0;
556         break;
557
558     case ACCESS_SET_PAUSE_STATE:
559         break;
560
561     /* Forward everything else to the source access */
562     default:
563         return access_vaControl( p_access->p_source, i_query, args );
564     }
565     return VLC_SUCCESS;
566 }
567
568 /*****************************************************************************
569  * GetTmpFilePath:
570  *****************************************************************************/
571 #ifdef WIN32
572 #define getpid() (int)GetCurrentProcessId()
573 #endif
574 static char *GetTmpFilePath( access_t *p_access )
575 {
576     char *psz_dir = var_GetNonEmptyString( p_access, "timeshift-dir" );
577     char *psz_filename_base;
578
579     if( psz_dir == NULL )
580     {
581 #ifdef WIN32
582         DWORD ret = GetTempPathW (0, NULL);
583         wchar_t wdir[ret + 3]; // can at least old "C:" + nul
584         const wchar_t *pwdir = wdir;
585         wchar_t *pwdir_free = NULL;
586
587         if (GetTempPathW (ret + 1, wdir) == 0)
588         {
589             pwdir_free = pwdir = _wgetcwd (NULL, 0);
590             if (pwdir == NULL)
591                 pwdir = L"C:";
592         }
593
594         psz_dir = FromWide (pwdir);
595         if (pwdir_free != NULL)
596             free (pwdir_free);
597
598         /* remove trailing antislash if any */
599         if (psz_dir[strlen (psz_dir) - 1] == '\\')
600             psz_dir[strlen (psz_dir) - 1] = '\0';
601 #else
602         psz_dir = strdup( "/tmp" );
603 #endif
604     }
605
606     if( asprintf( &psz_filename_base, "%s/vlc-timeshift-%d-%p-",
607               psz_dir, getpid(), p_access ) == -1 )
608         psz_filename_base = NULL;
609     free( psz_dir );
610
611     return psz_filename_base;
612 }