simple video subtitles support, defined in external text files, added a couple of...
[divverent/darkplaces.git] / cl_video.c
1
2 #include "quakedef.h"
3 #include "cl_dyntexture.h"
4 #include "cl_video.h"
5 #include "dpvsimpledecode.h"
6
7 // cvars
8 cvar_t cl_video_subtitles = {CVAR_SAVE, "cl_video_subtitles", "0", "show subtitles for videos (if they are presented)"};
9 cvar_t cl_video_subtitles_lines = {CVAR_SAVE, "cl_video_subtitles_lines", "4", "how many lines to occupy for subtitles"};
10 cvar_t cl_video_subtitles_textsize = {CVAR_SAVE, "cl_video_subtitles_textsize", "16", "textsize for subtitles"};
11 cvar_t cl_video_scale = {CVAR_SAVE, "cl_video_scale", "1", "scale of video, 1 = fullscreen, 0.75 - 3/4 of screen etc."};
12 cvar_t cl_video_brightness = {CVAR_SAVE, "cl_video_brightness", "1", "brightness of video, 1 = fullbright, 0.75 - 3/4 etc."};
13
14 // constants (and semi-constants)
15 static int  cl_videormask;
16 static int  cl_videobmask;
17 static int  cl_videogmask;
18 static int      cl_videobytesperpixel;
19
20 static int cl_num_videos;
21 static clvideo_t cl_videos[ MAXCLVIDEOS ];
22 static rtexturepool_t *cl_videotexturepool;
23
24 static clvideo_t *FindUnusedVid( void )
25 {
26         int i;
27         for( i = 1 ; i < MAXCLVIDEOS ; i++ )
28                 if( cl_videos[ i ].state == CLVIDEO_UNUSED )
29                         return &cl_videos[ i ];
30         return NULL;
31 }
32
33 static qboolean OpenStream( clvideo_t * video )
34 {
35         char *errorstring;
36         video->stream = dpvsimpledecode_open( video->filename, &errorstring);
37         if (!video->stream )
38         {
39                 Con_Printf("unable to open \"%s\", error: %s\n", video->filename, errorstring);
40                 return false;
41         }
42         return true;
43 }
44
45 static void VideoUpdateCallback(rtexture_t *rt, void *data) {
46         clvideo_t *video = (clvideo_t *) data;
47         R_UpdateTexture( video->cpif.tex, (unsigned char *)video->imagedata, 0, 0, video->cpif.width, video->cpif.height );
48 }
49
50 static void LinkVideoTexture( clvideo_t *video ) {
51         video->cpif.tex = R_LoadTexture2D( cl_videotexturepool, video->cpif.name,
52                 video->cpif.width, video->cpif.height, NULL, TEXTYPE_BGRA, TEXF_PERSISTENT | TEXF_ALLOWUPDATES, NULL );
53         R_MakeTextureDynamic( video->cpif.tex, VideoUpdateCallback, video );
54         CL_LinkDynTexture( video->cpif.name, video->cpif.tex );
55 }
56
57 static void UnlinkVideoTexture( clvideo_t *video ) {
58         CL_UnlinkDynTexture( video->cpif.name );
59         // free the texture
60         R_FreeTexture( video->cpif.tex );
61         // free the image data
62         Mem_Free( video->imagedata );
63 }
64
65 static void SuspendVideo( clvideo_t * video )
66 {
67         if( video->suspended )
68                 return;
69         video->suspended = true;
70         UnlinkVideoTexture( video );
71         // if we are in firstframe mode, also close the stream
72         if( video->state == CLVIDEO_FIRSTFRAME )
73                 dpvsimpledecode_close( video->stream );
74 }
75
76 static qboolean WakeVideo( clvideo_t * video )
77 {
78         if( !video->suspended )
79                 return true;
80         video->suspended = false;
81
82         if( video->state == CLVIDEO_FIRSTFRAME )
83                 if( !OpenStream( video ) ) {
84                         video->state = CLVIDEO_UNUSED;
85                         return false;
86                 }
87
88         video->imagedata = Mem_Alloc( cls.permanentmempool, video->cpif.width * video->cpif.height * cl_videobytesperpixel );
89         LinkVideoTexture( video );
90
91         // update starttime
92         video->starttime += realtime - video->lasttime;
93
94         return true;
95 }
96
97 static void LoadSubtitles( clvideo_t *video, const char *subtitlesfile )
98 {
99         char *subtitle_text, *data;
100         float subtime, sublen;
101         int numsubs = 0;
102
103         subtitle_text = (char *)FS_LoadFile(subtitlesfile, cls.permanentmempool, false, NULL);
104         if (!subtitle_text)
105         {
106                 Con_DPrintf( "LoadSubtitles: can't open subtitle file '%s'!\n", subtitlesfile );
107                 return;
108         }
109
110         // parse subtitle_text
111         // line is: x y "text" where
112         //    x - start time
113         //    y - seconds last (if 0 - last thru next sub, if negative - last to next sub - this amount of seconds)
114
115         data = subtitle_text;
116         for (;;)
117         {
118                 if (!COM_ParseToken_QuakeC(&data, false))
119                         break;
120                 subtime = atof( com_token );
121                 if (!COM_ParseToken_QuakeC(&data, false))
122                         break;
123                 sublen = atof( com_token );
124                 if (!COM_ParseToken_QuakeC(&data, false))
125                         break;
126                 if (!com_token[0])
127                         continue;
128                 // check limits
129                 if (video->subtitles == CLVIDEO_MAX_SUBTITLES)
130                 {
131                         Con_Printf("WARNING: CLVIDEO_MAX_SUBTITLES = %i reached when reading subtitles from '%s'\n", CLVIDEO_MAX_SUBTITLES, subtitlesfile);
132                         break;  
133                 }
134                 // add a sub
135                 video->subtitle_text[numsubs] = (char *) Mem_Alloc(cls.permanentmempool, strlen(com_token) + 1);
136                 memcpy(video->subtitle_text[numsubs], com_token, strlen(com_token) + 1);
137                 video->subtitle_start[numsubs] = subtime;
138                 video->subtitle_end[numsubs] = sublen;
139                 if (numsubs > 0) // make true len for prev sub, autofix overlapping subtitles
140                 {
141                         if (video->subtitle_end[numsubs-1] <= 0)
142                                 video->subtitle_end[numsubs-1] = max(video->subtitle_start[numsubs-1], video->subtitle_start[numsubs] - video->subtitle_end[numsubs-1]);
143                         else
144                                 video->subtitle_end[numsubs-1] = min(video->subtitle_start[numsubs-1] + video->subtitle_end[numsubs-1], video->subtitle_start[numsubs]);
145                 }
146                 numsubs++;
147                 // todo: check timing for consistency?
148         }
149         if (numsubs > 0) // make true len for prev sub, autofix overlapping subtitles
150         {
151                 if (video->subtitle_end[numsubs-1] <= 0)
152                         video->subtitle_end[numsubs-1] = 99999999; // fixme: make it end when video ends?
153                 else
154                         video->subtitle_end[numsubs-1] = min(video->subtitle_start[numsubs-1] + video->subtitle_end[numsubs-1], video->subtitle_start[numsubs]);
155         }
156         Z_Free( subtitle_text );
157         video->subtitles = numsubs;
158 /*
159         Con_Printf( "video->subtitles: %i\n", video->subtitles );
160         for (numsubs = 0; numsubs < video->subtitles; numsubs++)
161                 Con_Printf( "  %03.2f %03.2f : %s\n", video->subtitle_start[numsubs], video->subtitle_end[numsubs], video->subtitle_text[numsubs] );
162 */
163 }
164
165 static clvideo_t* OpenVideo( clvideo_t *video, const char *filename, const char *name, int owner, const char *subtitlesfile )
166 {
167         strlcpy( video->filename, filename, sizeof(video->filename) );
168         video->ownertag = owner;
169         if( strncmp( name, CLVIDEOPREFIX, sizeof( CLVIDEOPREFIX ) - 1 ) )
170                 return NULL;
171         strlcpy( video->cpif.name, name, sizeof(video->cpif.name) );
172
173         if( !OpenStream( video ) )
174                 return NULL;
175
176         video->state = CLVIDEO_FIRSTFRAME;
177         video->framenum = -1;
178         video->framerate = dpvsimpledecode_getframerate( video->stream );
179         video->lasttime = realtime;
180         video->subtitles = 0;
181
182         video->cpif.width = dpvsimpledecode_getwidth( video->stream );
183         video->cpif.height = dpvsimpledecode_getheight( video->stream );
184         video->imagedata = Mem_Alloc( cls.permanentmempool, video->cpif.width * video->cpif.height * cl_videobytesperpixel );
185         LinkVideoTexture( video );
186
187         // VorteX: load simple subtitle_text file
188         if (subtitlesfile[0])
189                 LoadSubtitles( video, subtitlesfile );
190
191         return video;
192 }
193
194 clvideo_t* CL_OpenVideo( const char *filename, const char *name, int owner, const char *subtitlesfile )
195 {
196         clvideo_t *video;
197         // sanity check
198         if( !name || !*name || strncmp( name, CLVIDEOPREFIX, sizeof( CLVIDEOPREFIX ) - 1 ) != 0 ) {
199                 Con_DPrintf( "CL_OpenVideo: Bad video texture name '%s'!\n", name );
200                 return NULL;
201         }
202
203         video = FindUnusedVid();
204         if( !video ) {
205                 Con_Printf( "CL_OpenVideo: unable to open video \"%s\" - video limit reached\n", filename );
206                 return NULL;
207         }
208         video = OpenVideo( video, filename, name, owner, subtitlesfile );
209         // expand the active range to include the new entry
210         if (video) {
211                 cl_num_videos = max(cl_num_videos, (int)(video - cl_videos) + 1);
212         }
213         return video;
214 }
215
216 static clvideo_t* CL_GetVideoBySlot( int slot )
217 {
218         clvideo_t *video = &cl_videos[ slot ];
219
220         if( video->suspended )
221         {
222                 if( !WakeVideo( video ) )
223                         return NULL;
224                 else if( video->state == CLVIDEO_RESETONWAKEUP )
225                         video->framenum = -1;
226         }
227
228         video->lasttime = realtime;
229
230         return video;
231 }
232
233 clvideo_t *CL_GetVideoByName( const char *name )
234 {
235         int i;
236
237         for( i = 0 ; i < cl_num_videos ; i++ )
238                 if( cl_videos[ i ].state != CLVIDEO_UNUSED
239                         &&      !strcmp( cl_videos[ i ].cpif.name , name ) )
240                         break;
241         if( i != cl_num_videos )
242                 return CL_GetVideoBySlot( i );
243         else
244                 return NULL;
245 }
246
247 void CL_SetVideoState( clvideo_t *video, clvideostate_t state )
248 {
249         if( !video )
250                 return;
251
252         video->lasttime = realtime;
253         video->state = state;
254         if( state == CLVIDEO_FIRSTFRAME )
255                 CL_RestartVideo( video );
256 }
257
258 void CL_RestartVideo( clvideo_t *video )
259 {
260         if( !video )
261                 return;
262
263         video->starttime = video->lasttime = realtime;
264         video->framenum = -1;
265
266         dpvsimpledecode_close( video->stream );
267         if( !OpenStream( video ) )
268                 video->state = CLVIDEO_UNUSED;
269 }
270
271 void CL_CloseVideo( clvideo_t * video )
272 {
273         int i;
274
275         if( !video || video->state == CLVIDEO_UNUSED )
276                 return;
277
278         if( !video->suspended || video->state != CLVIDEO_FIRSTFRAME )
279                 dpvsimpledecode_close( video->stream );
280         if( !video->suspended )
281                 UnlinkVideoTexture( video );
282         if (video->subtitles)
283         {
284                 for (i = 0; i < video->subtitles; i++)
285                         Z_Free( video->subtitle_text[i] );
286                 video->subtitles = 0;
287         }
288
289         video->state = CLVIDEO_UNUSED;
290 }
291
292 static void VideoFrame( clvideo_t *video )
293 {
294         int destframe;
295
296         if( video->state == CLVIDEO_FIRSTFRAME )
297                 destframe = 0;
298         else
299                 destframe = (int)((realtime - video->starttime) * video->framerate);
300         if( destframe < 0 )
301                 destframe = 0;
302         if( video->framenum < destframe ) {
303                 do {
304                         video->framenum++;
305                         if( dpvsimpledecode_video( video->stream, video->imagedata, cl_videormask,
306                                 cl_videogmask, cl_videobmask, cl_videobytesperpixel,
307                                 cl_videobytesperpixel * video->cpif.width )
308                                 ) { // finished?
309                                 CL_RestartVideo( video );
310                                 if( video->state == CLVIDEO_PLAY )
311                                                 video->state = CLVIDEO_FIRSTFRAME;
312                                 return;
313                         }
314                 } while( video->framenum < destframe );
315                 R_MarkDirtyTexture( video->cpif.tex );
316         }
317 }
318
319 void CL_Video_Frame( void ) // update all videos
320 {
321         int i;
322         clvideo_t *video;
323
324         if (!cl_num_videos)
325                 return;
326
327         for( video = cl_videos, i = 0 ; i < cl_num_videos ; video++, i++ )
328                 if( video->state != CLVIDEO_UNUSED && !video->suspended )
329                 {
330                         if( realtime - video->lasttime > CLTHRESHOLD )
331                                 SuspendVideo( video );
332                         else if( video->state == CLVIDEO_PAUSE )
333                                 video->starttime = realtime - video->framenum * video->framerate;
334                         else
335                                 VideoFrame( video );
336                 }
337
338         if( cl_videos->state == CLVIDEO_FIRSTFRAME )
339                 CL_VideoStop();
340
341         // reduce range to exclude unnecessary entries
342         while (cl_num_videos > 0 && cl_videos[cl_num_videos-1].state == CLVIDEO_UNUSED)
343                 cl_num_videos--;
344 }
345
346 void CL_Video_Shutdown( void )
347 {
348         int i;
349         for( i = 0 ; i < cl_num_videos ; i++ )
350                 CL_CloseVideo( &cl_videos[ i ] );
351 }
352
353 void CL_PurgeOwner( int owner )
354 {
355         int i;
356         for( i = 0 ; i < cl_num_videos ; i++ )
357                 if( cl_videos[ i ].ownertag == owner )
358                         CL_CloseVideo( &cl_videos[ i ] );
359 }
360
361 typedef struct
362 {
363         dp_font_t *font;
364         float x;
365         float y;
366         float width;
367         float height;
368         float alignment; // 0 = left, 0.5 = center, 1 = right
369         float fontsize;
370         float textalpha;
371 }
372 cl_video_subtitle_info_t;
373
374 float CL_DrawVideo_WordWidthFunc(void *passthrough, const char *w, size_t *length, float maxWidth)
375 {
376         cl_video_subtitle_info_t *si = (cl_video_subtitle_info_t *) passthrough;
377
378         if(w == NULL)
379                 return si->fontsize * si->font->maxwidth;
380         if(maxWidth >= 0)
381                 return DrawQ_TextWidth_UntilWidth(w, length, si->fontsize, si->fontsize, false, si->font, -maxWidth); // -maxWidth: we want at least one char
382         else if(maxWidth == -1)
383                 return DrawQ_TextWidth(w, *length, si->fontsize, si->fontsize, false, si->font);
384         else
385                 return 0;
386 }
387
388 int CL_DrawVideo_DisplaySubtitleLine(void *passthrough, const char *line, size_t length, float width, qboolean isContinuation)
389 {
390         cl_video_subtitle_info_t *si = (cl_video_subtitle_info_t *) passthrough;
391
392         int x = (int) (si->x + (si->width - width) * si->alignment);
393         if (length > 0)
394                 DrawQ_String(x, si->y, line, length, si->fontsize, si->fontsize, 1.0, 1.0, 1.0, si->textalpha, 0, NULL, false, si->font);
395         si->y += si->fontsize;
396         return 1;
397 }
398
399 int cl_videoplaying = false; // old, but still supported
400
401 void CL_DrawVideo(void)
402 {
403         clvideo_t *video;
404         float videotime;
405         cl_video_subtitle_info_t si;
406         int i;
407
408         if (!cl_videoplaying)
409                 return;
410
411         video = CL_GetVideoBySlot( 0 );
412
413         // fix cvars
414         if (cl_video_scale.value <= 0 || cl_video_scale.value > 1)
415                 Cvar_SetValueQuick( &cl_video_scale, 1);
416         if (cl_video_brightness.value <= 0 || cl_video_brightness.value > 10)
417                 Cvar_SetValueQuick( &cl_video_brightness, 1);
418
419 #if 0
420         // enable video-only polygon stipple (of global stipple is not active)
421         if (qglPolygonStipple && !scr_stipple.integer)
422         {
423                 GLubyte stipple[128];
424                 int i, s, width, parts;
425         
426                 s = 1;
427                 parts = (s & 007);
428                 width = (s & 070) >> 3;
429                 qglEnable(GL_POLYGON_STIPPLE);CHECKGLERROR // 0x0B42
430                 for(i = 0; i < 128; ++i)
431                 {
432                         int line = i/4;
433                         stipple[i] = ((line >> width) & ((1 << parts) - 1)) ? 0x00 : 0xFF;
434                 }
435                 qglPolygonStipple(stipple);CHECKGLERROR
436         }
437 #endif
438
439         // draw video
440         if (cl_video_scale.value == 1)
441                 DrawQ_Pic(0, 0, &video->cpif, vid_conwidth.integer, vid_conheight.integer, cl_video_brightness.value, cl_video_brightness.value, cl_video_brightness.value, 1, 0);
442         else
443         {
444                 DrawQ_Fill(0, 0, vid_conwidth.integer, vid_conheight.integer, 0, 0, 0, 1, 0);
445                 DrawQ_Pic((int)(vid_conwidth.integer * (1 - cl_video_scale.value) * 0.5), (int)(vid_conheight.integer * (1 - cl_video_scale.value) * 0.5), &video->cpif, (int)(vid_conwidth.integer * cl_video_scale.value), (int)(vid_conheight.integer * cl_video_scale.value), cl_video_brightness.value, cl_video_brightness.value, cl_video_brightness.value, 1, 0);
446         }
447
448         
449 #if 0
450         // disable video-only stipple
451         if (qglPolygonStipple && !scr_stipple.integer)
452         {
453                 qglDisable(GL_POLYGON_STIPPLE);CHECKGLERROR
454         }
455 #endif
456
457         // VorteX: draw subtitle_text
458         if (!video->subtitles || !cl_video_subtitles.integer)
459                 return;
460
461         // find current subtitle
462         videotime = realtime - video->starttime;
463         for (i = 0; i < video->subtitles; i++)
464         {
465                 if (videotime >= video->subtitle_start[i] && videotime <= video->subtitle_end[i])
466                 {
467                         // found, draw it
468                         si.font = FONT_NOTIFY;
469                         si.x = vid_conwidth.integer * 0.1;
470                         si.y = vid_conheight.integer - (max(1, cl_video_subtitles_lines.integer) * cl_video_subtitles_textsize.value);
471                         si.width = vid_conwidth.integer * 0.8;
472                         si.height = max(1, cl_video_subtitles_lines.integer) * cl_video_subtitles_textsize.value;
473                         si.alignment = 0.5;
474                         si.fontsize = cl_video_subtitles_textsize.value;
475                         si.textalpha = min(1, (videotime - video->subtitle_start[i])/0.5) * min(1, ((video->subtitle_end[i] - videotime)/0.3)); // fade in and fade out
476                         COM_Wordwrap(video->subtitle_text[i], strlen(video->subtitle_text[i]), 0, si.width, CL_DrawVideo_WordWidthFunc, &si, CL_DrawVideo_DisplaySubtitleLine, &si);
477                         break;
478                 }
479         }
480 }
481
482 void CL_VideoStart(char *filename, const char *subtitlesfile)
483 {
484         Host_StartVideo();
485
486         if( cl_videos->state != CLVIDEO_UNUSED )
487                 CL_CloseVideo( cl_videos );
488         // already contains video/
489         if( !OpenVideo( cl_videos, filename, va( CLDYNTEXTUREPREFIX "%s", filename ), 0, subtitlesfile ) )
490                 return;
491         // expand the active range to include the new entry
492         cl_num_videos = max(cl_num_videos, 1);
493
494         cl_videoplaying = true;
495
496         CL_SetVideoState( cl_videos, CLVIDEO_PLAY );
497         CL_RestartVideo( cl_videos );
498 }
499
500 void CL_Video_KeyEvent( int key, int ascii, qboolean down ) 
501 {
502         // only react to up events, to allow the user to delay the abortion point if it suddenly becomes interesting..
503         if( !down ) {
504                 if( key == K_ESCAPE || key == K_ENTER || key == K_SPACE ) {
505                         CL_VideoStop();
506                 }
507         }
508 }
509
510 void CL_VideoStop(void)
511 {
512         cl_videoplaying = false;
513
514         CL_CloseVideo( cl_videos );
515 }
516
517 static void CL_PlayVideo_f(void)
518 {
519         char name[MAX_QPATH], subtitlesfile[MAX_QPATH];
520
521         Host_StartVideo();
522
523         if (Cmd_Argc() < 2)
524         {
525                 Con_Print("usage: playvideo <videoname> [custom_subtitles_file]\nplays video named video/<videoname>.dpv\nif custom subtitles file is not presented\nit tries video/<videoname>.sub");
526                 return;
527         }
528
529         dpsnprintf(name, sizeof(name), "video/%s.dpv", Cmd_Argv(1));
530         if ( Cmd_Argc() > 2)
531                 CL_VideoStart(name, Cmd_Argv(2));
532         else
533         {
534                 dpsnprintf(subtitlesfile, sizeof(subtitlesfile), "video/%s.dpsubs", Cmd_Argv(1));
535                 CL_VideoStart(name, subtitlesfile);
536         }
537 }
538
539 static void CL_StopVideo_f(void)
540 {
541         CL_VideoStop();
542 }
543
544 static void cl_video_start( void )
545 {
546         int i;
547         clvideo_t *video;
548
549         cl_videotexturepool = R_AllocTexturePool();
550
551         for( video = cl_videos, i = 0 ; i < cl_num_videos ; i++, video++ )
552                 if( video->state != CLVIDEO_UNUSED && !video->suspended )
553                         LinkVideoTexture( video );
554 }
555
556 static void cl_video_shutdown( void )
557 {
558         // TODO: unlink video textures?
559         R_FreeTexturePool( &cl_videotexturepool );
560 }
561
562 static void cl_video_newmap( void )
563 {
564 }
565
566 void CL_Video_Init( void )
567 {
568         union
569         {
570                 unsigned char b[4];
571                 unsigned int i;
572         }
573         bgra;
574
575         cl_num_videos = 0;
576         cl_videobytesperpixel = 4;
577
578         // set masks in an endian-independent way (as they really represent bytes)
579         bgra.i = 0;bgra.b[0] = 0xFF;cl_videobmask = bgra.i;
580         bgra.i = 0;bgra.b[1] = 0xFF;cl_videogmask = bgra.i;
581         bgra.i = 0;bgra.b[2] = 0xFF;cl_videormask = bgra.i;
582
583         Cmd_AddCommand( "playvideo", CL_PlayVideo_f, "play a .dpv video file" );
584         Cmd_AddCommand( "stopvideo", CL_StopVideo_f, "stop playing a .dpv video file" );
585
586         Cvar_RegisterVariable(&cl_video_subtitles);
587         Cvar_RegisterVariable(&cl_video_subtitles_lines);
588         Cvar_RegisterVariable(&cl_video_subtitles_textsize);
589         Cvar_RegisterVariable(&cl_video_scale);
590         Cvar_RegisterVariable(&cl_video_brightness);
591
592         R_RegisterModule( "CL_Video", cl_video_start, cl_video_shutdown, cl_video_newmap );
593 }
594