1 //=============================================================================
2 //
3 // Adventure Game Studio (AGS)
4 //
5 // Copyright (C) 1999-2011 Chris Jones and 2011-20xx others
6 // The full list of copyright holders can be found in the Copyright.txt
7 // file, which is part of this source code distribution.
8 //
9 // The AGS source code is provided under the Artistic License 2.0.
10 // A copy of this license can be found in the file License.txt and at
11 // http://www.opensource.org/licenses/artistic-license-2.0.php
12 //
13 //=============================================================================
14 
15 #include <stdio.h>
16 #include "video.h"
17 #include "apeg.h"
18 #include "debug/debug_log.h"
19 #include "debug/out.h"
20 #include "ac/asset_helper.h"
21 #include "ac/common.h"
22 #include "ac/draw.h"
23 #include "ac/game_version.h"
24 #include "ac/gamesetupstruct.h"
25 #include "ac/gamestate.h"
26 #include "ac/global_display.h"
27 #include "ac/mouse.h"
28 #include "ac/record.h"
29 #include "ac/runtime_defines.h"
30 #include "ac/system.h"
31 #include "core/assetmanager.h"
32 #include "gfx/bitmap.h"
33 #include "gfx/ddb.h"
34 #include "gfx/graphicsdriver.h"
35 #include "main/game_run.h"
36 #include "media/audio/audio.h"
37 #include "util/stream.h"
38 
39 #if (ALLEGRO_DATE >= 20190303) || defined (WINDOWS_VERSION) || defined (ANDROID_VERSION)
40 #define AGS_FLI_FROM_PACK_FILE
41 #endif
42 
43 using namespace AGS::Common;
44 using namespace AGS::Engine;
45 
46 
47 extern GameSetupStruct game;
48 extern IGraphicsDriver *gfxDriver;
49 extern Bitmap *virtual_screen;
50 extern int psp_video_framedrop;
51 
52 enum VideoPlaybackType
53 {
54     kVideoNone,
55     kVideoFlic,
56     kVideoTheora
57 };
58 
59 VideoPlaybackType video_type = kVideoNone;
60 
61 // FLIC player start
62 Bitmap *fli_buffer = NULL;
63 short fliwidth,fliheight;
64 int canabort=0, stretch_flc = 1;
65 Bitmap *hicol_buf=NULL;
66 IDriverDependantBitmap *fli_ddb = NULL;
67 Bitmap *fli_target = NULL;
68 int fliTargetWidth, fliTargetHeight;
check_if_user_input_should_cancel_video()69 int check_if_user_input_should_cancel_video()
70 {
71     NEXT_ITERATION();
72     int key;
73     if (run_service_key_controls(key)) {
74         if ((key==27) && (canabort==1))
75             return 1;
76         if (canabort >= 2)
77             return 1;  // skip on any key
78     }
79     if (canabort == 3) {  // skip on mouse click
80         if (mgetbutton()!=NONE) return 1;
81     }
82     return 0;
83 }
84 
85 #if defined(WINDOWS_VERSION)
fli_callback()86 int __cdecl fli_callback() {
87 #else
88 extern "C" int fli_callback() {
89 #endif
90     Bitmap *usebuf = fli_buffer;
91 
92     update_polled_audio_and_crossfade ();
93 
94     if (game.color_depth > 1) {
95         hicol_buf->Blit(fli_buffer,0,0,0,0,fliwidth,fliheight);
96         usebuf=hicol_buf;
97     }
98     if (stretch_flc == 0)
99         fli_target->Blit(usebuf, 0,0,play.viewport.GetWidth()/2-fliwidth/2,play.viewport.GetHeight()/2-fliheight/2,play.viewport.GetWidth(),play.viewport.GetHeight());
100     else
101         fli_target->StretchBlt(usebuf, RectWH(0,0,fliwidth,fliheight), RectWH(0,0,play.viewport.GetWidth(),play.viewport.GetHeight()));
102 
103     gfxDriver->UpdateDDBFromBitmap(fli_ddb, fli_target, false);
104     gfxDriver->DrawSprite(0, 0, fli_ddb);
105     render_to_screen(fli_target, 0, 0);
106 
107     return check_if_user_input_should_cancel_video();
108 }
109 
110 void play_flc_file(int numb,int playflags) {
111     color oldpal[256];
112 
113     // AGS 2.x: If the screen is faded out, fade in again when playing a movie.
114     if (loaded_game_file_version <= kGameVersion_272)
115         play.screen_is_faded_out = 0;
116 
117     if (play.fast_forward)
118         return;
119 
120     get_palette_range(oldpal, 0, 255);
121 
122     int clearScreenAtStart = 1;
123     canabort = playflags % 10;
124     playflags -= canabort;
125 
126     if (canabort == 2) // convert to PlayVideo-compatible setting
127         canabort = 3;
128 
129     if (playflags % 100 == 0)
130         stretch_flc = 1;
131     else
132         stretch_flc = 0;
133 
134     if (playflags / 100)
135         clearScreenAtStart = 0;
136 
137     String flicname = String::FromFormat("flic%d.flc", numb);
138     Stream *in = AssetManager::OpenAsset(flicname);
139     if (!in)
140     {
141         flicname.Format("flic%d.fli", numb);
142         in = AssetManager::OpenAsset(flicname);
143     }
144     if (!in)
145     {
146         debug_script_warn("FLIC animation flic%d.flc nor flic%d.fli not found", numb, numb);
147         return;
148     }
149 
150     in->Seek(8);
151     fliwidth = in->ReadInt16();
152     fliheight = in->ReadInt16();
153     delete in;
154 
155     if (game.color_depth > 1) {
156         hicol_buf=BitmapHelper::CreateBitmap(fliwidth,fliheight,game.GetColorDepth());
157         hicol_buf->Clear();
158     }
159     // override the stretch option if necessary
160     if ((fliwidth==play.viewport.GetWidth()) && (fliheight==play.viewport.GetHeight()))
161         stretch_flc = 0;
162     else if ((fliwidth > play.viewport.GetWidth()) || (fliheight > play.viewport.GetHeight()))
163         stretch_flc = 1;
164     fli_buffer=BitmapHelper::CreateBitmap(fliwidth,fliheight,8);
165     if (fli_buffer==NULL) quit("Not enough memory to play animation");
166     fli_buffer->Clear();
167 
168     Bitmap *screen_bmp = BitmapHelper::GetScreenBitmap();
169 
170     if (clearScreenAtStart) {
171         screen_bmp->Clear();
172         render_to_screen(screen_bmp, 0, 0);
173     }
174 
175     video_type = kVideoFlic;
176     fli_target = BitmapHelper::CreateBitmap(screen_bmp->GetWidth(), screen_bmp->GetHeight(), game.GetColorDepth());
177     fli_ddb = gfxDriver->CreateDDBFromBitmap(fli_target, false, true);
178 
179     // TODO: find a better solution.
180     // Make only certain versions of the engineuse play_fli_pf from the patched version of Allegro for now.
181     // Add more versions as their Allegro lib becomes patched too, or they use newer version of Allegro 4.
182     // Ports can still play FLI if separate file is put into game's directory.
183 #if defined (AGS_FLI_FROM_PACK_FILE)
184     PACKFILE *pf = PackfileFromAsset(AssetPath("", flicname));
185     if (play_fli_pf(pf, (BITMAP*)fli_buffer->GetAllegroBitmap(), fli_callback)==FLI_ERROR)
186 #else
187     if (play_fli(flicname, (BITMAP*)fli_buffer->GetAllegroBitmap(), 0, fli_callback)==FLI_ERROR)
188 #endif
189     {
190         // This is not a fatal error that should prevent the game from continuing
191         Debug::Printf("FLI/FLC animation play error");
192     }
193 #if defined WINDOWS_VERSION
194     pack_fclose(pf);
195 #endif
196 
197     video_type = kVideoNone;
198     delete fli_buffer;
199     fli_buffer = NULL;
200     // NOTE: the screen bitmap could change in the meanwhile, if the display mode has changed
201     screen_bmp = BitmapHelper::GetScreenBitmap();
202     screen_bmp->Clear();
203     set_palette_range(oldpal, 0, 255, 0);
204     render_to_screen(screen_bmp, 0, 0);
205 
206     delete fli_target;
207     gfxDriver->DestroyDDB(fli_ddb);
208     fli_target = NULL;
209     fli_ddb = NULL;
210 
211 
212     delete hicol_buf;
213     hicol_buf=NULL;
214     //  SetVirtualScreen(screen); wputblock(0,0,backbuffer,0);
215     while (mgetbutton()!=NONE) ;
216     invalidate_screen();
217 }
218 
219 // FLIC player end
220 
221 // Theora player begin
222 // TODO: find a way to take Bitmap here?
223 Bitmap gl_TheoraBuffer;
224 int theora_playing_callback(BITMAP *theoraBuffer)
225 {
226 	if (theoraBuffer == NULL)
227     {
228         // No video, only sound
229         return check_if_user_input_should_cancel_video();
230     }
231 
232     gl_TheoraBuffer.WrapAllegroBitmap(theoraBuffer, true);
233 
234     int drawAtX = 0, drawAtY = 0;
235     if (fli_ddb == NULL)
236     {
237         fli_ddb = gfxDriver->CreateDDBFromBitmap(&gl_TheoraBuffer, false, true);
238     }
239     if (stretch_flc)
240     {
241         drawAtX = play.viewport.GetWidth() / 2 - fliTargetWidth / 2;
242         drawAtY = play.viewport.GetHeight() / 2 - fliTargetHeight / 2;
243         if (!gfxDriver->HasAcceleratedStretchAndFlip())
244         {
245             fli_target->StretchBlt(&gl_TheoraBuffer, RectWH(0, 0, gl_TheoraBuffer.GetWidth(), gl_TheoraBuffer.GetHeight()),
246                 RectWH(drawAtX, drawAtY, fliTargetWidth, fliTargetHeight));
247             gfxDriver->UpdateDDBFromBitmap(fli_ddb, fli_target, false);
248             drawAtX = 0;
249             drawAtY = 0;
250         }
251         else
252         {
253             gfxDriver->UpdateDDBFromBitmap(fli_ddb, &gl_TheoraBuffer, false);
254             fli_ddb->SetStretch(fliTargetWidth, fliTargetHeight, false);
255         }
256     }
257     else
258     {
259         gfxDriver->UpdateDDBFromBitmap(fli_ddb, &gl_TheoraBuffer, false);
260         drawAtX = play.viewport.GetWidth() / 2 - gl_TheoraBuffer.GetWidth() / 2;
261         drawAtY = play.viewport.GetHeight() / 2 - gl_TheoraBuffer.GetHeight() / 2;
262     }
263 
264     gfxDriver->DrawSprite(drawAtX, drawAtY, fli_ddb);
265     render_to_screen(virtual_screen, 0, 0);
266     update_polled_audio_and_crossfade ();
267 
268     return check_if_user_input_should_cancel_video();
269 }
270 
271 //
272 // Theora stream reader callbacks. We need these since we are creating PACKFILE
273 // stream by our own rules, and APEG library does not provide means to supply
274 // user's PACKFILE.
275 //
276 // Ironically, internally those callbacks will become a part of another
277 // PACKFILE's vtable, of APEG's library, so that will be a PACKFILE reading
278 // data using proxy PACKFILE through callback system...
279 //
280 class ApegStreamReader
281 {
282 public:
283     ApegStreamReader(const AssetPath path) : _path(path), _pf(NULL) {}
284     ~ApegStreamReader() { Close(); }
285 
286     bool Open()
287     {
288         Close(); // PACKFILE cannot be rewinded, sadly
289         _pf = PackfileFromAsset(_path);
290         return _pf != NULL;
291     }
292 
293     void Close()
294     {
295         if (_pf)
296             pack_fclose(_pf);
297         _pf = NULL;
298     }
299 
300     int Read(void *buffer, int bytes)
301     {
302         return _pf ? pack_fread(buffer, bytes, _pf) : 0;
303     }
304 
305     void Skip(int bytes)
306     {
307         if (_pf)
308             pack_fseek(_pf, bytes);
309     }
310 
311 private:
312     AssetPath _path; // path to the asset
313     PACKFILE *_pf;   // our stream
314 };
315 
316 // Open stream for reading (return suggested cache buffer size).
317 int apeg_stream_init(void *ptr)
318 {
319     return ((ApegStreamReader*)ptr)->Open() ? F_BUF_SIZE : 0;
320 }
321 
322 // Read requested number of bytes into provided buffer,
323 // return actual number of bytes managed to read.
324 int apeg_stream_read(void *buffer, int bytes, void *ptr)
325 {
326     return ((ApegStreamReader*)ptr)->Read(buffer, bytes);
327 }
328 
329 // Skip requested number of bytes
330 void apeg_stream_skip(int bytes, void *ptr)
331 {
332     ((ApegStreamReader*)ptr)->Skip(bytes);
333 }
334 
335 APEG_STREAM* get_theora_size(ApegStreamReader &reader, int *width, int *height)
336 {
337     APEG_STREAM* oggVid = apeg_open_stream_ex(&reader);
338     if (oggVid != NULL)
339     {
340         apeg_get_video_size(oggVid, width, height);
341     }
342     else
343     {
344         *width = 0;
345         *height = 0;
346     }
347     return oggVid;
348 }
349 
350 void calculate_destination_size_maintain_aspect_ratio(int vidWidth, int vidHeight, int *targetWidth, int *targetHeight)
351 {
352     float aspectRatioVideo = (float)vidWidth / (float)vidHeight;
353     float aspectRatioScreen = (float)play.viewport.GetWidth() / (float)play.viewport.GetHeight();
354 
355     if (aspectRatioVideo == aspectRatioScreen)
356     {
357         *targetWidth = play.viewport.GetWidth();
358         *targetHeight = play.viewport.GetHeight();
359     }
360     else if (aspectRatioVideo > aspectRatioScreen)
361     {
362         *targetWidth = play.viewport.GetWidth();
363         *targetHeight = (int)(((float)play.viewport.GetWidth() / aspectRatioVideo) + 0.5f);
364     }
365     else
366     {
367         *targetHeight = play.viewport.GetHeight();
368         *targetWidth = (float)play.viewport.GetHeight() * aspectRatioVideo;
369     }
370 
371 }
372 
373 void play_theora_video(const char *name, int skip, int flags)
374 {
375     ApegStreamReader reader(AssetPath("", name));
376     apeg_set_stream_reader(apeg_stream_init, apeg_stream_read, apeg_stream_skip);
377     apeg_set_display_depth(BitmapHelper::GetScreenBitmap()->GetColorDepth());
378     // we must disable length detection, otherwise it takes ages to start
379     // playing if the file is large because it seeks through the whole thing
380     apeg_disable_length_detection(TRUE);
381     // Disable framedrop because it can lead to the PSP not playing the video at all.
382     apeg_enable_framedrop(psp_video_framedrop);
383     update_polled_stuff_if_runtime();
384 
385     stretch_flc = (flags % 10);
386     canabort = skip;
387     apeg_ignore_audio((flags >= 10) ? 1 : 0);
388 
389     int videoWidth, videoHeight;
390     APEG_STREAM *oggVid = get_theora_size(reader, &videoWidth, &videoHeight);
391 
392     if (videoWidth == 0)
393     {
394         Display("Unable to load theora video '%s'", name);
395         return;
396     }
397 
398     if (flags < 10)
399     {
400         stop_all_sound_and_music();
401     }
402 
403     //fli_buffer = BitmapHelper::CreateBitmap_(scsystem.coldepth, videoWidth, videoHeight);
404     calculate_destination_size_maintain_aspect_ratio(videoWidth, videoHeight, &fliTargetWidth, &fliTargetHeight);
405 
406     if ((fliTargetWidth == videoWidth) && (fliTargetHeight == videoHeight) && (stretch_flc))
407     {
408         // don't need to stretch after all
409         stretch_flc = 0;
410     }
411 
412     if ((stretch_flc) && (!gfxDriver->HasAcceleratedStretchAndFlip()))
413     {
414         fli_target = BitmapHelper::CreateBitmap(play.viewport.GetWidth(), play.viewport.GetHeight(), game.GetColorDepth());
415         fli_target->Clear();
416         fli_ddb = gfxDriver->CreateDDBFromBitmap(fli_target, false, true);
417     }
418     else
419     {
420         fli_ddb = NULL;
421     }
422 
423     update_polled_stuff_if_runtime();
424 
425     virtual_screen->Clear();
426 
427     video_type = kVideoTheora;
428     if (apeg_play_apeg_stream(oggVid, NULL, 0, theora_playing_callback) == APEG_ERROR)
429     {
430         Display("Error playing theora video '%s'", name);
431     }
432     apeg_close_stream(oggVid);
433     video_type = kVideoNone;
434 
435     //destroy_bitmap(fli_buffer);
436     delete fli_target;
437     gfxDriver->DestroyDDB(fli_ddb);
438     fli_target = NULL;
439     fli_ddb = NULL;
440     invalidate_screen();
441 }
442 // Theora player end
443 
444 void video_on_gfxmode_changed()
445 {
446     if (video_type == kVideoFlic)
447     {
448         // If the FLIC video is playing, restore its palette
449         set_palette_range(fli_palette, 0, 255, 0);
450     }
451 }
452